feat: module budgets complet avec sécurité, performance et métier
## Fonctionnalités - Module Budgets : enveloppes, lignes budgétaires, arbitrage DSI/Direction - Suivi de l'exécution budgétaire avec alertes visuelles (dépassement, seuil 80%) - Blocage des commandes si budget insuffisant (store + update) - Audit trail complet des arbitrages via HistoriqueBudget - Page d'index budgets refaite en tableau avec filtres et tri côté client - Page Services avec sélecteur d'icônes FontAwesome (solid + regular + brands) ## Sécurité - BudgetPolicy centralisée (viewAny, view, create, update, addLigne, updateLigne, deleteLigne, arbitrerLigne) - Autorisation sur tous les endpoints LigneBudget et Budget - Protection XSS : remplacement v-html par classes dynamiques - Validation des paramètres d'export (type, envelope) - Validation montant_arbitre ≤ montant_propose côté serveur ## Performance - Eager loading lignes.commandes.commune dans execution() et exportPdf() - Calculs montant_consomme/engage en mémoire sur collections déjà chargées - Null-safety sur montant_arbitre dans getMontantDisponibleAttribute ## Technique - Migration historique_budgets, budgets, ligne_budgets, rôle raf - SearchableSelect avec affichage du disponible budgétaire - FontAwesome enregistré globalement (fas, far, fab) - 33 tests Feature (sécurité, performance, métier) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
416
resources/js/Pages/Budgets/Show.vue
Normal file
416
resources/js/Pages/Budgets/Show.vue
Normal file
@@ -0,0 +1,416 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'
|
||||
import { Head, Link, useForm, router } from '@inertiajs/vue3'
|
||||
import Modal from '@/Components/Modal.vue'
|
||||
import TextInput from '@/Components/TextInput.vue'
|
||||
import InputLabel from '@/Components/InputLabel.vue'
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue'
|
||||
import SecondaryButton from '@/Components/SecondaryButton.vue'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
budget: Object,
|
||||
lignes: Array,
|
||||
totals: Object,
|
||||
communes: Array
|
||||
})
|
||||
|
||||
function formatCurrency(value) {
|
||||
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(value)
|
||||
}
|
||||
|
||||
const investissements = computed(() => props.lignes.filter(l => l.type_depense === 'investissement'))
|
||||
const fonctionnements = computed(() => props.lignes.filter(l => l.type_depense === 'fonctionnement'))
|
||||
|
||||
const showAddLigneModal = ref(false)
|
||||
const ligneForm = useForm({
|
||||
id: null,
|
||||
budget_id: props.budget.id,
|
||||
commune_id: '',
|
||||
type_depense: 'investissement',
|
||||
nom: '',
|
||||
description: '',
|
||||
montant_propose: 0
|
||||
})
|
||||
|
||||
const isLocked = computed(() => ['arbitrage_direction', 'valide', 'cloture'].includes(props.budget.statut))
|
||||
|
||||
function submitLigne() {
|
||||
if (ligneForm.id) {
|
||||
ligneForm.put(route('lignes-budget.update', ligneForm.id), {
|
||||
onSuccess: () => {
|
||||
showAddLigneModal.value = false
|
||||
ligneForm.reset()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
ligneForm.post(route('lignes-budget.store'), {
|
||||
onSuccess: () => {
|
||||
showAddLigneModal.value = false
|
||||
ligneForm.reset('nom', 'description', 'montant_propose')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function openEditLigne(ligne) {
|
||||
ligneForm.id = ligne.id
|
||||
ligneForm.budget_id = props.budget.id
|
||||
ligneForm.commune_id = ligne.commune_id || ''
|
||||
ligneForm.type_depense = ligne.type_depense
|
||||
ligneForm.nom = ligne.nom
|
||||
ligneForm.description = ligne.description
|
||||
ligneForm.montant_propose = ligne.montant_propose
|
||||
showAddLigneModal.value = true
|
||||
}
|
||||
|
||||
function openAddLigne() {
|
||||
ligneForm.reset()
|
||||
ligneForm.id = null
|
||||
ligneForm.budget_id = props.budget.id
|
||||
showAddLigneModal.value = true
|
||||
}
|
||||
|
||||
// Historique
|
||||
const showHistoriqueModal = ref(false)
|
||||
const selectedLigneHistorique = ref(null)
|
||||
|
||||
function openHistorique(ligne) {
|
||||
selectedLigneHistorique.value = ligne
|
||||
showHistoriqueModal.value = true
|
||||
}
|
||||
|
||||
const statusLabelMap = {
|
||||
'brouillon': 'Brouillon', 'propose': 'Proposé', 'accepte_dsi': 'Accepté DSI',
|
||||
'accepte_direction': 'Accepté Direction', 'valide_definitif': 'Validé Définitif',
|
||||
'refuse': 'Refusé', 'reporte': 'Reporté',
|
||||
}
|
||||
function labelStatut(s) { return statusLabelMap[s] ?? s }
|
||||
|
||||
// Arbitrage
|
||||
const showArbitrageModal = ref(false)
|
||||
const selectedLigne = ref(null)
|
||||
const arbitrageForm = useForm({
|
||||
statut_arbitrage: '',
|
||||
montant_arbitre: '',
|
||||
commentaire: '',
|
||||
})
|
||||
|
||||
function openArbitrage(ligne) {
|
||||
selectedLigne.value = ligne
|
||||
arbitrageForm.statut_arbitrage = ligne.statut_arbitrage
|
||||
arbitrageForm.montant_arbitre = ligne.montant_arbitre || ligne.montant_propose
|
||||
showArbitrageModal.value = true
|
||||
}
|
||||
|
||||
function submitArbitrage() {
|
||||
arbitrageForm.patch(route('lignes-budget.arbitrer', selectedLigne.value.id), {
|
||||
onSuccess: () => showArbitrageModal.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const statusOptions = [
|
||||
{ value: 'propose', label: 'Proposé' },
|
||||
{ value: 'accepte_dsi', label: 'Accepté (Pré-arbitrage DSI)' },
|
||||
{ value: 'accepte_direction', label: 'Accepté (Pré-arbitrage Direction)' },
|
||||
{ value: 'valide_definitif', label: 'Validé Définitif' },
|
||||
{ value: 'refuse', label: 'Refusé' },
|
||||
{ value: 'reporte', label: 'Reporté à A+1' }
|
||||
]
|
||||
|
||||
const statusBadgeConfig = {
|
||||
'brouillon': { class: 'bg-gray-100 text-gray-800', label: 'Brouillon' },
|
||||
'propose': { class: 'bg-blue-100 text-blue-800', label: 'Proposé' },
|
||||
'accepte_dsi': { class: 'bg-indigo-100 text-indigo-800 border border-indigo-200', label: 'Accepté (Pré-arbitrage DSI)' },
|
||||
'accepte_direction': { class: 'bg-purple-100 text-purple-800 border border-purple-200', label: 'Accepté (Pré-arbitrage Direction)' },
|
||||
'valide_definitif': { class: 'bg-green-100 text-green-800', label: 'Validé Définitif' },
|
||||
'refuse': { class: 'bg-red-100 text-red-800', label: 'Refusé' },
|
||||
'reporte': { class: 'bg-orange-100 text-orange-800', label: 'Reporté à A+1' },
|
||||
}
|
||||
|
||||
function getStatusClass(status) {
|
||||
return statusBadgeConfig[status]?.class ?? statusBadgeConfig['brouillon'].class
|
||||
}
|
||||
|
||||
function getStatusLabel(status) {
|
||||
return statusBadgeConfig[status]?.label ?? status
|
||||
}
|
||||
|
||||
function updateBudgetStatus(newStatus) {
|
||||
router.put(route('budgets.update', props.budget.id), { statut: newStatus })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head :title="`Budget ${budget.annee} - ${budget.service.nom}`" />
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<div class="flex items-center gap-3">
|
||||
<Link :href="route('budgets.index')" class="text-gray-400 hover:text-gray-600">
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" /></svg>
|
||||
</Link>
|
||||
<div>
|
||||
<h1 class="text-xl font-bold text-gray-900">
|
||||
{{ budget.service.nom }} • Budget {{ budget.annee }}
|
||||
</h1>
|
||||
<p class="text-sm text-gray-500 flex items-center gap-2 mt-1">
|
||||
<span class="uppercase tracking-wider font-semibold text-xs text-indigo-600">{{ budget.type_budget === 'agglo' ? 'Agglo' : 'Mutualisé' }}</span>
|
||||
• Statut :
|
||||
<select v-if="$page.props.auth.user.roles.some(r => ['admin', 'directeur'].includes(r.name))"
|
||||
:value="budget.statut" @change="updateBudgetStatus($event.target.value)"
|
||||
class="text-xs rounded border-gray-300 py-0 pl-2 pr-6 h-6">
|
||||
<option value="preparation">En préparation</option>
|
||||
<option value="arbitrage_dsi">Arbitrage DSI</option>
|
||||
<option value="arbitrage_direction">Arbitrage Direction</option>
|
||||
<option value="valide">Validé</option>
|
||||
<option value="cloture">Clôturé</option>
|
||||
</select>
|
||||
<span v-else class="font-medium text-gray-700 capitalize">{{ budget.statut.replace('_', ' ') }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-auto flex gap-3">
|
||||
<button v-if="!isLocked" @click="openAddLigne" class="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-500">
|
||||
+ Ajouter une demande
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-8 pb-12">
|
||||
<!-- Tableaux -->
|
||||
<div class="grid xl:grid-cols-2 gap-8">
|
||||
<!-- Investissement -->
|
||||
<div>
|
||||
<div class="flex justify-between items-end mb-4">
|
||||
<h2 class="text-lg font-bold text-gray-900 border-b-2 border-indigo-500 pb-1 inline-block">Investissement</h2>
|
||||
<div class="text-right">
|
||||
<p class="text-sm text-gray-500 font-medium">Proposé : {{ formatCurrency(totals.invest_propose) }}</p>
|
||||
<p class="text-base text-indigo-700 font-bold">Arbitré : {{ formatCurrency(totals.invest_arbitre) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
||||
<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-semibold text-gray-500 uppercase">Projet / Achat</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-semibold text-gray-500 uppercase">Proposé</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-semibold text-indigo-600 uppercase">Arbitré</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-500 uppercase">Statut</th>
|
||||
<th class="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
<tr v-for="ligne in investissements" :key="ligne.id" class="hover:bg-gray-50 group">
|
||||
<td class="px-4 py-4 text-sm text-gray-900">
|
||||
<p class="font-medium">{{ ligne.nom }}</p>
|
||||
<div class="flex items-center gap-2 mt-0.5">
|
||||
<span v-if="ligne.commune_id" class="text-[10px] bg-blue-50 text-blue-600 px-1.5 py-0.5 rounded border border-blue-100 font-bold uppercase">
|
||||
{{ communes.find(c => c.id === ligne.commune_id)?.nom }}
|
||||
</span>
|
||||
<p class="text-xs text-gray-500 truncate max-w-[150px]" :title="ligne.description">{{ ligne.description }}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-4 text-sm text-right text-gray-500 whitespace-nowrap">{{ formatCurrency(ligne.montant_propose) }}</td>
|
||||
<td class="px-4 py-4 text-sm text-right font-bold text-gray-900 whitespace-nowrap">
|
||||
{{ ligne.montant_arbitre ? formatCurrency(ligne.montant_arbitre) : '-' }}
|
||||
</td>
|
||||
<td class="px-4 py-4 text-sm">
|
||||
<span class="px-2 py-1 text-xs rounded-md font-medium" :class="getStatusClass(ligne.statut_arbitrage)">
|
||||
{{ getStatusLabel(ligne.statut_arbitrage) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-4 text-right whitespace-nowrap">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button v-if="ligne.historique?.length" @click="openHistorique(ligne)" class="text-gray-400 hover:text-gray-700 text-xs font-medium" :title="`${ligne.historique.length} arbitrage(s)`">
|
||||
📋 {{ ligne.historique.length }}
|
||||
</button>
|
||||
<button v-if="!isLocked" @click="openEditLigne(ligne)" class="text-gray-600 hover:text-gray-900 text-xs font-medium">Éditer</button>
|
||||
<button v-if="$page.props.auth.user.roles.some(r => ['admin', 'directeur', 'raf'].includes(r.name))" @click="openArbitrage(ligne)" class="text-indigo-600 hover:text-indigo-900 text-xs font-medium">Arbitrer</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="investissements.length === 0">
|
||||
<td colspan="5" class="px-4 py-8 text-center text-sm text-gray-500">Aucun projet d'investissement.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fonctionnement -->
|
||||
<div>
|
||||
<div class="flex justify-between items-end mb-4">
|
||||
<h2 class="text-lg font-bold text-gray-900 border-b-2 border-emerald-500 pb-1 inline-block">Fonctionnement</h2>
|
||||
<div class="text-right">
|
||||
<p class="text-sm text-gray-500 font-medium">Proposé : {{ formatCurrency(totals.fonct_propose) }}</p>
|
||||
<p class="text-base text-emerald-700 font-bold">Arbitré : {{ formatCurrency(totals.fonct_arbitre) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
||||
<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-semibold text-gray-500 uppercase">Achat / Suivi</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-semibold text-gray-500 uppercase">Proposé</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-semibold text-emerald-600 uppercase">Arbitré</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-500 uppercase">Statut</th>
|
||||
<th class="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
<tr v-for="ligne in fonctionnements" :key="ligne.id" class="hover:bg-gray-50 group">
|
||||
<td class="px-4 py-4 text-sm text-gray-900">
|
||||
<p class="font-medium">{{ ligne.nom }}</p>
|
||||
<div class="flex items-center gap-3 mt-0.5">
|
||||
<span v-if="ligne.commune_id" class="text-[10px] bg-blue-50 text-blue-600 px-1.5 py-0.5 rounded border border-blue-100 font-bold uppercase whitespace-nowrap">
|
||||
{{ communes.find(c => c.id === ligne.commune_id)?.nom }}
|
||||
</span>
|
||||
<p class="text-xs text-gray-500 truncate max-w-[150px]" :title="ligne.description">{{ ligne.description }}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-4 text-sm text-right text-gray-500 whitespace-nowrap">{{ formatCurrency(ligne.montant_propose) }}</td>
|
||||
<td class="px-4 py-4 text-sm text-right font-bold text-gray-900 whitespace-nowrap">
|
||||
{{ ligne.montant_arbitre ? formatCurrency(ligne.montant_arbitre) : '-' }}
|
||||
</td>
|
||||
<td class="px-4 py-4 text-sm">
|
||||
<span class="px-2 py-1 text-xs rounded-md font-medium" :class="getStatusClass(ligne.statut_arbitrage)">
|
||||
{{ getStatusLabel(ligne.statut_arbitrage) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-4 text-right whitespace-nowrap">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button v-if="ligne.historique?.length" @click="openHistorique(ligne)" class="text-gray-400 hover:text-gray-700 text-xs font-medium" :title="`${ligne.historique.length} arbitrage(s)`">
|
||||
📋 {{ ligne.historique.length }}
|
||||
</button>
|
||||
<button v-if="!isLocked" @click="openEditLigne(ligne)" class="text-gray-600 hover:text-gray-900 text-xs font-medium">Éditer</button>
|
||||
<button v-if="$page.props.auth.user.roles.some(r => ['admin', 'directeur', 'raf'].includes(r.name))" @click="openArbitrage(ligne)" class="text-indigo-600 hover:text-indigo-900 text-xs font-medium">Arbitrer</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="fonctionnements.length === 0">
|
||||
<td colspan="5" class="px-4 py-8 text-center text-sm text-gray-500">Aucun achat de fonctionnement.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Ajout Ligne -->
|
||||
<Modal :show="showAddLigneModal" @close="showAddLigneModal = false">
|
||||
<div class="p-6">
|
||||
<h2 class="text-lg font-bold text-gray-900 mb-6">{{ ligneForm.id ? 'Modifier la demande' : 'Ajouter une demande budgétaire' }}</h2>
|
||||
<form @submit.prevent="submitLigne" class="space-y-4">
|
||||
<div>
|
||||
<InputLabel value="Type de dépense" />
|
||||
<select v-model="ligneForm.type_depense" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" required>
|
||||
<option value="investissement">Investissement (Projets, Nouveaux matériels...)</option>
|
||||
<option value="fonctionnement">Fonctionnement (Licences, Maintenance...)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="budget.type_budget === 'mutualise'">
|
||||
<InputLabel value="Commune bénéficiaire" />
|
||||
<select v-model="ligneForm.commune_id" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" required>
|
||||
<option value="">Sélectionner une ville</option>
|
||||
<option v-for="c in communes" :key="c.id" :value="c.id">{{ c.nom }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel value="Nom de l'achat ou du projet" />
|
||||
<TextInput v-model="ligneForm.nom" class="mt-1 block w-full" required />
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel value="Description (optionnel)" />
|
||||
<textarea v-model="ligneForm.description" rows="3" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel value="Montant estimé / proposé (€)" />
|
||||
<input v-model.number="ligneForm.montant_propose" type="number" step="0.01" min="0"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
|
||||
required />
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end gap-3">
|
||||
<SecondaryButton @click="showAddLigneModal = false">Annuler</SecondaryButton>
|
||||
<PrimaryButton :class="{ 'opacity-25': ligneForm.processing }" :disabled="ligneForm.processing">
|
||||
{{ ligneForm.id ? 'Mettre à jour' : 'Ajouter' }}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<!-- Modal Arbitrage -->
|
||||
<Modal :show="showArbitrageModal" @close="showArbitrageModal = false" max-width="md">
|
||||
<div class="p-6" v-if="selectedLigne">
|
||||
<h2 class="text-lg font-bold text-gray-900 mb-2">Arbitrer la ligne</h2>
|
||||
<p class="text-sm font-medium mb-1">{{ selectedLigne.nom }}</p>
|
||||
<div class="bg-gray-50 p-3 rounded-lg mb-6 border border-gray-200">
|
||||
<p class="text-xs text-gray-500">Montant initialement proposé :</p>
|
||||
<p class="text-lg font-bold text-gray-900">{{ formatCurrency(selectedLigne.montant_propose) }}</p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submitArbitrage" class="space-y-4">
|
||||
<div>
|
||||
<InputLabel value="Décision / Statut" />
|
||||
<select v-model="arbitrageForm.statut_arbitrage" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" required>
|
||||
<option v-for="opt in statusOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel value="Montant accordé (arbitré) en €" />
|
||||
<input v-model.number="arbitrageForm.montant_arbitre" type="number" step="0.01" min="0"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-lg font-bold text-indigo-700" />
|
||||
<p class="text-xs text-gray-500 mt-1">Le montant ne peut pas dépasser le montant proposé.</p>
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel value="Commentaire (optionnel)" />
|
||||
<textarea v-model="arbitrageForm.commentaire" rows="2" placeholder="Motif de la décision..." class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end gap-3">
|
||||
<SecondaryButton @click="showArbitrageModal = false">Annuler</SecondaryButton>
|
||||
<PrimaryButton :class="{ 'opacity-25': arbitrageForm.processing }" :disabled="arbitrageForm.processing">Enregistrer l'arbitrage</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
<!-- Modal Historique Arbitrages -->
|
||||
<Modal :show="showHistoriqueModal" @close="showHistoriqueModal = false" max-width="lg">
|
||||
<div class="p-6" v-if="selectedLigneHistorique">
|
||||
<h2 class="text-lg font-bold text-gray-900 mb-1">Historique des arbitrages</h2>
|
||||
<p class="text-sm text-gray-500 mb-5">{{ selectedLigneHistorique.nom }}</p>
|
||||
|
||||
<div v-if="selectedLigneHistorique.historique?.length" class="space-y-3">
|
||||
<div v-for="h in selectedLigneHistorique.historique" :key="h.id"
|
||||
class="bg-gray-50 border border-gray-200 rounded-lg p-4 text-sm">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium text-gray-900">{{ h.user }}</span>
|
||||
<span class="text-gray-400">·</span>
|
||||
<span class="text-gray-500 text-xs">{{ h.created_at }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 text-xs">
|
||||
<span class="text-gray-500">{{ labelStatut(h.ancien_statut) }}</span>
|
||||
<span class="text-gray-400">→</span>
|
||||
<span class="font-semibold text-indigo-700">{{ labelStatut(h.nouveau_statut) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="h.ancien_montant_arbitre !== h.nouveau_montant_arbitre" class="text-xs text-gray-600 mb-1">
|
||||
Montant : {{ formatCurrency(h.ancien_montant_arbitre ?? 0) }} → <span class="font-semibold">{{ formatCurrency(h.nouveau_montant_arbitre ?? 0) }}</span>
|
||||
</div>
|
||||
<p v-if="h.commentaire" class="text-xs text-gray-700 italic mt-1 border-l-2 border-indigo-300 pl-2">{{ h.commentaire }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="text-sm text-gray-500 text-center py-4">Aucun arbitrage enregistré.</p>
|
||||
|
||||
<div class="mt-5 flex justify-end">
|
||||
<SecondaryButton @click="showHistoriqueModal = false">Fermer</SecondaryButton>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
Reference in New Issue
Block a user