369 lines
23 KiB
Vue
369 lines
23 KiB
Vue
<script setup>
|
|
import { ref, reactive } from 'vue';
|
|
import { Head, Link, router } from '@inertiajs/vue3';
|
|
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
|
import StatusBadge from '@/Components/StatusBadge.vue';
|
|
|
|
const props = defineProps({
|
|
orders: Object,
|
|
filters: Object,
|
|
});
|
|
|
|
// État des filtres réactifs
|
|
const form = reactive({
|
|
search: props.filters?.search || '',
|
|
status: props.filters?.status || '',
|
|
requested_by: props.filters?.requested_by || '',
|
|
type: props.filters?.type || '',
|
|
date_start: props.filters?.date_start || '',
|
|
date_end: props.filters?.date_end || '',
|
|
});
|
|
|
|
// Appliquer les filtres
|
|
const applyFilters = () => {
|
|
router.get(route('commandes.index'), form, {
|
|
preserveState: true,
|
|
replace: true,
|
|
});
|
|
};
|
|
|
|
// Réinitialiser les filtres
|
|
const resetFilters = () => {
|
|
form.search = '';
|
|
form.status = '';
|
|
form.requested_by = '';
|
|
form.type = '';
|
|
form.date_start = '';
|
|
form.date_end = '';
|
|
applyFilters();
|
|
};
|
|
|
|
// Exporter en CSV
|
|
const exportCsv = () => {
|
|
// Génère l'URL d'export avec les filtres courants
|
|
const params = new URLSearchParams(form).toString();
|
|
window.location.href = `${route('commandes.index')}?export=1&${params}`;
|
|
};
|
|
|
|
// Types de commandes pour le filtre
|
|
const orderTypes = [
|
|
'Matériel réseau / serveur',
|
|
'Licences logicielles',
|
|
'Consommables / câblage',
|
|
'Prestations / services',
|
|
];
|
|
|
|
// Demandeur pour le filtre
|
|
const demandeurs = ['Jérémy', 'Sylvain', 'Kévin'];
|
|
</script>
|
|
|
|
<template>
|
|
<Head title="Suivi des Commandes" />
|
|
|
|
<AuthenticatedLayout>
|
|
<template #header>
|
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
<h2 class="text-xl font-bold leading-tight text-slate-800 dark:text-slate-200">
|
|
Suivi des Commandes
|
|
</h2>
|
|
<div class="flex gap-2">
|
|
<button
|
|
@click="exportCsv"
|
|
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-sky-500 dark:bg-slate-900 dark:text-slate-300 dark:border-slate-755 dark:hover:bg-slate-800 transition-colors"
|
|
>
|
|
<svg class="w-4 h-4 mr-2" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
|
</svg>
|
|
Exporter en CSV
|
|
</button>
|
|
<Link
|
|
:href="route('commandes.create')"
|
|
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-white bg-sky-600 rounded-lg hover:bg-sky-500 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-sky-500 dark:bg-sky-500 dark:hover:bg-sky-400 transition-colors"
|
|
>
|
|
<svg class="w-4 h-4 mr-2" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
|
</svg>
|
|
Créer une demande
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<div class="py-6">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<!-- Session Flash Messages -->
|
|
<div v-if="$page.props.flash?.success" class="mb-6 p-4 bg-emerald-50 border border-emerald-200 text-emerald-800 rounded-lg dark:bg-emerald-950/40 dark:border-emerald-900 dark:text-emerald-300 flex items-center shadow-sm">
|
|
<svg class="w-5 h-5 mr-3 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
|
</svg>
|
|
<span>{{ $page.props.flash?.success }}</span>
|
|
</div>
|
|
|
|
<!-- Filtres -->
|
|
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl p-5 shadow-sm mb-6">
|
|
<h3 class="text-sm font-bold text-slate-800 dark:text-slate-200 mb-4 flex items-center uppercase tracking-wider">
|
|
<svg class="w-4 h-4 mr-2 text-slate-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 0 1-.659 1.591l-5.432 5.432a2.25 2.25 0 0 0-.659 1.591v2.927a2.25 2.25 0 0 1-1.244 2.013L9.75 21v-6.562a2.25 2.25 0 0 0-.659-1.591L3.659 7.409A2.25 2.25 0 0 1 3 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0 1 12 3Z" />
|
|
</svg>
|
|
Filtres de recherche
|
|
</h3>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
|
<!-- Recherche textuelle -->
|
|
<div class="md:col-span-2 lg:col-span-1">
|
|
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 mb-1">Recherche</label>
|
|
<input
|
|
type="text"
|
|
v-model="form.search"
|
|
placeholder="Numéro, libellé, fournisseur..."
|
|
@keydown.enter="applyFilters"
|
|
class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Statut -->
|
|
<div>
|
|
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 mb-1">Statut</label>
|
|
<select
|
|
v-model="form.status"
|
|
@change="applyFilters"
|
|
class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200"
|
|
>
|
|
<option value="">Tous les statuts</option>
|
|
<option value="draft">Brouillon</option>
|
|
<option value="validated">Validée</option>
|
|
<option value="ordered">Commandée</option>
|
|
<option value="delivered">Livrée</option>
|
|
<option value="closed">Clôturée</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Demandeur -->
|
|
<div>
|
|
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 mb-1">Demandeur</label>
|
|
<select
|
|
v-model="form.requested_by"
|
|
@change="applyFilters"
|
|
class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200"
|
|
>
|
|
<option value="">Tous les demandeurs</option>
|
|
<option v-for="d in demandeurs" :key="d" :value="d">{{ d }}</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Type -->
|
|
<div>
|
|
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 mb-1">Type de commande</label>
|
|
<select
|
|
v-model="form.type"
|
|
@change="applyFilters"
|
|
class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200"
|
|
>
|
|
<option value="">Tous les types</option>
|
|
<option v-for="t in orderTypes" :key="t" :value="t">{{ t }}</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Période début -->
|
|
<div>
|
|
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 mb-1">Livraison souhaitée du</label>
|
|
<input
|
|
type="date"
|
|
v-model="form.date_start"
|
|
@change="applyFilters"
|
|
class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Période fin -->
|
|
<div>
|
|
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 mb-1">Au</label>
|
|
<input
|
|
type="date"
|
|
v-model="form.date_end"
|
|
@change="applyFilters"
|
|
class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Actions filtres -->
|
|
<div class="flex items-end gap-2 md:col-span-3 lg:col-span-2">
|
|
<button
|
|
@click="applyFilters"
|
|
class="inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-semibold rounded-lg text-white bg-slate-800 hover:bg-slate-700 dark:bg-slate-800 dark:hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500 transition-colors w-full sm:w-auto"
|
|
>
|
|
Filtrer
|
|
</button>
|
|
<button
|
|
@click="resetFilters"
|
|
class="inline-flex items-center justify-center px-4 py-2 border border-slate-300 text-sm font-semibold rounded-lg text-slate-700 bg-white hover:bg-slate-50 dark:bg-slate-900 dark:text-slate-300 dark:border-slate-800 dark:hover:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500 transition-colors w-full sm:w-auto"
|
|
>
|
|
Réinitialiser
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tableau des commandes -->
|
|
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden">
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-slate-200 dark:divide-slate-800 text-left">
|
|
<thead class="bg-slate-50 dark:bg-slate-950">
|
|
<tr>
|
|
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Numéro</th>
|
|
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Libellé / Article</th>
|
|
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Type</th>
|
|
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Fournisseur</th>
|
|
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Demandeur / Service</th>
|
|
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Date souhaitée</th>
|
|
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 text-right">Montant TTC</th>
|
|
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Statut</th>
|
|
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 text-right">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-200 dark:divide-slate-850">
|
|
<tr
|
|
v-for="order in orders?.data || []"
|
|
:key="order.id"
|
|
class="hover:bg-slate-50/50 dark:hover:bg-slate-900/40 transition-colors"
|
|
>
|
|
<!-- Numéro -->
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-semibold text-sky-600 dark:text-sky-400">
|
|
<Link :href="route('commandes.show', { commande: order.id })" class="hover:underline">
|
|
{{ order.number }}
|
|
</Link>
|
|
</td>
|
|
|
|
<!-- Libellé -->
|
|
<td class="px-6 py-4 text-sm text-slate-900 dark:text-slate-200 max-w-xs truncate">
|
|
<span class="font-medium">{{ order.label }}</span>
|
|
</td>
|
|
|
|
<!-- Type -->
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500 dark:text-slate-400">
|
|
{{ order.type }}
|
|
</td>
|
|
|
|
<!-- Fournisseur -->
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-900 dark:text-slate-300 font-medium">
|
|
{{ order.supplier }}
|
|
</td>
|
|
|
|
<!-- Demandeur / Service -->
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-900 dark:text-slate-350">
|
|
<div class="font-medium text-slate-800 dark:text-slate-200">{{ order.requested_by }}</div>
|
|
<div class="text-xs text-slate-450 dark:text-slate-400 font-semibold">{{ order.prescriber }}</div>
|
|
</td>
|
|
|
|
<!-- Date -->
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500 dark:text-slate-400">
|
|
<div class="flex items-center gap-1.5">
|
|
<span>{{ order.delivery_deadline_formatted }}</span>
|
|
<!-- Alerte de retard -->
|
|
<span
|
|
v-if="order.is_overdue"
|
|
class="inline-flex items-center px-1.5 py-0.5 rounded text-xxs font-bold bg-rose-100 text-rose-800 border border-rose-200 dark:bg-rose-950/40 dark:text-rose-300 dark:border-rose-900"
|
|
title="Date souhaitée de livraison dépassée"
|
|
>
|
|
RETARD
|
|
</span>
|
|
</div>
|
|
</td>
|
|
|
|
<!-- Montant TTC -->
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-900 dark:text-slate-100 text-right font-bold">
|
|
{{ new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(order.amount_ttc) }}
|
|
</td>
|
|
|
|
<!-- Statut -->
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
|
<StatusBadge :status="order.status" />
|
|
</td>
|
|
|
|
<!-- Actions -->
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-semibold text-right space-x-3">
|
|
<Link
|
|
:href="route('commandes.show', { commande: order.id })"
|
|
class="text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-200"
|
|
>
|
|
Voir
|
|
</Link>
|
|
<Link
|
|
v-if="order.can.update"
|
|
:href="route('commandes.edit', { commande: order.id })"
|
|
class="text-sky-600 hover:text-sky-900 dark:text-sky-400 dark:hover:text-sky-300"
|
|
>
|
|
Éditer
|
|
</Link>
|
|
</td>
|
|
</tr>
|
|
|
|
<tr v-if="!orders?.data || orders.data.length === 0">
|
|
<td colspan="9" class="px-6 py-8 text-center text-sm text-slate-500 dark:text-slate-400">
|
|
Aucune commande ne correspond aux critères de recherche.
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
<div
|
|
v-if="orders.meta && orders.meta.links && orders.meta.links.length > 3"
|
|
class="bg-slate-50 dark:bg-slate-950 border-t border-slate-200 dark:border-slate-800 px-6 py-4 flex items-center justify-between"
|
|
>
|
|
<div class="flex-1 flex justify-between sm:hidden">
|
|
<Link
|
|
:href="orders.links.prev || '#'"
|
|
:disabled="!orders.links.prev"
|
|
class="relative inline-flex items-center px-4 py-2 border border-slate-300 text-sm font-semibold rounded-md text-slate-700 bg-white hover:bg-slate-50 disabled:opacity-50"
|
|
>
|
|
Précédent
|
|
</Link>
|
|
<Link
|
|
:href="orders.links.next || '#'"
|
|
:disabled="!orders.links.next"
|
|
class="relative inline-flex items-center px-4 py-2 border border-slate-300 text-sm font-semibold rounded-md text-slate-700 bg-white hover:bg-slate-50 disabled:opacity-50"
|
|
>
|
|
Suivant
|
|
</Link>
|
|
</div>
|
|
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
|
<div>
|
|
<p class="text-sm text-slate-700 dark:text-slate-400">
|
|
Affichage de la page
|
|
<span class="font-bold">{{ orders.meta.current_page }}</span>
|
|
sur
|
|
<span class="font-bold">{{ orders.meta.last_page }}</span>
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
|
|
<template v-for="(link, key) in orders.meta.links" :key="key">
|
|
<div
|
|
v-if="link.url === null"
|
|
class="relative inline-flex items-center px-3 py-2 border border-slate-300 dark:border-slate-800 text-xs font-semibold text-slate-400 bg-white dark:bg-slate-900 rounded-md cursor-default select-none"
|
|
v-html="link.label"
|
|
/>
|
|
<Link
|
|
v-else
|
|
:href="link.url"
|
|
class="relative inline-flex items-center px-3 py-2 border text-xs font-semibold transition-colors"
|
|
:class="[
|
|
link.active
|
|
? 'z-10 bg-sky-50 border-sky-500 text-sky-600 dark:bg-sky-950/40 dark:border-sky-500 dark:text-sky-300'
|
|
: 'bg-white border-slate-300 text-slate-500 hover:bg-slate-50 dark:bg-slate-900 dark:border-slate-800 dark:text-slate-400 dark:hover:bg-slate-800'
|
|
]"
|
|
v-html="link.label"
|
|
/>
|
|
</template>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</AuthenticatedLayout>
|
|
</template>
|