356 lines
14 KiB
Vue
356 lines
14 KiB
Vue
<script setup>
|
||
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 {
|
||
activeStatusColors,
|
||
filterInputClass,
|
||
filterSelectClass,
|
||
getStatusColor,
|
||
inputClass,
|
||
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, useForm } from '@inertiajs/vue3';
|
||
import { computed, ref } from 'vue';
|
||
|
||
const props = defineProps({
|
||
rules: {
|
||
type: Object,
|
||
required: true,
|
||
},
|
||
establishments: {
|
||
type: Array,
|
||
default: () => [],
|
||
},
|
||
machineTypes: {
|
||
type: Object,
|
||
default: () => ({}),
|
||
},
|
||
dayTypes: {
|
||
type: Object,
|
||
default: () => ({}),
|
||
},
|
||
filters: {
|
||
type: Object,
|
||
default: () => ({}),
|
||
},
|
||
});
|
||
|
||
const { localFilters, toggleSort } = useListFilters('supervisor.pricing.index', {
|
||
establishment_id: props.filters.establishment_id ?? '',
|
||
machine_type: props.filters.machine_type ?? '',
|
||
day_type: props.filters.day_type ?? '',
|
||
is_active: props.filters.is_active ?? '',
|
||
search: props.filters.search ?? '',
|
||
sort: props.filters.sort ?? 'priority',
|
||
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);
|
||
|
||
const editingId = ref(null);
|
||
const showForm = ref(false);
|
||
const deleteTarget = ref(null);
|
||
const deleteForm = useForm({});
|
||
|
||
const emptyForm = () => ({
|
||
establishment_id: props.establishments[0]?.id ?? '',
|
||
machine_id: null,
|
||
machine_type: '',
|
||
day_type: 'all',
|
||
slot_start: '08:00',
|
||
slot_end: '22:00',
|
||
price: '',
|
||
label: '',
|
||
requires_app: false,
|
||
priority: 100,
|
||
is_active: true,
|
||
});
|
||
|
||
const form = useForm(emptyForm());
|
||
|
||
const startCreate = () => {
|
||
editingId.value = null;
|
||
form.defaults(emptyForm());
|
||
form.reset();
|
||
showForm.value = true;
|
||
};
|
||
|
||
const startEdit = (rule) => {
|
||
editingId.value = rule.id;
|
||
form.defaults({
|
||
establishment_id: rule.establishment_id,
|
||
machine_id: rule.machine_id,
|
||
machine_type: rule.machine_type ?? '',
|
||
day_type: rule.day_type,
|
||
slot_start: rule.slot_start,
|
||
slot_end: rule.slot_end,
|
||
price: rule.price,
|
||
label: rule.label ?? '',
|
||
requires_app: rule.requires_app,
|
||
priority: rule.priority,
|
||
is_active: rule.is_active,
|
||
});
|
||
form.reset();
|
||
showForm.value = true;
|
||
};
|
||
|
||
const cancelForm = () => {
|
||
showForm.value = false;
|
||
editingId.value = null;
|
||
form.reset();
|
||
};
|
||
|
||
const submit = () => {
|
||
const payload = {
|
||
...form.data(),
|
||
machine_id: form.machine_id || null,
|
||
machine_type: form.machine_type || null,
|
||
};
|
||
|
||
if (editingId.value) {
|
||
form.transform(() => payload).put(route('supervisor.pricing.update', editingId.value), {
|
||
onSuccess: cancelForm,
|
||
});
|
||
} else {
|
||
form.transform(() => payload).post(route('supervisor.pricing.store'), {
|
||
onSuccess: cancelForm,
|
||
});
|
||
}
|
||
};
|
||
|
||
const openDeleteModal = (rule) => {
|
||
deleteTarget.value = rule;
|
||
};
|
||
|
||
const closeDeleteModal = () => {
|
||
deleteTarget.value = null;
|
||
};
|
||
|
||
const confirmDelete = () => {
|
||
if (!deleteTarget.value) return;
|
||
|
||
deleteForm.delete(route('supervisor.pricing.destroy', deleteTarget.value.id), {
|
||
onSuccess: closeDeleteModal,
|
||
});
|
||
};
|
||
|
||
const formatCurrency = (value) =>
|
||
new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(value ?? 0);
|
||
</script>
|
||
|
||
<template>
|
||
<Head title="Tarifs" />
|
||
|
||
<SupervisorLayout title="Tarifs">
|
||
<template #actions>
|
||
<PrimaryButton @click="startCreate">
|
||
Nouvelle règle
|
||
</PrimaryButton>
|
||
</template>
|
||
|
||
<FormModal
|
||
:show="showForm"
|
||
:title="editingId ? 'Modifier la règle' : 'Nouvelle règle tarifaire'"
|
||
:submit-label="editingId ? 'Enregistrer' : 'Créer'"
|
||
:processing="form.processing"
|
||
max-width="3xl"
|
||
@close="cancelForm"
|
||
@submit="submit"
|
||
>
|
||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||
<FormField label="Établissement">
|
||
<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="Type de machine">
|
||
<select v-model="form.machine_type" :class="selectClass">
|
||
<option value="">Tous types</option>
|
||
<option v-for="(label, value) in machineTypes" :key="value" :value="value">
|
||
{{ label }}
|
||
</option>
|
||
</select>
|
||
</FormField>
|
||
<FormField label="Jour">
|
||
<select v-model="form.day_type" required :class="selectClass">
|
||
<option v-for="(label, value) in dayTypes" :key="value" :value="value">
|
||
{{ label }}
|
||
</option>
|
||
</select>
|
||
</FormField>
|
||
<FormField label="Début créneau">
|
||
<input v-model="form.slot_start" type="time" required :class="inputClass" />
|
||
</FormField>
|
||
<FormField label="Fin créneau">
|
||
<input v-model="form.slot_end" type="time" required :class="inputClass" />
|
||
</FormField>
|
||
<FormField label="Prix (€)">
|
||
<input v-model="form.price" type="number" step="0.01" min="0" required :class="inputClass" />
|
||
</FormField>
|
||
<FormField label="Libellé">
|
||
<input v-model="form.label" type="text" :class="inputClass" />
|
||
</FormField>
|
||
<FormField label="Priorité">
|
||
<input v-model="form.priority" type="number" min="0" :class="inputClass" />
|
||
</FormField>
|
||
<div class="flex items-end gap-4 sm:col-span-2 lg:col-span-3">
|
||
<label class="flex items-center gap-2 text-sm text-slate-700">
|
||
<input v-model="form.requires_app" type="checkbox" class="rounded border-slate-300 text-indigo-600 focus:ring-indigo-500/20" />
|
||
App requise
|
||
</label>
|
||
<label class="flex items-center gap-2 text-sm text-slate-700">
|
||
<input v-model="form.is_active" type="checkbox" class="rounded border-slate-300 text-indigo-600 focus:ring-indigo-500/20" />
|
||
Active
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</FormModal>
|
||
|
||
<DataTable>
|
||
<template #head>
|
||
<tr>
|
||
<TableHeaderCell
|
||
label="Établissement"
|
||
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="Créneau"
|
||
sortable
|
||
sort-key="slot_start"
|
||
:active-sort="localFilters.sort"
|
||
:sort-direction="localFilters.direction"
|
||
@sort="handleSort"
|
||
>
|
||
<select v-model="localFilters.day_type" :class="filterSelectClass">
|
||
<option value="">Tous</option>
|
||
<option v-for="(label, value) in dayTypes" :key="value" :value="value">
|
||
{{ label }}
|
||
</option>
|
||
</select>
|
||
</TableHeaderCell>
|
||
<TableHeaderCell
|
||
label="Type"
|
||
sortable
|
||
sort-key="machine_type"
|
||
:active-sort="localFilters.sort"
|
||
:sort-direction="localFilters.direction"
|
||
@sort="handleSort"
|
||
>
|
||
<select v-model="localFilters.machine_type" :class="filterSelectClass">
|
||
<option value="">Tous</option>
|
||
<option v-for="(label, value) in machineTypes" :key="value" :value="value">
|
||
{{ label }}
|
||
</option>
|
||
</select>
|
||
</TableHeaderCell>
|
||
<TableHeaderCell
|
||
label="Prix"
|
||
sortable
|
||
sort-key="price"
|
||
:active-sort="localFilters.sort"
|
||
:sort-direction="localFilters.direction"
|
||
@sort="handleSort"
|
||
>
|
||
<input
|
||
v-model="localFilters.search"
|
||
type="text"
|
||
placeholder="Libellé…"
|
||
:class="filterInputClass"
|
||
/>
|
||
</TableHeaderCell>
|
||
<TableHeaderCell
|
||
label="Statut"
|
||
sortable
|
||
sort-key="is_active"
|
||
:active-sort="localFilters.sort"
|
||
:sort-direction="localFilters.direction"
|
||
@sort="handleSort"
|
||
>
|
||
<select v-model="localFilters.is_active" :class="filterSelectClass">
|
||
<option value="">Tous</option>
|
||
<option value="1">Active</option>
|
||
<option value="0">Inactive</option>
|
||
</select>
|
||
</TableHeaderCell>
|
||
<TableHeaderCell label="Actions" align="right" />
|
||
</tr>
|
||
</template>
|
||
|
||
<tr v-if="rules.data.length === 0">
|
||
<td colspan="6" class="px-6 py-12 text-center text-sm text-slate-400">
|
||
Aucune règle tarifaire trouvée.
|
||
</td>
|
||
</tr>
|
||
<tr v-for="rule in rules.data" :key="rule.id" :class="rowClass">
|
||
<td :class="[tdClass, 'font-medium text-slate-800']">{{ rule.establishment_name }}</td>
|
||
<td :class="tdClass">
|
||
{{ rule.slot_start }} – {{ rule.slot_end }}
|
||
<span class="text-xs text-slate-400">({{ rule.day_type_label }})</span>
|
||
</td>
|
||
<td :class="tdClass">{{ rule.machine_type_label ?? 'Tous' }}</td>
|
||
<td :class="[tdClass, 'font-semibold text-slate-800']">{{ formatCurrency(rule.price) }}</td>
|
||
<td :class="tdClass">
|
||
<StatusBadge
|
||
:label="rule.is_active ? 'Active' : 'Inactive'"
|
||
:color-class="getStatusColor(rule.is_active ? 'active' : 'inactive', activeStatusColors)"
|
||
/>
|
||
</td>
|
||
<td :class="[tdClass, 'text-right']">
|
||
<button class="font-medium text-indigo-600 transition hover:text-indigo-800" @click="startEdit(rule)">
|
||
Modifier
|
||
</button>
|
||
<button class="ms-3 font-medium text-red-600 transition hover:text-red-800" @click="openDeleteModal(rule)">
|
||
Supprimer
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
|
||
<template #footer>
|
||
<Pagination
|
||
:paginator="rules"
|
||
:per-page="localFilters.per_page"
|
||
@update:per-page="(value) => { localFilters.per_page = value; }"
|
||
/>
|
||
</template>
|
||
</DataTable>
|
||
|
||
<ConfirmDeleteModal
|
||
:show="deleteTarget !== null"
|
||
title="Supprimer la règle tarifaire"
|
||
:message="deleteTarget ? `Voulez-vous supprimer la règle « ${deleteTarget.label || deleteTarget.establishment_name} » ?` : ''"
|
||
:processing="deleteForm.processing"
|
||
@close="closeDeleteModal"
|
||
@confirm="confirmDelete"
|
||
/>
|
||
</SupervisorLayout>
|
||
</template>
|