Files
dsi-commander/resources/js/Layouts/AuthenticatedLayout.vue
jeremy bayse 04fc56cd70 feat: dashboard amélioré, exports budgets, alertes expiration et correctifs
## 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>
2026-04-11 20:20:49 +02:00

274 lines
18 KiB
Vue

<script setup>
import { computed } from 'vue'
import { Link, usePage } from '@inertiajs/vue3'
import FlashMessage from '@/Components/FlashMessage.vue'
const page = usePage()
const user = computed(() => page.props.auth.user)
const userInitials = computed(() => {
return user.value?.name
?.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2) ?? '??'
})
const roleLabels = { admin: 'Administrateur', responsable: 'Responsable', acheteur: 'Acheteur', lecteur: 'Lecteur', raf: 'Responsable Administratif et Financier' }
const userRole = computed(() => {
const role = user.value?.roles?.[0]?.name
return roleLabels[role] ?? role ?? ''
})
const isAdmin = computed(() => user.value?.roles?.some(r => r.name === 'admin'))
const isRAF = computed(() => user.value?.roles?.some(r => r.name === 'raf'))
const canCreate = computed(() => user.value?.roles?.some(r => ['admin', 'responsable', 'acheteur'].includes(r.name)))
function isActive(...names) {
return names.some(name => route().current(name + '*'))
}
</script>
<template>
<div class="flex h-screen bg-gray-100 overflow-hidden">
<!-- Sidebar -->
<aside class="flex h-full w-64 flex-shrink-0 flex-col bg-gray-900 text-white shadow-xl">
<!-- Logo -->
<div class="flex items-center gap-3 border-b border-gray-700 px-6 py-5">
<div class="flex h-8 w-8 items-center justify-center rounded-lg bg-blue-600">
<svg class="h-5 w-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
</div>
<span class="text-lg font-bold">DSI COMMANDER</span>
</div>
<!-- Navigation -->
<nav class="flex-1 overflow-y-auto px-3 py-4 space-y-6">
<!-- Dashboard -->
<div>
<Link :href="route('dashboard')"
:class="['flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
route().current('dashboard') ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800 hover:text-white']">
<svg class="h-5 w-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
Tableau de bord
</Link>
</div>
<!-- Calendrier -->
<div>
<Link :href="route('calendar.index')"
:class="['flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
isActive('calendar') ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800 hover:text-white']">
<svg class="h-5 w-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
Calendrier
</Link>
</div>
<!-- Budgets -->
<div>
<p class="mb-1 px-3 text-xs font-semibold uppercase tracking-wider text-gray-500">Budgets</p>
<div class="space-y-1">
<Link :href="route('budgets.index')"
:class="['flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
route().current('budgets.index') || route().current('budgets.show') ? 'bg-indigo-600 text-white' : 'text-gray-300 hover:bg-gray-800 hover:text-white']">
<svg class="h-5 w-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Planification
</Link>
<Link :href="route('budgets.execution')"
:class="['flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
route().current('budgets.execution') ? 'bg-indigo-600 text-white' : 'text-gray-300 hover:bg-gray-800 hover:text-white']">
<svg class="h-5 w-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
Exécution réelle
</Link>
</div>
</div>
<!-- Commandes -->
<div>
<p class="mb-1 px-3 text-xs font-semibold uppercase tracking-wider text-gray-500">Commandes</p>
<div class="space-y-1">
<Link :href="route('commandes.index')"
:class="['flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
isActive('commandes') ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800 hover:text-white']">
<svg class="h-5 w-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 6h16M4 10h16M4 14h16M4 18h16" />
</svg>
Commandes
</Link>
</div>
</div>
<!-- Contrats -->
<div>
<p class="mb-1 px-3 text-xs font-semibold uppercase tracking-wider text-gray-500">Contrats</p>
<div class="space-y-1">
<Link :href="route('contrats.index')"
:class="['flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
isActive('contrats') ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800 hover:text-white']">
<svg class="h-5 w-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Contrats
</Link>
<Link :href="route('licences.index')"
:class="['flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
isActive('licences') ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800 hover:text-white']">
<svg class="h-5 w-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
</svg>
Licences
</Link>
</div>
</div>
<!-- Domaines -->
<div v-if="!isRAF">
<p class="mb-1 px-3 text-xs font-semibold uppercase tracking-wider text-gray-500">Infrastructures</p>
<div class="space-y-1">
<Link :href="route('domaines.index')"
:class="['flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
isActive('domaines') ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800 hover:text-white']">
<svg class="h-5 w-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
</svg>
Noms de domaine
</Link>
<Link :href="route('assets.index')"
:class="['flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
isActive('assets') ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800 hover:text-white']">
<svg class="h-5 w-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
</svg>
Matériels
</Link>
</div>
</div>
<!-- Catalogue -->
<div v-if="!isRAF">
<p class="mb-1 px-3 text-xs font-semibold uppercase tracking-wider text-gray-500">Catalogue</p>
<div class="space-y-1">
<Link :href="route('fournisseurs.index')"
:class="['flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
isActive('fournisseurs') ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800 hover:text-white']">
<svg class="h-5 w-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
Fournisseurs
</Link>
<Link :href="route('categories.index')"
:class="['flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
isActive('categories') ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800 hover:text-white']">
<svg class="h-5 w-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
</svg>
Catégories
</Link>
<Link :href="route('articles.index')"
:class="['flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
isActive('articles') ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800 hover:text-white']">
<svg class="h-5 w-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
Articles
</Link>
</div>
</div>
<!-- Administration -->
<div v-if="isAdmin">
<p class="mb-1 px-3 text-xs font-semibold uppercase tracking-wider text-gray-500">Administration</p>
<div class="space-y-1">
<Link :href="route('users.index')"
:class="['flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
isActive('users') ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800 hover:text-white']">
<svg class="h-5 w-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
Utilisateurs
</Link>
<Link :href="route('services.index')"
:class="['flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
isActive('services') ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800 hover:text-white']">
<svg class="h-5 w-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
</svg>
Services
</Link>
<Link :href="route('communes.index')"
:class="['flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
isActive('communes') ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800 hover:text-white']">
<svg class="h-5 w-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
Communes
</Link>
</div>
</div>
</nav>
<!-- User info -->
<div class="border-t border-gray-700 p-4">
<div class="flex items-center gap-3">
<div class="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-full bg-blue-600 text-sm font-semibold">
{{ userInitials }}
</div>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium">{{ user?.name }}</p>
<p class="truncate text-xs text-gray-400">{{ userRole }}</p>
</div>
<Link :href="route('logout')" method="post" as="button"
class="flex-shrink-0 text-gray-400 hover:text-white transition-colors" title="Déconnexion">
<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="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
</Link>
</div>
<Link :href="route('profile.edit')" class="mt-2 block text-xs text-gray-400 hover:text-white transition-colors">
Mon profil
</Link>
</div>
</aside>
<!-- Main content -->
<div class="flex flex-1 flex-col overflow-hidden">
<!-- Page header -->
<header v-if="$slots.header" class="bg-white shadow-sm z-10">
<div class="px-6 py-4">
<slot name="header" />
</div>
</header>
<!-- Flash messages -->
<FlashMessage />
<!-- Page content -->
<main class="flex-1 overflow-y-auto p-6">
<slot />
</main>
</div>
</div>
</template>