338 lines
12 KiB
Vue
338 lines
12 KiB
Vue
<script setup>
|
|
import AlertBanner from '@/Components/Supervisor/AlertBanner.vue';
|
|
import DataTable from '@/Components/Supervisor/DataTable.vue';
|
|
import FormField from '@/Components/Supervisor/FormField.vue';
|
|
import FormModal from '@/Components/Supervisor/FormModal.vue';
|
|
import Pagination from '@/Components/Supervisor/Pagination.vue';
|
|
import StatusBadge from '@/Components/Supervisor/StatusBadge.vue';
|
|
import TableHeaderCell from '@/Components/Supervisor/TableHeaderCell.vue';
|
|
import { useListFilters } from '@/Components/Supervisor/useListFilters.js';
|
|
import {
|
|
filterInputClass,
|
|
filterSelectClass,
|
|
getStatusColor,
|
|
inputClass,
|
|
linkClass,
|
|
machineStatusColors,
|
|
rowClass,
|
|
selectClass,
|
|
tdClass,
|
|
} from '@/Components/Supervisor/ui.js';
|
|
import ConfirmDeleteModal from '@/Components/ConfirmDeleteModal.vue';
|
|
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
|
import SupervisorLayout from '@/Layouts/SupervisorLayout.vue';
|
|
import { Head, Link, useForm, usePage } from '@inertiajs/vue3';
|
|
import { ref, computed } from 'vue';
|
|
|
|
const props = defineProps({
|
|
machines: {
|
|
type: Object,
|
|
required: true,
|
|
},
|
|
establishments: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
canManageMachines: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
filters: {
|
|
type: Object,
|
|
default: () => ({}),
|
|
},
|
|
statusOptions: {
|
|
type: Object,
|
|
default: () => ({}),
|
|
},
|
|
typeOptions: {
|
|
type: Object,
|
|
default: () => ({}),
|
|
},
|
|
});
|
|
|
|
const page = usePage();
|
|
const editingUuid = ref(null);
|
|
const showForm = ref(false);
|
|
const deleteTarget = ref(null);
|
|
const deleteForm = useForm({});
|
|
|
|
const { localFilters, toggleSort } = useListFilters('supervisor.machines.index', {
|
|
establishment_id: props.filters.establishment_id ?? '',
|
|
search: props.filters.search ?? '',
|
|
status: props.filters.status ?? '',
|
|
type: props.filters.type ?? '',
|
|
sort: props.filters.sort ?? 'name',
|
|
direction: props.filters.direction ?? 'asc',
|
|
per_page: props.filters.per_page ?? 10,
|
|
}, { debounceKeys: ['search'] });
|
|
|
|
const showEstablishmentFilter = computed(() => props.establishments.length > 1);
|
|
|
|
const handleSort = (key) => toggleSort(key, ['last_heartbeat_at']);
|
|
|
|
const emptyForm = () => ({
|
|
establishment_id: props.establishments[0]?.id ?? '',
|
|
name: '',
|
|
type: '',
|
|
qr_code: '',
|
|
status: 'available',
|
|
});
|
|
|
|
const form = useForm(emptyForm());
|
|
|
|
const startCreate = () => {
|
|
editingUuid.value = null;
|
|
form.defaults(emptyForm());
|
|
form.reset();
|
|
showForm.value = true;
|
|
};
|
|
|
|
const startEdit = (machine) => {
|
|
editingUuid.value = machine.uuid;
|
|
form.defaults({
|
|
establishment_id: machine.establishment_id,
|
|
name: machine.name,
|
|
type: machine.type,
|
|
qr_code: machine.qr_code,
|
|
status: machine.status,
|
|
});
|
|
form.reset();
|
|
showForm.value = true;
|
|
};
|
|
|
|
const cancelForm = () => {
|
|
showForm.value = false;
|
|
editingUuid.value = null;
|
|
form.reset();
|
|
};
|
|
|
|
const submit = () => {
|
|
const payload = {
|
|
...form.data(),
|
|
qr_code: form.qr_code || null,
|
|
};
|
|
|
|
if (editingUuid.value) {
|
|
form.transform(() => payload).put(route('supervisor.machines.update', editingUuid.value), {
|
|
onSuccess: cancelForm,
|
|
});
|
|
} else {
|
|
form.transform(() => payload).post(route('supervisor.machines.store'), {
|
|
onSuccess: cancelForm,
|
|
});
|
|
}
|
|
};
|
|
|
|
const openDeleteModal = (machine) => {
|
|
deleteTarget.value = machine;
|
|
};
|
|
|
|
const closeDeleteModal = () => {
|
|
deleteTarget.value = null;
|
|
};
|
|
|
|
const confirmDelete = () => {
|
|
if (!deleteTarget.value) return;
|
|
|
|
deleteForm.delete(route('supervisor.machines.destroy', deleteTarget.value.uuid), {
|
|
onSuccess: closeDeleteModal,
|
|
});
|
|
};
|
|
|
|
const formatDate = (iso) => {
|
|
if (!iso) return '—';
|
|
return new Intl.DateTimeFormat('fr-FR', {
|
|
dateStyle: 'short',
|
|
timeStyle: 'short',
|
|
}).format(new Date(iso));
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<Head title="Machines" />
|
|
|
|
<SupervisorLayout title="Machines">
|
|
<template #actions>
|
|
<PrimaryButton
|
|
v-if="canManageMachines && establishments.length > 0"
|
|
@click="startCreate"
|
|
>
|
|
Nouvelle machine
|
|
</PrimaryButton>
|
|
</template>
|
|
|
|
<AlertBanner v-if="page.props.errors?.delete">
|
|
{{ page.props.errors.delete }}
|
|
</AlertBanner>
|
|
|
|
<FormModal
|
|
:show="showForm"
|
|
:title="editingUuid ? 'Modifier la machine' : 'Nouvelle machine'"
|
|
:submit-label="editingUuid ? 'Enregistrer' : 'Créer'"
|
|
:processing="form.processing"
|
|
@close="cancelForm"
|
|
@submit="submit"
|
|
>
|
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
<FormField label="Enseigne">
|
|
<select v-model="form.establishment_id" required :class="selectClass">
|
|
<option v-for="est in establishments" :key="est.id" :value="est.id">
|
|
{{ est.name }}
|
|
</option>
|
|
</select>
|
|
</FormField>
|
|
<FormField label="Nom">
|
|
<input v-model="form.name" type="text" required :class="inputClass" />
|
|
</FormField>
|
|
<FormField label="Type">
|
|
<select v-model="form.type" required :class="selectClass">
|
|
<option value="" disabled>Sélectionner…</option>
|
|
<option v-for="(label, value) in typeOptions" :key="value" :value="value">
|
|
{{ label }}
|
|
</option>
|
|
</select>
|
|
</FormField>
|
|
<FormField label="QR code" :hint="editingUuid ? '' : 'Généré automatiquement si vide'">
|
|
<input v-model="form.qr_code" type="text" :class="inputClass" />
|
|
</FormField>
|
|
<FormField label="Statut">
|
|
<select v-model="form.status" required :class="selectClass">
|
|
<option v-for="(label, value) in statusOptions" :key="value" :value="value">
|
|
{{ label }}
|
|
</option>
|
|
</select>
|
|
</FormField>
|
|
</div>
|
|
</FormModal>
|
|
|
|
<DataTable>
|
|
<template #head>
|
|
<tr>
|
|
<TableHeaderCell
|
|
label="Machine"
|
|
sortable
|
|
sort-key="name"
|
|
:active-sort="localFilters.sort"
|
|
:sort-direction="localFilters.direction"
|
|
@sort="handleSort"
|
|
>
|
|
<input
|
|
v-model="localFilters.search"
|
|
type="text"
|
|
placeholder="Nom ou QR…"
|
|
:class="filterInputClass"
|
|
/>
|
|
</TableHeaderCell>
|
|
<TableHeaderCell
|
|
label="Enseigne"
|
|
sortable
|
|
sort-key="establishment"
|
|
:active-sort="localFilters.sort"
|
|
:sort-direction="localFilters.direction"
|
|
@sort="handleSort"
|
|
>
|
|
<select
|
|
v-if="showEstablishmentFilter"
|
|
v-model="localFilters.establishment_id"
|
|
:class="filterSelectClass"
|
|
>
|
|
<option value="">Toutes</option>
|
|
<option v-for="est in establishments" :key="est.id" :value="est.id">
|
|
{{ est.name }}
|
|
</option>
|
|
</select>
|
|
</TableHeaderCell>
|
|
<TableHeaderCell
|
|
label="Type"
|
|
sortable
|
|
sort-key="type"
|
|
:active-sort="localFilters.sort"
|
|
:sort-direction="localFilters.direction"
|
|
@sort="handleSort"
|
|
>
|
|
<select v-model="localFilters.type" :class="filterSelectClass">
|
|
<option value="">Tous</option>
|
|
<option v-for="(label, value) in typeOptions" :key="value" :value="value">
|
|
{{ label }}
|
|
</option>
|
|
</select>
|
|
</TableHeaderCell>
|
|
<TableHeaderCell
|
|
label="Statut"
|
|
sortable
|
|
sort-key="status"
|
|
:active-sort="localFilters.sort"
|
|
:sort-direction="localFilters.direction"
|
|
@sort="handleSort"
|
|
>
|
|
<select v-model="localFilters.status" :class="filterSelectClass">
|
|
<option value="">Tous</option>
|
|
<option v-for="(label, value) in statusOptions" :key="value" :value="value">
|
|
{{ label }}
|
|
</option>
|
|
</select>
|
|
</TableHeaderCell>
|
|
<TableHeaderCell
|
|
label="Dernier signal"
|
|
sortable
|
|
sort-key="last_heartbeat_at"
|
|
:active-sort="localFilters.sort"
|
|
:sort-direction="localFilters.direction"
|
|
@sort="handleSort"
|
|
/>
|
|
<TableHeaderCell v-if="canManageMachines" label="Actions" align="right" />
|
|
</tr>
|
|
</template>
|
|
|
|
<tr v-if="machines.data.length === 0">
|
|
<td :colspan="canManageMachines ? 6 : 5" class="px-6 py-12 text-center text-sm text-slate-400">
|
|
Aucune machine trouvée.
|
|
</td>
|
|
</tr>
|
|
<tr v-for="machine in machines.data" :key="machine.uuid" :class="rowClass">
|
|
<td :class="tdClass">
|
|
<Link :href="route('supervisor.machines.show', machine.uuid)" :class="linkClass">
|
|
{{ machine.name }}
|
|
</Link>
|
|
<p class="mt-0.5 font-mono text-xs text-slate-400">{{ machine.qr_code }}</p>
|
|
</td>
|
|
<td :class="tdClass">{{ machine.establishment_name ?? '—' }}</td>
|
|
<td :class="tdClass">{{ machine.type_label }}</td>
|
|
<td :class="tdClass">
|
|
<StatusBadge
|
|
:label="machine.status_label"
|
|
:color-class="getStatusColor(machine.status, machineStatusColors)"
|
|
/>
|
|
</td>
|
|
<td :class="tdClass">{{ formatDate(machine.last_heartbeat_at) }}</td>
|
|
<td v-if="canManageMachines" :class="[tdClass, 'text-right']">
|
|
<button class="font-medium text-indigo-600 transition hover:text-indigo-800" @click="startEdit(machine)">
|
|
Modifier
|
|
</button>
|
|
<button class="ms-3 font-medium text-red-600 transition hover:text-red-800" @click="openDeleteModal(machine)">
|
|
Supprimer
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
|
|
<template #footer>
|
|
<Pagination
|
|
:paginator="machines"
|
|
:per-page="localFilters.per_page"
|
|
@update:per-page="(value) => { localFilters.per_page = value; }"
|
|
/>
|
|
</template>
|
|
</DataTable>
|
|
|
|
<ConfirmDeleteModal
|
|
:show="deleteTarget !== null"
|
|
title="Supprimer la machine"
|
|
:message="deleteTarget ? `Voulez-vous supprimer « ${deleteTarget.name} » ?` : ''"
|
|
:processing="deleteForm.processing"
|
|
@close="closeDeleteModal"
|
|
@confirm="confirmDelete"
|
|
/>
|
|
</SupervisorLayout>
|
|
</template>
|