when($request->service_id, fn ($q) => $q->parService($request->service_id)) ->when($request->fournisseur_id, fn ($q) => $q->parFournisseur($request->fournisseur_id)) ->when($request->commune_id, fn ($q) => $q->where('commune_id', $request->commune_id)) ->when($request->statut, fn ($q) => $q->parStatut($request->statut)) ->when($request->priorite, fn ($q) => $q->where('priorite', $request->priorite)) ->when($request->date_from, fn ($q) => $q->whereDate('date_demande', '>=', $request->date_from)) ->when($request->date_to, fn ($q) => $q->whereDate('date_demande', '<=', $request->date_to)) ->when($request->search, function ($q) use ($request) { $q->where(function ($sub) use ($request) { $sub->where('numero_commande', 'like', "%{$request->search}%") ->orWhere('objet', 'like', "%{$request->search}%") ->orWhereHas('fournisseur', fn ($f) => $f->where('nom', 'like', "%{$request->search}%")); }); }); $commandes = $query->latest()->paginate(20)->withQueryString(); return Inertia::render('Commandes/Index', [ 'commandes' => $commandes, 'services' => Service::all(), 'fournisseurs' => Fournisseur::active()->orderBy('nom')->get(), 'communes' => \App\Models\Commune::orderBy('nom')->get(), 'filters' => $request->only(['search', 'service_id', 'fournisseur_id', 'statut', 'priorite', 'date_from', 'date_to']), ]); } public function create(): Response { $this->authorize('create', Commande::class); return Inertia::render('Commandes/Create', [ 'services' => Service::all(), 'fournisseurs' => Fournisseur::active()->orderBy('nom')->get(), 'communes' => \App\Models\Commune::orderBy('nom')->get(), 'categories' => Categorie::active()->orderBy('ordre')->get(), 'articles' => Article::active()->with('categorie')->orderBy('designation')->get(), 'lignesBudget' => LigneBudget::with('budget.service')->orderByDesc('created_at')->get(), ]); } public function store(Request $request): RedirectResponse { $this->authorize('create', Commande::class); $validated = $request->validate([ 'service_id' => 'required|exists:services,id', 'fournisseur_id' => 'nullable|exists:fournisseurs,id', 'commune_id' => 'nullable|exists:communes,id', 'objet' => 'required|string|max:255', 'description' => 'nullable|string', 'justification' => 'nullable|string', 'priorite' => 'required|in:normale,haute,urgente', 'reference_fournisseur' => 'nullable|string|max:100', 'imputation_budgetaire' => 'nullable|string|max:100', 'ligne_budget_id' => 'nullable|exists:ligne_budgets,id', 'date_demande' => 'required|date', 'date_souhaitee' => 'nullable|date|after_or_equal:date_demande', 'date_livraison_prevue' => 'nullable|date', 'notes' => 'nullable|string', 'notes_fournisseur' => 'nullable|string', 'lignes' => 'array', 'lignes.*.designation' => 'required|string|max:255', 'lignes.*.reference' => 'nullable|string|max:100', 'lignes.*.quantite' => 'required|numeric|min:0.001', 'lignes.*.unite' => 'nullable|string|max:30', 'lignes.*.prix_unitaire_ht' => 'nullable|numeric|min:0', 'lignes.*.taux_tva' => 'nullable|numeric|min:0|max:100', 'lignes.*.categorie_id' => 'nullable|exists:categories,id', 'lignes.*.article_id' => 'nullable|exists:articles,id', 'lignes.*.notes' => 'nullable|string', ]); // Vérification du budget disponible avant création if (!empty($validated['ligne_budget_id'])) { $montantEstime = $this->estimerMontantTtc($validated); $budgetError = $this->checkBudgetDisponible( $validated['ligne_budget_id'], $montantEstime ); if ($budgetError) { return back()->withErrors(['ligne_budget_id' => $budgetError])->withInput(); } } DB::transaction(function () use ($validated, $request) { $commande = Commande::create([ ...$validated, 'numero_commande' => Commande::genererNumero(), 'user_id' => $request->user()->id, 'statut' => 'brouillon', ]); foreach ($validated['lignes'] ?? [] as $index => $ligne) { $commande->lignes()->create(array_merge($ligne, ['ordre' => $index])); } }); return redirect()->route('commandes.index') ->with('success', 'Commande créée avec succès.'); } public function show(Commande $commande): Response { $this->authorize('view', $commande); $commande->load([ 'service', 'fournisseur', 'demandeur', 'validateur', 'acheteur', 'commune', 'lignes.categorie', 'historique.user', 'piecesJointes.user', 'assets', 'ligneBudget.budget.service' ]); $transitionsDisponibles = collect(Commande::STATUT_TRANSITIONS[$commande->statut] ?? []) ->filter(fn ($statut) => request()->user()->hasRole('admin') || $this->userCanTransition(request()->user(), $commande, $statut)) ->values(); return Inertia::render('Commandes/Show', [ 'commande' => $commande, 'transitionsDisponibles' => $transitionsDisponibles, ]); } public function edit(Commande $commande): Response { $this->authorize('update', $commande); $commande->load('lignes.categorie'); return Inertia::render('Commandes/Edit', [ 'commande' => $commande, 'services' => Service::all(), 'fournisseurs' => Fournisseur::active()->orderBy('nom')->get(), 'communes' => \App\Models\Commune::orderBy('nom')->get(), 'categories' => Categorie::active()->orderBy('ordre')->get(), 'articles' => Article::active()->with('categorie')->orderBy('designation')->get(), 'lignesBudget' => LigneBudget::with('budget.service')->orderByDesc('created_at')->get(), ]); } public function update(Request $request, Commande $commande): RedirectResponse { $this->authorize('update', $commande); $validated = $request->validate([ 'service_id' => 'required|exists:services,id', 'fournisseur_id' => 'nullable|exists:fournisseurs,id', 'commune_id' => 'nullable|exists:communes,id', 'objet' => 'required|string|max:255', 'description' => 'nullable|string', 'justification' => 'nullable|string', 'priorite' => 'required|in:normale,haute,urgente', 'reference_fournisseur' => 'nullable|string|max:100', 'imputation_budgetaire' => 'nullable|string|max:100', 'ligne_budget_id' => 'nullable|exists:ligne_budgets,id', 'date_demande' => 'required|date', 'date_souhaitee' => 'nullable|date', 'date_livraison_prevue' => 'nullable|date', 'notes' => 'nullable|string', 'notes_fournisseur' => 'nullable|string', 'lignes' => 'array', 'lignes.*.id' => 'nullable|integer', 'lignes.*.designation' => 'required|string|max:255', 'lignes.*.reference' => 'nullable|string|max:100', 'lignes.*.quantite' => 'required|numeric|min:0.001', 'lignes.*.quantite_recue' => 'nullable|numeric|min:0', 'lignes.*.unite' => 'nullable|string|max:30', 'lignes.*.prix_unitaire_ht' => 'nullable|numeric|min:0', 'lignes.*.taux_tva' => 'nullable|numeric|min:0|max:100', 'lignes.*.categorie_id' => 'nullable|exists:categories,id', 'lignes.*.article_id' => 'nullable|exists:articles,id', 'lignes.*.notes' => 'nullable|string', ]); // Vérification du budget disponible si la ligne budget change ou si le montant change if (!empty($validated['ligne_budget_id'])) { $montantEstime = $this->estimerMontantTtc($validated); $budgetError = $this->checkBudgetDisponible( $validated['ligne_budget_id'], $montantEstime, $commande->id ); if ($budgetError) { return back()->withErrors(['ligne_budget_id' => $budgetError])->withInput(); } } DB::transaction(function () use ($validated, $commande) { $commande->update($validated); $lignesData = $validated['lignes'] ?? []; $lignesIds = collect($lignesData)->pluck('id')->filter()->all(); // Supprimer les lignes retirées $commande->lignes()->whereNotIn('id', $lignesIds)->delete(); foreach ($lignesData as $index => $ligneData) { $commande->lignes()->updateOrCreate( ['id' => $ligneData['id'] ?? null], array_merge($ligneData, ['ordre' => $index, 'commande_id' => $commande->id]) ); } }); return redirect()->route('commandes.show', $commande) ->with('success', 'Commande mise à jour.'); } public function destroy(Commande $commande): RedirectResponse { $this->authorize('delete', $commande); $commande->delete(); return redirect()->route('commandes.index') ->with('success', 'Commande supprimée.'); } public function transition(Request $request, Commande $commande): RedirectResponse { $validated = $request->validate([ 'statut' => 'required|string', 'commentaire' => 'nullable|string|max:1000', ]); $this->authorize('transition', [$commande, $validated['statut']]); $ok = $commande->transitionnerVers($validated['statut'], $request->user(), $validated['commentaire'] ?? null); if (!$ok) { return back()->with('error', 'Transition non autorisée.'); } return redirect()->route('commandes.show', $commande) ->with('success', 'Statut mis à jour : ' . Commande::STATUTS_LABELS[$validated['statut']]); } private function userCanTransition($user, Commande $commande, string $statut): bool { try { $this->authorize('transition', [$commande, $statut]); return true; } catch (\Exception) { return false; } } /** * Estime le montant TTC total depuis les lignes soumises dans la requête. * Utilisé avant la création/mise à jour pour vérifier le budget disponible. */ private function estimerMontantTtc(array $validated): float { $total = 0.0; foreach ($validated['lignes'] ?? [] as $ligne) { $ht = (float) ($ligne['prix_unitaire_ht'] ?? 0); $qty = (float) ($ligne['quantite'] ?? 1); $tva = (float) ($ligne['taux_tva'] ?? 20); $total += $ht * $qty * (1 + $tva / 100); } return $total; } /** * Vérifie que la ligne budgétaire a suffisamment de disponible pour accueillir le montant. * Retourne un message d'erreur ou null si tout va bien. * Le paramètre $excludeCommandeId permet d'exclure la commande en cours d'édition du calcul. */ private function checkBudgetDisponible(int $ligneBudgetId, float $montantTtc, ?int $excludeCommandeId = null): ?string { $ligne = LigneBudget::find($ligneBudgetId); if (!$ligne || $ligne->montant_arbitre === null) { return null; // Pas encore arbitrée, pas de blocage } // Recalcule le disponible en excluant éventuellement la commande en cours d'édition, // en utilisant les mêmes statuts que getMontantDisponibleAttribute pour la cohérence. $statutsConsommes = ['validee', 'commandee', 'partiellement_recue', 'recue_complete', 'cloturee']; $statutsEngages = ['brouillon', 'en_attente_validation']; $queryConsomme = $ligne->commandes()->whereIn('statut', $statutsConsommes); $queryEngage = $ligne->commandes()->whereIn('statut', $statutsEngages); if ($excludeCommandeId) { $queryConsomme->where('id', '!=', $excludeCommandeId); $queryEngage->where('id', '!=', $excludeCommandeId); } $disponible = (float) $ligne->montant_arbitre - (float) $queryConsomme->sum('montant_ttc') - (float) $queryEngage->sum('montant_ttc'); if ($montantTtc > $disponible) { return sprintf( 'Budget insuffisant sur "%s". Disponible : %s €, demandé : %s €.', $ligne->nom, number_format($disponible, 2, ',', ' '), number_format($montantTtc, 2, ',', ' ') ); } return null; } public function exportPdf(Commande $commande) { $this->authorize('view', $commande); $commande->load([ 'service', 'fournisseur', 'demandeur', 'validateur', 'acheteur', 'commune', 'lignes.categorie', ]); $pdf = \Barryvdh\DomPDF\Facade\Pdf::loadView('pdf.commande', [ 'commande' => $commande, ]); return $pdf->stream('commande-' . $commande->numero_commande . '.pdf'); } }