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>
This commit is contained in:
89
app/Console/Commands/CheckExpirations.php
Normal file
89
app/Console/Commands/CheckExpirations.php
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Asset;
|
||||||
|
use App\Models\Contrat;
|
||||||
|
use App\Models\Licence;
|
||||||
|
use App\Models\Domaine;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
use App\Mail\ExpiringElementsMail;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
|
||||||
|
class CheckExpirations extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'app:check-expirations';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Vérifie les éléments arrivant à échéance (contrats, licences, garanties) et envoie une notification par email.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$threshold = 30; // jours
|
||||||
|
$today = Carbon::today();
|
||||||
|
$limitDate = Carbon::today()->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
130
app/Exports/BudgetExecutionExport.php
Normal file
130
app/Exports/BudgetExecutionExport.php
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exports;
|
||||||
|
|
||||||
|
use App\Models\LigneBudget;
|
||||||
|
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithMapping;
|
||||||
|
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithTitle;
|
||||||
|
|
||||||
|
class BudgetExecutionExport implements FromCollection, WithHeadings, WithMapping, ShouldAutoSize, WithTitle
|
||||||
|
{
|
||||||
|
protected $user;
|
||||||
|
protected $annee;
|
||||||
|
protected $filters;
|
||||||
|
|
||||||
|
public function __construct($user, $annee, $filters = [])
|
||||||
|
{
|
||||||
|
$this->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 . '%'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ class ContratController extends Controller
|
|||||||
|
|
||||||
$query = Contrat::with(['fournisseur', 'service']);
|
$query = Contrat::with(['fournisseur', 'service']);
|
||||||
|
|
||||||
if (!$request->user()->hasRole('admin')) {
|
if (!$request->user()->hasAnyRole(['admin', 'raf'])) {
|
||||||
$query->where('service_id', $request->user()->service_id);
|
$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}%"));
|
->orWhereHas('fournisseur', fn($f) => $f->where('nom', 'like', "%{$search}%"));
|
||||||
});
|
});
|
||||||
})->when($request->service_id, function ($q, $serviceId) use ($request) {
|
})->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);
|
$q->where('service_id', $serviceId);
|
||||||
}
|
}
|
||||||
})->when($request->fournisseur_id, function ($q, $fournisseurId) {
|
})->when($request->fournisseur_id, function ($q, $fournisseurId) {
|
||||||
@@ -50,7 +50,7 @@ class ContratController extends Controller
|
|||||||
|
|
||||||
return Inertia::render('Contrats/Index', [
|
return Inertia::render('Contrats/Index', [
|
||||||
'contrats' => $contrats,
|
'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(),
|
'fournisseurs' => Fournisseur::active()->orderBy('nom')->get(),
|
||||||
'communes' => \App\Models\Commune::orderBy('nom')->get(),
|
'communes' => \App\Models\Commune::orderBy('nom')->get(),
|
||||||
'filters' => $request->only(['search', 'service_id', 'fournisseur_id', 'statut']),
|
'filters' => $request->only(['search', 'service_id', 'fournisseur_id', 'statut']),
|
||||||
@@ -63,7 +63,7 @@ class ContratController extends Controller
|
|||||||
$this->authorize('create', Contrat::class);
|
$this->authorize('create', Contrat::class);
|
||||||
|
|
||||||
return Inertia::render('Contrats/Create', [
|
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(),
|
'fournisseurs' => Fournisseur::active()->orderBy('nom')->get(),
|
||||||
'communes' => \App\Models\Commune::orderBy('nom')->get(),
|
'communes' => \App\Models\Commune::orderBy('nom')->get(),
|
||||||
'statuts' => Contrat::STATUTS_LABELS,
|
'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
|
// 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';
|
$rules['service_id'] = 'required|exists:services,id';
|
||||||
}
|
}
|
||||||
|
|
||||||
$validated = $request->validate($rules);
|
$validated = $request->validate($rules);
|
||||||
|
|
||||||
if (!$request->user()->hasRole('admin')) {
|
if (!$request->user()->hasAnyRole(['admin', 'raf'])) {
|
||||||
$validated['service_id'] = $request->user()->service_id;
|
$validated['service_id'] = $request->user()->service_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ class ContratController extends Controller
|
|||||||
|
|
||||||
return Inertia::render('Contrats/Edit', [
|
return Inertia::render('Contrats/Edit', [
|
||||||
'contrat' => $contrat,
|
'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(),
|
'fournisseurs' => Fournisseur::active()->orderBy('nom')->get(),
|
||||||
'communes' => \App\Models\Commune::orderBy('nom')->get(),
|
'communes' => \App\Models\Commune::orderBy('nom')->get(),
|
||||||
'statuts' => Contrat::STATUTS_LABELS,
|
'statuts' => Contrat::STATUTS_LABELS,
|
||||||
@@ -142,7 +142,7 @@ class ContratController extends Controller
|
|||||||
'preavis_jours' => 'nullable|integer|min:0',
|
'preavis_jours' => 'nullable|integer|min:0',
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($request->user()->hasRole('admin')) {
|
if ($request->user()->hasAnyRole(['admin', 'raf'])) {
|
||||||
$rules['service_id'] = 'required|exists:services,id';
|
$rules['service_id'] = 'required|exists:services,id';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ use App\Models\Commande;
|
|||||||
use App\Models\Service;
|
use App\Models\Service;
|
||||||
use App\Models\Contrat;
|
use App\Models\Contrat;
|
||||||
use App\Models\Domaine;
|
use App\Models\Domaine;
|
||||||
|
use App\Models\Budget;
|
||||||
|
use App\Models\LigneBudget;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
@@ -16,47 +18,61 @@ class DashboardController extends Controller
|
|||||||
public function index(Request $request): Response
|
public function index(Request $request): Response
|
||||||
{
|
{
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
|
$isRestricted = !$user->hasRole(['admin', 'directeur', 'raf']);
|
||||||
|
$serviceId = $user->service_id;
|
||||||
|
|
||||||
|
$baseCommande = Commande::query();
|
||||||
|
if ($isRestricted) {
|
||||||
|
$baseCommande->where('service_id', $serviceId);
|
||||||
|
}
|
||||||
|
|
||||||
$stats = [
|
$stats = [
|
||||||
'total' => Commande::count(),
|
'total' => (clone $baseCommande)->count(),
|
||||||
'en_cours' => Commande::enCours()->count(),
|
'en_cours' => (clone $baseCommande)->enCours()->count(),
|
||||||
'en_retard' => Commande::enRetard()->count(),
|
'en_retard' => (clone $baseCommande)->enRetard()->count(),
|
||||||
'brouillons' => Commande::parStatut('brouillon')->count(),
|
'brouillons' => (clone $baseCommande)->parStatut('brouillon')->count(),
|
||||||
'en_attente_validation' => Commande::parStatut('en_attente_validation')->count(),
|
'en_attente_validation' => (clone $baseCommande)->parStatut('en_attente_validation')->count(),
|
||||||
'validees' => Commande::parStatut('validee')->count(),
|
'validees' => (clone $baseCommande)->parStatut('validee')->count(),
|
||||||
'commandees' => Commande::parStatut('commandee')->count(),
|
'commandees' => (clone $baseCommande)->parStatut('commandee')->count(),
|
||||||
'partiellement_recues' => Commande::parStatut('partiellement_recue')->count(),
|
'partiellement_recues' => (clone $baseCommande)->parStatut('partiellement_recue')->count(),
|
||||||
'recues_complete' => Commande::parStatut('recue_complete')->count(),
|
'recues_complete' => (clone $baseCommande)->parStatut('recue_complete')->count(),
|
||||||
];
|
];
|
||||||
|
|
||||||
$commandesRecentes = Commande::with(['service', 'fournisseur', 'demandeur'])
|
$commandesRecentes = (clone $baseCommande)->with(['service', 'fournisseur', 'demandeur'])
|
||||||
->latest()
|
->latest()
|
||||||
->limit(8)
|
->limit(8)
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$commandesEnRetard = Commande::enRetard()
|
$commandesEnRetard = (clone $baseCommande)->enRetard()
|
||||||
->with(['service', 'fournisseur', 'demandeur'])
|
->with(['service', 'fournisseur', 'demandeur'])
|
||||||
->orderBy('date_souhaitee')
|
->orderBy('date_souhaitee')
|
||||||
->limit(10)
|
->limit(10)
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$commandesUrgentes = Commande::urgentes()
|
$commandesUrgentes = (clone $baseCommande)->urgentes()
|
||||||
->enCours()
|
->enCours()
|
||||||
->with(['service', 'fournisseur', 'demandeur'])
|
->with(['service', 'fournisseur', 'demandeur'])
|
||||||
->latest()
|
->latest()
|
||||||
->limit(5)
|
->limit(5)
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$statsParStatut = Commande::select('statut', DB::raw('count(*) as total'))
|
$statsParStatut = (clone $baseCommande)->select('statut', DB::raw('count(*) as total'))
|
||||||
->groupBy('statut')
|
->groupBy('statut')
|
||||||
->get()
|
->get()
|
||||||
->keyBy('statut');
|
->keyBy('statut');
|
||||||
|
|
||||||
$statsParService = Service::withCount([
|
$statsParServiceQuery = Service::withCount([
|
||||||
'commandes',
|
'commandes',
|
||||||
'commandes as commandes_en_cours_count' => fn ($q) => $q->enCours(),
|
'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('YEAR(date_demande) as annee'),
|
||||||
DB::raw('MONTH(date_demande) as mois'),
|
DB::raw('MONTH(date_demande) as mois'),
|
||||||
DB::raw('SUM(montant_ttc) as total_ttc'),
|
DB::raw('SUM(montant_ttc) as total_ttc'),
|
||||||
@@ -70,7 +86,7 @@ class DashboardController extends Controller
|
|||||||
|
|
||||||
// Stats Contrats
|
// Stats Contrats
|
||||||
$contratsQuery = Contrat::query();
|
$contratsQuery = Contrat::query();
|
||||||
if (!$user->hasRole('admin')) {
|
if (!$user->hasRole(['admin', 'raf'])) {
|
||||||
$contratsQuery->where('service_id', $user->service_id);
|
$contratsQuery->where('service_id', $user->service_id);
|
||||||
}
|
}
|
||||||
$tousContrats = $contratsQuery->get();
|
$tousContrats = $contratsQuery->get();
|
||||||
@@ -88,6 +104,40 @@ class DashboardController extends Controller
|
|||||||
'en_retard' => $tousDomaines->filter(fn($d) => $d->est_en_retard)->count(),
|
'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(
|
return Inertia::render('Dashboard/Index', compact(
|
||||||
'stats',
|
'stats',
|
||||||
'commandesRecentes',
|
'commandesRecentes',
|
||||||
@@ -98,6 +148,7 @@ class DashboardController extends Controller
|
|||||||
'montantParMois',
|
'montantParMois',
|
||||||
'statsContrats',
|
'statsContrats',
|
||||||
'statsDomaines',
|
'statsDomaines',
|
||||||
|
'budgetsStats',
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
56
app/Mail/ExpiringElementsMail.php
Normal file
56
app/Mail/ExpiringElementsMail.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Mail;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Mail\Mailables\Content;
|
||||||
|
use Illuminate\Mail\Mailables\Envelope;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
class ExpiringElementsMail extends Mailable
|
||||||
|
{
|
||||||
|
use Queueable, SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new message instance.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public Collection $contrats,
|
||||||
|
public Collection $licences,
|
||||||
|
public Collection $assets,
|
||||||
|
public Collection $domaines
|
||||||
|
) {}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the message envelope.
|
||||||
|
*/
|
||||||
|
public function envelope(): Envelope
|
||||||
|
{
|
||||||
|
return new Envelope(
|
||||||
|
subject: '[DSICommander] Alerte : Éléments arrivant à échéance prochainement',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the message content definition.
|
||||||
|
*/
|
||||||
|
public function content(): Content
|
||||||
|
{
|
||||||
|
return new Content(
|
||||||
|
markdown: 'emails.expiring_elements',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attachments for the message.
|
||||||
|
*
|
||||||
|
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
|
||||||
|
*/
|
||||||
|
public function attachments(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ class Commande extends Model
|
|||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'numero_commande', 'service_id', 'fournisseur_id', 'user_id', 'commune_id',
|
'numero_commande', 'service_id', 'fournisseur_id', 'user_id', 'commune_id',
|
||||||
'validateur_id', 'acheteur_id', 'objet', 'description', 'justification',
|
'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',
|
'montant_ht', 'montant_ttc',
|
||||||
'date_demande', 'date_souhaitee', 'date_validation', 'date_commande',
|
'date_demande', 'date_souhaitee', 'date_validation', 'date_commande',
|
||||||
'date_livraison_prevue', 'date_reception', 'date_reception_complete', 'date_cloture',
|
'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');
|
return $this->hasMany(PieceJointe::class)->orderBy('created_at', 'desc');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function ligneBudget(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(LigneBudget::class, 'ligne_budget_id');
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Scopes
|
// Scopes
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|||||||
@@ -19,12 +19,12 @@ class CommandePolicy
|
|||||||
|
|
||||||
public function create(User $user): bool
|
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
|
public function update(User $user, Commande $commande): bool
|
||||||
{
|
{
|
||||||
if ($user->hasRole('admin')) {
|
if ($user->hasAnyRole(['admin', 'raf'])) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ class CommandePolicy
|
|||||||
|
|
||||||
public function delete(User $user, Commande $commande): bool
|
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
|
public function transition(User $user, Commande $commande, string $targetStatut): bool
|
||||||
@@ -48,7 +48,7 @@ class CommandePolicy
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($user->hasRole('admin')) {
|
if ($user->hasAnyRole(['admin', 'raf'])) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class ContratPolicy
|
|||||||
|
|
||||||
public function view(User $user, Contrat $contrat): bool
|
public function view(User $user, Contrat $contrat): bool
|
||||||
{
|
{
|
||||||
if ($user->hasRole('admin')) {
|
if ($user->hasAnyRole(['admin', 'raf'])) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ class ContratPolicy
|
|||||||
|
|
||||||
public function update(User $user, Contrat $contrat): bool
|
public function update(User $user, Contrat $contrat): bool
|
||||||
{
|
{
|
||||||
if ($user->hasRole('admin')) {
|
if ($user->hasAnyRole(['admin', 'raf'])) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ class ContratPolicy
|
|||||||
|
|
||||||
public function delete(User $user, Contrat $contrat): bool
|
public function delete(User $user, Contrat $contrat): bool
|
||||||
{
|
{
|
||||||
if ($user->hasRole('admin')) {
|
if ($user->hasAnyRole(['admin', 'raf'])) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"laravel/framework": "^13.0",
|
"laravel/framework": "^13.0",
|
||||||
"laravel/sanctum": "^4.0",
|
"laravel/sanctum": "^4.0",
|
||||||
"laravel/tinker": "^3.0",
|
"laravel/tinker": "^3.0",
|
||||||
|
"maatwebsite/excel": "^3.1",
|
||||||
"spatie/laravel-activitylog": "^4.11",
|
"spatie/laravel-activitylog": "^4.11",
|
||||||
"spatie/laravel-permission": "^7.1",
|
"spatie/laravel-permission": "^7.1",
|
||||||
"tightenco/ziggy": "^2.0"
|
"tightenco/ziggy": "^2.0"
|
||||||
|
|||||||
593
composer.lock
generated
593
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "f9fee4ecbd004da6b6646fdb7509c872",
|
"content-hash": "aa28b946cc1fa745ad8a1dc2fd38cae6",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "barryvdh/laravel-dompdf",
|
"name": "barryvdh/laravel-dompdf",
|
||||||
@@ -212,6 +212,162 @@
|
|||||||
],
|
],
|
||||||
"time": "2024-02-09T16:56:22+00:00"
|
"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",
|
"name": "dflydev/dot-access-data",
|
||||||
"version": "v3.0.3",
|
"version": "v3.0.3",
|
||||||
@@ -740,6 +896,67 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-03-06T22:45:56+00:00"
|
"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",
|
"name": "fruitcake/php-cors",
|
||||||
"version": "v1.4.0",
|
"version": "v1.4.0",
|
||||||
@@ -2453,6 +2670,272 @@
|
|||||||
],
|
],
|
||||||
"time": "2026-03-08T20:05:35+00:00"
|
"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",
|
"name": "masterminds/html5",
|
||||||
"version": "2.10.0",
|
"version": "2.10.0",
|
||||||
@@ -3031,6 +3514,114 @@
|
|||||||
],
|
],
|
||||||
"time": "2026-02-16T23:10:27+00:00"
|
"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",
|
"name": "phpoption/phpoption",
|
||||||
"version": "1.9.5",
|
"version": "1.9.5",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class RolesPermissionsSeeder extends Seeder
|
|||||||
{
|
{
|
||||||
app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
|
app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
|
||||||
|
|
||||||
$roles = ['admin', 'responsable', 'acheteur', 'lecteur'];
|
$roles = ['admin', 'responsable', 'acheteur', 'lecteur', 'directeur', 'raf'];
|
||||||
|
|
||||||
foreach ($roles as $role) {
|
foreach ($roles as $role) {
|
||||||
Role::firstOrCreate(['name' => $role, 'guard_name' => 'web']);
|
Role::firstOrCreate(['name' => $role, 'guard_name' => 'web']);
|
||||||
|
|||||||
@@ -11,13 +11,14 @@ const userInitials = computed(() => {
|
|||||||
?.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2) ?? '??'
|
?.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 userRole = computed(() => {
|
||||||
const role = user.value?.roles?.[0]?.name
|
const role = user.value?.roles?.[0]?.name
|
||||||
return roleLabels[role] ?? role ?? ''
|
return roleLabels[role] ?? role ?? ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const isAdmin = computed(() => user.value?.roles?.some(r => r.name === 'admin'))
|
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)))
|
const canCreate = computed(() => user.value?.roles?.some(r => ['admin', 'responsable', 'acheteur'].includes(r.name)))
|
||||||
|
|
||||||
function isActive(...names) {
|
function isActive(...names) {
|
||||||
@@ -42,6 +43,8 @@ function isActive(...names) {
|
|||||||
|
|
||||||
<!-- Navigation -->
|
<!-- Navigation -->
|
||||||
<nav class="flex-1 overflow-y-auto px-3 py-4 space-y-6">
|
<nav class="flex-1 overflow-y-auto px-3 py-4 space-y-6">
|
||||||
|
|
||||||
|
|
||||||
<!-- Dashboard -->
|
<!-- Dashboard -->
|
||||||
<div>
|
<div>
|
||||||
<Link :href="route('dashboard')"
|
<Link :href="route('dashboard')"
|
||||||
@@ -55,6 +58,42 @@ function isActive(...names) {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</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 -->
|
<!-- Commandes -->
|
||||||
<div>
|
<div>
|
||||||
<p class="mb-1 px-3 text-xs font-semibold uppercase tracking-wider text-gray-500">Commandes</p>
|
<p class="mb-1 px-3 text-xs font-semibold uppercase tracking-wider text-gray-500">Commandes</p>
|
||||||
@@ -66,15 +105,9 @@ function isActive(...names) {
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
d="M4 6h16M4 10h16M4 14h16M4 18h16" />
|
d="M4 6h16M4 10h16M4 14h16M4 18h16" />
|
||||||
</svg>
|
</svg>
|
||||||
Toutes les commandes
|
Commandes
|
||||||
</Link>
|
|
||||||
<Link v-if="canCreate" :href="route('commandes.create')"
|
|
||||||
class="flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-800 hover:text-white transition-colors">
|
|
||||||
<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 4v16m8-8H4" />
|
|
||||||
</svg>
|
|
||||||
Nouvelle commande
|
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -88,13 +121,22 @@ function isActive(...names) {
|
|||||||
<svg class="h-5 w-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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" />
|
<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>
|
</svg>
|
||||||
Tous les contrats
|
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>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Domaines -->
|
<!-- Domaines -->
|
||||||
<div>
|
<div v-if="!isRAF">
|
||||||
<p class="mb-1 px-3 text-xs font-semibold uppercase tracking-wider text-gray-500">Infrastructures</p>
|
<p class="mb-1 px-3 text-xs font-semibold uppercase tracking-wider text-gray-500">Infrastructures</p>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<Link :href="route('domaines.index')"
|
<Link :href="route('domaines.index')"
|
||||||
@@ -111,13 +153,13 @@ function isActive(...names) {
|
|||||||
<svg class="h-5 w-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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" />
|
<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>
|
</svg>
|
||||||
Assets (Matériels)
|
Matériels
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Catalogue -->
|
<!-- Catalogue -->
|
||||||
<div>
|
<div v-if="!isRAF">
|
||||||
<p class="mb-1 px-3 text-xs font-semibold uppercase tracking-wider text-gray-500">Catalogue</p>
|
<p class="mb-1 px-3 text-xs font-semibold uppercase tracking-wider text-gray-500">Catalogue</p>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<Link :href="route('fournisseurs.index')"
|
<Link :href="route('fournisseurs.index')"
|
||||||
@@ -172,24 +214,7 @@ function isActive(...names) {
|
|||||||
</svg>
|
</svg>
|
||||||
Services
|
Services
|
||||||
</Link>
|
</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>
|
|
||||||
<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>
|
|
||||||
<Link :href="route('communes.index')"
|
<Link :href="route('communes.index')"
|
||||||
:class="['flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
|
: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']">
|
isActive('communes') ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800 hover:text-white']">
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'
|
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'
|
||||||
import { Head, Link, useForm } from '@inertiajs/vue3'
|
import { Head, Link, useForm } from '@inertiajs/vue3'
|
||||||
import { ref } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
import SearchableSelect from '@/Components/SearchableSelect.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
asset: Object,
|
asset: Object,
|
||||||
@@ -27,6 +28,13 @@ const form = useForm({
|
|||||||
notes: props.asset?.notes || '',
|
notes: props.asset?.notes || '',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const commandesOptions = computed(() => {
|
||||||
|
return props.commandes.map(cmd => ({
|
||||||
|
value: cmd.id,
|
||||||
|
label: `N° ${cmd.numero_commande ?? 'Sans-N'} - ${cmd.objet}`
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
const isSearchingEan = ref(false)
|
const isSearchingEan = ref(false)
|
||||||
const eanError = ref('')
|
const eanError = ref('')
|
||||||
const eanSuccess = ref('')
|
const eanSuccess = ref('')
|
||||||
@@ -220,12 +228,11 @@ function submit() {
|
|||||||
<h2 class="mb-5 text-sm font-semibold uppercase tracking-wide text-gray-500 font-bold">Achat & Commande</h2>
|
<h2 class="mb-5 text-sm font-semibold uppercase tracking-wide text-gray-500 font-bold">Achat & Commande</h2>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700">Commande associée (optionnel)</label>
|
<label class="block text-sm font-medium text-gray-700">Commande associée (optionnel)</label>
|
||||||
<select v-model="form.commande_id" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
|
<SearchableSelect
|
||||||
<option value="">Aucune commande liée</option>
|
v-model="form.commande_id"
|
||||||
<option v-for="cmd in commandes" :key="cmd.id" :value="cmd.id">
|
:options="commandesOptions"
|
||||||
N° {{ cmd.numero_commande ?? 'Sans-N' }} - {{ cmd.objet }}
|
placeholder="Rechercher par numéro ou objet..."
|
||||||
</option>
|
/>
|
||||||
</select>
|
|
||||||
<p class="mt-1 text-xs text-gray-500">Permet de retrouver l'origine de l'achat et les justificatifs.</p>
|
<p class="mt-1 text-xs text-gray-500">Permet de retrouver l'origine de l'achat et les justificatifs.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,31 +1,39 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'
|
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'
|
||||||
import StatCard from '@/Components/StatCard.vue'
|
|
||||||
import StatutBadge from '@/Components/Commandes/StatutBadge.vue'
|
import StatutBadge from '@/Components/Commandes/StatutBadge.vue'
|
||||||
import PrioriteBadge from '@/Components/Commandes/PrioriteBadge.vue'
|
import PrioriteBadge from '@/Components/Commandes/PrioriteBadge.vue'
|
||||||
import { Link } from '@inertiajs/vue3'
|
import { Link, Head } from '@inertiajs/vue3'
|
||||||
import { Head } from '@inertiajs/vue3'
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
stats: Object,
|
stats: Object,
|
||||||
commandesRecentes: Array,
|
commandesRecentes: Array,
|
||||||
commandesEnRetard: Array,
|
commandesEnRetard: Array,
|
||||||
commandesUrgentes: Array,
|
commandesUrgentes: Array,
|
||||||
statsParStatut: Object,
|
|
||||||
statsParService: Array,
|
|
||||||
montantParMois: Array,
|
|
||||||
statsContrats: Object,
|
statsContrats: Object,
|
||||||
statsDomaines: Object,
|
statsDomaines: Object,
|
||||||
|
budgetsStats: Object,
|
||||||
})
|
})
|
||||||
|
|
||||||
function formatDate(d) {
|
const formatDate = (d) => {
|
||||||
if (!d) return '—'
|
if (!d) return '—'
|
||||||
return new Intl.DateTimeFormat('fr-FR').format(new Date(d))
|
return new Intl.DateTimeFormat('fr-FR').format(new Date(d))
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatCurrency(v) {
|
const formatCurrency = (v) => {
|
||||||
if (v == null) return '—'
|
if (v == null) return '0 €'
|
||||||
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(v)
|
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>
|
</script>
|
||||||
|
|
||||||
@@ -34,179 +42,240 @@ function formatCurrency(v) {
|
|||||||
|
|
||||||
<AuthenticatedLayout>
|
<AuthenticatedLayout>
|
||||||
<template #header>
|
<template #header>
|
||||||
<h1 class="text-xl font-semibold text-gray-900">Tableau de bord</h1>
|
<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>
|
</template>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-8 pb-10">
|
||||||
<!-- Stat cards -->
|
<!-- Top Stats Row -->
|
||||||
<div class="grid grid-cols-2 gap-4 sm:grid-cols-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
<StatCard label="Total commandes" :value="stats.total" color="blue" />
|
<div class="bg-white p-6 rounded-2xl shadow-sm border border-gray-100">
|
||||||
<StatCard label="En cours" :value="stats.en_cours" color="indigo" />
|
<p class="text-sm font-medium text-gray-500">Commandes en cours</p>
|
||||||
<StatCard label="En retard" :value="stats.en_retard" color="red" />
|
<div class="mt-2 flex items-baseline gap-2">
|
||||||
<StatCard label="Urgentes actives" :value="stats.commandees + stats.validees" color="amber" />
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Contrats & Domaines Stats -->
|
<!-- Budget Gauges -->
|
||||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
<!-- Contrats -->
|
<!-- Agglo Card -->
|
||||||
<div class="rounded-xl bg-white p-5 shadow-sm border border-gray-100 flex items-center justify-between">
|
<div class="bg-white p-6 rounded-3xl shadow-sm border border-gray-100">
|
||||||
<div>
|
<h3 class="font-bold text-gray-900 mb-6 flex items-center gap-2">
|
||||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-gray-500 mb-2">Contrats en cours</h2>
|
<span class="w-2 h-2 rounded-full bg-blue-500"></span>
|
||||||
<div class="flex gap-6">
|
Exécution Agglomération
|
||||||
<Link :href="route('contrats.index')" class="flex flex-col">
|
</h3>
|
||||||
<span class="text-xs text-gray-400">Total</span>
|
|
||||||
<span class="font-bold text-gray-900">{{ statsContrats.total }}</span>
|
<div class="grid grid-cols-2 gap-8">
|
||||||
</Link>
|
<!-- Fonctionnement -->
|
||||||
<Link :href="route('contrats.index')" class="flex flex-col">
|
<div class="flex flex-col items-center">
|
||||||
<span class="text-xs text-gray-400">À renouveler (⏳)</span>
|
<div class="relative w-full h-24 flex items-center justify-center">
|
||||||
<span class="font-bold text-orange-600">{{ statsContrats.proches }}</span>
|
<svg class="w-full h-full" viewBox="0 0 100 60">
|
||||||
</Link>
|
<path d="M 12 55 A 38 38 0 0 1 88 55" fill="#edf2f7" stroke="#2d3748" stroke-width="1.5" />
|
||||||
<Link :href="route('contrats.index')" class="flex flex-col">
|
<path d="M 12 55 A 38 38 0 0 1 31 22 L 50 55 Z" fill="#48bb78" />
|
||||||
<span class="text-xs text-gray-400">Expirés (⚠️)</span>
|
<path d="M 31 22 A 38 38 0 0 1 69 22 L 50 55 Z" fill="#ecc94b" />
|
||||||
<span class="font-bold text-red-600">{{ statsContrats.en_retard }}</span>
|
<path d="M 69 22 A 38 38 0 0 1 88 55 L 50 55 Z" fill="#f6ad55" />
|
||||||
</Link>
|
<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>
|
</div>
|
||||||
<Link :href="route('contrats.index')" class="text-gray-400 hover:text-indigo-600 transition-colors">
|
|
||||||
<svg class="h-8 w-8" 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>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Domaines -->
|
<!-- Mutualise Card -->
|
||||||
<div class="rounded-xl bg-white p-5 shadow-sm border border-gray-100 flex items-center justify-between">
|
<div class="bg-white p-6 rounded-3xl shadow-sm border border-gray-100">
|
||||||
<div>
|
<h3 class="font-bold text-gray-900 mb-6 flex items-center gap-2">
|
||||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-gray-500 mb-2">Noms de domaine</h2>
|
<span class="w-2 h-2 rounded-full bg-indigo-500"></span>
|
||||||
<div class="flex gap-6">
|
Exécution Mutualisation
|
||||||
<Link :href="route('domaines.index')" class="flex flex-col">
|
</h3>
|
||||||
<span class="text-xs text-gray-400">Total</span>
|
|
||||||
<span class="font-bold text-gray-900">{{ statsDomaines.total }}</span>
|
<div class="grid grid-cols-2 gap-8">
|
||||||
</Link>
|
<!-- Fonctionnement -->
|
||||||
<Link :href="route('domaines.index')" class="flex flex-col">
|
<div class="flex flex-col items-center">
|
||||||
<span class="text-xs text-gray-400">À renouveler (⏳)</span>
|
<div class="relative w-full h-24 flex items-center justify-center">
|
||||||
<span class="font-bold text-orange-600">{{ statsDomaines.proches }}</span>
|
<svg class="w-full h-full" viewBox="0 0 100 60">
|
||||||
</Link>
|
<path d="M 12 55 A 38 38 0 0 1 88 55" fill="#edf2f7" stroke="#2d3748" stroke-width="1.5" />
|
||||||
<Link :href="route('domaines.index')" class="flex flex-col">
|
<path d="M 12 55 A 38 38 0 0 1 31 22 L 50 55 Z" fill="#48bb78" />
|
||||||
<span class="text-xs text-gray-400">Expirés (⚠️)</span>
|
<path d="M 31 22 A 38 38 0 0 1 69 22 L 50 55 Z" fill="#ecc94b" />
|
||||||
<span class="font-bold text-red-600">{{ statsDomaines.en_retard }}</span>
|
<path d="M 69 22 A 38 38 0 0 1 88 55 L 50 55 Z" fill="#f6ad55" />
|
||||||
</Link>
|
<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>
|
||||||
<Link :href="route('domaines.index')" class="text-gray-400 hover:text-indigo-600 transition-colors">
|
|
||||||
<svg class="h-8 w-8" 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>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Statut breakdown -->
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
<div class="grid grid-cols-2 gap-4 sm:grid-cols-4">
|
<!-- Main Content: Recent Orders -->
|
||||||
<div class="rounded-xl bg-white p-4 shadow-sm border border-gray-100">
|
<div class="lg:col-span-2 space-y-6">
|
||||||
<p class="text-xs font-medium text-gray-500 uppercase">Brouillons</p>
|
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
|
||||||
<p class="mt-1 text-2xl font-bold text-gray-700">{{ stats.brouillons }}</p>
|
<div class="px-6 py-4 border-b border-gray-50 flex items-center justify-between">
|
||||||
</div>
|
<h3 class="font-bold text-gray-900">Dernières commandes</h3>
|
||||||
<div class="rounded-xl bg-white p-4 shadow-sm border border-gray-100">
|
<Link :href="route('commandes.index')" class="text-sm text-blue-600 font-medium hover:underline">Voir tout</Link>
|
||||||
<p class="text-xs font-medium text-yellow-600 uppercase">En validation</p>
|
</div>
|
||||||
<p class="mt-1 text-2xl font-bold text-yellow-700">{{ stats.en_attente_validation }}</p>
|
<div class="divide-y divide-gray-50">
|
||||||
</div>
|
<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="rounded-xl bg-white p-4 shadow-sm border border-gray-100">
|
<div class="flex items-center gap-4">
|
||||||
<p class="text-xs font-medium text-blue-600 uppercase">Validées</p>
|
<div class="p-2 rounded-lg bg-gray-50">
|
||||||
<p class="mt-1 text-2xl font-bold text-blue-700">{{ stats.validees }}</p>
|
<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>
|
||||||
<div class="rounded-xl bg-white p-4 shadow-sm border border-gray-100">
|
<div>
|
||||||
<p class="text-xs font-medium text-indigo-600 uppercase">Commandées</p>
|
<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="mt-1 text-2xl font-bold text-indigo-700">{{ stats.commandees }}</p>
|
<p class="text-xs text-gray-500 truncate max-w-[200px]">{{ cmd.objet }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
<!-- Alertes retard + Services -->
|
<StatutBadge :statut="cmd.statut" size="sm" />
|
||||||
<div class="grid gap-6 lg:grid-cols-2">
|
<span class="text-sm font-bold text-gray-900 min-w-[80px] text-right">{{ formatCurrency(cmd.montant_ttc) }}</span>
|
||||||
<!-- En retard -->
|
</div>
|
||||||
<div class="rounded-xl bg-white shadow-sm border border-gray-100">
|
</div>
|
||||||
<div class="flex items-center justify-between border-b border-gray-100 px-5 py-4">
|
</div>
|
||||||
<h2 class="font-semibold text-gray-900">Commandes en retard</h2>
|
|
||||||
<span class="rounded-full bg-red-100 px-2.5 py-1 text-xs font-medium text-red-700">{{ stats.en_retard }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="divide-y divide-gray-50">
|
</div>
|
||||||
<div v-for="cmd in commandesEnRetard" :key="cmd.id"
|
|
||||||
class="flex items-center justify-between px-5 py-3 hover:bg-gray-50 transition-colors">
|
<!-- Sidebar: Action alerts -->
|
||||||
<div class="min-w-0">
|
<div class="space-y-6">
|
||||||
<Link :href="route('commandes.show', cmd.id)"
|
<!-- Urgent/Late Orders -->
|
||||||
class="truncate text-sm font-medium text-blue-600 hover:underline">
|
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 border-l-4 border-l-red-500 overflow-hidden">
|
||||||
{{ cmd.numero_commande }}
|
<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>
|
</Link>
|
||||||
<p class="truncate text-xs text-gray-500">{{ cmd.objet }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-4 flex-shrink-0 text-right">
|
<div v-if="!commandesEnRetard.length" class="p-10 text-center">
|
||||||
<p class="text-xs text-red-600 font-medium">{{ formatDate(cmd.date_souhaitee) }}</p>
|
<div class="inline-flex p-3 rounded-full bg-green-50 text-green-600 mb-2">
|
||||||
<p class="text-xs text-gray-400">{{ cmd.service?.nom }}</p>
|
<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>
|
</div>
|
||||||
<p v-if="!commandesEnRetard.length" class="px-5 py-6 text-center text-sm text-gray-400">
|
|
||||||
Aucune commande en retard.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Par service -->
|
<!-- Contract Expiry -->
|
||||||
<div class="rounded-xl bg-white shadow-sm border border-gray-100">
|
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
|
||||||
<div class="border-b border-gray-100 px-5 py-4">
|
<div class="px-5 py-4 border-b border-gray-50">
|
||||||
<h2 class="font-semibold text-gray-900">Par service</h2>
|
<h3 class="font-bold text-gray-900 text-sm">Échéancier Contrats</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="divide-y divide-gray-50">
|
<div class="p-5 space-y-4">
|
||||||
<div v-for="service in statsParService" :key="service.id" class="px-5 py-4">
|
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-3">
|
<span class="text-xs text-gray-500">Expirés</span>
|
||||||
<div class="h-3 w-3 rounded-full" :style="{ backgroundColor: service.couleur || '#6B7280' }" />
|
<span class="px-2 py-0.5 rounded-full bg-red-100 text-red-700 text-xs font-bold">{{ statsContrats.en_retard }}</span>
|
||||||
<span class="text-sm font-medium text-gray-900">{{ service.nom }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="text-right">
|
|
||||||
<p class="text-sm font-bold text-gray-900">{{ service.commandes_count }}</p>
|
|
||||||
<p class="text-xs text-gray-500">{{ service.commandes_en_cours_count }} en cours</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 h-1.5 rounded-full bg-gray-100 overflow-hidden">
|
<div class="flex items-center justify-between">
|
||||||
<div class="h-full rounded-full transition-all"
|
<span class="text-xs text-gray-500">À renouveler (90j)</span>
|
||||||
:style="{ width: stats.total ? (service.commandes_count / stats.total * 100) + '%' : '0%', backgroundColor: service.couleur || '#6B7280' }" />
|
<span class="px-2 py-0.5 rounded-full bg-orange-100 text-orange-700 text-xs font-bold">{{ statsContrats.proches }}</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Commandes récentes -->
|
|
||||||
<div class="rounded-xl bg-white shadow-sm border border-gray-100">
|
|
||||||
<div class="flex items-center justify-between border-b border-gray-100 px-5 py-4">
|
|
||||||
<h2 class="font-semibold text-gray-900">Commandes récentes</h2>
|
|
||||||
<Link :href="route('commandes.index')" class="text-sm text-blue-600 hover:underline">Voir tout</Link>
|
|
||||||
</div>
|
|
||||||
<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-5 py-3 text-left text-xs font-medium text-gray-500 uppercase">N°</th>
|
|
||||||
<th class="px-5 py-3 text-left text-xs font-medium text-gray-500 uppercase">Objet</th>
|
|
||||||
<th class="px-5 py-3 text-left text-xs font-medium text-gray-500 uppercase">Service</th>
|
|
||||||
<th class="px-5 py-3 text-left text-xs font-medium text-gray-500 uppercase">Statut</th>
|
|
||||||
<th class="px-5 py-3 text-left text-xs font-medium text-gray-500 uppercase">Priorité</th>
|
|
||||||
<th class="px-5 py-3 text-right text-xs font-medium text-gray-500 uppercase">Montant TTC</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="divide-y divide-gray-50">
|
|
||||||
<tr v-for="cmd in commandesRecentes" :key="cmd.id" class="hover:bg-gray-50 transition-colors">
|
|
||||||
<td class="px-5 py-3 font-medium">
|
|
||||||
<Link :href="route('commandes.show', cmd.id)" class="text-blue-600 hover:underline">
|
|
||||||
{{ cmd.numero_commande }}
|
|
||||||
</Link>
|
|
||||||
</td>
|
|
||||||
<td class="px-5 py-3 max-w-xs truncate text-gray-700">{{ cmd.objet }}</td>
|
|
||||||
<td class="px-5 py-3 text-gray-500">{{ cmd.service?.nom }}</td>
|
|
||||||
<td class="px-5 py-3"><StatutBadge :statut="cmd.statut" /></td>
|
|
||||||
<td class="px-5 py-3"><PrioriteBadge :priorite="cmd.priorite" /></td>
|
|
||||||
<td class="px-5 py-3 text-right font-medium text-gray-900">{{ formatCurrency(cmd.montant_ttc) }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</AuthenticatedLayout>
|
</AuthenticatedLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
51
resources/views/emails/expiring_elements.blade.php
Normal file
51
resources/views/emails/expiring_elements.blade.php
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<x-mail::message>
|
||||||
|
# Alerte d'échéance
|
||||||
|
|
||||||
|
Bonjour,
|
||||||
|
|
||||||
|
Ceci est un message automatique pour vous informer que les éléments suivants arrivent à échéance dans les **30 prochains jours**.
|
||||||
|
|
||||||
|
@if($contrats->isNotEmpty())
|
||||||
|
## 📄 Contrats
|
||||||
|
| Titre | Fournisseur | Échéance |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
@foreach($contrats as $contrat)
|
||||||
|
| {{ $contrat->titre }} | {{ $contrat->fournisseur?->nom ?? 'N/A' }} | {{ $contrat->date_echeance->format('d/m/Y') }} |
|
||||||
|
@endforeach
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($licences->isNotEmpty())
|
||||||
|
## 🔑 Licences
|
||||||
|
| Nom | Fournisseur | Expiration |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
@foreach($licences as $licence)
|
||||||
|
| {{ $licence->nom }} | {{ $licence->fournisseur?->nom ?? 'N/A' }} | {{ $licence->date_expiration->format('d/m/Y') }} |
|
||||||
|
@endforeach
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($assets->isNotEmpty())
|
||||||
|
## 💻 Matériels (Garantie)
|
||||||
|
| Nom | Modèle | Fin Garantie |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
@foreach($assets as $asset)
|
||||||
|
| {{ $asset->nom }} | {{ $asset->modele }} | {{ $asset->date_fin_garantie->format('d/m/Y') }} |
|
||||||
|
@endforeach
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($domaines->isNotEmpty())
|
||||||
|
## 🌐 Domaines
|
||||||
|
| Nom | Prestataire | Échéance |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
@foreach($domaines as $domaine)
|
||||||
|
| {{ $domaine->nom }} | {{ $domaine->prestataire ?? 'N/A' }} | {{ $domaine->date_echeance->format('d/m/Y') }} |
|
||||||
|
@endforeach
|
||||||
|
@endif
|
||||||
|
|
||||||
|
|
||||||
|
<x-mail::button :url="config('app.url')">
|
||||||
|
Accéder au tableau de bord
|
||||||
|
</x-mail::button>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
{{ config('app.name') }}
|
||||||
|
</x-mail::message>
|
||||||
85
resources/views/exports/budgets_pdf.blade.php
Normal file
85
resources/views/exports/budgets_pdf.blade.php
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Rapport Budgétaire {{ $annee }}</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Helvetica', sans-serif; font-size: 10px; color: #333; }
|
||||||
|
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
|
||||||
|
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
||||||
|
th { bg-color: #f8fafc; font-weight: bold; }
|
||||||
|
.header { text-align: center; margin-bottom: 20px; }
|
||||||
|
.footer { position: fixed; bottom: 0; width: 100%; text-align: right; font-size: 8px; }
|
||||||
|
.text-right { text-align: right; }
|
||||||
|
.font-bold { font-weight: bold; }
|
||||||
|
.bg-gray { background-color: #f1f5f9; }
|
||||||
|
.title { font-size: 18px; font-bold; color: #1e293b; margin-bottom: 5px; }
|
||||||
|
.subtitle { font-size: 12px; color: #64748b; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<div class="title">Rapport d'Exécution Budgétaire {{ $annee }}</div>
|
||||||
|
<div class="subtitle">
|
||||||
|
{{ $service }} | {{ $type }} | {{ $envelope }}
|
||||||
|
<br>
|
||||||
|
Généré le {{ date('d/m/Y H:i') }} par {{ $user->name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-gray">
|
||||||
|
<th>Budget / Ligne</th>
|
||||||
|
<th>Service</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th class="text-right">Arbitré (€)</th>
|
||||||
|
<th class="text-right">Consommé (€)</th>
|
||||||
|
<th class="text-right">Engagé (€)</th>
|
||||||
|
<th class="text-right">Total (€)</th>
|
||||||
|
<th class="text-right">Reste (€)</th>
|
||||||
|
<th class="text-right border-l">%</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($lignes as $lb)
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="font-bold">{{ $lb->nom }}</div>
|
||||||
|
<div style="font-size: 8px; color: #64748b;">{{ ucfirst($lb->type_depense) }}</div>
|
||||||
|
</td>
|
||||||
|
<td>{{ $lb->budget->service->nom ?? 'Agglo' }}</td>
|
||||||
|
<td>{{ $lb->budget->type_budget === 'agglo' ? 'Agglomération' : 'Mutualisé' }}</td>
|
||||||
|
<td class="text-right">{{ number_format($lb->montant_arbitre, 2, ',', ' ') }}</td>
|
||||||
|
<td class="text-right">{{ number_format($lb->consomme, 2, ',', ' ') }}</td>
|
||||||
|
<td class="text-right">{{ number_format($lb->engage, 2, ',', ' ') }}</td>
|
||||||
|
<td class="text-right font-bold">{{ number_format($lb->total_cumule, 2, ',', ' ') }}</td>
|
||||||
|
<td class="text-right" style="color: {{ $lb->reste < 0 ? '#e11d48' : '#334155' }};">
|
||||||
|
{{ number_format($lb->reste, 2, ',', ' ') }}
|
||||||
|
</td>
|
||||||
|
<td class="text-right font-bold">
|
||||||
|
{{ $lb->montant_arbitre > 0 ? round(($lb->total_cumule / $lb->montant_arbitre) * 100, 1) : 0 }}%
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr class="bg-gray">
|
||||||
|
<td colspan="3" class="font-bold">TOTAL GÉNÉRAL</td>
|
||||||
|
<td class="text-right font-bold">{{ number_format($lignes->sum('montant_arbitre'), 2, ',', ' ') }} €</td>
|
||||||
|
<td class="text-right font-bold">{{ number_format($lignes->sum('consomme'), 2, ',', ' ') }} €</td>
|
||||||
|
<td class="text-right font-bold">{{ number_format($lignes->sum('engage'), 2, ',', ' ') }} €</td>
|
||||||
|
<td class="text-right font-bold">{{ number_format($lignes->sum('total_cumule'), 2, ',', ' ') }} €</td>
|
||||||
|
<td class="text-right font-bold">{{ number_format($lignes->sum('montant_arbitre') - $lignes->sum('total_cumule'), 2, ',', ' ') }} €</td>
|
||||||
|
<td class="text-right font-bold">
|
||||||
|
{{ $lignes->sum('montant_arbitre') > 0 ? round(($lignes->sum('total_cumule') / $lignes->sum('montant_arbitre')) * 100, 1) : 0 }}%
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
DSI-Commander - Système de gestion budgétaire - Page 1/1
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -6,3 +6,8 @@ use Illuminate\Support\Facades\Artisan;
|
|||||||
Artisan::command('inspire', function () {
|
Artisan::command('inspire', function () {
|
||||||
$this->comment(Inspiring::quote());
|
$this->comment(Inspiring::quote());
|
||||||
})->purpose('Display an inspiring quote');
|
})->purpose('Display an inspiring quote');
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Schedule;
|
||||||
|
|
||||||
|
Schedule::command('app:check-expirations')->dailyAt('08:00');
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,15 @@ Route::middleware(['auth', 'verified'])->group(function () {
|
|||||||
Route::get('/calendar', [CalendarController::class, 'index'])->name('calendar.index');
|
Route::get('/calendar', [CalendarController::class, 'index'])->name('calendar.index');
|
||||||
Route::get('/calendar/events', [CalendarController::class, 'events'])->name('calendar.events');
|
Route::get('/calendar/events', [CalendarController::class, 'events'])->name('calendar.events');
|
||||||
|
|
||||||
|
// Budgets
|
||||||
|
Route::get('/budgets/execution', [\App\Http\Controllers\BudgetController::class, 'execution'])->name('budgets.execution');
|
||||||
|
Route::get('/budgets/export/excel', [\App\Http\Controllers\BudgetController::class, 'exportExcel'])->name('budgets.export.excel');
|
||||||
|
Route::get('/budgets/export/ods', [\App\Http\Controllers\BudgetController::class, 'exportOds'])->name('budgets.export.ods');
|
||||||
|
Route::get('/budgets/export/pdf', [\App\Http\Controllers\BudgetController::class, 'exportPdf'])->name('budgets.export.pdf');
|
||||||
|
Route::resource('budgets', \App\Http\Controllers\BudgetController::class);
|
||||||
|
Route::resource('lignes-budget', \App\Http\Controllers\LigneBudgetController::class)->except(['index', 'show', 'create', 'edit']);
|
||||||
|
Route::patch('/lignes-budget/{ligneBudget}/arbitrer', [\App\Http\Controllers\LigneBudgetController::class, 'arbitrer'])->name('lignes-budget.arbitrer');
|
||||||
|
|
||||||
// Assets
|
// Assets
|
||||||
Route::resource('assets', AssetController::class);
|
Route::resource('assets', AssetController::class);
|
||||||
Route::get('/api/ean-lookup/{ean}', [AssetController::class, 'lookupEan'])->name('assets.ean-lookup');
|
Route::get('/api/ean-lookup/{ean}', [AssetController::class, 'lookupEan'])->name('assets.ean-lookup');
|
||||||
|
|||||||
Reference in New Issue
Block a user