364 lines
14 KiB
Vue
364 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 {
|
|
filterInputClass,
|
|
filterSelectClass,
|
|
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({
|
|
promotions: {
|
|
type: Object,
|
|
required: true,
|
|
},
|
|
establishments: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
machineTypes: {
|
|
type: Object,
|
|
default: () => ({}),
|
|
},
|
|
discountTypes: {
|
|
type: Object,
|
|
default: () => ({}),
|
|
},
|
|
filters: {
|
|
type: Object,
|
|
default: () => ({}),
|
|
},
|
|
});
|
|
|
|
const { localFilters, toggleSort } = useListFilters('supervisor.promotions.index', {
|
|
establishment_id: props.filters.establishment_id ?? '',
|
|
machine_type: props.filters.machine_type ?? '',
|
|
is_active: props.filters.is_active ?? '',
|
|
search: props.filters.search ?? '',
|
|
sort: props.filters.sort ?? 'starts_at',
|
|
direction: props.filters.direction ?? 'desc',
|
|
per_page: props.filters.per_page ?? 10,
|
|
}, { debounceKeys: ['search'] });
|
|
|
|
const showEstablishmentFilter = computed(() => props.establishments.length > 1);
|
|
const handleSort = (key) => toggleSort(key, ['starts_at', 'ends_at', 'discount_value']);
|
|
|
|
const editingId = ref(null);
|
|
const showForm = ref(false);
|
|
const deleteTarget = ref(null);
|
|
const deleteForm = useForm({});
|
|
|
|
const emptyForm = () => ({
|
|
establishment_id: props.establishments[0]?.id ?? '',
|
|
machine_type: 'all',
|
|
discount_type: 'percent',
|
|
discount_value: '',
|
|
starts_at: '',
|
|
ends_at: '',
|
|
description: '',
|
|
is_active: true,
|
|
});
|
|
|
|
const form = useForm(emptyForm());
|
|
|
|
const startCreate = () => {
|
|
editingId.value = null;
|
|
form.defaults(emptyForm());
|
|
form.reset();
|
|
showForm.value = true;
|
|
};
|
|
|
|
const startEdit = (promotion) => {
|
|
editingId.value = promotion.id;
|
|
form.defaults({
|
|
establishment_id: promotion.establishment_id,
|
|
machine_type: promotion.machine_type,
|
|
discount_type: promotion.discount_type,
|
|
discount_value: promotion.discount_value,
|
|
starts_at: promotion.starts_at,
|
|
ends_at: promotion.ends_at,
|
|
description: promotion.description ?? '',
|
|
is_active: promotion.is_active,
|
|
});
|
|
form.reset();
|
|
showForm.value = true;
|
|
};
|
|
|
|
const cancelForm = () => {
|
|
showForm.value = false;
|
|
editingId.value = null;
|
|
form.reset();
|
|
};
|
|
|
|
const submit = () => {
|
|
if (editingId.value) {
|
|
form.put(route('supervisor.promotions.update', editingId.value), {
|
|
onSuccess: cancelForm,
|
|
});
|
|
} else {
|
|
form.post(route('supervisor.promotions.store'), {
|
|
onSuccess: cancelForm,
|
|
});
|
|
}
|
|
};
|
|
|
|
const openDeleteModal = (promotion) => {
|
|
deleteTarget.value = promotion;
|
|
};
|
|
|
|
const closeDeleteModal = () => {
|
|
deleteTarget.value = null;
|
|
};
|
|
|
|
const confirmDelete = () => {
|
|
if (!deleteTarget.value) return;
|
|
|
|
deleteForm.delete(route('supervisor.promotions.destroy', deleteTarget.value.id), {
|
|
onSuccess: closeDeleteModal,
|
|
});
|
|
};
|
|
|
|
const formatDate = (iso) => {
|
|
if (!iso) return '—';
|
|
return new Intl.DateTimeFormat('fr-FR', {
|
|
dateStyle: 'short',
|
|
timeStyle: 'short',
|
|
}).format(new Date(iso));
|
|
};
|
|
|
|
const formatDiscount = (promotion) => {
|
|
if (promotion.discount_type === 'percent') {
|
|
return `-${promotion.discount_value} %`;
|
|
}
|
|
return `-${new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(promotion.discount_value)}`;
|
|
};
|
|
|
|
const isCurrentlyActive = (promotion) => {
|
|
const now = new Date();
|
|
return (
|
|
promotion.is_active &&
|
|
new Date(promotion.starts_at) <= now &&
|
|
new Date(promotion.ends_at) >= now
|
|
);
|
|
};
|
|
|
|
const promotionStatusColor = (promotion) => {
|
|
if (isCurrentlyActive(promotion)) {
|
|
return 'bg-emerald-100 text-emerald-800 ring-emerald-600/20';
|
|
}
|
|
if (promotion.is_active) {
|
|
return 'bg-amber-100 text-amber-800 ring-amber-600/20';
|
|
}
|
|
return 'bg-slate-100 text-slate-600 ring-slate-500/20';
|
|
};
|
|
|
|
const promotionStatusLabel = (promotion) => {
|
|
if (isCurrentlyActive(promotion)) return 'En cours';
|
|
if (promotion.is_active) return 'Programmée';
|
|
return 'Inactive';
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<Head title="Promotions" />
|
|
|
|
<SupervisorLayout title="Promotions">
|
|
<template #actions>
|
|
<PrimaryButton @click="startCreate">
|
|
Nouvelle promotion
|
|
</PrimaryButton>
|
|
</template>
|
|
|
|
<FormModal
|
|
:show="showForm"
|
|
:title="editingId ? 'Modifier la promotion' : 'Nouvelle promotion'"
|
|
: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" required :class="selectClass">
|
|
<option v-for="(label, value) in machineTypes" :key="value" :value="value">
|
|
{{ label }}
|
|
</option>
|
|
</select>
|
|
</FormField>
|
|
<FormField label="Type de remise">
|
|
<select v-model="form.discount_type" required :class="selectClass">
|
|
<option v-for="(label, value) in discountTypes" :key="value" :value="value">
|
|
{{ label }}
|
|
</option>
|
|
</select>
|
|
</FormField>
|
|
<FormField label="Valeur">
|
|
<input v-model="form.discount_value" type="number" step="0.01" min="0" required :class="inputClass" />
|
|
</FormField>
|
|
<FormField label="Début">
|
|
<input v-model="form.starts_at" type="datetime-local" required :class="inputClass" />
|
|
</FormField>
|
|
<FormField label="Fin">
|
|
<input v-model="form.ends_at" type="datetime-local" required :class="inputClass" />
|
|
</FormField>
|
|
<FormField label="Description" field-class="sm:col-span-2 lg:col-span-3">
|
|
<textarea v-model="form.description" rows="2" :class="inputClass" />
|
|
</FormField>
|
|
<div class="flex items-center sm:col-span-2 lg:col-span-3">
|
|
<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"
|
|
>
|
|
<div class="flex flex-col gap-1.5">
|
|
<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>
|
|
<input
|
|
v-model="localFilters.search"
|
|
type="text"
|
|
placeholder="Description…"
|
|
:class="filterInputClass"
|
|
/>
|
|
</div>
|
|
</TableHeaderCell>
|
|
<TableHeaderCell
|
|
label="Remise"
|
|
sortable
|
|
sort-key="discount_value"
|
|
:active-sort="localFilters.sort"
|
|
:sort-direction="localFilters.direction"
|
|
@sort="handleSort"
|
|
/>
|
|
<TableHeaderCell
|
|
label="Période"
|
|
sortable
|
|
sort-key="starts_at"
|
|
:active-sort="localFilters.sort"
|
|
:sort-direction="localFilters.direction"
|
|
@sort="handleSort"
|
|
/>
|
|
<TableHeaderCell
|
|
label="Machines"
|
|
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="">Toutes</option>
|
|
<option v-for="(label, value) in machineTypes" :key="value" :value="value">
|
|
{{ label }}
|
|
</option>
|
|
</select>
|
|
</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="promotions.data.length === 0">
|
|
<td colspan="6" class="px-6 py-12 text-center text-sm text-slate-400">
|
|
Aucune promotion trouvée.
|
|
</td>
|
|
</tr>
|
|
<tr v-for="promotion in promotions.data" :key="promotion.id" :class="rowClass">
|
|
<td :class="[tdClass, 'font-medium text-slate-800']">{{ promotion.establishment_name }}</td>
|
|
<td :class="[tdClass, 'font-semibold text-emerald-700']">{{ formatDiscount(promotion) }}</td>
|
|
<td :class="tdClass">
|
|
<div>{{ formatDate(promotion.starts_at) }}</div>
|
|
<div class="text-xs text-slate-400">→ {{ formatDate(promotion.ends_at) }}</div>
|
|
</td>
|
|
<td :class="tdClass">{{ promotion.machine_type_label }}</td>
|
|
<td :class="tdClass">
|
|
<StatusBadge
|
|
:label="promotionStatusLabel(promotion)"
|
|
:color-class="promotionStatusColor(promotion)"
|
|
/>
|
|
</td>
|
|
<td :class="[tdClass, 'text-right']">
|
|
<button class="font-medium text-indigo-600 transition hover:text-indigo-800" @click="startEdit(promotion)">
|
|
Modifier
|
|
</button>
|
|
<button class="ms-3 font-medium text-red-600 transition hover:text-red-800" @click="openDeleteModal(promotion)">
|
|
Supprimer
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
|
|
<template #footer>
|
|
<Pagination
|
|
:paginator="promotions"
|
|
:per-page="localFilters.per_page"
|
|
@update:per-page="(value) => { localFilters.per_page = value; }"
|
|
/>
|
|
</template>
|
|
</DataTable>
|
|
|
|
<ConfirmDeleteModal
|
|
:show="deleteTarget !== null"
|
|
title="Supprimer la promotion"
|
|
:message="deleteTarget ? `Voulez-vous supprimer la promotion « ${deleteTarget.description || deleteTarget.establishment_name} » ?` : ''"
|
|
:processing="deleteForm.processing"
|
|
@close="closeDeleteModal"
|
|
@confirm="confirmDelete"
|
|
/>
|
|
</SupervisorLayout>
|
|
</template>
|