merge(main): resolve conflicts keeping codefou + email on suppliers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Sonnet 4.6 committed 2026-05-15 11:14:35 +02:00
commit b8ecacee3b
20 files changed
+719 -299

No files matched your search

+102 -72
View File
@@ -3,6 +3,8 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useQuery } from "@tanstack/react-query";
import { useAuthUnified } from "@/hooks/useAuthUnified";
import { useState, useRef } from "react";
import { Loader2, Search } from "lucide-react";
import {
Form,
FormControl,
@@ -68,15 +70,6 @@ export function CustomerOrderForm({
const { user } = useAuthUnified();
const { selectedStoreId } = useStore();
// DEBUG: Log user data to see what we actually receive
console.log("🔍 FRONTEND USER DEBUG:", {
user: user,
userRole: user?.role,
userGroups: user?.userGroups,
userGroupsType: typeof user?.userGroups,
userGroupsLength: user?.userGroups?.length,
fullUserObject: JSON.stringify(user, null, 2)
});
// Fetch groups for store selection
const { data: groups = [] } = useQuery<Group[]>({
@@ -128,51 +121,18 @@ export function CustomerOrderForm({
});
const handleSubmit = (data: CustomerOrderFormData) => {
console.log("🚀 FORM SUBMIT STARTED");
console.log("📝 Form submission data:", data);
console.log("🔍 Form errors:", form.formState.errors);
console.log("✅ Form is valid:", form.formState.isValid);
console.log("👤 User context DETAILED:", {
role: user?.role,
userGroups: user?.userGroups,
userGroupsLength: user?.userGroups?.length,
userGroupsData: user?.userGroups?.map(ug => ({
groupId: ug.groupId,
group: ug.group,
fullObject: ug
})),
selectedStoreId,
fullUser: user
});
// Validate required fields
if (!data.customerName || data.customerName.trim() === '') {
console.error("❌ Customer name is required but empty");
return;
}
// Always use user's assigned group - no override needed
if (!data.customerName || data.customerName.trim() === '') return;
const groupId = getUserAssignedGroupId();
if (!groupId) {
console.error("❌❌❌ CRITICAL: No group available for user:", {
role: user?.role,
userGroups: user?.userGroups,
userGroupsCount: user?.userGroups?.length,
selectedStoreId,
getUserAssignedGroupIdResult: groupId
});
alert("ERREUR: Votre utilisateur n'a pas de magasin assigné. Contactez l'administrateur.");
return;
}
console.log("✅ Customer Order: Using assigned group:", groupId, "for user:", user?.role);
// Prepare final data with user's assigned group
const submitData = {
orderTaker: data.orderTaker || user?.firstName + ' ' + user?.lastName || user?.username || "Inconnu",
customerName: data.customerName.trim(),
customerPhone: data.contactNumber || '',
customerPhone: data.contactNumber || '',
customerEmail: data.customerEmail || '',
productDesignation: data.productName || '',
productReference: data.productReference || '',
@@ -184,29 +144,75 @@ export function CustomerOrderForm({
isPromotionalPrice: data.isPromotionalPrice || false,
customerNotified: data.customerNotified || false,
notes: data.notes || '',
groupId: groupId, // Always use the assigned group
groupId: groupId,
};
console.log("✅ Customer Order Submit FINAL:", {
userRole: user?.role,
assignedGroupId: groupId,
submitData: submitData
});
console.log("🔥 CALLING onSubmit with data:", submitData);
onSubmit(submitData);
console.log("🔥 onSubmit CALLED");
};
// Auto-ensure groupId is always set to user's assigned group
const currentGroupId = form.getValues('groupId');
const userGroupId = getUserAssignedGroupId();
if (!currentGroupId && userGroupId) {
form.setValue('groupId', userGroupId);
console.log("🏪 Auto-setting user's assigned group:", userGroupId);
}
// Lookup API ffnancy par gencode ou référence (déclenché manuellement)
const [articleLookupLoading, setArticleLookupLoading] = useState(false);
const [articleNotFound, setArticleNotFound] = useState(false);
const fetchAndFillArticle = async (params: URLSearchParams) => {
setArticleLookupLoading(true);
setArticleNotFound(false);
try {
const res = await fetch(`/api/ffnancy/articles?${params}&limit=1`, { credentials: 'include' });
if (!res.ok) { setArticleNotFound(true); return; }
const data = await res.json();
const article = data.articles?.[0];
if (!article) { setArticleNotFound(true); return; }
setArticleNotFound(false);
form.setValue("productName", article.libelle1, { shouldValidate: true });
if (article.gtin) form.setValue("gencode", article.gtin, { shouldValidate: true });
if (article.ref_fou_principale) form.setValue("productReference", article.ref_fou_principale, { shouldValidate: true });
// Fournisseur : dernière entrée en stock en priorité
let codefouToMatch = article.codefou_principal;
try {
const mvtRes = await fetch(
`/api/ffnancy/mouvements/entrees?artNoId=${article.no_id}&limit=1&dateDebut=2000-01-01`,
{ credentials: 'include' }
);
if (mvtRes.ok) {
const mvtData = await mvtRes.json();
const lastEntree = mvtData.entrees?.[0];
if (lastEntree?.codefou) codefouToMatch = lastEntree.codefou;
}
} catch { /* fallback codefou_principal */ }
if (codefouToMatch) {
const matched = (suppliers as any[]).find((s: any) =>
s.codefou && s.codefou.trim().toLowerCase() === codefouToMatch.trim().toLowerCase()
) || (suppliers as any[]).find((s: any) =>
s.name.toLowerCase().trim().includes(article.nom_fou_principal?.toLowerCase().trim() || '') ||
(article.nom_fou_principal?.toLowerCase().trim() || '').includes(s.name.toLowerCase().trim())
);
if (matched) form.setValue("supplierId", matched.id, { shouldValidate: true });
}
} catch { setArticleNotFound(true); } finally {
setArticleLookupLoading(false);
}
};
const handleSearchByGencode = () => {
const val = form.getValues("gencode");
if (val) fetchAndFillArticle(new URLSearchParams({ ean: val }));
};
const handleSearchByReference = () => {
const val = form.getValues("productReference");
if (val) fetchAndFillArticle(new URLSearchParams({ codein: val }));
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
@@ -310,7 +316,19 @@ export function CustomerOrderForm({
<FormItem>
<FormLabel>Référence (optionnel)</FormLabel>
<FormControl>
<Input placeholder="REF-123456" {...field} />
<div className="flex gap-2">
<Input placeholder="REF-123456" {...field} />
<Button
type="button"
variant="outline"
size="sm"
onClick={handleSearchByReference}
disabled={articleLookupLoading}
className="shrink-0"
>
{articleLookupLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Search className="w-4 h-4" />}
</Button>
</div>
</FormControl>
<FormMessage />
</FormItem>
@@ -324,7 +342,19 @@ export function CustomerOrderForm({
<FormItem>
<FormLabel>Gencode (obligatoire)</FormLabel>
<FormControl>
<Input placeholder="Code à barres" {...field} />
<div className="flex gap-2">
<Input placeholder="Code à barres" {...field} />
<Button
type="button"
variant="outline"
size="sm"
onClick={handleSearchByGencode}
disabled={articleLookupLoading}
className="shrink-0"
>
{articleLookupLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Search className="w-4 h-4" />}
</Button>
</div>
</FormControl>
<FormMessage />
</FormItem>
@@ -403,26 +433,26 @@ export function CustomerOrderForm({
{/* Case "Client appelé" cachée pour nouvelles commandes */}
</div>
{/* Produit non référencé — uniquement en création */}
{articleNotFound && !order && (
<div className="flex items-center gap-2 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
<span className="font-medium">Produit non référencé</span> — ce produit n'existe pas dans la base. La commande ne peut pas être créée.
</div>
)}
{/* Actions */}
<div className="flex justify-end space-x-3 pt-4">
<Button
type="button"
variant="outline"
<Button
type="button"
variant="outline"
onClick={onCancel}
disabled={isLoading}
>
Annuler
</Button>
<Button
<Button
type="submit"
disabled={isLoading}
onClick={(e) => {
console.log("Submit button clicked");
console.log("Form state:", form.formState);
console.log("Form values:", form.getValues());
console.log("Form validation errors:", form.formState.errors);
console.log("Current user context:", user?.userGroups, "available groups:", groups);
}}
disabled={isLoading || (articleNotFound && !order) || articleLookupLoading}
>
{isLoading ? "Enregistrement..." : order ? "Modifier" : "Créer"}
</Button>
+2 -50
View File
@@ -28,9 +28,7 @@ export default function Layout({ children }: LayoutProps) {
const [selectedStoreId, setSelectedStoreId] = useState<number | null>(() => {
// Restaurer le selectedStoreId depuis localStorage si disponible
const saved = localStorage.getItem('selectedStoreId');
const restoredId = saved ? parseInt(saved) : null;
console.log('🏪 Layout - Restoring selectedStoreId from localStorage:', { saved, restoredId });
return restoredId;
return saved ? parseInt(saved) : null;
});
const [storeInitialized, setStoreInitialized] = useState(false);
@@ -39,10 +37,8 @@ export default function Layout({ children }: LayoutProps) {
// Restaurer l'état de la sidebar depuis localStorage, mais forcer collapsed sur mobile
try {
const saved = localStorage.getItem('sidebarCollapsed');
console.log('🔧 Layout - Restoring sidebar state:', { saved, isMobile });
return isMobile ? true : (saved ? JSON.parse(saved) : false);
} catch (error) {
console.error('Error reading sidebar state from localStorage:', error);
} catch {
return isMobile ? true : false;
}
});
@@ -63,55 +59,21 @@ export default function Layout({ children }: LayoutProps) {
if (selectedStoreId) {
const storeExists = stores.find(store => store.id === selectedStoreId);
if (!storeExists) {
console.log('🏪 Selected store not found in available stores, clearing selection:', {
selectedStoreId,
userRole: user.role,
availableStores: stores.map(s => s.id)
});
setSelectedStoreId(null);
localStorage.removeItem('selectedStoreId');
needsUpdate = true;
} else {
console.log('🏪 Selected store validated successfully:', {
selectedStoreId,
userRole: user.role,
storeName: storeExists.name
});
}
}
// IMPORTANT FIX: Pour les rôles directeur/manager, forcer la sélection d'un magasin spécifique
// si aucun n'est sélectionné et qu'ils n'ont accès qu'à un seul magasin
if ((user.role === 'directeur' || user.role === 'manager') && !selectedStoreId && stores.length === 1 && !needsUpdate) {
console.log('🏪 Auto-selecting single available store for directeur/manager:', {
storeId: stores[0].id,
storeName: stores[0].name,
userRole: user.role,
userId: user.id,
previousSelectedStoreId: selectedStoreId,
timestamp: new Date().toISOString()
});
const singleStoreId = stores[0].id;
setSelectedStoreId(singleStoreId);
localStorage.setItem('selectedStoreId', singleStoreId.toString());
}
// VALIDATION CRITIQUE: Pour les directeurs/managers, s'assurer qu'un magasin est toujours sélectionné
if ((user.role === 'directeur' || user.role === 'manager') && !selectedStoreId && !needsUpdate) {
console.log('🚨 CRITICAL: Directeur/Manager has no store selected, this will cause empty data display:', {
userId: user.id,
userRole: user.role,
availableStores: stores.map(s => ({ id: s.id, name: s.name }))
});
}
console.log('🏪 Store initialization complete:', {
user: user.role,
selectedStoreId,
storesCount: stores.length,
validatedStore: selectedStoreId ? stores.find(s => s.id === selectedStoreId)?.name : 'None',
autoSelected: (user.role === 'directeur' || user.role === 'manager') && !selectedStoreId && stores.length === 1
});
setStoreInitialized(true);
}
}, [user, stores, selectedStoreId]);
@@ -172,15 +134,7 @@ export default function Layout({ children }: LayoutProps) {
<Select
value={selectedStoreId?.toString() || (user.role === 'admin' ? "all" : "")}
onValueChange={(value) => {
console.log('🏪 Store selector changed:', {
value,
userRole: user.role,
parsed: value === "all" ? null : parseInt(value)
});
const newStoreId = value === "all" ? null : parseInt(value);
// Invalider toutes les variantes de queryKey pour changement magasin
console.log('🧹 Invalidating data caches for store change');
queryClient.invalidateQueries({
predicate: (query) => {
const key = query.queryKey;
@@ -194,10 +148,8 @@ export default function Layout({ children }: LayoutProps) {
// Sauvegarder dans localStorage et mettre à jour l'état
if (newStoreId) {
localStorage.setItem('selectedStoreId', newStoreId.toString());
console.log('💾 Store saved to localStorage:', newStoreId);
} else {
localStorage.removeItem('selectedStoreId');
console.log('🗑️ Store removed from localStorage');
}
setSelectedStoreId(newStoreId);
}}
@@ -26,7 +26,6 @@ import SavTickets from "@/pages/SavTickets";
import Avoirs from "@/pages/Avoirs";
import WeatherSettings from "@/pages/WeatherSettings";
import Analytics from "@/pages/Analytics";
import SalesAnalysisPage from "@/pages/SalesAnalysisPage";
import PaymentSchedulePage from "@/pages/PaymentSchedulePage";
import Contacts from "@/pages/Contacts";
import Layout from "@/components/Layout";
@@ -127,7 +126,6 @@ function RouterProduction() {
<Route path="/sav" component={MobileSavPage} />
<Route path="/avoirs" component={MobileAvoirsPage} />
<Route path="/analytics" component={Analytics} />
<Route path="/sales-analysis" component={SalesAnalysisPage} />
<Route path="/payment-schedule" component={PaymentSchedulePage} />
{/* Redirection /auth vers dashboard */}
@@ -169,7 +167,6 @@ function RouterProduction() {
<Route path="/sav" component={SavTickets} />
<Route path="/avoirs" component={Avoirs} />
<Route path="/analytics" component={Analytics} />
<Route path="/sales-analysis" component={SalesAnalysisPage} />
<Route path="/payment-schedule" component={PaymentSchedulePage} />
{/* Routes de compatibilité - redirection vers utilities */}
+7 -33
View File
@@ -186,11 +186,6 @@ export default function Sidebar() {
}
};
// Debug logging pour production
console.log('Sidebar - User:', user);
console.log('Sidebar - isLoading:', isLoading);
console.log('Sidebar - error:', error);
const handleLogout = async () => {
try {
// Force logout via fetch to ensure session is destroyed
@@ -208,13 +203,11 @@ export default function Sidebar() {
const toggleSidebar = () => {
const newCollapsed = !sidebarCollapsed;
console.log('🔧 Sidebar - Toggling sidebar:', { from: sidebarCollapsed, to: newCollapsed });
setSidebarCollapsed(newCollapsed);
try {
localStorage.setItem('sidebarCollapsed', JSON.stringify(newCollapsed));
console.log('💾 Sidebar state saved to localStorage:', newCollapsed);
} catch (error) {
console.error('Error saving sidebar state:', error);
} catch {
// ignore
}
};
@@ -291,12 +284,6 @@ export default function Sidebar() {
icon: BarChart3,
roles: ["admin", "directeur", "manager"]
},
{
path: "/sales-analysis",
label: "Analyse Vente",
icon: TrendingUp,
roles: ["admin", "directeur"]
},
]
},
{
@@ -377,12 +364,7 @@ export default function Sidebar() {
];
const hasPermission = (roles: string[]) => {
const hasRole = user?.role && roles.includes(user.role);
// Debug uniquement en développement pour éviter spam console
if (import.meta.env.MODE === 'development') {
console.log('hasPermission check:', { userRole: user?.role, roles, hasRole });
}
return hasRole;
return user?.role && roles.includes(user.role);
};
@@ -405,14 +387,6 @@ export default function Sidebar() {
const sidebarClasses = getSidebarClasses();
// Debug log pour vérifier l'état de la sidebar
console.log('🔧 Sidebar Debug:', {
isMobile,
sidebarCollapsed,
mobileMenuOpen,
width: sidebarCollapsed ? 'collapsed (64px)' : 'expanded (256px)',
classes: sidebarClasses
});
// Si l'utilisateur n'est pas encore chargé, afficher un état de chargement
if (isLoading) {
@@ -584,7 +558,7 @@ export default function Sidebar() {
</div>
{/* Navigation */}
<nav className="flex-1 py-2 px-3 overflow-y-hidden">
<nav className="flex-1 py-2 px-3 overflow-y-auto">
<div className="space-y-3">
{menuSections.map((section, sectionIndex) => {
// Vérifier si au moins un item de la section est accessible
@@ -817,12 +791,12 @@ export default function Sidebar() {
<SelectValue placeholder="🎯 Choisir le destinataire" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Prissela" className="py-3">
<SelectItem value="Laurie" className="py-3">
<div className="flex items-center space-x-3">
<div className="h-8 w-8 bg-purple-100 rounded-full flex items-center justify-center">
<span className="text-sm font-medium text-purple-600">P</span>
<span className="text-sm font-medium text-purple-600">L</span>
</div>
<span className="font-medium">Prissela</span>
<span className="font-medium">Laurie</span>
</div>
</SelectItem>
<SelectItem value="Jeremy" className="py-3">
+115 -34
View File
@@ -29,7 +29,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { Pagination, usePagination } from "@/components/ui/pagination";
import { Plus, Edit, Trash2, Phone, PhoneCall, Printer, Eye, Package, AlertCircle } from "lucide-react";
import { Plus, Edit, Trash2, Phone, PhoneCall, Printer, Eye, Package, AlertCircle, MessageSquare } from "lucide-react";
import JsBarcode from 'jsbarcode';
import { safeFormat, safeDate } from "@/lib/dateUtils";
import { format } from "date-fns";
@@ -56,6 +56,9 @@ export default function CustomerOrders() {
const [showStatusModal, setShowStatusModal] = useState(false);
const [showClientCallsModal, setShowClientCallsModal] = useState(false);
const [selectedOrder, setSelectedOrder] = useState<CustomerOrderWithRelations | null>(null);
const [showContactModal, setShowContactModal] = useState(false);
const [contactOrder, setContactOrder] = useState<CustomerOrderWithRelations | null>(null);
const [contactComment, setContactComment] = useState("");
const [searchTerm, setSearchTerm] = useState("");
const [sortBy, setSortBy] = useState<"date" | "status" | "supplier">("date");
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
@@ -177,16 +180,28 @@ export default function CustomerOrders() {
},
});
// Notification mutation
const notificationMutation = useMutation({
mutationFn: ({ id, customerNotified }: { id: number; customerNotified: boolean }) =>
apiRequest(`/api/customer-orders/${id}`, 'PUT', { customerNotified }),
// Mutation marquer client contacté (avec date + commentaire)
const markCalledMutation = useMutation({
mutationFn: ({ id, comment }: { id: number; comment?: string }) =>
apiRequest(`/api/customer-orders/${id}/mark-called`, 'PATCH', { comment }),
onSuccess: () => {
queryClient.invalidateQueries({ predicate: (query) => query.queryKey[0]?.toString()?.includes('/api/customer-orders') ?? false });
toast({
title: "Succès",
description: "Statut de notification mis à jour",
});
toast({ title: "Client marqué comme contacté" });
setShowContactModal(false);
setContactComment("");
setContactOrder(null);
},
});
// Mutation annuler contact
const unmarkCalledMutation = useMutation({
mutationFn: (id: number) =>
apiRequest(`/api/customer-orders/${id}`, 'PUT', { customerNotified: false, notifiedAt: null, notifiedComment: null }),
onSuccess: () => {
queryClient.invalidateQueries({ predicate: (query) => query.queryKey[0]?.toString()?.includes('/api/customer-orders') ?? false });
toast({ title: "Contact annulé" });
setShowContactModal(false);
setContactOrder(null);
},
});
@@ -266,10 +281,9 @@ export default function CustomerOrders() {
};
const handleNotificationToggle = (order: CustomerOrderWithRelations) => {
notificationMutation.mutate({
id: order.id,
customerNotified: !order.customerNotified
});
setContactOrder(order);
setContactComment("");
setShowContactModal(true);
};
// Fonction pour calculer le checksum EAN13
@@ -892,28 +906,30 @@ export default function CustomerOrders() {
<Edit className="h-4 w-4" />
</Button>
{canShowButtons(order.status) && (
<>
<Button
variant="outline"
size="sm"
onClick={() => handlePrintLabel(order)}
>
<Printer className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleNotificationToggle(order)}
className={order.customerNotified ? "bg-green-100" : ""}
>
{order.customerNotified ? (
<PhoneCall className="h-4 w-4" />
) : (
<Phone className="h-4 w-4" />
)}
</Button>
</>
<Button
variant="outline"
size="sm"
onClick={() => handlePrintLabel(order)}
>
<Printer className="h-4 w-4" />
</Button>
)}
<Button
variant="outline"
size="sm"
onClick={() => handleNotificationToggle(order)}
className={order.customerNotified ? "bg-green-100 border-green-300" : ""}
title={order.customerNotified && (order as any).notifiedAt
? `Contacté le ${format(new Date((order as any).notifiedAt), "dd/MM/yyyy 'à' HH:mm", { locale: fr })}${(order as any).notifiedComment ? ` — ${(order as any).notifiedComment}` : ''}`
: "Marquer client comme contacté"
}
>
{order.customerNotified ? (
<PhoneCall className="h-4 w-4 text-green-700" />
) : (
<Phone className="h-4 w-4" />
)}
</Button>
{permissions.canDelete('customer-orders') && (
<Button
variant="outline"
@@ -1053,6 +1069,71 @@ export default function CustomerOrders() {
onClose={() => setShowClientCallsModal(false)}
pendingCalls={pendingCalls}
/>
{/* Modal contact client */}
<Dialog open={showContactModal} onOpenChange={(open) => { setShowContactModal(open); if (!open) { setContactOrder(null); setContactComment(""); } }}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
{contactOrder?.customerNotified ? (
<><PhoneCall className="h-5 w-5 text-green-600" /> Client déjà contacté</>
) : (
<><Phone className="h-5 w-5 text-blue-600" /> Marquer client comme contacté</>
)}
</DialogTitle>
{contactOrder && (
<DialogDescription>
{contactOrder.customerName} — {contactOrder.productDesignation}
</DialogDescription>
)}
</DialogHeader>
{contactOrder?.customerNotified ? (
<div className="space-y-4">
<div className="rounded-md bg-green-50 border border-green-200 p-3 space-y-1 text-sm">
<p className="font-medium text-green-800">
Contacté le {contactOrder.notifiedAt ? format(new Date((contactOrder as any).notifiedAt), "dd/MM/yyyy 'à' HH:mm", { locale: fr }) : "—"}
</p>
{(contactOrder as any).notifiedComment && (
<p className="text-green-700 flex items-start gap-1.5">
<MessageSquare className="h-4 w-4 mt-0.5 shrink-0" />
{(contactOrder as any).notifiedComment}
</p>
)}
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => { setShowContactModal(false); setContactOrder(null); }}>Fermer</Button>
<Button variant="destructive" onClick={() => unmarkCalledMutation.mutate(contactOrder.id)} disabled={unmarkCalledMutation.isPending}>
Annuler le contact
</Button>
</div>
</div>
) : (
<div className="space-y-4">
<div>
<label className="text-sm font-medium text-gray-700">Commentaire (optionnel)</label>
<textarea
className="mt-1 w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
rows={3}
placeholder="Ex : Laissé un message, Rappeler demain matin..."
value={contactComment}
onChange={(e) => setContactComment(e.target.value)}
/>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => { setShowContactModal(false); setContactOrder(null); setContactComment(""); }}>Annuler</Button>
<Button
onClick={() => markCalledMutation.mutate({ id: contactOrder!.id, comment: contactComment || undefined })}
disabled={markCalledMutation.isPending}
className="bg-green-600 hover:bg-green-700"
>
<PhoneCall className="h-4 w-4 mr-2" /> Confirmer
</Button>
</div>
</div>
)}
</DialogContent>
</Dialog>
</div>
);
}
+4 -22
View File
@@ -198,7 +198,8 @@ export default function Dashboard() {
queryKey: ['/api/ad-campaigns', 'upcoming'],
queryFn: async () => {
// Essayer d'abord 2024, puis 2025 pour avoir toutes les publicités
const years = [2024, 2025];
const currentYear = new Date().getFullYear();
const years = [currentYear - 1, currentYear, currentYear + 1];
let allPublicities: PublicityWithRelations[] = [];
for (const year of years) {
@@ -307,15 +308,9 @@ export default function Dashboard() {
}) : []; // Afficher toutes les commandes en attente
const upcomingDeliveries = Array.isArray(allDeliveries) ? allDeliveries
.filter((d: any) => {
const isPlanned = d.status === 'planned';
console.log('🚚 Dashboard - Delivery filter:', { id: d.id, status: d.status, isPlanned, scheduledDate: d.scheduledDate });
return isPlanned;
})
.filter((d: any) => d.status === 'planned')
.sort((a: any, b: any) => new Date(a.scheduledDate).getTime() - new Date(b.scheduledDate).getTime())
.slice(0, 4) : []; // Afficher les 4 prochaines livraisons
console.log('🚚 Dashboard - Upcoming deliveries result:', upcomingDeliveries.length, upcomingDeliveries);
.slice(0, 4) : [];
// Calculs pour les statistiques
const pendingOrdersCount = Array.isArray(allOrders) ? allOrders.filter((order: any) => order.status === 'pending').length : 0;
@@ -372,19 +367,6 @@ export default function Dashboard() {
}
};
console.log('📊 Dashboard Debug - Raw Data:', {
allOrders: Array.isArray(allOrders) ? allOrders.length : 'NOT_ARRAY',
allDeliveries: Array.isArray(allDeliveries) ? allDeliveries.length : 'NOT_ARRAY',
customerOrders: Array.isArray(customerOrders) ? customerOrders.length : 'NOT_ARRAY',
upcomingPublicities: Array.isArray(upcomingPublicities) ? upcomingPublicities.length : 'NOT_ARRAY',
stats: stats,
samples: {
order: Array.isArray(allOrders) && allOrders.length > 0 ? allOrders[0] : null,
delivery: Array.isArray(allDeliveries) && allDeliveries.length > 0 ? allDeliveries[0] : null,
customerOrder: Array.isArray(customerOrders) && customerOrders.length > 0 ? customerOrders[0] : null,
publicity: Array.isArray(upcomingPublicities) && upcomingPublicities.length > 0 ? upcomingPublicities[0] : null
}
});
// Statistiques pour les commandes clients
const ordersByStatus = {
+73 -7
View File
@@ -1,4 +1,4 @@
import { useState, useEffect, useMemo } from "react";
import { useState, useEffect, useMemo, useRef } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
@@ -12,7 +12,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Textarea } from "@/components/ui/textarea";
import { AlertTriangle, Plus, Eye, Edit, Trash2, CheckCircle, CheckCircle2, Package, Clock, AlertCircle, Filter, Download, FileText, PackageX, RotateCcw, X } from "lucide-react";
import { AlertTriangle, Plus, Eye, Edit, Trash2, CheckCircle, CheckCircle2, Package, Clock, AlertCircle, Filter, Download, FileText, PackageX, RotateCcw, X, Loader2 } from "lucide-react";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -115,6 +115,66 @@ export default function DlcPage() {
},
});
// EAN lookup from external API
const [eanLookupLoading, setEanLookupLoading] = useState(false);
const eanDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const gencodeValue = form.watch("gencode");
useEffect(() => {
if (!gencodeValue || gencodeValue.length < 8) return;
if (eanDebounceRef.current) clearTimeout(eanDebounceRef.current);
eanDebounceRef.current = setTimeout(async () => {
setEanLookupLoading(true);
try {
// Étape 1 : récupérer l'article par EAN
const res = await fetch(`/api/ffnancy/articles?ean=${encodeURIComponent(gencodeValue)}&limit=1`, { credentials: 'include' });
if (!res.ok) return;
const data = await res.json();
const article = data.articles?.[0];
if (!article) return;
form.setValue("productName", article.libelle1, { shouldValidate: true });
// Étape 2 : chercher le fournisseur de la dernière entrée en stock
let codefouToMatch = article.codefou_principal;
try {
const mvtRes = await fetch(
`/api/ffnancy/mouvements/entrees?artNoId=${article.no_id}&limit=1&dateDebut=2000-01-01`,
{ credentials: 'include' }
);
if (mvtRes.ok) {
const mvtData = await mvtRes.json();
const lastEntree = mvtData.entrees?.[0];
if (lastEntree?.codefou) codefouToMatch = lastEntree.codefou;
}
} catch { /* fallback sur codefou_principal */ }
// Matching : codefou exact en priorité, fallback par nom
if (codefouToMatch) {
const matched = (suppliers as any[]).find((s: any) =>
s.codefou && s.codefou.trim().toLowerCase() === codefouToMatch.trim().toLowerCase()
) || (suppliers as any[]).find((s: any) =>
s.name.toLowerCase().trim().includes(article.nom_fou_principal?.toLowerCase().trim() || '') ||
(article.nom_fou_principal?.toLowerCase().trim() || '').includes(s.name.toLowerCase().trim())
);
if (matched) {
form.setValue("supplierId", matched.id, { shouldValidate: true });
}
}
} catch {
// Lookup best-effort, on ignore les erreurs
} finally {
setEanLookupLoading(false);
}
}, 600);
return () => {
if (eanDebounceRef.current) clearTimeout(eanDebounceRef.current);
};
}, [gencodeValue, suppliers]);
// Create mutation - optimized cache invalidation
const createMutation = useMutation({
mutationFn: (data: InsertDlcProduct) => apiRequest("/api/dlc-products", "POST", data),
@@ -670,11 +730,17 @@ export default function DlcPage() {
<FormItem>
<FormLabel>Code EAN13 (optionnel)</FormLabel>
<FormControl>
<Input
{...field}
value={field.value || ""}
placeholder="1234567890123"
/>
<div className="relative">
<Input
{...field}
value={field.value || ""}
placeholder="1234567890123"
className={eanLookupLoading ? "pr-8" : ""}
/>
{eanLookupLoading && (
<Loader2 className="absolute right-2 top-1/2 -translate-y-1/2 w-4 h-4 animate-spin text-blue-500" />
)}
</div>
</FormControl>
<FormMessage />
</FormItem>
+13
View File
@@ -44,6 +44,7 @@ export default function Suppliers() {
name: "",
contact: "",
phone: "",
codefou: "",
email: "",
hasDlc: false,
automaticReconciliation: false,
@@ -200,6 +201,7 @@ export default function Suppliers() {
name: "",
contact: "",
phone: "",
codefou: "",
email: "",
hasDlc: false,
automaticReconciliation: false,
@@ -239,6 +241,7 @@ export default function Suppliers() {
name: supplier.name || "",
contact: supplier.contact || "",
phone: supplier.phone || "",
codefou: (supplier as any).codefou || "",
email: (supplier as any).email || "",
hasDlc: supplier.hasDlc || false,
automaticReconciliation: supplier.automaticReconciliation || false,
@@ -538,6 +541,16 @@ export default function Suppliers() {
/>
</div>
<div>
<Label htmlFor="codefou">Code fournisseur API <span className="text-xs text-gray-400 font-normal">(liaison ffnancy)</span></Label>
<Input
id="codefou"
value={formData.codefou}
onChange={(e) => handleChange('codefou', e.target.value)}
placeholder="ex: FOU01"
/>
</div>
<div>
<Label htmlFor="email">Email de contact</Label>
<Input
+2 -19
View File
@@ -9,7 +9,6 @@ import {
Cloud,
Send,
Code,
TrendingUp
} from "lucide-react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
@@ -22,7 +21,6 @@ import DatabaseDebug from "./DatabaseDebug";
import WeatherSettings from "./WeatherSettings";
import WebhookBAPConfig from "./WebhookBAPConfig";
import SQLExecutor from "./SQLExecutor";
import SalesAnalysisConfig from "./SalesAnalysisConfig";
export default function Utilities() {
const { user } = useAuthUnified();
@@ -43,10 +41,8 @@ export default function Utilities() {
const canManageWeather = user?.role === 'admin';
const canManageWebhookBAP = user?.role === 'admin';
const canExecuteSQL = user?.role === 'admin';
const canManageSalesAnalysis = user?.role === 'admin' || user?.role === 'directeur';
// Si l'utilisateur n'a aucune permission, afficher le message d'accès refusé
if (!canManageBackups && !canManageNocoDB && !canDebugDatabase && !canManageWeather && !canManageWebhookBAP && !canExecuteSQL && !canManageSalesAnalysis) {
if (!canManageBackups && !canManageNocoDB && !canDebugDatabase && !canManageWeather && !canManageWebhookBAP && !canExecuteSQL) {
return (
<div className="flex-1 flex items-center justify-center">
<Card className="w-96">
@@ -82,7 +78,7 @@ export default function Utilities() {
{/* Content avec onglets */}
<div className="flex-1 p-6 overflow-hidden">
<Tabs value={activeTab} onValueChange={setActiveTab} className="h-full flex flex-col">
<TabsList className="grid w-full grid-cols-7 mb-6">
<TabsList className="grid w-full grid-cols-6 mb-6">
{canManageBackups && (
<TabsTrigger value="backups" className="flex items-center gap-2">
<Database className="w-4 h-4" />
@@ -119,12 +115,6 @@ export default function Utilities() {
Exécution SQL
</TabsTrigger>
)}
{canManageSalesAnalysis && (
<TabsTrigger value="salesanalysis" className="flex items-center gap-2">
<TrendingUp className="w-4 h-4" />
Analyse Vente
</TabsTrigger>
)}
</TabsList>
{/* Contenu des onglets */}
@@ -176,13 +166,6 @@ export default function Utilities() {
</TabsContent>
)}
{canManageSalesAnalysis && (
<TabsContent value="salesanalysis" className="flex-1 overflow-hidden">
<div className="h-full bg-gray-50 -m-6 p-6">
<SalesAnalysisConfig />
</div>
</TabsContent>
)}
</Tabs>
</div>
</div>
+1 -1
View File
@@ -163,7 +163,7 @@ export default function WebhookBAPConfig() {
Configuration Webhook BAP
</CardTitle>
<CardDescription>
Configurez l'URL du webhook n8n pour l'envoi des fichiers BAP vers Prissela et Jeremy
Configurez l'URL du webhook n8n pour l'envoi des fichiers BAP vers Laurie et Jeremy
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
+173 -6
View File
@@ -2,7 +2,7 @@
* MobileCustomerOrdersPage.tsx
* Version mobile de la page Commandes Clients avec création
*/
import { useState } from "react";
import { useState, useEffect, useRef } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useAuthUnified } from "@/hooks/useAuthUnified";
import { useStore } from "@/contexts/StoreContext";
@@ -15,12 +15,16 @@ import {
Package,
Search,
Phone,
PhoneCall,
User,
ClipboardList,
Plus,
MoreVertical,
X,
Filter
Filter,
Loader2,
MessageSquare,
Search
} from "lucide-react";
import { format } from "date-fns";
import { fr } from "date-fns/locale";
@@ -79,6 +83,8 @@ export default function MobileCustomerOrdersPage() {
const { selectedStoreId } = useStore();
const [searchTerm, setSearchTerm] = useState("");
const [isCreateOpen, setIsCreateOpen] = useState(false);
const [contactOrder, setContactOrder] = useState<any | null>(null);
const [contactComment, setContactComment] = useState("");
const { toast } = useToast();
const queryClient = useQueryClient();
@@ -123,6 +129,27 @@ export default function MobileCustomerOrdersPage() {
},
});
const markCalledMutation = useMutation({
mutationFn: ({ id, comment }: { id: number; comment?: string }) =>
apiRequest(`/api/customer-orders/${id}/mark-called`, 'PATCH', { comment }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/customer-orders"] });
toast({ title: "Client marqué comme contacté" });
setContactOrder(null);
setContactComment("");
},
});
const unmarkCalledMutation = useMutation({
mutationFn: (id: number) =>
apiRequest(`/api/customer-orders/${id}`, 'PUT', { customerNotified: false, notifiedAt: null, notifiedComment: null }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/customer-orders"] });
toast({ title: "Contact annulé" });
setContactOrder(null);
},
});
const createMutation = useMutation({
mutationFn: (data: any) => apiRequest('/api/customer-orders', 'POST', data),
onSuccess: () => {
@@ -157,6 +184,62 @@ export default function MobileCustomerOrdersPage() {
}
});
// Lookup API ffnancy par gencode ou référence (déclenché manuellement)
const [articleLookupLoading, setArticleLookupLoading] = useState(false);
const [articleNotFound, setArticleNotFound] = useState(false);
const fetchAndFillArticle = async (params: URLSearchParams) => {
setArticleLookupLoading(true);
setArticleNotFound(false);
try {
const res = await fetch(`/api/ffnancy/articles?${params}&limit=1`, { credentials: 'include' });
if (!res.ok) { setArticleNotFound(true); return; }
const data = await res.json();
const article = data.articles?.[0];
if (!article) { setArticleNotFound(true); return; }
setArticleNotFound(false);
form.setValue("productName", article.libelle1, { shouldValidate: true });
if (article.gtin) form.setValue("gencode", article.gtin, { shouldValidate: true });
if (article.ref_fou_principale) form.setValue("productReference", article.ref_fou_principale, { shouldValidate: true });
let codefouToMatch = article.codefou_principal;
try {
const mvtRes = await fetch(
`/api/ffnancy/mouvements/entrees?artNoId=${article.no_id}&limit=1&dateDebut=2000-01-01`,
{ credentials: 'include' }
);
if (mvtRes.ok) {
const mvtData = await mvtRes.json();
const lastEntree = mvtData.entrees?.[0];
if (lastEntree?.codefou) codefouToMatch = lastEntree.codefou;
}
} catch { /* fallback codefou_principal */ }
if (codefouToMatch) {
const matched = (suppliers as any[]).find((s: any) =>
s.codefou && s.codefou.trim().toLowerCase() === codefouToMatch.trim().toLowerCase()
) || (suppliers as any[]).find((s: any) =>
s.name.toLowerCase().trim().includes(article.nom_fou_principal?.toLowerCase().trim() || '') ||
(article.nom_fou_principal?.toLowerCase().trim() || '').includes(s.name.toLowerCase().trim())
);
if (matched) form.setValue("supplierId", matched.id, { shouldValidate: true });
}
} catch { setArticleNotFound(true); } finally {
setArticleLookupLoading(false);
}
};
const handleSearchByGencode = () => {
const val = form.getValues("gencode");
if (val) fetchAndFillArticle(new URLSearchParams({ ean: val }));
};
const handleSearchByReference = () => {
const val = form.getValues("productReference");
if (val) fetchAndFillArticle(new URLSearchParams({ codein: val }));
};
const onSubmit = (data: any) => {
// Validation basique groupe
let groupId = selectedStoreId;
@@ -274,7 +357,7 @@ export default function MobileCustomerOrdersPage() {
</DropdownMenu>
</div>
<div className="flex items-center gap-4 mt-3 pt-3 border-t border-gray-100 text-sm text-gray-600">
<div className="flex items-center gap-2 mt-3 pt-3 border-t border-gray-100 text-sm text-gray-600 flex-wrap">
{order.customerPhone && (
<a
href={`tel:${order.customerPhone}`}
@@ -284,6 +367,13 @@ export default function MobileCustomerOrdersPage() {
<span className="font-medium">Appeler</span>
</a>
)}
<button
onClick={() => { setContactOrder(order); setContactComment(""); }}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full transition-colors text-xs font-medium ${order.customerNotified ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'}`}
>
<PhoneCall className="h-3.5 w-3.5" />
{order.customerNotified ? 'Contacté' : 'Marquer contacté'}
</button>
<div className="flex items-center gap-1.5 ml-auto">
<div className="bg-gray-100 px-2 py-1 rounded text-xs">
Qty: {order.quantity || 1}
@@ -380,7 +470,12 @@ export default function MobileCustomerOrdersPage() {
<FormItem>
<FormLabel>Référence (Optionnel)</FormLabel>
<FormControl>
<Input placeholder="REF-123..." {...field} />
<div className="flex gap-2">
<Input placeholder="REF-123..." {...field} />
<Button type="button" variant="outline" size="sm" onClick={handleSearchByReference} disabled={articleLookupLoading} className="shrink-0">
{articleLookupLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Search className="w-4 h-4" />}
</Button>
</div>
</FormControl>
</FormItem>
)}
@@ -444,7 +539,12 @@ export default function MobileCustomerOrdersPage() {
<FormItem>
<FormLabel>Gencode (Scanner si dispo)</FormLabel>
<FormControl>
<Input placeholder="EAN13" {...field} />
<div className="flex gap-2">
<Input placeholder="EAN13" {...field} />
<Button type="button" variant="outline" size="sm" onClick={handleSearchByGencode} disabled={articleLookupLoading} className="shrink-0">
{articleLookupLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Search className="w-4 h-4" />}
</Button>
</div>
</FormControl>
</FormItem>
)}
@@ -504,7 +604,12 @@ export default function MobileCustomerOrdersPage() {
)}
/>
<Button type="submit" className="w-full bg-blue-600 h-12 text-lg font-bold shadow-lg mt-4" disabled={createMutation.isPending}>
{articleNotFound && (
<div className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
<span className="font-medium">Produit non référencé</span> — ce produit n'existe pas dans la base. La commande ne peut pas être créée.
</div>
)}
<Button type="submit" className="w-full bg-blue-600 h-12 text-lg font-bold shadow-lg mt-4" disabled={createMutation.isPending || articleNotFound || articleLookupLoading}>
{createMutation.isPending ? "Création..." : "Valider la commande"}
</Button>
</form>
@@ -519,6 +624,68 @@ export default function MobileCustomerOrdersPage() {
>
<Plus className="h-6 w-6 text-white" />
</Button>
{/* Modal contact client */}
{contactOrder && (
<div className="fixed inset-0 z-50 flex items-end justify-center bg-black/50" onClick={() => { setContactOrder(null); setContactComment(""); }}>
<div className="w-full max-w-lg bg-white rounded-t-2xl p-5 space-y-4" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center gap-2">
{contactOrder.customerNotified
? <PhoneCall className="h-5 w-5 text-green-600" />
: <Phone className="h-5 w-5 text-blue-600" />}
<h3 className="font-semibold text-gray-900">
{contactOrder.customerNotified ? "Client déjà contacté" : "Marquer comme contacté"}
</h3>
</div>
<p className="text-sm text-gray-500">{contactOrder.customerName} — {contactOrder.productDesignation}</p>
{contactOrder.customerNotified ? (
<div className="rounded-md bg-green-50 border border-green-200 p-3 space-y-1 text-sm">
<p className="font-medium text-green-800">
Contacté le {contactOrder.notifiedAt ? format(new Date(contactOrder.notifiedAt), "dd/MM/yyyy 'à' HH:mm") : "—"}
</p>
{contactOrder.notifiedComment && (
<p className="text-green-700 flex items-start gap-1.5">
<MessageSquare className="h-4 w-4 mt-0.5 shrink-0" />
{contactOrder.notifiedComment}
</p>
)}
</div>
) : (
<textarea
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
rows={3}
placeholder="Commentaire optionnel (ex: laissé un message...)"
value={contactComment}
onChange={(e) => setContactComment(e.target.value)}
/>
)}
<div className="flex gap-2 pt-1">
<button onClick={() => { setContactOrder(null); setContactComment(""); }} className="flex-1 py-2.5 rounded-lg border border-gray-300 text-sm font-medium text-gray-700">
{contactOrder.customerNotified ? "Fermer" : "Annuler"}
</button>
{contactOrder.customerNotified ? (
<button
onClick={() => unmarkCalledMutation.mutate(contactOrder.id)}
disabled={unmarkCalledMutation.isPending}
className="flex-1 py-2.5 rounded-lg bg-red-600 text-white text-sm font-medium"
>
Annuler le contact
</button>
) : (
<button
onClick={() => markCalledMutation.mutate({ id: contactOrder.id, comment: contactComment || undefined })}
disabled={markCalledMutation.isPending}
className="flex-1 py-2.5 rounded-lg bg-green-600 text-white text-sm font-medium flex items-center justify-center gap-2"
>
<PhoneCall className="h-4 w-4" /> Confirmer
</button>
)}
</div>
</div>
</div>
)}
</MobileLayout>
);
}
+73 -3
View File
@@ -2,7 +2,7 @@
* MobileDlcPage.tsx
* Version mobile de la page Gestion des DLC
*/
import { useState, useMemo } from "react";
import { useState, useMemo, useEffect, useRef } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useAuthUnified } from "@/hooks/useAuthUnified";
import { useStore } from "@/contexts/StoreContext";
@@ -20,7 +20,8 @@ import {
CheckCircle,
X,
Calendar,
MoreVertical
MoreVertical,
Loader2
} from "lucide-react";
import { format, parseISO } from "date-fns";
import { fr } from "date-fns/locale";
@@ -139,6 +140,66 @@ export default function MobileDlcPage() {
}
});
// EAN lookup from external API
const [eanLookupLoading, setEanLookupLoading] = useState(false);
const eanDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const gencodeValue = form.watch("gencode");
useEffect(() => {
if (!gencodeValue || gencodeValue.length < 8) return;
if (eanDebounceRef.current) clearTimeout(eanDebounceRef.current);
eanDebounceRef.current = setTimeout(async () => {
setEanLookupLoading(true);
try {
// Étape 1 : récupérer l'article par EAN
const res = await fetch(`/api/ffnancy/articles?ean=${encodeURIComponent(gencodeValue)}&limit=1`, { credentials: 'include' });
if (!res.ok) return;
const data = await res.json();
const article = data.articles?.[0];
if (!article) return;
form.setValue("productName", article.libelle1, { shouldValidate: true });
// Étape 2 : chercher le fournisseur de la dernière entrée en stock
let codefouToMatch = article.codefou_principal;
try {
const mvtRes = await fetch(
`/api/ffnancy/mouvements/entrees?artNoId=${article.no_id}&limit=1&dateDebut=2000-01-01`,
{ credentials: 'include' }
);
if (mvtRes.ok) {
const mvtData = await mvtRes.json();
const lastEntree = mvtData.entrees?.[0];
if (lastEntree?.codefou) codefouToMatch = lastEntree.codefou;
}
} catch { /* fallback sur codefou_principal */ }
// Matching : codefou exact en priorité, fallback par nom
if (codefouToMatch) {
const matched = (suppliers as any[]).find((s: any) =>
s.codefou && s.codefou.trim().toLowerCase() === codefouToMatch.trim().toLowerCase()
) || (suppliers as any[]).find((s: any) =>
s.name.toLowerCase().trim().includes(article.nom_fou_principal?.toLowerCase().trim() || '') ||
(article.nom_fou_principal?.toLowerCase().trim() || '').includes(s.name.toLowerCase().trim())
);
if (matched) {
form.setValue("supplierId", matched.id, { shouldValidate: true });
}
}
} catch {
// Lookup best-effort, on ignore les erreurs
} finally {
setEanLookupLoading(false);
}
}, 600);
return () => {
if (eanDebounceRef.current) clearTimeout(eanDebounceRef.current);
};
}, [gencodeValue, suppliers]);
const onSubmit = (data: any) => {
let groupId = selectedStoreId;
if (!groupId && user?.userGroups?.[0]?.groupId) groupId = user.userGroups[0].groupId;
@@ -386,7 +447,16 @@ export default function MobileDlcPage() {
<FormItem>
<FormLabel>Gencode (Scanner si dispo)</FormLabel>
<FormControl>
<Input placeholder="EAN13" {...field} />
<div className="relative">
<Input
placeholder="EAN13"
{...field}
className={eanLookupLoading ? "pr-8" : ""}
/>
{eanLookupLoading && (
<Loader2 className="absolute right-2 top-1/2 -translate-y-1/2 w-4 h-4 animate-spin text-blue-500" />
)}
</div>
</FormControl>
</FormItem>
)}