Files
dsi-commander/resources/js/Pages/Contrats/Index.vue

191 lines
11 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'
import Pagination from '@/Components/Pagination.vue'
import ConfirmModal from '@/Components/ConfirmModal.vue'
import { Head, Link, router, useForm, usePage } from '@inertiajs/vue3'
import { ref, reactive } from 'vue'
const props = defineProps({
contrats: Object,
services: Array,
fournisseurs: Array,
filters: Object,
statuts: Object,
})
const page = usePage()
const filters = reactive({ ...props.filters })
const deleteTarget = ref(null)
function applyFilters() {
router.get(route('contrats.index'), filters, { preserveState: true, replace: true })
}
function resetFilters() {
Object.keys(filters).forEach(k => filters[k] = '')
applyFilters()
}
function confirmDelete(cnt) {
deleteTarget.value = cnt
}
const deleteForm = useForm({})
function doDelete() {
deleteForm.delete(route('contrats.destroy', deleteTarget.value.id), {
onSuccess: () => deleteTarget.value = null,
})
}
function formatDate(d) {
if (!d) return '—'
return new Intl.DateTimeFormat('fr-FR').format(new Date(d))
}
function formatCurrency(v) {
if (v == null) return '—'
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(v)
}
const statutColors = {
actif: 'bg-green-100 text-green-800',
a_renouveler: 'bg-yellow-100 text-yellow-800',
expire: 'bg-red-100 text-red-800',
resilie: 'bg-gray-100 text-gray-800',
}
</script>
<template>
<Head title="Contrats" />
<AuthenticatedLayout>
<template #header>
<div class="flex items-center justify-between">
<h1 class="text-xl font-semibold text-gray-900">Contrats en cours</h1>
<Link :href="route('contrats.create')"
class="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 transition-colors">
+ Nouveau contrat
</Link>
</div>
</template>
<div class="space-y-4">
<!-- Filtres -->
<div class="rounded-xl bg-white p-4 shadow-sm border border-gray-100">
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5">
<input v-model="filters.search" @input="applyFilters" type="text" placeholder="Recherche (titre, fournisseur)..."
class="rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-indigo-500 focus:outline-none xl:col-span-2" />
<select v-model="filters.service_id" @change="applyFilters"
class="rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-indigo-500 focus:outline-none">
<option value="">Tous les services</option>
<option v-for="s in services" :key="s.id" :value="s.id">{{ s.nom }}</option>
</select>
<select v-model="filters.fournisseur_id" @change="applyFilters"
class="rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-indigo-500 focus:outline-none">
<option value="">Tous les fournisseurs</option>
<option v-for="f in fournisseurs" :key="f.id" :value="f.id">{{ f.nom }}</option>
</select>
<select v-model="filters.statut" @change="applyFilters"
class="rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-indigo-500 focus:outline-none">
<option value="">Tous les statuts</option>
<option v-for="(label, key) in statuts" :key="key" :value="key">{{ label }}</option>
</select>
</div>
<div class="mt-3 flex justify-end">
<button @click="resetFilters"
class="rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-600 hover:bg-gray-50 transition-colors">
Réinitialiser
</button>
</div>
</div>
<!-- Table -->
<div class="rounded-xl bg-white shadow-sm border border-gray-100 overflow-hidden">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-100 text-sm">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Titre</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Service</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Fournisseur</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Statut</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Date début</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Échéance</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">Montant</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-50">
<tr v-for="cnt in contrats.data" :key="cnt.id"
:class="['hover:bg-gray-50 transition-colors', cnt.est_en_retard ? 'bg-red-50/50' : (cnt.est_proche_echeance ? 'bg-orange-50/50' : '')]">
<td class="px-4 py-3 font-medium">
<Link :href="route('contrats.show', cnt.id)" class="text-indigo-600 hover:underline">
{{ cnt.titre }}
</Link>
</td>
<td class="px-4 py-3 whitespace-nowrap text-gray-600">{{ cnt.service?.nom }}</td>
<td class="px-4 py-3 whitespace-nowrap text-gray-600">{{ cnt.fournisseur?.nom ?? '—' }}</td>
<td class="px-4 py-3">
<span :class="['inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium', statutColors[cnt.statut]]">
{{ statuts[cnt.statut] }}
</span>
</td>
<td class="px-4 py-3 whitespace-nowrap text-gray-600">{{ formatDate(cnt.date_debut) }}</td>
<td class="px-4 py-3 whitespace-nowrap" :class="[cnt.est_en_retard ? 'text-red-600 font-bold' : (cnt.est_proche_echeance ? 'text-orange-600 font-bold' : 'text-gray-600')]">
{{ formatDate(cnt.date_echeance) }}
<span v-if="cnt.est_en_retard" class="ml-1 text-xs" title="Échéance dépassée"></span>
<span v-else-if="cnt.est_proche_echeance" class="ml-1 text-xs" title="Proche de l'échéance"></span>
</td>
<td class="px-4 py-3 text-right font-medium whitespace-nowrap text-gray-900">
{{ formatCurrency(cnt.montant) }}
</td>
<td class="px-4 py-3 text-center">
<div class="flex items-center justify-center gap-2">
<Link :href="route('contrats.show', cnt.id)"
class="text-gray-400 hover:text-indigo-600 transition-colors" title="Voir">
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</Link>
<Link :href="route('contrats.edit', cnt.id)"
class="text-gray-400 hover:text-blue-600 transition-colors" title="Modifier">
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</Link>
<button v-if="$page.props.auth.user?.roles?.some(r => r.name === 'admin')" @click="confirmDelete(cnt)"
class="text-gray-400 hover:text-red-600 transition-colors" title="Supprimer">
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div v-if="!contrats.data.length" class="py-12 text-center text-gray-400">
Aucun contrat trouvé.
</div>
<div class="border-t border-gray-100 px-4 py-3">
<Pagination :links="contrats.links" :meta="contrats.meta" />
</div>
</div>
</div>
<ConfirmModal :show="!!deleteTarget"
title="Supprimer le contrat"
:message="`Supprimer définitivement le contrat '${deleteTarget?.titre}' ?`"
:processing="deleteForm.processing"
@confirm="doDelete"
@cancel="deleteTarget = null" />
</AuthenticatedLayout>
</template>