mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-23 23:18:18 +02:00
merge(main): resolve conflicts keeping codefou + email on suppliers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
b8ecacee3b
20 files changed
+719
-299
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in new issue
Block a user