mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-21 22:18:17 +02:00
- Routes en React.lazy (sauf connexion et accueil) avec un Suspense dans la zone de contenu ; recharts n'est plus préchargé. JS initial : 1,72 Mo -> 625 Ko (183 Ko gzip). Rechargement automatique unique si un ancien fichier JS a disparu après un déploiement, et ErrorBoundary par page pour garder le menu affiché en cas d'erreur. - Authentification unifiée sur le cache React Query ['/api/user'] : un seul GET /api/user pour toute l'application (au lieu de 4 à 5 par page), plus de rechargement complet après la connexion. - Pas de nouvel essai sur les erreurs 4xx ; fournisseurs et magasins en cache 5 minutes ; valeur du StoreContext mémoïsée et redimensionnement qui ne re-rend que lorsque le palier d'écran change. - Pages : requêtes inutilisées ou en double supprimées, appels lancés seulement une fois l'utilisateur connu, invalidations ciblées au lieu de rechargements complets, filtres et tris mémoïsés, anciennes données gardées (et estompées) pendant un changement de filtre ou de mois, recherche DLC mobile temporisée, vérifications de factures à concurrence bornée, navigation interne sans rechargement de la page. - index.html : script de bannière Replit retiré, lang="fr". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MsDJjQrAggcJwbbBtKhgyb
184 lines
6.5 KiB
TypeScript
184 lines
6.5 KiB
TypeScript
import { useState } from "react";
|
|
import { useForm } from "react-hook-form";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { z } from "zod";
|
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
|
import { Input } from "@/components/ui/input";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import { apiRequest } from "@/lib/queryClient";
|
|
import { isUnauthorizedError } from "@/lib/authUtils";
|
|
import { Check, X, AlertTriangle } from "lucide-react";
|
|
|
|
const validateDeliverySchema = z.object({
|
|
blNumber: z.string().min(1, "Le numéro de bon de livraison est obligatoire"),
|
|
});
|
|
|
|
type ValidateDeliveryForm = z.infer<typeof validateDeliverySchema>;
|
|
|
|
interface ValidateDeliveryModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
delivery: any;
|
|
onValidated?: () => void; // Callback pour fermer le modal parent
|
|
}
|
|
|
|
export default function ValidateDeliveryModal({
|
|
isOpen,
|
|
onClose,
|
|
delivery,
|
|
onValidated,
|
|
}: ValidateDeliveryModalProps) {
|
|
const { toast } = useToast();
|
|
const queryClient = useQueryClient();
|
|
|
|
const form = useForm<ValidateDeliveryForm>({
|
|
resolver: zodResolver(validateDeliverySchema),
|
|
defaultValues: {
|
|
blNumber: "",
|
|
},
|
|
});
|
|
|
|
const validateDeliveryMutation = useMutation({
|
|
mutationFn: async (data: ValidateDeliveryForm) => {
|
|
const payload = {
|
|
blNumber: data.blNumber.trim()
|
|
};
|
|
|
|
await apiRequest(`/api/deliveries/${delivery.id}/validate`, "POST", payload);
|
|
},
|
|
onSuccess: () => {
|
|
toast({
|
|
title: "Succès",
|
|
description: "Livraison validée avec succès",
|
|
});
|
|
// Invalider tous les caches liés aux livraisons et les caches BL/Rapprochement
|
|
// avec toutes les variations de clés, en un seul appel (un second appel sur
|
|
// ['/api/deliveries'] relançait le chargement des listes affichées)
|
|
queryClient.invalidateQueries({
|
|
predicate: (query) =>
|
|
query.queryKey[0] === '/api/deliveries/bl' ||
|
|
query.queryKey[0] === '/api/deliveries'
|
|
});
|
|
queryClient.invalidateQueries({ queryKey: ['/api/orders'] });
|
|
queryClient.invalidateQueries({ queryKey: ['/api/stats/monthly'] });
|
|
form.reset();
|
|
onClose();
|
|
// Fermer aussi le modal parent si fourni
|
|
if (onValidated) {
|
|
onValidated();
|
|
}
|
|
},
|
|
onError: (error) => {
|
|
if (isUnauthorizedError(error)) {
|
|
toast({
|
|
title: "Non autorisé",
|
|
description: "Vous êtes déconnecté. Reconnexion...",
|
|
variant: "destructive",
|
|
});
|
|
setTimeout(() => {
|
|
window.location.href = "/api/login";
|
|
}, 500);
|
|
return;
|
|
}
|
|
toast({
|
|
title: "Erreur",
|
|
description: "Impossible de valider la livraison",
|
|
variant: "destructive",
|
|
});
|
|
},
|
|
});
|
|
|
|
const onSubmit = (data: ValidateDeliveryForm) => {
|
|
validateDeliveryMutation.mutate(data);
|
|
};
|
|
|
|
if (!delivery) return null;
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
|
<DialogContent className="sm:max-w-md" aria-describedby="validate-delivery-modal-description">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center space-x-2">
|
|
<Check className="w-5 h-5 text-green-600 border border-gray-300 rounded p-0.5" />
|
|
<span>Valider la livraison</span>
|
|
</DialogTitle>
|
|
<p id="validate-delivery-modal-description" className="text-sm text-gray-600 mt-1">
|
|
Saisir les informations du bon de livraison
|
|
</p>
|
|
</DialogHeader>
|
|
|
|
{/* Alerte si le fournisseur nécessite un contrôle */}
|
|
{delivery.supplier?.requiresControl && (
|
|
<div className="bg-orange-50 border border-orange-200 rounded-lg p-3 flex items-start space-x-3">
|
|
<AlertTriangle className="w-5 h-5 text-orange-600 flex-shrink-0 mt-0.5" />
|
|
<div className="flex-1">
|
|
<h4 className="text-sm font-semibold text-orange-800">
|
|
⚠️ Contrôle à effectuer sur cette livraison
|
|
</h4>
|
|
<p className="text-xs text-orange-700 mt-1">
|
|
Ce fournisseur nécessite un contrôle qualité lors de la réception de la livraison.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<Form {...form}>
|
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
|
{/* Informations de la livraison */}
|
|
<div className="bg-gray-50 rounded-lg p-4 space-y-2">
|
|
<h4 className="font-medium text-gray-900">Détails de la livraison</h4>
|
|
<div className="text-sm text-gray-600">
|
|
<p><strong>Fournisseur:</strong> {delivery.supplier?.name}</p>
|
|
<p><strong>Magasin:</strong> {delivery.group?.name}</p>
|
|
<p><strong>Quantité:</strong> {delivery.quantity} {delivery.unit === 'palettes' ? 'Palettes' : 'Colis'}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Numéro de BL */}
|
|
<FormField
|
|
control={form.control}
|
|
name="blNumber"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>N° Bon de Livraison *</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
placeholder="Ex: BL-2024-001"
|
|
{...field}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
|
|
{/* Actions */}
|
|
<div className="flex justify-end space-x-3 pt-4">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={onClose}
|
|
disabled={validateDeliveryMutation.isPending}
|
|
>
|
|
<X className="w-4 h-4 mr-2" />
|
|
Annuler
|
|
</Button>
|
|
<Button
|
|
type="submit"
|
|
disabled={validateDeliveryMutation.isPending}
|
|
className="bg-green-600 hover:bg-green-700"
|
|
>
|
|
<Check className="w-4 h-4 mr-2 border border-gray-300 rounded p-0.5" />
|
|
{validateDeliveryMutation.isPending ? "Validation..." : "Valider livraison"}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</Form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
} |