mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-21 05:58:23 +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
281 lines
9.9 KiB
TypeScript
281 lines
9.9 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { useQueryClient, useMutation, useQuery } from "@tanstack/react-query";
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { useAuthUnified } from "@/hooks/useAuthUnified";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import { apiRequest } from "@/lib/queryClient";
|
|
import { isUnauthorizedError } from "@/lib/authUtils";
|
|
import { format } from "date-fns";
|
|
import { safeFormat } from "@/lib/dateUtils";
|
|
import type { Group, Supplier, DeliveryWithRelations, OrderWithRelations } from "@shared/schema";
|
|
|
|
interface EditDeliveryModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
delivery: DeliveryWithRelations | null;
|
|
}
|
|
|
|
export default function EditDeliveryModal({
|
|
isOpen,
|
|
onClose,
|
|
delivery,
|
|
}: EditDeliveryModalProps) {
|
|
const { user } = useAuthUnified();
|
|
const { toast } = useToast();
|
|
const queryClient = useQueryClient();
|
|
|
|
const [formData, setFormData] = useState({
|
|
supplierId: "",
|
|
groupId: "",
|
|
orderId: "",
|
|
scheduledDate: "",
|
|
quantity: "",
|
|
unit: "palettes",
|
|
notes: "",
|
|
});
|
|
|
|
const { data: suppliers = [] } = useQuery<Supplier[]>({
|
|
queryKey: ['/api/suppliers'],
|
|
});
|
|
|
|
const { data: groups = [] } = useQuery<Group[]>({
|
|
queryKey: ['/api/groups'],
|
|
});
|
|
|
|
// Commandes du magasin du formulaire uniquement (paramètre storeId, dont
|
|
// l'accès est contrôlé par le serveur) au lieu de tout l'historique de tous
|
|
// les magasins ; même clé que la modale de création
|
|
const { data: allOrders = [] } = useQuery<OrderWithRelations[]>({
|
|
queryKey: ['/api/orders', 'store', formData.groupId],
|
|
queryFn: async () => {
|
|
const data = await apiRequest(`/api/orders?storeId=${formData.groupId}`);
|
|
return Array.isArray(data) ? data : [];
|
|
},
|
|
enabled: !!formData.groupId,
|
|
});
|
|
|
|
// Filtrer les commandes par fournisseur sélectionné et groupe - montrer toutes les commandes non livrées du même groupe
|
|
const availableOrders = allOrders.filter(order =>
|
|
formData.supplierId && formData.groupId ? (
|
|
order.supplierId === parseInt(formData.supplierId) &&
|
|
order.groupId === parseInt(formData.groupId) &&
|
|
order.status !== 'delivered'
|
|
) : false
|
|
);
|
|
|
|
// Initialiser le formulaire avec les données de la livraison
|
|
useEffect(() => {
|
|
if (delivery) {
|
|
setFormData({
|
|
supplierId: delivery.supplierId.toString(),
|
|
groupId: delivery.groupId.toString(),
|
|
orderId: delivery.orderId ? delivery.orderId.toString() : "none",
|
|
scheduledDate: safeFormat(delivery.scheduledDate, 'yyyy-MM-dd'),
|
|
quantity: delivery.quantity.toString(),
|
|
unit: delivery.unit,
|
|
notes: delivery.notes || "",
|
|
});
|
|
}
|
|
}, [delivery]);
|
|
|
|
const updateDeliveryMutation = useMutation({
|
|
mutationFn: async (data: any) => {
|
|
await apiRequest(`/api/deliveries/${delivery?.id}`, "PUT", data);
|
|
},
|
|
onSuccess: () => {
|
|
toast({
|
|
title: "Succès",
|
|
description: "Livraison modifiée avec succès",
|
|
});
|
|
// Invalider tous les caches liés aux livraisons
|
|
queryClient.invalidateQueries({ queryKey: ['/api/deliveries'] });
|
|
queryClient.invalidateQueries({ queryKey: ['/api/orders'] });
|
|
queryClient.invalidateQueries({ queryKey: ['/api/stats/monthly'] });
|
|
onClose();
|
|
},
|
|
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 modifier la livraison",
|
|
variant: "destructive",
|
|
});
|
|
},
|
|
});
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
if (!formData.supplierId || !formData.groupId || !formData.scheduledDate || !formData.quantity) {
|
|
toast({
|
|
title: "Erreur",
|
|
description: "Veuillez remplir tous les champs obligatoires",
|
|
variant: "destructive",
|
|
});
|
|
return;
|
|
}
|
|
|
|
updateDeliveryMutation.mutate({
|
|
supplierId: parseInt(formData.supplierId),
|
|
groupId: parseInt(formData.groupId),
|
|
orderId: formData.orderId && formData.orderId !== "none" ? parseInt(formData.orderId) : null,
|
|
scheduledDate: formData.scheduledDate,
|
|
quantity: parseInt(formData.quantity),
|
|
unit: formData.unit,
|
|
notes: formData.notes || undefined,
|
|
});
|
|
};
|
|
|
|
const handleChange = (field: string, value: string) => {
|
|
setFormData(prev => ({ ...prev, [field]: value }));
|
|
};
|
|
|
|
if (!delivery) return null;
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
|
<DialogContent className="sm:max-w-md" aria-describedby="edit-delivery-modal-description">
|
|
<DialogHeader>
|
|
<DialogTitle>Modifier la Livraison #{delivery.id}</DialogTitle>
|
|
<p id="edit-delivery-modal-description" className="text-sm text-gray-600 mt-1">
|
|
Modifier les détails de cette livraison
|
|
</p>
|
|
</DialogHeader>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div>
|
|
<Label htmlFor="supplier">Fournisseur *</Label>
|
|
<Select value={formData.supplierId} onValueChange={(value) => handleChange('supplierId', value)}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Sélectionnez un fournisseur" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{suppliers.map((supplier) => (
|
|
<SelectItem key={supplier.id} value={supplier.id.toString()}>
|
|
{supplier.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="group">Magasin/Groupe *</Label>
|
|
<Select value={formData.groupId} onValueChange={(value) => handleChange('groupId', value)}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Sélectionnez un magasin" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{groups.map((group) => (
|
|
<SelectItem key={group.id} value={group.id.toString()}>
|
|
<div className="flex items-center space-x-2">
|
|
<div
|
|
className="w-3 h-3 rounded-full"
|
|
style={{ backgroundColor: group.color || undefined }}
|
|
/>
|
|
<span>{group.name}</span>
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{availableOrders.length > 0 && (
|
|
<div>
|
|
<Label htmlFor="order">Commande liée (optionnel)</Label>
|
|
<Select value={formData.orderId} onValueChange={(value) => handleChange('orderId', value)}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Sélectionnez une commande" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="none">Aucune commande liée</SelectItem>
|
|
{availableOrders.map((order) => (
|
|
<SelectItem key={order.id} value={order.id.toString()}>
|
|
Commande #{order.id} - {order.supplier.name} - {safeFormat(order.plannedDate, 'dd/MM/yyyy')}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
)}
|
|
|
|
<div>
|
|
<Label htmlFor="scheduledDate">Date prévue *</Label>
|
|
<Input
|
|
id="scheduledDate"
|
|
type="date"
|
|
value={formData.scheduledDate}
|
|
onChange={(e) => handleChange('scheduledDate', e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<Label htmlFor="quantity">Quantité *</Label>
|
|
<Input
|
|
id="quantity"
|
|
type="number"
|
|
min="1"
|
|
value={formData.quantity}
|
|
onChange={(e) => handleChange('quantity', e.target.value)}
|
|
placeholder="Ex: 10"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="unit">Unité *</Label>
|
|
<Select value={formData.unit} onValueChange={(value) => handleChange('unit', value)}>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="palettes">Palettes</SelectItem>
|
|
<SelectItem value="colis">Colis</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="notes">Commentaires</Label>
|
|
<Textarea
|
|
id="notes"
|
|
placeholder="Commentaires optionnels..."
|
|
value={formData.notes}
|
|
onChange={(e) => handleChange('notes', e.target.value)}
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex justify-end space-x-2 pt-4">
|
|
<Button type="button" variant="outline" onClick={onClose}>
|
|
Annuler
|
|
</Button>
|
|
<Button
|
|
type="submit"
|
|
disabled={updateDeliveryMutation.isPending}
|
|
className="bg-primary hover:bg-blue-700"
|
|
>
|
|
{updateDeliveryMutation.isPending ? "Modification..." : "Modifier"}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
} |