## Dashboard - Refonte complète du tableau de bord avec widgets budgets, commandes, contrats - Intégration des données d'exécution budgétaire en temps réel ## Exports & Rapports - BudgetExecutionExport : export Excel de l'exécution budgétaire - Template PDF budgets (budgets_pdf.blade.php) - Routes d'export PDF et Excel ## Alertes & Notifications - Commande CheckExpirations : détection des contrats/assets arrivant à échéance - Mail ExpiringElementsMail avec template Blade - Planification via routes/console.php ## Correctifs - CommandePolicy et ContratPolicy : ajustements des règles d'autorisation - ContratController : corrections mineures - Commande model : ajustements relations/casts - AuthenticatedLayout : refonte navigation avec icônes budgets - Assets/Form.vue : corrections formulaire - Seeder rôles/permissions mis à jour - Dépendances composer mises à jour (barryvdh/laravel-dompdf, maatwebsite/excel) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
282 lines
19 KiB
Vue
282 lines
19 KiB
Vue
<script setup>
|
|
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'
|
|
import StatutBadge from '@/Components/Commandes/StatutBadge.vue'
|
|
import PrioriteBadge from '@/Components/Commandes/PrioriteBadge.vue'
|
|
import { Link, Head } from '@inertiajs/vue3'
|
|
|
|
const props = defineProps({
|
|
stats: Object,
|
|
commandesRecentes: Array,
|
|
commandesEnRetard: Array,
|
|
commandesUrgentes: Array,
|
|
statsContrats: Object,
|
|
statsDomaines: Object,
|
|
budgetsStats: Object,
|
|
})
|
|
|
|
const formatDate = (d) => {
|
|
if (!d) return '—'
|
|
return new Intl.DateTimeFormat('fr-FR').format(new Date(d))
|
|
}
|
|
|
|
const formatCurrency = (v) => {
|
|
if (v == null) return '0 €'
|
|
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }).format(v)
|
|
}
|
|
|
|
const calculatePercent = (consumed, total) => {
|
|
if (!total || total <= 0) return 0
|
|
return Math.min(Math.round((consumed / total) * 100), 100)
|
|
}
|
|
|
|
const getProgressBarHexColor = (percent) => {
|
|
if (percent < 50) return '#3b82f6' // Blue 500
|
|
if (percent < 80) return '#eab308' // Yellow 500
|
|
if (percent < 100) return '#f97316' // Orange 500
|
|
return '#ef4444' // Red 500
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<Head title="Tableau de bord" />
|
|
|
|
<AuthenticatedLayout>
|
|
<template #header>
|
|
<div class="flex items-center justify-between">
|
|
<h1 class="text-2xl font-bold text-gray-900 tracking-tight">Tableau de bord <span class="text-gray-400 font-normal">/ {{ new Date().getFullYear() }}</span></h1>
|
|
<div class="flex items-center gap-2">
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
|
Système Opérationnel
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<div class="space-y-8 pb-10">
|
|
<!-- Top Stats Row -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<div class="bg-white p-6 rounded-2xl shadow-sm border border-gray-100">
|
|
<p class="text-sm font-medium text-gray-500">Commandes en cours</p>
|
|
<div class="mt-2 flex items-baseline gap-2">
|
|
<span class="text-3xl font-bold text-gray-900">{{ stats.en_cours }}</span>
|
|
<span class="text-sm text-red-600 font-medium" v-if="stats.en_retard > 0">{{ stats.en_retard }} en retard</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white p-6 rounded-2xl shadow-sm border border-gray-100">
|
|
<p class="text-sm font-medium text-gray-500">Budget Consommé (Agglo)</p>
|
|
<div class="mt-2 flex items-baseline gap-2">
|
|
<span class="text-3xl font-bold text-blue-600">{{ formatCurrency(budgetsStats.agglo.consomme) }}</span>
|
|
<span class="text-xs text-gray-400">/ {{ formatCurrency(budgetsStats.agglo.total_arbitre) }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white p-6 rounded-2xl shadow-sm border border-gray-100">
|
|
<p class="text-sm font-medium text-gray-500">Budget Consommé (Mutualisé)</p>
|
|
<div class="mt-2 flex items-baseline gap-2">
|
|
<span class="text-3xl font-bold text-indigo-600">{{ formatCurrency(budgetsStats.mutualise.consomme) }}</span>
|
|
<span class="text-xs text-gray-400">/ {{ formatCurrency(budgetsStats.mutualise.total_arbitre) }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white p-6 rounded-2xl shadow-sm border border-gray-100">
|
|
<p class="text-sm font-medium text-gray-500">Contrats à renouveler</p>
|
|
<div class="mt-2 flex items-baseline gap-2">
|
|
<span class="text-3xl font-bold text-orange-600">{{ statsContrats.proches + statsContrats.en_retard }}</span>
|
|
<span class="text-sm text-gray-400">échéances</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Budget Gauges -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<!-- Agglo Card -->
|
|
<div class="bg-white p-6 rounded-3xl shadow-sm border border-gray-100">
|
|
<h3 class="font-bold text-gray-900 mb-6 flex items-center gap-2">
|
|
<span class="w-2 h-2 rounded-full bg-blue-500"></span>
|
|
Exécution Agglomération
|
|
</h3>
|
|
|
|
<div class="grid grid-cols-2 gap-8">
|
|
<!-- Fonctionnement -->
|
|
<div class="flex flex-col items-center">
|
|
<div class="relative w-full h-24 flex items-center justify-center">
|
|
<svg class="w-full h-full" viewBox="0 0 100 60">
|
|
<path d="M 12 55 A 38 38 0 0 1 88 55" fill="#edf2f7" stroke="#2d3748" stroke-width="1.5" />
|
|
<path d="M 12 55 A 38 38 0 0 1 31 22 L 50 55 Z" fill="#48bb78" />
|
|
<path d="M 31 22 A 38 38 0 0 1 69 22 L 50 55 Z" fill="#ecc94b" />
|
|
<path d="M 69 22 A 38 38 0 0 1 88 55 L 50 55 Z" fill="#f6ad55" />
|
|
<g :style="{ transform: `rotate(${(calculatePercent(budgetsStats.agglo.fonctionnement.consomme, budgetsStats.agglo.fonctionnement.total_arbitre) * 1.8) - 90}deg)`, transformOrigin: '50px 55px' }" class="transition-transform duration-1000 ease-out">
|
|
<path d="M 48.5 55 L 50 20 L 51.5 55 Z" fill="#e53e3e" stroke="#2d3748" stroke-width="0.3" />
|
|
<circle cx="50" cy="55" r="2" fill="#2d3748" />
|
|
</g>
|
|
</svg>
|
|
<div class="absolute -bottom-1 left-0 flex items-baseline gap-1">
|
|
<span class="text-xl font-black text-gray-900">{{ calculatePercent(budgetsStats.agglo.fonctionnement.consomme, budgetsStats.agglo.fonctionnement.total_arbitre) }}%</span>
|
|
</div>
|
|
</div>
|
|
<div class="mt-4 w-full">
|
|
<p class="text-[10px] uppercase font-black text-gray-400 tracking-widest leading-none mb-1">Fonctionnement</p>
|
|
<p class="text-sm font-bold text-gray-900">{{ formatCurrency(budgetsStats.agglo.fonctionnement.consomme) }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Investissement -->
|
|
<div class="flex flex-col items-center">
|
|
<div class="relative w-full h-24 flex items-center justify-center">
|
|
<svg class="w-full h-full" viewBox="0 0 100 60">
|
|
<path d="M 12 55 A 38 38 0 0 1 88 55" fill="#edf2f7" stroke="#2d3748" stroke-width="1.5" />
|
|
<path d="M 12 55 A 38 38 0 0 1 31 22 L 50 55 Z" fill="#48bb78" />
|
|
<path d="M 31 22 A 38 38 0 0 1 69 22 L 50 55 Z" fill="#ecc94b" />
|
|
<path d="M 69 22 A 38 38 0 0 1 88 55 L 50 55 Z" fill="#f6ad55" />
|
|
<g :style="{ transform: `rotate(${(calculatePercent(budgetsStats.agglo.investissement.consomme, budgetsStats.agglo.investissement.total_arbitre) * 1.8) - 90}deg)`, transformOrigin: '50px 55px' }" class="transition-transform duration-1000 ease-out">
|
|
<path d="M 48.5 55 L 50 20 L 51.5 55 Z" fill="#e53e3e" stroke="#2d3748" stroke-width="0.3" />
|
|
<circle cx="50" cy="55" r="2" fill="#2d3748" />
|
|
</g>
|
|
</svg>
|
|
<div class="absolute -bottom-1 left-0 flex items-baseline gap-1">
|
|
<span class="text-xl font-black text-gray-900">{{ calculatePercent(budgetsStats.agglo.investissement.consomme, budgetsStats.agglo.investissement.total_arbitre) }}%</span>
|
|
</div>
|
|
</div>
|
|
<div class="mt-4 w-full">
|
|
<p class="text-[10px] uppercase font-black text-gray-400 tracking-widest leading-none mb-1">Investissement</p>
|
|
<p class="text-sm font-bold text-gray-900">{{ formatCurrency(budgetsStats.agglo.investissement.consomme) }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Mutualise Card -->
|
|
<div class="bg-white p-6 rounded-3xl shadow-sm border border-gray-100">
|
|
<h3 class="font-bold text-gray-900 mb-6 flex items-center gap-2">
|
|
<span class="w-2 h-2 rounded-full bg-indigo-500"></span>
|
|
Exécution Mutualisation
|
|
</h3>
|
|
|
|
<div class="grid grid-cols-2 gap-8">
|
|
<!-- Fonctionnement -->
|
|
<div class="flex flex-col items-center">
|
|
<div class="relative w-full h-24 flex items-center justify-center">
|
|
<svg class="w-full h-full" viewBox="0 0 100 60">
|
|
<path d="M 12 55 A 38 38 0 0 1 88 55" fill="#edf2f7" stroke="#2d3748" stroke-width="1.5" />
|
|
<path d="M 12 55 A 38 38 0 0 1 31 22 L 50 55 Z" fill="#48bb78" />
|
|
<path d="M 31 22 A 38 38 0 0 1 69 22 L 50 55 Z" fill="#ecc94b" />
|
|
<path d="M 69 22 A 38 38 0 0 1 88 55 L 50 55 Z" fill="#f6ad55" />
|
|
<g :style="{ transform: `rotate(${(calculatePercent(budgetsStats.mutualise.fonctionnement.consomme, budgetsStats.mutualise.fonctionnement.total_arbitre) * 1.8) - 90}deg)`, transformOrigin: '50px 55px' }" class="transition-transform duration-1000 ease-out">
|
|
<path d="M 48.5 55 L 50 20 L 51.5 55 Z" fill="#e53e3e" stroke="#2d3748" stroke-width="0.3" />
|
|
<circle cx="50" cy="55" r="2" fill="#2d3748" />
|
|
</g>
|
|
</svg>
|
|
<div class="absolute -bottom-1 left-0 flex items-baseline gap-1">
|
|
<span class="text-xl font-black text-gray-900">{{ calculatePercent(budgetsStats.mutualise.fonctionnement.consomme, budgetsStats.mutualise.fonctionnement.total_arbitre) }}%</span>
|
|
</div>
|
|
</div>
|
|
<div class="mt-4 w-full">
|
|
<p class="text-[10px] uppercase font-black text-gray-400 tracking-widest leading-none mb-1">Fonctionnement</p>
|
|
<p class="text-sm font-bold text-gray-900">{{ formatCurrency(budgetsStats.mutualise.fonctionnement.consomme) }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Investissement -->
|
|
<div class="flex flex-col items-center">
|
|
<div class="relative w-full h-24 flex items-center justify-center">
|
|
<svg class="w-full h-full" viewBox="0 0 100 60">
|
|
<path d="M 12 55 A 38 38 0 0 1 88 55" fill="#edf2f7" stroke="#2d3748" stroke-width="1.5" />
|
|
<path d="M 12 55 A 38 38 0 0 1 31 22 L 50 55 Z" fill="#48bb78" />
|
|
<path d="M 31 22 A 38 38 0 0 1 69 22 L 50 55 Z" fill="#ecc94b" />
|
|
<path d="M 69 22 A 38 38 0 0 1 88 55 L 50 55 Z" fill="#f6ad55" />
|
|
<g :style="{ transform: `rotate(${(calculatePercent(budgetsStats.mutualise.investissement.consomme, budgetsStats.mutualise.investissement.total_arbitre) * 1.8) - 90}deg)`, transformOrigin: '50px 55px' }" class="transition-transform duration-1000 ease-out">
|
|
<path d="M 48.5 55 L 50 20 L 51.5 55 Z" fill="#e53e3e" stroke="#2d3748" stroke-width="0.3" />
|
|
<circle cx="50" cy="55" r="2" fill="#2d3748" />
|
|
</g>
|
|
</svg>
|
|
<div class="absolute -bottom-1 left-0 flex items-baseline gap-1">
|
|
<span class="text-xl font-black text-gray-900">{{ calculatePercent(budgetsStats.mutualise.investissement.consomme, budgetsStats.mutualise.investissement.total_arbitre) }}%</span>
|
|
</div>
|
|
</div>
|
|
<div class="mt-4 w-full">
|
|
<p class="text-[10px] uppercase font-black text-gray-400 tracking-widest leading-none mb-1">Investissement</p>
|
|
<p class="text-sm font-bold text-gray-900">{{ formatCurrency(budgetsStats.mutualise.investissement.consomme) }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
<!-- Main Content: Recent Orders -->
|
|
<div class="lg:col-span-2 space-y-6">
|
|
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
|
|
<div class="px-6 py-4 border-b border-gray-50 flex items-center justify-between">
|
|
<h3 class="font-bold text-gray-900">Dernières commandes</h3>
|
|
<Link :href="route('commandes.index')" class="text-sm text-blue-600 font-medium hover:underline">Voir tout</Link>
|
|
</div>
|
|
<div class="divide-y divide-gray-50">
|
|
<div v-for="cmd in commandesRecentes" :key="cmd.id" class="px-6 py-4 hover:bg-gray-50 transition-colors flex items-center justify-between">
|
|
<div class="flex items-center gap-4">
|
|
<div class="p-2 rounded-lg bg-gray-50">
|
|
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" /></svg>
|
|
</div>
|
|
<div>
|
|
<Link :href="route('commandes.show', cmd.id)" class="text-sm font-bold text-gray-900 hover:text-blue-600">{{ cmd.numero_commande }}</Link>
|
|
<p class="text-xs text-gray-500 truncate max-w-[200px]">{{ cmd.objet }}</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-4">
|
|
<StatutBadge :statut="cmd.statut" size="sm" />
|
|
<span class="text-sm font-bold text-gray-900 min-w-[80px] text-right">{{ formatCurrency(cmd.montant_ttc) }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sidebar: Action alerts -->
|
|
<div class="space-y-6">
|
|
<!-- Urgent/Late Orders -->
|
|
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 border-l-4 border-l-red-500 overflow-hidden">
|
|
<div class="px-5 py-4 border-b border-gray-50">
|
|
<h3 class="font-bold text-gray-900 text-sm">Alertes prioritaires</h3>
|
|
</div>
|
|
<div class="p-2">
|
|
<div v-for="cmd in commandesEnRetard" :key="cmd.id" class="p-3 mb-1 rounded-xl bg-red-50 hover:bg-red-100 transition-colors">
|
|
<Link :href="route('commandes.show', cmd.id)" class="block">
|
|
<div class="flex justify-between items-start mb-1">
|
|
<span class="text-xs font-bold text-red-700">{{ cmd.numero_commande }}</span>
|
|
<span class="text-[10px] uppercase font-bold text-red-600">Retard</span>
|
|
</div>
|
|
<p class="text-xs text-red-800 line-clamp-1">{{ cmd.objet }}</p>
|
|
</Link>
|
|
</div>
|
|
<div v-if="!commandesEnRetard.length" class="p-10 text-center">
|
|
<div class="inline-flex p-3 rounded-full bg-green-50 text-green-600 mb-2">
|
|
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /></svg>
|
|
</div>
|
|
<p class="text-sm text-gray-500 font-medium">Tout est à jour !</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Contract Expiry -->
|
|
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
|
|
<div class="px-5 py-4 border-b border-gray-50">
|
|
<h3 class="font-bold text-gray-900 text-sm">Échéancier Contrats</h3>
|
|
</div>
|
|
<div class="p-5 space-y-4">
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-xs text-gray-500">Expirés</span>
|
|
<span class="px-2 py-0.5 rounded-full bg-red-100 text-red-700 text-xs font-bold">{{ statsContrats.en_retard }}</span>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-xs text-gray-500">À renouveler (90j)</span>
|
|
<span class="px-2 py-0.5 rounded-full bg-orange-100 text-orange-700 text-xs font-bold">{{ statsContrats.proches }}</span>
|
|
</div>
|
|
<Link :href="route('contrats.index')" class="block text-center py-2 text-xs font-bold text-indigo-600 bg-indigo-50 rounded-lg hover:bg-indigo-100">Gérer les contrats</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</AuthenticatedLayout>
|
|
</template>
|