import { format, startOfMonth, endOfMonth, eachDayOfInterval, isSameDay, isSameMonth, isToday } from "date-fns"; import { fr } from "date-fns/locale"; import { safeDate } from "@/lib/dateUtils"; import { Plus, Check, MoreHorizontal, Package, Link, MessageSquare } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { useState } from "react"; // Composant modal pour afficher les commentaires function CommentModal({ isOpen, onClose, comment }: { isOpen: boolean, onClose: () => void, comment: string }) { if (!isOpen) return null; return ( Commentaire

{comment}

); } import type { OrderWithRelations, DeliveryWithRelations } from "@shared/schema"; interface CalendarGridProps { currentDate: Date; orders: OrderWithRelations[]; deliveries: DeliveryWithRelations[]; publicities: any[]; selectedStoreId: number | null; userGroups: any[]; onDateClick: (date: Date) => void; onItemClick: (item: any, type: 'order' | 'delivery') => void; } // Composant pour afficher un élément (commande ou livraison) function CalendarItem({ item, type, onItemClick }: { item: any, type: 'order' | 'delivery', onItemClick: (item: any, type: 'order' | 'delivery') => void }) { const [showCommentModal, setShowCommentModal] = useState(false); const [commentClicked, setCommentClicked] = useState(false); const formatQuantity = (quantity: number, unit: string) => { return `${quantity}${unit === 'palettes' ? 'P' : 'C'}`; }; if (type === 'order') { const getOrderStyle = () => { switch (item.status) { case 'delivered': return 'bg-gray-400 text-white shadow-md border-l-4 border-gray-500'; case 'planned': return 'bg-yellow-300 text-gray-800 shadow-md border-l-4 border-yellow-500 font-medium'; default: return 'bg-blue-300 text-gray-800 shadow-md border-l-4 border-blue-500'; } }; return (
{ e.stopPropagation(); onItemClick(item, 'order'); }} style={{marginBottom: '3px', display: 'block', width: '100%', minHeight: '24px'}} >
{item.supplier?.name || 'Commande'}
{/* Badge pour commande liée à des livraisons */} {item.deliveries && item.deliveries.length > 0 && (
)} {item.status === 'planned' && (
)} {item.status === 'delivered' && ( )}
); } // Livraison const getDeliveryStyle = () => { switch (item.status) { case 'delivered': return 'bg-gray-400 text-white shadow-md border-l-4 border-gray-500'; case 'pending': return 'bg-green-200 text-gray-800 shadow-md border-l-4 border-green-500'; default: return 'bg-green-200 text-gray-800 shadow-md border-l-4 border-green-500'; } }; return (
{ if (commentClicked) { setCommentClicked(false); return; } e.stopPropagation(); onItemClick(item, 'delivery'); }} style={{marginBottom: '3px', display: 'block', width: '100%', minHeight: '24px'}} >
{item.supplier?.name || 'Livraison'} - {formatQuantity(item.quantity, item.unit)}
{/* Icône commentaire pour les livraisons avec notes */} {item.notes && item.notes.trim() && (
{ e.preventDefault(); e.stopPropagation(); setCommentClicked(true); setShowCommentModal(true); }} >
)} {/* Badge pour livraison liée à une commande */} {item.orderId && (
)} {item.status === 'pending' && (
)} {item.status === 'delivered' && ( )}
{/* Modal pour afficher le commentaire */} { setShowCommentModal(false); // Reset après un délai pour éviter les conflits setTimeout(() => setCommentClicked(false), 100); }} comment={item.notes || ''} />
); } // Composant pour gérer l'overflow avec modal - DEV = PRODUCTION function DayItemsContainer({ dayOrders, dayDeliveries, onItemClick }: { dayOrders: any[], dayDeliveries: any[], onItemClick: (item: any, type: 'order' | 'delivery') => void }) { const [isOpen, setIsOpen] = useState(false); const [showCommentModal, setShowCommentModal] = useState(false); const [selectedComment, setSelectedComment] = useState(''); const [commentClickedItems, setCommentClickedItems] = useState>(new Set()); const MAX_VISIBLE_ITEMS = 2; // FORCÉ : 2 éléments en dev ET production const totalItems = dayOrders.length + dayDeliveries.length; if (totalItems === 0) return null; // Combiner tous les éléments avec leur type const allItems = [ ...dayOrders.map(order => ({ ...order, itemType: 'order' as const })), ...dayDeliveries.map(delivery => ({ ...delivery, itemType: 'delivery' as const })) ]; const visibleItems = allItems.slice(0, MAX_VISIBLE_ITEMS); const hiddenCount = Math.max(0, totalItems - MAX_VISIBLE_ITEMS); return (
{/* Éléments visibles - IDENTIQUE DEV/PROD */} {visibleItems.map((item, index) => ( ))} {/* Badge "+X autres" - BLANC/GRIS UNIFORME */} {hiddenCount > 0 && (
e.stopPropagation()}> Éléments du jour - {totalItems}
{allItems.map((item, index) => { const isOrder = item.itemType === 'order'; const itemKey = `${item.itemType}-${item.id}`; const statusColor = item.status === 'delivered' ? 'bg-gray-100 border-gray-300' : item.status === 'planned' ? 'bg-yellow-50 border-yellow-200' : isOrder ? 'bg-blue-50 border-blue-200' : 'bg-green-50 border-green-200'; const statusText = item.status === 'delivered' ? 'Livré' : item.status === 'planned' ? 'Planifié' : 'En attente'; const statusIcon = item.status === 'delivered' ? : item.status === 'planned' ?
: isOrder ?
:
; return (
{ if (commentClickedItems.has(itemKey)) { setCommentClickedItems(prev => { const newSet = new Set(prev); newSet.delete(itemKey); return newSet; }); return; } e.preventDefault(); e.stopPropagation(); onItemClick(item, item.itemType); setIsOpen(false); }} >
{statusIcon} {isOrder ? 'COMMANDE' : 'LIVRAISON'} {/* Icône commentaire pour les livraisons avec notes */} {!isOrder && item.notes && item.notes.trim() && (
{ e.preventDefault(); e.stopPropagation(); setCommentClickedItems(prev => new Set(prev).add(itemKey)); setSelectedComment(item.notes); setShowCommentModal(true); }} >
)} {/* Badge pour liaison - commandes avec livraisons ou livraisons avec commandes */} {((isOrder && item.deliveries && item.deliveries.length > 0) || (!isOrder && item.orderId)) && (
)} {statusText}
{item.supplier?.name || (isOrder ? 'Commande' : 'Livraison')}
{!isOrder && ( {item.quantity} {item.unit === 'palettes' ? 'P' : 'C'} )} {item.scheduledDate ? new Date(item.scheduledDate).toLocaleDateString('fr-FR') : item.deliveredDate ? new Date(item.deliveredDate).toLocaleDateString('fr-FR') : new Date(item.createdAt).toLocaleDateString('fr-FR') }
); })}
{/* Modal pour afficher le commentaire depuis la modal des éléments du jour */} { setShowCommentModal(false); // Reset après un délai pour éviter les conflits setTimeout(() => setCommentClickedItems(new Set()), 100); }} comment={selectedComment} />
)}
); } export default function CalendarGrid({ currentDate, orders, deliveries, publicities, selectedStoreId, userGroups, onDateClick, onItemClick, }: CalendarGridProps) { if (import.meta.env.DEV) { console.log('🗓️ CalendarGrid rendered with:', { currentDate: currentDate?.toISOString(), ordersCount: orders?.length || 0, deliveriesCount: deliveries?.length || 0, publicitiesCount: publicities?.length || 0, selectedStoreId, userGroupsCount: userGroups?.length || 0 }); } const monthStart = startOfMonth(currentDate); const monthEnd = endOfMonth(currentDate); // Helper function to filter publicities based on user's assigned stores const getPublicitiesForDate = (date: Date) => { const dateStr = format(date, 'yyyy-MM-dd'); // Protection contre les données invalides qui causent des plantages en production if (!publicities || !Array.isArray(publicities)) { console.warn('⚠️ Invalid publicities data:', publicities); return []; } if (import.meta.env.DEV) { console.log('🔍 CalendarGrid getPublicitiesForDate:', { date: dateStr, totalPublicities: publicities.length, selectedStoreId, userGroups: userGroups?.length || 0 }); } try { return publicities.filter(pub => { // Vérifications de sécurité pour éviter les plantages if (!pub || typeof pub !== 'object') { console.warn('⚠️ Invalid publicity object:', pub); return false; } // Check if the date is within the publicity period const pubStart = pub.startDate; const pubEnd = pub.endDate; if (!pubStart || !pubEnd) { console.warn('⚠️ Publicity missing dates:', { pubNumber: pub.pubNumber, startDate: pubStart, endDate: pubEnd }); return false; } if (dateStr < pubStart || dateStr > pubEnd) { return false; } // If no store is selected and user has no assigned groups, show only publicities with participations if (!selectedStoreId && (!userGroups || userGroups.length === 0)) { const hasParticipations = pub.participations && Array.isArray(pub.participations) && pub.participations.length > 0; if (hasParticipations && import.meta.env.DEV) { console.log('📋 Publicity has participations (no store selected):', { pubNumber: pub.pubNumber, participationCount: pub.participations.length }); } return hasParticipations; } // If a specific store is selected, check if that store participates if (selectedStoreId) { const matches = pub.participations && Array.isArray(pub.participations) && pub.participations.some((pg: any) => pg?.groupId === selectedStoreId); if (matches && import.meta.env.DEV) { console.log('🎯 Publicity matches selected store:', { pubNumber: pub.pubNumber, selectedStoreId }); } return matches; } // If no specific store selected but user has assigned stores, // show publicities where any of user's stores participate if (userGroups && Array.isArray(userGroups) && userGroups.length > 0) { const userGroupIds = userGroups.map((ug: any) => ug?.groupId).filter(id => id !== undefined); const matches = pub.participations && Array.isArray(pub.participations) && pub.participations.some((pg: any) => pg?.groupId && userGroupIds.includes(pg.groupId)); if (matches) { console.log('👥 Publicity matches user groups:', { pubNumber: pub.pubNumber, userGroupIds }); } return matches; } // Default case: only show publicities with participations const hasParticipations = pub.participations && Array.isArray(pub.participations) && pub.participations.length > 0; if (hasParticipations) { console.log('📋 Publicity has participations (default case):', { pubNumber: pub.pubNumber, participationCount: pub.participations.length }); } return hasParticipations; }); } catch (error) { console.error('❌ Error filtering publicities:', error); return []; } }; // Get all days in the month const monthDays = eachDayOfInterval({ start: monthStart, end: monthEnd, }); // Pad the calendar to start on Monday const firstDayOfWeek = monthStart.getDay(); const startPadding = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1; const paddedDays = []; // Add padding days from previous month for (let i = startPadding; i > 0; i--) { const paddingDate = new Date(monthStart); paddingDate.setDate(paddingDate.getDate() - i); paddedDays.push(paddingDate); } // Add current month days paddedDays.push(...monthDays); // Add padding days from next month to complete the grid const remainingCells = 42 - paddedDays.length; // 6 weeks * 7 days for (let i = 1; i <= remainingCells; i++) { const paddingDate = new Date(monthEnd); paddingDate.setDate(paddingDate.getDate() + i); paddedDays.push(paddingDate); } const weekDays = ["Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim"]; const getItemsForDate = (date: Date) => { // Debug: Log des commandes reçues (seulement une fois) if (orders.length > 0 && date.getDate() === 1) { console.log('📅 CalendarGrid Debug - Orders received:', orders.length); console.log('📅 First order structure:', orders[0]); console.log('📅 All orders dates:', orders.map(o => ({ id: o.id, plannedDate: o.plannedDate, supplier: o.supplier?.name }))); } // Debug: Log des livraisons reçues (seulement une fois) if (deliveries.length > 0 && date.getDate() === 1) { console.log('🚛 CalendarGrid Debug - Deliveries received:', deliveries.length); console.log('🚛 First delivery structure:', deliveries[0]); console.log('🚛 All deliveries dates:', deliveries.map(d => ({ id: d.id, scheduledDate: d.scheduledDate, supplier: d.supplier?.name }))); } const dayOrders = orders.filter(order => { // Protection contre undefined/null if (!order || !order.supplier) { console.warn('📅 Invalid order found:', order); return false; } // Essayer plusieurs champs de date possibles const orderDate = safeDate(order.plannedDate || order.createdAt); const matches = orderDate && isSameDay(orderDate, date); if (matches) { console.log('📅 Order matches date:', { orderId: order.id, supplier: order.supplier?.name, plannedDate: order.plannedDate, matchingDate: format(date, 'yyyy-MM-dd') }); } return matches; }); const dayDeliveries = deliveries.filter(delivery => { // Protection contre undefined/null if (!delivery || !delivery.supplier) { console.warn('🚛 Invalid delivery found:', delivery); return false; } // Essayer plusieurs champs de date possibles const deliveryDate = safeDate(delivery.scheduledDate || delivery.deliveredDate || delivery.createdAt); const matches = deliveryDate && isSameDay(deliveryDate, date); if (matches) { console.log('🚛 Delivery matches date:', { deliveryId: delivery.id, supplier: delivery.supplier?.name, scheduledDate: delivery.scheduledDate, matchingDate: format(date, 'yyyy-MM-dd') }); } return matches; }); return { orders: dayOrders, deliveries: dayDeliveries }; }; return (
{/* Calendar Header - Design moderne épuré */}
{weekDays.map((day, index) => (
= 5 ? 'text-slate-700' : 'text-slate-800' } uppercase border-r border-gray-100 last:border-r-0`} > {day}
))}
{/* Calendar Days - Grid moderne sans arrondi */}
{paddedDays.map((date, index) => { const isCurrentMonth = isSameMonth(date, currentDate); const isTodayDate = isToday(date); const isWeekend = date.getDay() === 0 || date.getDay() === 6; const { orders: dayOrders, deliveries: dayDeliveries } = getItemsForDate(date); const dayPublicities = getPublicitiesForDate(date); return (
{ // Ne pas ouvrir le modal de création si on clique sur le bouton "+X autres" ou à l'intérieur const target = e.target as HTMLElement; if (target.closest('button') || target.closest('[role="dialog"]') || target.closest('[data-modal-trigger]')) { e.preventDefault(); e.stopPropagation(); return; } onDateClick(date); }} >
{/* Numéro du jour avec design moderne */}
{format(date, 'd')} {/* Indicateur weekend moderne */} {isWeekend && isCurrentMonth && (
)}
{/* Orders and Deliveries - DEV = PRODUCTION */}
{/* Publicities et Quick Create Button ensemble */} {isCurrentMonth && (
{/* Publicities avec Quick Create Button à gauche */} {dayPublicities.length > 0 && ( <> {dayPublicities.slice(0, 2).map((pub, idx) => (
{/* Quick Create Button à gauche de la première publicité */} {idx === 0 && (
)}
pg.group?.name).join(', ')}` : ''}`} > {pub.pubNumber}
))} {dayPublicities.length > 2 && (
+{dayPublicities.length - 2}
)} )} {/* Quick Create Button quand pas de publicité */} {dayPublicities.length === 0 && (
)}
)}
); })}
); }