import { useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Plus, Calendar, Edit, Trash2, Eye, Filter, Grid, List } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; import { useToast } from "@/hooks/use-toast"; import { useAuthUnified } from "@/hooks/useAuthUnified"; import { useStore } from "@/components/Layout"; import { format, startOfMonth, endOfMonth, eachWeekOfInterval, eachDayOfInterval, startOfWeek, endOfWeek, isSameMonth, isWithinInterval, startOfYear, endOfYear, getWeek, getMonth } from "date-fns"; import { fr } from "date-fns/locale"; import { safeFormat, safeDate, safeCompareDate } from "@/lib/dateUtils"; import type { PublicityWithRelations, Group } from "@shared/schema"; import PublicityForm from "@/components/PublicityForm"; export default function Publicities() { const [selectedYear, setSelectedYear] = useState(new Date().getFullYear()); // Année courante par défaut const [selectedMonth, setSelectedMonth] = useState(new Date().getMonth()); // Mois courant par défaut const [viewMode, setViewMode] = useState<'list' | 'calendar'>('list'); // Mode d'affichage const [showYearOverview, setShowYearOverview] = useState(true); // Affichage vue d'ensemble const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [isViewModalOpen, setIsViewModalOpen] = useState(false); const [selectedPublicity, setSelectedPublicity] = useState(null); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const { user } = useAuthUnified(); const { selectedStoreId } = useStore(); const { toast } = useToast(); const queryClient = useQueryClient(); // Seuls les admins peuvent modifier/supprimer const canModify = user?.role === 'admin'; // Generate year options (2025 to 2035) const currentYear = new Date().getFullYear(); const yearOptions = Array.from({ length: 11 }, (_, i) => currentYear + i); const { data: publicities = [], isLoading } = useQuery({ queryKey: ['/api/publicities', selectedYear, selectedStoreId], queryFn: async () => { const params = new URLSearchParams(); params.append('year', selectedYear.toString()); if (selectedStoreId) { params.append('storeId', selectedStoreId.toString()); } const response = await fetch(`/api/publicities?${params}`); if (!response.ok) { throw new Error('Failed to fetch publicities'); } const data = await response.json(); console.log('📢 Publicities received:', data.length, 'items', data.slice(0, 2)); // CRITICAL FIX: Force frontend sorting by pubNumber to ensure correct order const sortedData = Array.isArray(data) ? data.sort((a, b) => { const numA = parseInt(a.pubNumber) || 0; const numB = parseInt(b.pubNumber) || 0; return numA - numB; }) : []; console.log('📢 Publicities after frontend sort:', { first5: sortedData.slice(0, 5).map(p => `N°${p.pubNumber}`), total: sortedData.length }); return sortedData; }, }); console.log('📢 Publicities Debug:', { isLoading, publicitiesCount: publicities?.length, selectedYear, selectedStoreId, publicities: publicities?.slice(0, 2) }); console.log('📢 Publicities Debug:', { isLoading, publicitiesCount: publicities?.length, selectedYear, selectedStoreId, publicities: publicities?.slice(0, 2) }); const { data: groups = [] } = useQuery({ queryKey: ['/api/groups'], enabled: !!user, }); const deleteMutation = useMutation({ mutationFn: async (id: number) => { const response = await fetch(`/api/publicities/${id}`, { method: 'DELETE' }); if (!response.ok) { const error = await response.json(); throw new Error(error.message || 'Erreur lors de la suppression'); } // Handle empty response (204 No Content) in production if (response.status === 204) { return { message: "Publicity deleted successfully" }; } return response.json(); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['/api/publicities'] }); toast({ description: "Publicité supprimée avec succès" }); setIsDeleteModalOpen(false); setSelectedPublicity(null); }, onError: (error: Error) => { toast({ variant: "destructive", description: error.message || "Erreur lors de la suppression" }); } }); // Generate month options const monthOptions = Array.from({ length: 12 }, (_, i) => ({ value: i, label: format(new Date(selectedYear, i, 1), 'MMMM', { locale: fr }) })); // Calendar helper functions const getWeekParticipation = (weekStart: Date, weekEnd: Date) => { // Vérifier que publicities est un tableau if (!Array.isArray(publicities)) { return { publicities: [], participatingStores: [], storeColors: [] }; } const weekPublicities = publicities.filter(pub => { const pubStart = safeDate(pub.startDate); const pubEnd = safeDate(pub.endDate); // More robust overlap detection: check if any part of the publicity period overlaps with the week return pubStart && pubEnd && (pubStart <= weekEnd && pubEnd >= weekStart); }); const participatingStores = new Set(); weekPublicities.forEach(pub => { // Vérifier que participations existe et est un tableau if (pub.participations && Array.isArray(pub.participations)) { pub.participations.forEach(participation => { participatingStores.add(participation.groupId); }); } }); return { publicities: weekPublicities, participatingStores: Array.from(participatingStores), storeColors: Array.from(participatingStores).map(storeId => { const group = groups.find(g => g.id === storeId); return group?.color || '#666666'; }) }; }; // Year overview - Generate weeks for the selected year const getYearWeeks = () => { const yearStart = startOfYear(new Date(selectedYear, 0, 1)); const yearEnd = endOfYear(new Date(selectedYear, 11, 31)); const weeks = eachWeekOfInterval( { start: yearStart, end: yearEnd }, { weekStartsOn: 1 } ); return weeks.map((weekStart, index) => { const weekEnd = endOfWeek(weekStart, { weekStartsOn: 1 }); const weekNumber = index + 1; const weekMidpoint = new Date(weekStart.getTime() + (3.5 * 24 * 60 * 60 * 1000)); let month = getMonth(weekMidpoint); if (weekStart.getFullYear() < selectedYear && weekEnd.getFullYear() === selectedYear) { month = 0; // Janvier } else if (weekStart.getFullYear() === selectedYear && weekEnd.getFullYear() > selectedYear) { month = 11; // Décembre } const participation = getWeekParticipation(weekStart, weekEnd); return { weekStart, weekEnd, weekNumber, month, ...participation }; }); }; const yearWeeks = getYearWeeks(); const handleView = (publicity: PublicityWithRelations) => { setSelectedPublicity(publicity); setIsViewModalOpen(true); }; const handleEdit = (publicity: PublicityWithRelations) => { setSelectedPublicity(publicity); setIsEditModalOpen(true); }; const handleDelete = (publicity: PublicityWithRelations) => { setSelectedPublicity(publicity); setIsDeleteModalOpen(true); }; const confirmDelete = () => { if (selectedPublicity) { deleteMutation.mutate(selectedPublicity.id); } }; const closeModals = () => { setIsCreateModalOpen(false); setIsEditModalOpen(false); setIsViewModalOpen(false); setIsDeleteModalOpen(false); setSelectedPublicity(null); }; const canCreateOrEdit = user?.role === 'admin' || user?.role === 'manager'; const canDelete = user?.role === 'admin'; return (
{/* Header */}

Publicités

Gestion des campagnes publicitaires

{/* View Mode Toggle */}
{/* Year Overview Toggle */} {/* Year Filter */}
{/* Month Filter (only for calendar view) */} {viewMode === 'calendar' && ( )} {canModify && ( )}
{/* Stats */}

Total {selectedYear}

{Array.isArray(publicities) ? publicities.length : 0}

En cours

{publicities.filter(p => { const now = new Date(); const start = safeDate(p.startDate); const end = safeDate(p.endDate); return start && end && start <= now && now <= end; }).length}

À venir

{publicities.filter(p => { const startDate = safeDate(p.startDate); return startDate && startDate > new Date(); }).length}

{/* Year Overview - Weekly Timeline */} {showYearOverview && ( Vue d'ensemble {selectedYear} - Semaines avec publicités
{/* Months Grid */}
{Array.from({ length: 12 }, (_, monthIndex) => { const monthName = format(new Date(selectedYear, monthIndex, 1), 'MMM', { locale: fr }); const monthWeeks = yearWeeks.filter(week => week.month === monthIndex); return (
{monthName}
{monthWeeks.map((week, weekIndex) => { const hasPublicity = week.publicities.length > 0; const storeColors = week.storeColors; return (
p.pubNumber).join(', ')}` : 'Aucune publicité'}`} > {week.weekNumber} {/* Store participation indicators */} {storeColors.length > 0 && (
{storeColors.slice(0, 4).map((color, idx) => (
))} {storeColors.length > 4 && (
+
)}
)}
); })}
); })}
{/* Legend */}
Semaine avec publicité
Semaine sans publicité
{groups.slice(0, 3).map(group => (
))}
Indicateurs magasins (coin supérieur droit)
• Survolez les semaines pour voir les détails des publicités
)} {/* Content */} {isLoading ? (
) : publicities.length === 0 ? (

Aucune publicité pour {selectedYear}

Commencez par créer votre première campagne publicitaire.

{canModify && ( )}
) : viewMode === 'calendar' ? ( Vue calendrier - {monthOptions[selectedMonth].label} {selectedYear}
{/* Calendar Grid */}
{/* Days of week headers */} {['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'].map(day => (
{day}
))} {/* Calendar days */} {(() => { const monthStart = startOfMonth(new Date(selectedYear, selectedMonth, 1)); const monthEnd = endOfMonth(monthStart); const startDate = startOfWeek(monthStart, { weekStartsOn: 1 }); const endDate = endOfWeek(monthEnd, { weekStartsOn: 1 }); const days = eachDayOfInterval({ start: startDate, end: endDate }); return days.map(day => { const isCurrentMonth = isSameMonth(day, monthStart); const dayPublicities = publicities.filter(pub => { const pubStart = safeDate(pub.startDate); const pubEnd = safeDate(pub.endDate); return pubStart && pubEnd && day >= pubStart && day <= pubEnd; }); const participatingStores = new Set(); dayPublicities.forEach(pub => { pub.participations.forEach(participation => { participatingStores.add(participation.groupId); }); }); return (
0 ? 'ring-2 ring-blue-200' : ''} `} >
{format(day, 'd')}
{dayPublicities.length > 0 && (
{dayPublicities.slice(0, 2).map(pub => (
{pub.pubNumber}
))} {dayPublicities.length > 2 && (
+{dayPublicities.length - 2} autre{dayPublicities.length > 3 ? 's' : ''}
)}
)} {/* Store participation indicators */} {participatingStores.size > 0 && (
{Array.from(participatingStores).slice(0, 3).map(storeId => { const group = groups.find(g => g.id === storeId); return (
); })} {participatingStores.size > 3 && (
+{participatingStores.size - 3}
)}
)}
); }); })()}
{/* Legend */}

Légende

Jour avec publicité
{groups.slice(0, 3).map(group => (
))}
Magasins participants
) : (
{publicities .map((publicity) => { const now = new Date(); const start = safeDate(publicity.startDate); const end = safeDate(publicity.endDate); const isActive = start && end && now >= start && now <= end; const isUpcoming = start && start > now; const isPast = end && end < now; return ( ); })}
N° PUB Désignation Période Statut Magasins Créé par Actions
{publicity.pubNumber}
{publicity.designation}
{safeFormat(publicity.startDate, "dd/MM/yy")} - {safeFormat(publicity.endDate, "dd/MM/yy")}
{isActive && En cours} {isUpcoming && À venir} {isPast && Terminée}
{publicity.participations.length === 0 ? ( Aucun magasin ) : ( <> {publicity.participations.slice(0, 2).map((participation) => (
{participation.group?.name || 'Magasin'} ))} {publicity.participations.length > 2 && ( +{publicity.participations.length - 2} )} )}
{publicity.creator?.name || publicity.creator?.username || 'Utilisateur'}
{canModify && ( <> )}
)} {/* Create Modal */} Créer une nouvelle publicité Remplissez les informations de la campagne publicitaire et sélectionnez les magasins participants. {/* Edit Modal */} Modifier la publicité Modifiez les informations de la campagne publicitaire. {/* View Modal */} Détails de la publicité Informations complètes de la campagne publicitaire. {selectedPublicity && (

{selectedPublicity.pubNumber}

{selectedPublicity.year}

{selectedPublicity.designation}

{safeFormat(selectedPublicity.startDate, "dd/MM/yyyy")}

{safeFormat(selectedPublicity.endDate, "dd/MM/yyyy")}

{selectedPublicity.participations.length === 0 ? ( Aucun magasin ) : ( selectedPublicity.participations.map((participation) => (
{participation.group?.name || 'Magasin'} )) )}

{selectedPublicity.creator?.name || selectedPublicity.creator?.username || 'Utilisateur'}

{safeFormat(selectedPublicity.createdAt, "dd/MM/yyyy à HH:mm")}

)}
{/* Delete Confirmation Modal */} Supprimer la publicité Êtes-vous sûr de vouloir supprimer cette publicité ? Cette action est irréversible. {selectedPublicity && (

{selectedPublicity.pubNumber}

{selectedPublicity.designation}

)}
); }