From 04fc56cd704f41ccee997ba04d2bb81fd68e4d65 Mon Sep 17 00:00:00 2001 From: jeremy bayse Date: Sat, 11 Apr 2026 20:20:49 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20dashboard=20am=C3=A9lior=C3=A9,=20expor?= =?UTF-8?q?ts=20budgets,=20alertes=20expiration=20et=20correctifs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 --- app/Console/Commands/CheckExpirations.php | 89 +++ app/Exports/BudgetExecutionExport.php | 130 ++++ app/Http/Controllers/ContratController.php | 16 +- app/Http/Controllers/DashboardController.php | 85 ++- app/Mail/ExpiringElementsMail.php | 56 ++ app/Models/Commande.php | 7 +- app/Policies/CommandePolicy.php | 8 +- app/Policies/ContratPolicy.php | 6 +- composer.json | 1 + composer.lock | 593 +++++++++++++++++- database/seeders/RolesPermissionsSeeder.php | 2 +- resources/js/Layouts/AuthenticatedLayout.vue | 87 ++- resources/js/Pages/Assets/Form.vue | 21 +- resources/js/Pages/Dashboard/Index.vue | 381 ++++++----- .../views/emails/expiring_elements.blade.php | 51 ++ resources/views/exports/budgets_pdf.blade.php | 85 +++ routes/console.php | 5 + routes/web.php | 9 + 18 files changed, 1403 insertions(+), 229 deletions(-) create mode 100644 app/Console/Commands/CheckExpirations.php create mode 100644 app/Exports/BudgetExecutionExport.php create mode 100644 app/Mail/ExpiringElementsMail.php create mode 100644 resources/views/emails/expiring_elements.blade.php create mode 100644 resources/views/exports/budgets_pdf.blade.php diff --git a/app/Console/Commands/CheckExpirations.php b/app/Console/Commands/CheckExpirations.php new file mode 100644 index 0000000..1f6b85d --- /dev/null +++ b/app/Console/Commands/CheckExpirations.php @@ -0,0 +1,89 @@ +addDays($threshold); + + // 1. Contrats arrivant à échéance + $contrats = Contrat::whereBetween('date_echeance', [$today, $limitDate]) + ->whereNotIn('statut', ['resilie', 'expire']) + ->get(); + + // 2. Licences expirant bientôt + $licences = Licence::whereBetween('date_expiration', [$today, $limitDate]) + ->get(); + + // 3. Garanties d'assets expirant bientôt + $assets = Asset::whereBetween('date_fin_garantie', [$today, $limitDate]) + ->get(); + + // 4. Domaines arrivant à échéance + $domaines = Domaine::whereBetween('date_echeance', [$today, $limitDate]) + ->get(); + + if ($contrats->isEmpty() && $licences->isEmpty() && $assets->isEmpty() && $domaines->isEmpty()) { + $this->info('Aucun élément n\'arrive à échéance prochainement.'); + return 0; + } + + $this->info(sprintf( + 'Trouvé : %d contrats, %d licences, %d assets et %d domaines à notifier.', + $contrats->count(), + $licences->count(), + $assets->count(), + $domaines->count() + )); + + + // Récupérer les administrateurs et responsables + $recipients = User::role(['admin', 'responsable'])->get(); + + if ($recipients->isEmpty()) { + $this->warn('Aucun destinataire (admin ou responsable) trouvé.'); + return 0; + } + + foreach ($recipients as $recipient) { + Mail::to($recipient->email)->send(new ExpiringElementsMail($contrats, $licences, $assets, $domaines)); + } + + + $this->info('Emails de notification envoyés.'); + + return 0; + } +} diff --git a/app/Exports/BudgetExecutionExport.php b/app/Exports/BudgetExecutionExport.php new file mode 100644 index 0000000..587848f --- /dev/null +++ b/app/Exports/BudgetExecutionExport.php @@ -0,0 +1,130 @@ +user = $user; + $this->annee = $annee; + $this->filters = $filters; + } + + public function title(): string + { + $parts = ['Exécution Budgétaire', $this->annee]; + + if (!empty($this->filters['type'])) { + $parts[] = ucfirst($this->filters['type']); + } + + if (!empty($this->filters['envelope'])) { + $parts[] = ucfirst($this->filters['envelope']); + } + + if (!empty($this->filters['service'])) { + $parts[] = $this->filters['service']; + } + + return substr(implode(' - ', $parts), 0, 31); // Max 31 chars for Excel sheet title + } + + public function collection() + { + $query = LigneBudget::with(['budget.service', 'commandes']) + ->whereHas('budget', function($q) { + $q->where('annee', $this->annee); + }); + + // Role based access + if (!$this->user->hasRole(['admin', 'directeur'])) { + $query->whereHas('budget', function($q) { + $q->where('service_id', $this->user->service_id); + }); + } + + // Dynamic filters + if (!empty($this->filters['type'])) { + $query->where('type_depense', $this->filters['type']); + } + + if (!empty($this->filters['envelope'])) { + $query->whereHas('budget', function($q) { + $q->where('type_budget', $this->filters['envelope']); + }); + } + + if (!empty($this->filters['service'])) { + $query->whereHas('budget.service', function($q) { + $q->where('nom', $this->filters['service']); + }); + } + + $statutsConsommes = ['validee', 'commandee', 'partiellement_recue', 'recue_complete', 'cloturee']; + $statutsEngages = ['brouillon', 'en_attente_validation']; + + return $query->get()->map(function($lb) use ($statutsConsommes, $statutsEngages) { + // Travail en mémoire sur la relation déjà eager-loadée — zéro requête supplémentaire + $commandes = $lb->commandes; + + $consomme = (float) $commandes->whereIn('statut', $statutsConsommes)->sum('montant_ttc'); + $engage = (float) $commandes->whereIn('statut', $statutsEngages)->sum('montant_ttc'); + + $lb->consomme = $consomme; + $lb->engage = $engage; + $lb->total_cumule = $consomme + $engage; + $lb->reste = (float) ($lb->montant_arbitre ?? 0) - $lb->total_cumule; + $lb->percent = ($lb->montant_arbitre ?? 0) > 0 + ? round(($lb->total_cumule / $lb->montant_arbitre) * 100, 2) + : 0; + + return $lb; + }); + } + + public function headings(): array + { + return [ + 'ID', + 'Service', + 'Type Budget', + 'Ligne Budgétaire', + 'Type Dépense', + 'Enveloppe Arbitrée (€)', + 'Consommé (€)', + 'Engagé (€)', + 'Total Engagé (€)', + 'Reste (€)', + 'Taux d\'exécution (%)' + ]; + } + + public function map($lb): array + { + return [ + $lb->id, + $lb->budget->service->nom ?? 'Agglo', + $lb->budget->type_budget === 'agglo' ? 'Agglomération' : 'Mutualisé', + $lb->nom, + $lb->type_depense === 'fonctionnement' ? 'Fonctionnement' : 'Investissement', + $lb->montant_arbitre, + $lb->consomme, + $lb->engage, + $lb->total_cumule, + $lb->reste, + $lb->percent . '%' + ]; + } +} diff --git a/app/Http/Controllers/ContratController.php b/app/Http/Controllers/ContratController.php index 2e09ab3..2157237 100644 --- a/app/Http/Controllers/ContratController.php +++ b/app/Http/Controllers/ContratController.php @@ -19,7 +19,7 @@ class ContratController extends Controller $query = Contrat::with(['fournisseur', 'service']); - if (!$request->user()->hasRole('admin')) { + if (!$request->user()->hasAnyRole(['admin', 'raf'])) { $query->where('service_id', $request->user()->service_id); } @@ -29,7 +29,7 @@ class ContratController extends Controller ->orWhereHas('fournisseur', fn($f) => $f->where('nom', 'like', "%{$search}%")); }); })->when($request->service_id, function ($q, $serviceId) use ($request) { - if ($request->user()->hasRole('admin')) { + if ($request->user()->hasAnyRole(['admin', 'raf'])) { $q->where('service_id', $serviceId); } })->when($request->fournisseur_id, function ($q, $fournisseurId) { @@ -50,7 +50,7 @@ class ContratController extends Controller return Inertia::render('Contrats/Index', [ 'contrats' => $contrats, - 'services' => $request->user()->hasRole('admin') ? Service::all() : Service::where('id', $request->user()->service_id)->get(), + 'services' => $request->user()->hasAnyRole(['admin', 'raf']) ? Service::all() : Service::where('id', $request->user()->service_id)->get(), 'fournisseurs' => Fournisseur::active()->orderBy('nom')->get(), 'communes' => \App\Models\Commune::orderBy('nom')->get(), 'filters' => $request->only(['search', 'service_id', 'fournisseur_id', 'statut']), @@ -63,7 +63,7 @@ class ContratController extends Controller $this->authorize('create', Contrat::class); return Inertia::render('Contrats/Create', [ - 'services' => $request->user()->hasRole('admin') ? Service::all() : Service::where('id', $request->user()->service_id)->get(), + 'services' => $request->user()->hasAnyRole(['admin', 'raf']) ? Service::all() : Service::where('id', $request->user()->service_id)->get(), 'fournisseurs' => Fournisseur::active()->orderBy('nom')->get(), 'communes' => \App\Models\Commune::orderBy('nom')->get(), 'statuts' => Contrat::STATUTS_LABELS, @@ -86,13 +86,13 @@ class ContratController extends Controller ]; // Only admins can select other services, otherwise we force the user's service - if ($request->user()->hasRole('admin')) { + if ($request->user()->hasAnyRole(['admin', 'raf'])) { $rules['service_id'] = 'required|exists:services,id'; } $validated = $request->validate($rules); - if (!$request->user()->hasRole('admin')) { + if (!$request->user()->hasAnyRole(['admin', 'raf'])) { $validated['service_id'] = $request->user()->service_id; } @@ -120,7 +120,7 @@ class ContratController extends Controller return Inertia::render('Contrats/Edit', [ 'contrat' => $contrat, - 'services' => $request->user()->hasRole('admin') ? Service::all() : Service::where('id', $request->user()->service_id)->get(), + 'services' => $request->user()->hasAnyRole(['admin', 'raf']) ? Service::all() : Service::where('id', $request->user()->service_id)->get(), 'fournisseurs' => Fournisseur::active()->orderBy('nom')->get(), 'communes' => \App\Models\Commune::orderBy('nom')->get(), 'statuts' => Contrat::STATUTS_LABELS, @@ -142,7 +142,7 @@ class ContratController extends Controller 'preavis_jours' => 'nullable|integer|min:0', ]; - if ($request->user()->hasRole('admin')) { + if ($request->user()->hasAnyRole(['admin', 'raf'])) { $rules['service_id'] = 'required|exists:services,id'; } diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index f270e08..72022aa 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -6,6 +6,8 @@ use App\Models\Commande; use App\Models\Service; use App\Models\Contrat; use App\Models\Domaine; +use App\Models\Budget; +use App\Models\LigneBudget; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Inertia\Inertia; @@ -16,47 +18,61 @@ class DashboardController extends Controller public function index(Request $request): Response { $user = $request->user(); + $isRestricted = !$user->hasRole(['admin', 'directeur', 'raf']); + $serviceId = $user->service_id; + + $baseCommande = Commande::query(); + if ($isRestricted) { + $baseCommande->where('service_id', $serviceId); + } + $stats = [ - 'total' => Commande::count(), - 'en_cours' => Commande::enCours()->count(), - 'en_retard' => Commande::enRetard()->count(), - 'brouillons' => Commande::parStatut('brouillon')->count(), - 'en_attente_validation' => Commande::parStatut('en_attente_validation')->count(), - 'validees' => Commande::parStatut('validee')->count(), - 'commandees' => Commande::parStatut('commandee')->count(), - 'partiellement_recues' => Commande::parStatut('partiellement_recue')->count(), - 'recues_complete' => Commande::parStatut('recue_complete')->count(), + 'total' => (clone $baseCommande)->count(), + 'en_cours' => (clone $baseCommande)->enCours()->count(), + 'en_retard' => (clone $baseCommande)->enRetard()->count(), + 'brouillons' => (clone $baseCommande)->parStatut('brouillon')->count(), + 'en_attente_validation' => (clone $baseCommande)->parStatut('en_attente_validation')->count(), + 'validees' => (clone $baseCommande)->parStatut('validee')->count(), + 'commandees' => (clone $baseCommande)->parStatut('commandee')->count(), + 'partiellement_recues' => (clone $baseCommande)->parStatut('partiellement_recue')->count(), + 'recues_complete' => (clone $baseCommande)->parStatut('recue_complete')->count(), ]; - $commandesRecentes = Commande::with(['service', 'fournisseur', 'demandeur']) + $commandesRecentes = (clone $baseCommande)->with(['service', 'fournisseur', 'demandeur']) ->latest() ->limit(8) ->get(); - $commandesEnRetard = Commande::enRetard() + $commandesEnRetard = (clone $baseCommande)->enRetard() ->with(['service', 'fournisseur', 'demandeur']) ->orderBy('date_souhaitee') ->limit(10) ->get(); - $commandesUrgentes = Commande::urgentes() + $commandesUrgentes = (clone $baseCommande)->urgentes() ->enCours() ->with(['service', 'fournisseur', 'demandeur']) ->latest() ->limit(5) ->get(); - $statsParStatut = Commande::select('statut', DB::raw('count(*) as total')) + $statsParStatut = (clone $baseCommande)->select('statut', DB::raw('count(*) as total')) ->groupBy('statut') ->get() ->keyBy('statut'); - $statsParService = Service::withCount([ + $statsParServiceQuery = Service::withCount([ 'commandes', 'commandes as commandes_en_cours_count' => fn ($q) => $q->enCours(), - ])->get(); + ]); + + if ($isRestricted) { + $statsParServiceQuery->where('id', $serviceId); + } + + $statsParService = $statsParServiceQuery->get(); - $montantParMois = Commande::select( + $montantParMois = (clone $baseCommande)->select( DB::raw('YEAR(date_demande) as annee'), DB::raw('MONTH(date_demande) as mois'), DB::raw('SUM(montant_ttc) as total_ttc'), @@ -70,7 +86,7 @@ class DashboardController extends Controller // Stats Contrats $contratsQuery = Contrat::query(); - if (!$user->hasRole('admin')) { + if (!$user->hasRole(['admin', 'raf'])) { $contratsQuery->where('service_id', $user->service_id); } $tousContrats = $contratsQuery->get(); @@ -88,6 +104,40 @@ class DashboardController extends Controller 'en_retard' => $tousDomaines->filter(fn($d) => $d->est_en_retard)->count(), ]; + // Stats Budget + $anneeCourante = now()->year; + + $budgetQuery = LigneBudget::whereHas('budget', function($q) use ($anneeCourante) { + $q->where('annee', $anneeCourante); + }); + + if ($isRestricted) { + $budgetQuery->whereHas('budget', function($q) use ($serviceId) { + $q->where('service_id', $serviceId); + }); + } + + $budgetsStats = []; + foreach (['agglo', 'mutualise'] as $typeBudget) { + $budgetQueryType = (clone $budgetQuery)->whereHas('budget', fn($q) => $q->where('type_budget', $typeBudget)); + $baseCommandeType = (clone $baseCommande)->whereHas('ligneBudget.budget', function($q) use ($anneeCourante, $typeBudget) { + $q->where('annee', $anneeCourante)->where('type_budget', $typeBudget); + })->whereNotIn('statut', ['annulee']); + + $budgetsStats[$typeBudget] = [ + 'total_arbitre' => $budgetQueryType->sum('montant_arbitre'), + 'consomme' => $baseCommandeType->sum('montant_ttc'), + 'fonctionnement' => [ + 'total_arbitre' => (clone $budgetQueryType)->where('type_depense', 'fonctionnement')->sum('montant_arbitre'), + 'consomme' => (clone $baseCommandeType)->whereHas('ligneBudget', fn($q) => $q->where('type_depense', 'fonctionnement'))->sum('montant_ttc'), + ], + 'investissement' => [ + 'total_arbitre' => (clone $budgetQueryType)->where('type_depense', 'investissement')->sum('montant_arbitre'), + 'consomme' => (clone $baseCommandeType)->whereHas('ligneBudget', fn($q) => $q->where('type_depense', 'investissement'))->sum('montant_ttc'), + ], + ]; + } + return Inertia::render('Dashboard/Index', compact( 'stats', 'commandesRecentes', @@ -98,6 +148,7 @@ class DashboardController extends Controller 'montantParMois', 'statsContrats', 'statsDomaines', + 'budgetsStats', )); } } diff --git a/app/Mail/ExpiringElementsMail.php b/app/Mail/ExpiringElementsMail.php new file mode 100644 index 0000000..93896da --- /dev/null +++ b/app/Mail/ExpiringElementsMail.php @@ -0,0 +1,56 @@ + + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Models/Commande.php b/app/Models/Commande.php index 116afa4..a7098be 100644 --- a/app/Models/Commande.php +++ b/app/Models/Commande.php @@ -16,7 +16,7 @@ class Commande extends Model protected $fillable = [ 'numero_commande', 'service_id', 'fournisseur_id', 'user_id', 'commune_id', 'validateur_id', 'acheteur_id', 'objet', 'description', 'justification', - 'statut', 'priorite', 'reference_fournisseur', 'imputation_budgetaire', + 'statut', 'priorite', 'reference_fournisseur', 'imputation_budgetaire', 'ligne_budget_id', 'montant_ht', 'montant_ttc', 'date_demande', 'date_souhaitee', 'date_validation', 'date_commande', 'date_livraison_prevue', 'date_reception', 'date_reception_complete', 'date_cloture', @@ -133,6 +133,11 @@ class Commande extends Model return $this->hasMany(PieceJointe::class)->orderBy('created_at', 'desc'); } + public function ligneBudget(): BelongsTo + { + return $this->belongsTo(LigneBudget::class, 'ligne_budget_id'); + } + // ----------------------------------------------------------------------- // Scopes // ----------------------------------------------------------------------- diff --git a/app/Policies/CommandePolicy.php b/app/Policies/CommandePolicy.php index 3f5a00e..40981d1 100644 --- a/app/Policies/CommandePolicy.php +++ b/app/Policies/CommandePolicy.php @@ -19,12 +19,12 @@ class CommandePolicy public function create(User $user): bool { - return $user->hasAnyRole(['admin', 'responsable', 'acheteur']); + return $user->hasAnyRole(['admin', 'responsable', 'acheteur', 'raf']); } public function update(User $user, Commande $commande): bool { - if ($user->hasRole('admin')) { + if ($user->hasAnyRole(['admin', 'raf'])) { return true; } @@ -39,7 +39,7 @@ class CommandePolicy public function delete(User $user, Commande $commande): bool { - return $user->hasRole('admin'); + return $user->hasAnyRole(['admin', 'raf']); } public function transition(User $user, Commande $commande, string $targetStatut): bool @@ -48,7 +48,7 @@ class CommandePolicy return false; } - if ($user->hasRole('admin')) { + if ($user->hasAnyRole(['admin', 'raf'])) { return true; } diff --git a/app/Policies/ContratPolicy.php b/app/Policies/ContratPolicy.php index 524b835..75db525 100644 --- a/app/Policies/ContratPolicy.php +++ b/app/Policies/ContratPolicy.php @@ -14,7 +14,7 @@ class ContratPolicy public function view(User $user, Contrat $contrat): bool { - if ($user->hasRole('admin')) { + if ($user->hasAnyRole(['admin', 'raf'])) { return true; } @@ -28,7 +28,7 @@ class ContratPolicy public function update(User $user, Contrat $contrat): bool { - if ($user->hasRole('admin')) { + if ($user->hasAnyRole(['admin', 'raf'])) { return true; } @@ -37,7 +37,7 @@ class ContratPolicy public function delete(User $user, Contrat $contrat): bool { - if ($user->hasRole('admin')) { + if ($user->hasAnyRole(['admin', 'raf'])) { return true; } diff --git a/composer.json b/composer.json index af1f94c..0ed4e46 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,7 @@ "laravel/framework": "^13.0", "laravel/sanctum": "^4.0", "laravel/tinker": "^3.0", + "maatwebsite/excel": "^3.1", "spatie/laravel-activitylog": "^4.11", "spatie/laravel-permission": "^7.1", "tightenco/ziggy": "^2.0" diff --git a/composer.lock b/composer.lock index 9a43eaf..58c6f0d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f9fee4ecbd004da6b6646fdb7509c872", + "content-hash": "aa28b946cc1fa745ad8a1dc2fd38cae6", "packages": [ { "name": "barryvdh/laravel-dompdf", @@ -212,6 +212,162 @@ ], "time": "2024-02-09T16:56:22+00:00" }, + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.4", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.4" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2025-08-20T19:15:30+00:00" + }, { "name": "dflydev/dot-access-data", "version": "v3.0.3", @@ -740,6 +896,67 @@ ], "time": "2025-03-06T22:45:56+00:00" }, + { + "name": "ezyang/htmlpurifier", + "version": "v4.19.0", + "source": { + "type": "git", + "url": "https://github.com/ezyang/htmlpurifier.git", + "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/b287d2a16aceffbf6e0295559b39662612b77fcf", + "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf", + "shasum": "" + }, + "require": { + "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "require-dev": { + "cerdic/css-tidy": "^1.7 || ^2.0", + "simpletest/simpletest": "dev-master" + }, + "suggest": { + "cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.", + "ext-bcmath": "Used for unit conversion and imagecrash protection", + "ext-iconv": "Converts text to and from non-UTF-8 encodings", + "ext-tidy": "Used for pretty-printing HTML" + }, + "type": "library", + "autoload": { + "files": [ + "library/HTMLPurifier.composer.php" + ], + "psr-0": { + "HTMLPurifier": "library/" + }, + "exclude-from-classmap": [ + "/library/HTMLPurifier/Language/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com" + } + ], + "description": "Standards compliant HTML filter written in PHP", + "homepage": "http://htmlpurifier.org/", + "keywords": [ + "html" + ], + "support": { + "issues": "https://github.com/ezyang/htmlpurifier/issues", + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.19.0" + }, + "time": "2025-10-17T16:34:55+00:00" + }, { "name": "fruitcake/php-cors", "version": "v1.4.0", @@ -2453,6 +2670,272 @@ ], "time": "2026-03-08T20:05:35+00:00" }, + { + "name": "maatwebsite/excel", + "version": "3.1.68", + "source": { + "type": "git", + "url": "https://github.com/SpartnerNL/Laravel-Excel.git", + "reference": "1854739267d81d38eae7d8c623caf523f30f256b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SpartnerNL/Laravel-Excel/zipball/1854739267d81d38eae7d8c623caf523f30f256b", + "reference": "1854739267d81d38eae7d8c623caf523f30f256b", + "shasum": "" + }, + "require": { + "composer/semver": "^3.3", + "ext-json": "*", + "illuminate/support": "5.8.*||^6.0||^7.0||^8.0||^9.0||^10.0||^11.0||^12.0||^13.0", + "php": "^7.0||^8.0", + "phpoffice/phpspreadsheet": "^1.30.0", + "psr/simple-cache": "^1.0||^2.0||^3.0" + }, + "require-dev": { + "laravel/scout": "^7.0||^8.0||^9.0||^10.0||^11.0", + "orchestra/testbench": "^6.0||^7.0||^8.0||^9.0||^10.0||^11.0", + "predis/predis": "^1.1" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Excel": "Maatwebsite\\Excel\\Facades\\Excel" + }, + "providers": [ + "Maatwebsite\\Excel\\ExcelServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Maatwebsite\\Excel\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Patrick Brouwers", + "email": "patrick@spartner.nl" + } + ], + "description": "Supercharged Excel exports and imports in Laravel", + "keywords": [ + "PHPExcel", + "batch", + "csv", + "excel", + "export", + "import", + "laravel", + "php", + "phpspreadsheet" + ], + "support": { + "issues": "https://github.com/SpartnerNL/Laravel-Excel/issues", + "source": "https://github.com/SpartnerNL/Laravel-Excel/tree/3.1.68" + }, + "funding": [ + { + "url": "https://laravel-excel.com/commercial-support", + "type": "custom" + }, + { + "url": "https://github.com/patrickbrouwers", + "type": "github" + } + ], + "time": "2026-03-17T20:51:10+00:00" + }, + { + "name": "maennchen/zipstream-php", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/maennchen/ZipStream-PHP.git", + "reference": "682f1098a8fddbaf43edac2306a691c7ad508ec5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/682f1098a8fddbaf43edac2306a691c7ad508ec5", + "reference": "682f1098a8fddbaf43edac2306a691c7ad508ec5", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "ext-zlib": "*", + "php-64bit": "^8.3" + }, + "require-dev": { + "brianium/paratest": "^7.7", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.86", + "guzzlehttp/guzzle": "^7.5", + "mikey179/vfsstream": "^1.6", + "php-coveralls/php-coveralls": "^2.5", + "phpunit/phpunit": "^12.0", + "vimeo/psalm": "^6.0" + }, + "suggest": { + "guzzlehttp/psr7": "^2.4", + "psr/http-message": "^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "ZipStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paul Duncan", + "email": "pabs@pablotron.org" + }, + { + "name": "Jonatan Männchen", + "email": "jonatan@maennchen.ch" + }, + { + "name": "Jesse Donat", + "email": "donatj@gmail.com" + }, + { + "name": "András Kolesár", + "email": "kolesar@kolesar.hu" + } + ], + "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", + "keywords": [ + "stream", + "zip" + ], + "support": { + "issues": "https://github.com/maennchen/ZipStream-PHP/issues", + "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/maennchen", + "type": "github" + } + ], + "time": "2025-12-10T09:58:31+00:00" + }, + { + "name": "markbaker/complex", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPComplex.git", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Complex\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@lange.demon.co.uk" + } + ], + "description": "PHP Class for working with complex numbers", + "homepage": "https://github.com/MarkBaker/PHPComplex", + "keywords": [ + "complex", + "mathematics" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPComplex/issues", + "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2" + }, + "time": "2022-12-06T16:21:08+00:00" + }, + { + "name": "markbaker/matrix", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPMatrix.git", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpdocumentor/phpdocumentor": "2.*", + "phploc/phploc": "^4.0", + "phpmd/phpmd": "2.*", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "sebastian/phpcpd": "^4.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Matrix\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@demon-angel.eu" + } + ], + "description": "PHP Class for working with matrices", + "homepage": "https://github.com/MarkBaker/PHPMatrix", + "keywords": [ + "mathematics", + "matrix", + "vector" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPMatrix/issues", + "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1" + }, + "time": "2022-12-02T22:17:43+00:00" + }, { "name": "masterminds/html5", "version": "2.10.0", @@ -3031,6 +3514,114 @@ ], "time": "2026-02-16T23:10:27+00:00" }, + { + "name": "phpoffice/phpspreadsheet", + "version": "1.30.3", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", + "reference": "d28d4827f934469e7ca4de940ab0abd0788d1e65" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/d28d4827f934469e7ca4de940ab0abd0788d1e65", + "reference": "d28d4827f934469e7ca4de940ab0abd0788d1e65", + "shasum": "" + }, + "require": { + "composer/pcre": "^1||^2||^3", + "ext-ctype": "*", + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-gd": "*", + "ext-iconv": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-xml": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "ext-zip": "*", + "ext-zlib": "*", + "ezyang/htmlpurifier": "^4.15", + "maennchen/zipstream-php": "^2.1 || ^3.0", + "markbaker/complex": "^3.0", + "markbaker/matrix": "^3.0", + "php": ">=7.4.0 <8.5.0", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-main", + "doctrine/instantiator": "^1.5", + "dompdf/dompdf": "^1.0 || ^2.0 || ^3.0", + "friendsofphp/php-cs-fixer": "^3.2", + "mitoteam/jpgraph": "^10.3", + "mpdf/mpdf": "^8.1.1", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.1", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^8.5 || ^9.0", + "squizlabs/php_codesniffer": "^3.7", + "tecnickcom/tcpdf": "^6.5" + }, + "suggest": { + "dompdf/dompdf": "Option for rendering PDF with PDF Writer", + "ext-intl": "PHP Internationalization Functions", + "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers", + "mpdf/mpdf": "Option for rendering PDF with PDF Writer", + "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maarten Balliauw", + "homepage": "https://blog.maartenballiauw.be" + }, + { + "name": "Mark Baker", + "homepage": "https://markbakeruk.net" + }, + { + "name": "Franck Lefevre", + "homepage": "https://rootslabs.net" + }, + { + "name": "Erik Tilt" + }, + { + "name": "Adrien Crivelli" + }, + { + "name": "Owen Leibman" + } + ], + "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", + "homepage": "https://github.com/PHPOffice/PhpSpreadsheet", + "keywords": [ + "OpenXML", + "excel", + "gnumeric", + "ods", + "php", + "spreadsheet", + "xls", + "xlsx" + ], + "support": { + "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.3" + }, + "time": "2026-04-10T03:47:16+00:00" + }, { "name": "phpoption/phpoption", "version": "1.9.5", diff --git a/database/seeders/RolesPermissionsSeeder.php b/database/seeders/RolesPermissionsSeeder.php index e1f7c1b..f8654b5 100644 --- a/database/seeders/RolesPermissionsSeeder.php +++ b/database/seeders/RolesPermissionsSeeder.php @@ -11,7 +11,7 @@ class RolesPermissionsSeeder extends Seeder { app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions(); - $roles = ['admin', 'responsable', 'acheteur', 'lecteur']; + $roles = ['admin', 'responsable', 'acheteur', 'lecteur', 'directeur', 'raf']; foreach ($roles as $role) { Role::firstOrCreate(['name' => $role, 'guard_name' => 'web']); diff --git a/resources/js/Layouts/AuthenticatedLayout.vue b/resources/js/Layouts/AuthenticatedLayout.vue index 63f2838..a0cdbfd 100644 --- a/resources/js/Layouts/AuthenticatedLayout.vue +++ b/resources/js/Layouts/AuthenticatedLayout.vue @@ -11,13 +11,14 @@ const userInitials = computed(() => { ?.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2) ?? '??' }) -const roleLabels = { admin: 'Administrateur', responsable: 'Responsable', acheteur: 'Acheteur', lecteur: 'Lecteur' } +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) { @@ -42,6 +43,8 @@ function isActive(...names) {