orderBy('created_at', 'desc'); // Recherche full-text sur libellé / fournisseur / numéro if ($request->filled('search')) { $search = $request->input('search'); $query->where(function ($q) use ($search) { $q->where('number', 'like', "%{$search}%") ->orWhere('label', 'like', "%{$search}%") ->orWhere('supplier', 'like', "%{$search}%"); }); } // Filtre par statut if ($request->filled('status')) { $query->byStatus($request->input('status')); } // Filtre par demandeur if ($request->filled('requested_by')) { $query->byDemandeur($request->input('requested_by')); } // Filtre par type if ($request->filled('type')) { $query->where('type', $request->input('type')); } // Filtre par période (date souhaitée de livraison) if ($request->filled('date_start')) { $query->whereDate('delivery_deadline', '>=', $request->input('date_start')); } if ($request->filled('date_end')) { $query->whereDate('delivery_deadline', '<=', $request->input('date_end')); } // Export CSV si demandé if ($request->has('export')) { $orders = $query->get(); $headers = [ "Content-type" => "text/csv; charset=UTF-8", "Content-Disposition" => "attachment; filename=commandes_" . now()->format('Y-m-d_H-i') . ".csv", "Pragma" => "no-cache", "Cache-Control" => "must-revalidate, post-check=0, pre-check=0", "Expires" => "0" ]; $callback = function () use ($orders) { $file = fopen('php://output', 'w'); // Ajouter le BOM UTF-8 pour Excel fprintf($file, chr(0xEF).chr(0xBB).chr(0xBF)); // En-têtes CSV en français fputcsv($file, [ 'Numéro', 'Libellé / Réf Article', 'Type', 'Fournisseur', 'N° Devis', 'Montant HT (€)', 'Montant TTC (€)', 'Demandeur', 'Prescripteur', 'Date livraison souhaitée', 'Statut', 'Date création' ], ';'); foreach ($orders as $order) { fputcsv($file, [ $order->number, $order->label, $order->type, $order->supplier, $order->quote_number, number_format($order->amount_ht, 2, ',', ''), number_format($order->amount_ttc, 2, ',', ''), $order->requested_by, $order->prescriber, $order->delivery_deadline?->format('d/m/Y'), match ($order->status) { 'draft' => 'Brouillon', 'validated' => 'Validée', 'ordered' => 'Commandée', 'delivered' => 'Livrée', 'closed' => 'Clôturée', default => $order->status }, $order->created_at?->format('d/m/Y H:i') ], ';'); } fclose($file); }; return response()->stream($callback, 200, $headers); } // Pagination classique $orders = $query->paginate(10)->withQueryString(); return Inertia::render('Commandes/Index', [ 'orders' => OrderResource::collection($orders), 'filters' => $request->only(['search', 'status', 'requested_by', 'type', 'date_start', 'date_end']), ]); } /** * Formulaire de création. */ public function create() { Gate::authorize('create', Order::class); return Inertia::render('Commandes/Form', [ 'isEdit' => false, ]); } /** * Enregistre une nouvelle commande en base de données. */ public function store(StoreOrderRequest $request, OrderService $orderService) { return DB::transaction(function () use ($request, $orderService) { $validated = $request->validated(); // Calcul automatique de la TVA 20% (sauf si exonéré) $excludeVat = (bool) ($validated['exclude_vat'] ?? false); $validated['amount_ttc'] = $excludeVat ? $validated['amount_ht'] : $validated['amount_ht'] * 1.20; // Génération unique et sécurisée du numéro CMD $validated['number'] = $orderService->generateOrderNumber(); $validated['status'] = 'draft'; // Statut initial $order = Order::create($validated); // Gestion de l'historique initial OrderStatusLog::create([ 'order_id' => $order->id, 'user_id' => $request->user()->id, 'old_status' => null, 'new_status' => 'draft', 'changed_at' => now(), ]); // Gestion de l'upload des fichiers $fileTypes = [ 'quote_file' => 'quote', 'delivery_note_file' => 'delivery_note', 'invoice_file' => 'invoice', ]; foreach ($fileTypes as $inputName => $type) { if ($request->hasFile($inputName)) { $file = $request->file($inputName); // Stockage dans storage/app/public/commandes/{id}/ $path = $file->storeAs("commandes/{$order->id}", $file->getClientOriginalName(), 'public'); Attachment::create([ 'order_id' => $order->id, 'file_path' => $path, 'file_name' => $file->getClientOriginalName(), 'file_type' => $type, ]); } } return redirect()->route('commandes.show', $order->id) ->with('success', 'La demande de commande a été créée avec succès au statut Brouillon.'); }); } /** * Affiche les détails d'une commande. */ public function show(Order $order) { Gate::authorize('view', $order); $order->load(['attachments', 'statusLogs.user']); return Inertia::render('Commandes/Show', [ 'order' => new OrderResource($order), ]); } /** * Formulaire d'édition. */ public function edit(Order $order) { Gate::authorize('update', $order); $order->load('attachments'); return Inertia::render('Commandes/Form', [ 'order' => new OrderResource($order), 'isEdit' => true, ]); } /** * Met à jour les informations d'une commande. */ public function update(UpdateOrderRequest $request, Order $order) { return DB::transaction(function () use ($request, $order) { $validated = $request->validated(); // Recalcul de la TVA (sauf si exonéré) $excludeVat = (bool) ($validated['exclude_vat'] ?? false); $validated['amount_ttc'] = $excludeVat ? $validated['amount_ht'] : $validated['amount_ht'] * 1.20; $order->update($validated); // Gestion de l'upload de nouvelles pièces jointes (ou mise à jour) $fileTypes = [ 'quote_file' => 'quote', 'delivery_note_file' => 'delivery_note', 'invoice_file' => 'invoice', ]; foreach ($fileTypes as $inputName => $type) { if ($request->hasFile($inputName)) { $file = $request->file($inputName); // On supprime l'ancienne pièce jointe de ce type si elle existe $existingAttachment = $order->attachments()->where('file_type', $type)->first(); if ($existingAttachment) { Storage::disk('public')->delete($existingAttachment->file_path); $existingAttachment->delete(); } // Stockage du nouveau fichier $path = $file->storeAs("commandes/{$order->id}", $file->getClientOriginalName(), 'public'); Attachment::create([ 'order_id' => $order->id, 'file_path' => $path, 'file_name' => $file->getClientOriginalName(), 'file_type' => $type, ]); } } return redirect()->route('commandes.show', $order->id) ->with('success', 'La commande a été mise à jour.'); }); } /** * Supprime une commande de la base de données. */ public function destroy(Order $order) { Gate::authorize('delete', $order); return DB::transaction(function () use ($order) { // Suppression physique du répertoire contenant les pièces jointes Storage::disk('public')->deleteDirectory("commandes/{$order->id}"); $order->delete(); return redirect()->route('commandes.index') ->with('success', 'La commande et toutes ses pièces jointes ont été supprimées définitivement.'); }); } /** * Gère les changements de statut (transitions). */ public function transition(Request $request, Order $order, OrderService $orderService) { $request->validate([ 'new_status' => ['required', 'string', 'in:validated,ordered,delivered,closed'], ]); $newStatus = $request->input('new_status'); // Autorisation de la transition selon le rôle et le statut cible Gate::authorize('transition', [$order, $newStatus]); try { $orderService->transitionStatus($order, $newStatus, $request->user()); $statusLabel = match ($newStatus) { 'validated' => 'Validée', 'ordered' => 'Commandée', 'delivered' => 'Livrée', 'closed' => 'Clôturée', default => $newStatus }; return redirect()->back()->with('success', "Le statut de la commande a été mis à jour avec succès : {$statusLabel}."); } catch (\InvalidArgumentException $e) { return redirect()->back()->withErrors(['error' => $e->getMessage()]); } } }