Intégration fonctionnalites V1
This commit is contained in:
@@ -1,13 +1,42 @@
|
||||
<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, router } from '@inertiajs/vue3';
|
||||
import { reactive, watch } from '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: () => ({}),
|
||||
@@ -22,34 +51,93 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const localFilters = reactive({
|
||||
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',
|
||||
});
|
||||
|
||||
let debounceTimer = null;
|
||||
const form = useForm(emptyForm());
|
||||
|
||||
watch(localFilters, () => {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => {
|
||||
router.get(route('supervisor.machines.index'), localFilters, {
|
||||
preserveState: true,
|
||||
replace: true,
|
||||
});
|
||||
}, 300);
|
||||
});
|
||||
const startCreate = () => {
|
||||
editingUuid.value = null;
|
||||
form.defaults(emptyForm());
|
||||
form.reset();
|
||||
showForm.value = true;
|
||||
};
|
||||
|
||||
const statusColor = (status) => {
|
||||
const colors = {
|
||||
available: 'bg-green-100 text-green-800',
|
||||
reserved: 'bg-yellow-100 text-yellow-800',
|
||||
running: 'bg-blue-100 text-blue-800',
|
||||
maintenance: 'bg-orange-100 text-orange-800',
|
||||
offline: 'bg-gray-100 text-gray-800',
|
||||
error: 'bg-red-100 text-red-800',
|
||||
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,
|
||||
};
|
||||
return colors[status] ?? 'bg-gray-100 text-gray-800';
|
||||
|
||||
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) => {
|
||||
@@ -65,124 +153,185 @@ const formatDate = (iso) => {
|
||||
<Head title="Machines" />
|
||||
|
||||
<SupervisorLayout title="Machines">
|
||||
<!-- Filters -->
|
||||
<div class="mb-6 flex flex-wrap gap-4 rounded-lg bg-white p-4 shadow">
|
||||
<div class="min-w-[200px] flex-1">
|
||||
<label class="block text-xs font-medium text-gray-500">Recherche</label>
|
||||
<input
|
||||
v-model="localFilters.search"
|
||||
type="text"
|
||||
placeholder="Nom ou QR code…"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-500">Statut</label>
|
||||
<select
|
||||
v-model="localFilters.status"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
|
||||
>
|
||||
<option value="">Tous</option>
|
||||
<option v-for="(label, value) in statusOptions" :key="value" :value="value">
|
||||
{{ label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-500">Type</label>
|
||||
<select
|
||||
v-model="localFilters.type"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
|
||||
>
|
||||
<option value="">Tous</option>
|
||||
<option v-for="(label, value) in typeOptions" :key="value" :value="value">
|
||||
{{ label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<template #actions>
|
||||
<PrimaryButton
|
||||
v-if="canManageMachines && establishments.length > 0"
|
||||
@click="startCreate"
|
||||
>
|
||||
Nouvelle machine
|
||||
</PrimaryButton>
|
||||
</template>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="overflow-hidden rounded-lg bg-white shadow">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
|
||||
Machine
|
||||
</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
|
||||
Établissement
|
||||
</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
|
||||
Type
|
||||
</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
|
||||
Statut
|
||||
</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
|
||||
Dernier signal
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100 bg-white">
|
||||
<tr v-if="machines.data.length === 0">
|
||||
<td colspan="5" class="px-4 py-8 text-center text-sm text-gray-500">
|
||||
Aucune machine trouvée.
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-for="machine in machines.data" :key="machine.uuid" class="hover:bg-gray-50">
|
||||
<td class="whitespace-nowrap px-4 py-3">
|
||||
<Link
|
||||
:href="route('supervisor.machines.show', machine.uuid)"
|
||||
class="font-medium text-indigo-600 hover:text-indigo-800"
|
||||
>
|
||||
{{ machine.name }}
|
||||
</Link>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-600">
|
||||
{{ machine.establishment_name ?? '—' }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-600">
|
||||
{{ machine.type_label }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-3">
|
||||
<span
|
||||
class="rounded-full px-2.5 py-0.5 text-xs font-medium"
|
||||
:class="statusColor(machine.status)"
|
||||
>
|
||||
{{ machine.status_label }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-500">
|
||||
{{ formatDate(machine.last_heartbeat_at) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<AlertBanner v-if="page.props.errors?.delete">
|
||||
{{ page.props.errors.delete }}
|
||||
</AlertBanner>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div v-if="machines.links.length > 3" class="flex items-center justify-between border-t px-4 py-3">
|
||||
<p class="text-sm text-gray-600">
|
||||
{{ machines.from ?? 0 }}–{{ machines.to ?? 0 }} sur {{ machines.total }}
|
||||
</p>
|
||||
<div class="flex gap-1">
|
||||
<Link
|
||||
v-for="link in machines.links"
|
||||
:key="link.label"
|
||||
:href="link.url"
|
||||
v-html="link.label"
|
||||
class="rounded px-3 py-1 text-sm"
|
||||
:class="
|
||||
link.active
|
||||
? 'bg-indigo-600 text-white'
|
||||
: link.url
|
||||
? 'text-gray-600 hover:bg-gray-100'
|
||||
: 'cursor-not-allowed text-gray-300'
|
||||
"
|
||||
preserve-state
|
||||
<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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user