feat: Implement new dashboard, publicity management, and calendar pages with corresponding server routes.

This commit is contained in:
Michael committed 2026-01-06 11:39:28 +01:00
1 parent eb3fb119ee
commit 78b53f576b
10 files changed
+696 -630

No files matched your search

+24 -16
View File
@@ -51,8 +51,8 @@ export default function PublicityForm({ publicity, groups, onSuccess, selectedYe
defaultValues: {
pubNumber: publicity?.pubNumber || "",
designation: publicity?.designation || "",
startDate: publicity?.startDate ? safeDate(publicity.startDate) : undefined,
endDate: publicity?.endDate ? safeDate(publicity.endDate) : undefined,
startDate: (publicity?.startDate ? safeDate(publicity.startDate) : undefined) as Date,
endDate: (publicity?.endDate ? safeDate(publicity.endDate) : undefined) as Date,
year: publicity?.year || selectedYear || 2025, // Utilise l'année sélectionnée ou 2025 par défaut
participatingGroups: publicity?.participations?.map(p => p.groupId) || [],
},
@@ -62,7 +62,7 @@ export default function PublicityForm({ publicity, groups, onSuccess, selectedYe
const createMutation = useMutation({
mutationFn: async (data: PublicityFormData) => {
const response = await fetch('/api/publicities', {
const response = await fetch('/api/ad-campaigns', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -81,21 +81,21 @@ export default function PublicityForm({ publicity, groups, onSuccess, selectedYe
return response.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/publicities'] });
queryClient.invalidateQueries({ queryKey: ['/api/ad-campaigns'] });
toast({ description: "Publicité créée avec succès" });
onSuccess();
},
onError: (error: Error) => {
toast({
toast({
variant: "destructive",
description: error.message || "Erreur lors de la création"
description: error.message || "Erreur lors de la création"
});
}
});
const updateMutation = useMutation({
mutationFn: async (data: PublicityFormData) => {
const response = await fetch(`/api/publicities/${publicity!.id}`, {
const response = await fetch(`/api/ad-campaigns/${publicity!.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -114,23 +114,31 @@ export default function PublicityForm({ publicity, groups, onSuccess, selectedYe
return response.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/publicities'] });
queryClient.invalidateQueries({ queryKey: ['/api/ad-campaigns'] });
toast({ description: "Publicité modifiée avec succès" });
onSuccess();
},
onError: (error: Error) => {
toast({
toast({
variant: "destructive",
description: error.message || "Erreur lors de la modification"
description: error.message || "Erreur lors de la modification"
});
}
});
const onSubmit = (data: PublicityFormData) => {
const onSubmit = (data: any) => {
// Transformer l'année en nombre si c'est une string
const formattedData = {
...data,
year: parseInt(data.year.toString()),
// s'assurer que les dates sont bien des objets Date ou null
startDate: data.startDate ? new Date(data.startDate) : undefined,
endDate: data.endDate ? new Date(data.endDate) : undefined,
};
if (publicity) {
updateMutation.mutate(data);
updateMutation.mutate(formattedData);
} else {
createMutation.mutate(data);
createMutation.mutate(formattedData);
}
};
@@ -284,9 +292,9 @@ export default function PublicityForm({ publicity, groups, onSuccess, selectedYe
htmlFor={`group-${group.id}`}
className="flex items-center space-x-2 cursor-pointer"
>
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: group.color }}
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: group.color || '#cccccc' }}
/>
<span>{group.name}</span>
</Label>
+22 -22
View File
@@ -35,9 +35,9 @@ export default function Calendar() {
// Fetch orders and deliveries for the current month with store filtering
const { data: orders = [], isLoading: loadingOrders } = useQuery({
queryKey: ['/api/orders', selectedStoreId, {
startDate: format(monthStart, 'yyyy-MM-dd'),
endDate: format(monthEnd, 'yyyy-MM-dd')
queryKey: ['/api/orders', selectedStoreId, {
startDate: format(monthStart, 'yyyy-MM-dd'),
endDate: format(monthEnd, 'yyyy-MM-dd')
}],
queryFn: async () => {
const params = new URLSearchParams({
@@ -48,7 +48,7 @@ export default function Calendar() {
if (selectedStoreId) {
params.append('storeId', selectedStoreId.toString());
}
const url = `/api/orders?${params.toString()}`;
if (import.meta.env.DEV) {
console.log('📅 Calendar fetching orders:', {
@@ -58,20 +58,20 @@ export default function Calendar() {
params: params.toString()
});
}
const response = await fetch(url, {
credentials: 'include'
});
if (!response.ok) {
throw new Error('Failed to fetch orders');
}
const data = await response.json();
if (import.meta.env.DEV) {
console.log('📅 Calendar orders received:', Array.isArray(data) ? data.length : 'NOT_ARRAY', 'items');
}
// Protection contre les données invalides en production
if (!Array.isArray(data)) {
console.warn('⚠️ Orders data is not an array, returning empty array');
@@ -84,9 +84,9 @@ export default function Calendar() {
});
const { data: deliveries = [], isLoading: loadingDeliveries } = useQuery({
queryKey: ['/api/deliveries', selectedStoreId, {
startDate: format(monthStart, 'yyyy-MM-dd'),
endDate: format(monthEnd, 'yyyy-MM-dd')
queryKey: ['/api/deliveries', selectedStoreId, {
startDate: format(monthStart, 'yyyy-MM-dd'),
endDate: format(monthEnd, 'yyyy-MM-dd')
}],
queryFn: async () => {
const params = new URLSearchParams({
@@ -97,7 +97,7 @@ export default function Calendar() {
if (selectedStoreId) {
params.append('storeId', selectedStoreId.toString());
}
const url = `/api/deliveries?${params.toString()}`;
if (import.meta.env.DEV) {
console.log('📅 Calendar fetching deliveries:', {
@@ -107,20 +107,20 @@ export default function Calendar() {
params: params.toString()
});
}
const response = await fetch(url, {
credentials: 'include'
});
if (!response.ok) {
throw new Error('Failed to fetch deliveries');
}
const data = await response.json();
if (import.meta.env.DEV) {
console.log('📅 Calendar deliveries received:', Array.isArray(data) ? data.length : 'NOT_ARRAY', 'items');
}
// Protection contre les données invalides en production
if (!Array.isArray(data)) {
console.warn('⚠️ Deliveries data is not an array, returning empty array');
@@ -134,15 +134,15 @@ export default function Calendar() {
// Fetch publicities for the current year
const { data: publicities = [], isLoading: loadingPublicities } = useQuery({
queryKey: ['/api/publicities', currentDate.getFullYear(), selectedStoreId],
queryKey: ['/api/ad-campaigns', currentDate.getFullYear(), selectedStoreId],
queryFn: async () => {
const params = new URLSearchParams();
params.append('year', currentDate.getFullYear().toString());
if (selectedStoreId && user?.role === 'admin') {
params.append('storeId', selectedStoreId.toString());
}
const url = `/api/publicities?${params.toString()}`;
const url = `/api/ad-campaigns?${params.toString()}`;
if (import.meta.env.DEV) {
console.log('📅 Calendar fetching publicities:', {
url,
@@ -151,15 +151,15 @@ export default function Calendar() {
year: currentDate.getFullYear()
});
}
const response = await fetch(url, {
credentials: 'include'
});
if (!response.ok) {
throw new Error('Failed to fetch publicities');
}
const data = await response.json();
if (import.meta.env.DEV) {
console.log('📅 Calendar publicities received:', Array.isArray(data) ? data.length : 'NOT_ARRAY', 'items');
+81 -82
View File
@@ -33,19 +33,19 @@ export default function Dashboard() {
year: currentDate.getFullYear().toString(),
month: (currentDate.getMonth() + 1).toString(),
});
if (selectedStoreId && user?.role === 'admin') {
params.append('storeId', selectedStoreId.toString());
}
const response = await fetch(`/api/stats/monthly?${params.toString()}`, {
credentials: 'include'
});
if (!response.ok) {
throw new Error('Failed to fetch stats');
}
return response.json();
},
});
@@ -58,19 +58,19 @@ export default function Dashboard() {
const params = new URLSearchParams({
year: currentDate.getFullYear().toString(),
});
if (selectedStoreId && user?.role === 'admin') {
params.append('storeId', selectedStoreId.toString());
}
const response = await fetch(`/api/stats/yearly?${params.toString()}`, {
credentials: 'include'
});
if (!response.ok) {
throw new Error('Failed to fetch yearly stats');
}
return response.json();
},
});
@@ -102,34 +102,34 @@ export default function Dashboard() {
params.append('storeId', selectedStoreId.toString());
}
params.append('recent', 'true');
const response = await fetch(`/api/announcements?${params.toString()}`, {
credentials: 'include'
});
if (!response.ok) {
return null;
}
const announcements: DashboardMessage[] = await response.json();
// Filtrer les annonces créées il y a moins de 2 jours côté client
const twoDaysAgo = new Date();
twoDaysAgo.setDate(twoDaysAgo.getDate() - 2);
const recentAnnouncements = announcements.filter(announcement => {
if (!announcement.createdAt) return false;
const createdAt = safeDate(announcement.createdAt);
return createdAt && createdAt >= twoDaysAgo;
});
// Retourner la plus récente
return recentAnnouncements.length > 0
return recentAnnouncements.length > 0
? recentAnnouncements.sort((a, b) => {
const dateA = safeDate(a.createdAt);
const dateB = safeDate(b.createdAt);
return (dateB?.getTime() || 0) - (dateA?.getTime() || 0);
})[0]
const dateA = safeDate(a.createdAt);
const dateB = safeDate(b.createdAt);
return (dateB?.getTime() || 0) - (dateA?.getTime() || 0);
})[0]
: null;
},
enabled: !!user,
@@ -139,30 +139,30 @@ export default function Dashboard() {
const getAnnouncementPriorityConfig = (type: string) => {
switch (type) {
case 'error':
return {
label: 'Nouveauté',
return {
label: 'Nouveauté',
color: 'bg-purple-100 text-purple-800',
icon: Sparkles,
iconColor: 'text-purple-600'
};
case 'warning':
return {
label: 'Attention',
return {
label: 'Attention',
color: 'bg-orange-100 text-orange-800',
icon: AlertTriangle,
iconColor: 'text-orange-600'
};
case 'success':
return {
label: 'Important',
return {
label: 'Important',
color: 'bg-red-100 text-red-800',
icon: Star,
iconColor: 'text-red-600'
};
case 'info':
default:
return {
label: 'Information',
return {
label: 'Information',
color: 'bg-blue-100 text-blue-800',
icon: Info,
iconColor: 'text-blue-600'
@@ -195,19 +195,19 @@ export default function Dashboard() {
// Récupérer les publicités à venir (chercher dans 2024 ET 2025) - TOUTES les publicités
const { data: upcomingPublicities = [] } = useQuery<PublicityWithRelations[]>({
queryKey: ['/api/publicities', 'upcoming'],
queryKey: ['/api/ad-campaigns', 'upcoming'],
queryFn: async () => {
// Essayer d'abord 2024, puis 2025 pour avoir toutes les publicités
const years = [2024, 2025];
let allPublicities: PublicityWithRelations[] = [];
for (const year of years) {
const params = new URLSearchParams();
params.append('year', year.toString());
// NE PAS filtrer par magasin - on veut toutes les publicités
try {
const response = await fetch(`/api/publicities?${params}`, { credentials: 'include' });
const response = await fetch(`/api/ad-campaigns?${params}`, { credentials: 'include' });
if (response.ok) {
const yearPublicities = await response.json();
allPublicities = [...allPublicities, ...yearPublicities];
@@ -216,12 +216,12 @@ export default function Dashboard() {
console.log(`Erreur lors de la récupération des publicités ${year}:`, error);
}
}
// Filtrer les publicités à venir et les trier par date
const futurePublicities = allPublicities
.filter((publicity: any) => new Date(publicity.startDate) > new Date())
.sort((a: any, b: any) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime());
return futurePublicities;
},
});
@@ -249,10 +249,10 @@ export default function Dashboard() {
}
return;
}
// Vérifier s'il y a des problèmes DLC
const shouldShowDlcModal = dlcStats.expired > 0 || dlcStats.expiringSoon > 0;
if (shouldShowDlcModal && !showDlcAlertModal) {
// Vérifier s'il n'y a pas de snooze actif
const snoozedUntil = localStorage.getItem('dlcAlertSnooze');
@@ -305,7 +305,7 @@ export default function Dashboard() {
const dateB = safeDate(b.createdAt);
return (dateB ? dateB.getTime() : 0) - (dateA ? dateA.getTime() : 0);
}) : []; // Afficher toutes les commandes en attente
const upcomingDeliveries = Array.isArray(allDeliveries) ? allDeliveries
.filter((d: any) => {
const isPlanned = d.status === 'planned';
@@ -314,7 +314,7 @@ export default function Dashboard() {
})
.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);
// Calculs pour les statistiques
@@ -324,9 +324,9 @@ export default function Dashboard() {
const deliveryDate = safeDate(delivery.deliveredDate || delivery.createdAt);
if (!deliveryDate) return false;
const now = new Date();
return deliveryDate.getMonth() === now.getMonth() &&
deliveryDate.getFullYear() === now.getFullYear() &&
delivery.status === 'delivered';
return deliveryDate.getMonth() === now.getMonth() &&
deliveryDate.getFullYear() === now.getFullYear() &&
delivery.status === 'delivered';
}).length : 0;
// Calculer le total des palettes pour les livraisons 'delivered' du mois en cours
@@ -346,35 +346,35 @@ export default function Dashboard() {
const getPriorityConfig = (priority: string) => {
switch (priority) {
case 'high':
return {
color: 'destructive' as const,
icon: AlertTriangle,
label: 'Élevée'
return {
color: 'destructive' as const,
icon: AlertTriangle,
label: 'Élevée'
};
case 'medium':
return {
color: 'default' as const,
icon: Clock,
label: 'Moyenne'
return {
color: 'default' as const,
icon: Clock,
label: 'Moyenne'
};
case 'low':
return {
color: 'secondary' as const,
icon: Circle,
label: 'Faible'
return {
color: 'secondary' as const,
icon: Circle,
label: 'Faible'
};
default:
return {
color: 'secondary' as const,
icon: Circle,
label: 'Moyenne'
return {
color: 'secondary' as const,
icon: Circle,
label: 'Moyenne'
};
}
};
console.log('📊 Dashboard Debug - Raw Data:', {
allOrders: Array.isArray(allOrders) ? allOrders.length : 'NOT_ARRAY',
allDeliveries: Array.isArray(allDeliveries) ? allDeliveries.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,
@@ -423,7 +423,7 @@ export default function Dashboard() {
{/* Alerts */}
<div className="space-y-3">
{dlcStats.expiringSoon > 0 && (
<div className="bg-yellow-50 border-l-4 border-yellow-400 p-4 flex items-center space-x-3 shadow-sm">
<Clock className="h-5 w-5 text-yellow-600" />
@@ -432,7 +432,7 @@ export default function Dashboard() {
</span>
</div>
)}
{dlcStats.expired > 0 && (
<div className="bg-red-50 border-l-4 border-red-400 p-4 flex items-center space-x-3 shadow-sm">
<XCircle className="h-5 w-5 text-red-600" />
@@ -523,11 +523,10 @@ export default function Dashboard() {
const orderDate = safeDate(order.plannedDate);
const daysPending = orderDate ? Math.floor((new Date().getTime() - orderDate.getTime()) / (1000 * 60 * 60 * 24)) : 0;
const isOverdue = daysPending > 10;
return (
<div key={order.id} className={`flex items-center justify-between p-4 transition-colors border-l-3 ${
isOverdue ? 'bg-red-50 hover:bg-red-100 border-red-500' : 'bg-orange-50 hover:bg-orange-100 border-orange-500'
}`}>
<div key={order.id} className={`flex items-center justify-between p-4 transition-colors border-l-3 ${isOverdue ? 'bg-red-50 hover:bg-red-100 border-red-500' : 'bg-orange-50 hover:bg-orange-100 border-orange-500'
}`}>
<div className="flex items-center space-x-3">
<div className={`h-2 w-2 ${isOverdue ? 'bg-red-500' : 'bg-orange-500'}`}></div>
<div>
@@ -536,7 +535,7 @@ export default function Dashboard() {
</div>
</div>
<div className="text-right">
<Badge
<Badge
variant={isOverdue ? 'destructive' : 'secondary'}
className="text-xs"
>
@@ -604,7 +603,7 @@ export default function Dashboard() {
.map((publicity: any) => {
const participatingStores = publicity.participations || [];
const isCurrentStoreParticipating = selectedStoreId && participatingStores.some((p: any) => p.groupId === parseInt(selectedStoreId.toString()));
return (
<div key={publicity.id} className="p-4 bg-gray-50 hover:bg-gray-100 transition-colors border-l-3 border-purple-500 space-y-2">
<div className="flex items-center justify-between">
@@ -624,7 +623,7 @@ export default function Dashboard() {
</p>
</div>
</div>
{/* Magasins participants */}
<div className="flex items-center space-x-2 text-xs">
<span className="text-gray-500">Magasins:</span>
@@ -635,12 +634,12 @@ export default function Dashboard() {
{participatingStores.map((participation: any) => {
const isCurrentStore = selectedStoreId && participation.groupId === parseInt(selectedStoreId.toString());
const groupColor = participation.group?.color || '#1976D2';
return (
<Badge
key={participation.groupId}
<Badge
key={participation.groupId}
className={`text-xs text-white border ${isCurrentStore ? 'ring-2 ring-white ring-offset-1' : ''}`}
style={{
style={{
backgroundColor: groupColor,
borderColor: groupColor
}}
@@ -690,7 +689,7 @@ export default function Dashboard() {
.map((task: any) => {
const priorityConfig = getPriorityConfig(task.priority);
const PriorityIcon = priorityConfig.icon;
return (
<div key={task.id} className="flex items-center justify-between p-4 bg-gray-50 hover:bg-gray-100 transition-colors border-l-3 border-blue-500">
<div className="flex items-center space-x-3">
@@ -702,7 +701,7 @@ export default function Dashboard() {
</div>
<div className="text-right space-y-1">
<div className="flex items-center justify-end space-x-2">
<Badge
<Badge
className={`text-xs flex items-center gap-1 ${priorityConfig.color}`}
>
<PriorityIcon className="h-3 w-3" />
@@ -716,11 +715,11 @@ export default function Dashboard() {
</div>
);
}) : (
<div className="text-center py-8">
<p className="text-gray-600">Aucune tâche en cours</p>
<p className="text-xs text-gray-400 mt-1">Toutes les tâches sont terminées</p>
</div>
)}
<div className="text-center py-8">
<p className="text-gray-600">Aucune tâche en cours</p>
<p className="text-xs text-gray-400 mt-1">Toutes les tâches sont terminées</p>
</div>
)}
</CardContent>
</Card>
</div>
@@ -734,7 +733,7 @@ export default function Dashboard() {
Nouvelle Annonce
</DialogTitle>
</DialogHeader>
{recentAnnouncement && (
<div className="space-y-4">
<div className="flex items-center justify-between">
@@ -756,7 +755,7 @@ export default function Dashboard() {
{safeFormat(recentAnnouncement.createdAt, "d MMMM yyyy 'à' HH:mm")}
</p>
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
{recentAnnouncement.title}
@@ -767,7 +766,7 @@ export default function Dashboard() {
</p>
</div>
</div>
{recentAnnouncement.createdBy && (
<div className="flex items-center gap-2 pt-2 border-t border-gray-100">
<User className="h-4 w-4 text-gray-400" />
@@ -778,7 +777,7 @@ export default function Dashboard() {
)}
</div>
)}
<div className="flex justify-end pt-4">
<Button onClick={handleCloseAnnouncementModal} className="flex items-center gap-2">
<X className="h-4 w-4" />
@@ -793,7 +792,7 @@ export default function Dashboard() {
isOpen={showDlcAlertModal}
onClose={handleCloseDlcAlertModal}
dlcStats={dlcStats}
selectedStoreId={selectedStoreId}
selectedStoreId={selectedStoreId ?? undefined}
/>
</div>
);
+6 -6
View File
@@ -39,15 +39,15 @@ export default function Publicities() {
const startYear = currentYear - 2;
const yearOptions = Array.from({ length: 13 }, (_, i) => startYear + i);
const { data: publicities = [], isLoading } = useQuery({
queryKey: ['/api/publicities', selectedYear, selectedStoreId],
const { data: publicities = [], isLoading } = useQuery<PublicityWithRelations[]>({
queryKey: ['/api/ad-campaigns', 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}`);
const response = await fetch(`/api/ad-campaigns?${params}`);
if (!response.ok) {
throw new Error('Failed to fetch publicities');
}
@@ -99,7 +99,7 @@ export default function Publicities() {
console.log(`🗑️ [Frontend] Attempting to delete publicity ${id} using POST method`);
try {
const response = await fetch(`/api/publicities/${id}/delete`, {
const response = await fetch(`/api/ad-campaigns/${id}/delete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
@@ -111,7 +111,7 @@ export default function Publicities() {
} else if (response.status === 404) {
// Route not found, try DELETE method as fallback (for dev environment)
console.log(`⚠️ [Frontend] POST route not found, trying DELETE method`);
const deleteResponse = await fetch(`/api/publicities/${id}`, { method: 'DELETE' });
const deleteResponse = await fetch(`/api/ad-campaigns/${id}`, { method: 'DELETE' });
if (!deleteResponse.ok) {
const error = await deleteResponse.json();
@@ -133,7 +133,7 @@ export default function Publicities() {
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/publicities'] });
queryClient.invalidateQueries({ queryKey: ['/api/ad-campaigns'] });
toast({ description: "Publicité supprimée avec succès" });
setIsDeleteModalOpen(false);
setSelectedPublicity(null);
+5 -4
View File
@@ -34,16 +34,17 @@ export default function MobilePublicitiesPage() {
const [selectedYear, setSelectedYear] = useState<number>(new Date().getFullYear());
const [searchTerm, setSearchTerm] = useState("");
// Générer les années pour le select (année courante +/- 2 ans)
// Générer les années pour le select (année courante - 2 ans à + 10 ans)
const currentYear = new Date().getFullYear();
const years = Array.from({ length: 5 }, (_, i) => currentYear - 2 + i);
const startYear = currentYear - 2;
const years = Array.from({ length: 13 }, (_, i) => startYear + i);
const { data: publicities = [], isLoading } = useQuery({
queryKey: ["/api/publicities", selectedStoreId, selectedYear],
queryKey: ["/api/ad-campaigns", selectedStoreId, selectedYear],
queryFn: async () => {
if (!selectedStoreId) return [];
const response = await fetch(
`/api/publicities?storeId=${selectedStoreId}&year=${selectedYear}`,
`/api/ad-campaigns?storeId=${selectedStoreId}&year=${selectedYear}`,
{ credentials: 'include' }
);
if (!response.ok) throw new Error("Failed to fetch publicities");
+21 -12
View File
@@ -45,7 +45,6 @@
"@types/form-data": "^2.2.1",
"@types/memoizee": "^0.4.12",
"@types/multer": "^2.0.0",
"@types/node-fetch": "^2.6.13",
"@types/pg": "^8.15.4",
"bcrypt": "^6.0.0",
"busboy": "^1.6.0",
@@ -102,13 +101,15 @@
"@tailwindcss/typography": "^0.5.15",
"@tailwindcss/vite": "^4.1.3",
"@types/connect-pg-simple": "^7.0.3",
"@types/express": "4.17.21",
"@types/date-fns": "^2.5.3",
"@types/express": "^4.17.21",
"@types/express-session": "^1.18.0",
"@types/node": "20.16.11",
"@types/node": "^20.16.11",
"@types/node-fetch": "^2.6.13",
"@types/passport": "^1.0.16",
"@types/passport-local": "^1.0.38",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.1",
"@types/react": "^18.3.27",
"@types/react-dom": "^18.3.7",
"@types/ws": "^8.5.13",
"@vitejs/plugin-react": "^4.3.2",
"autoprefixer": "^10.4.20",
@@ -3720,6 +3721,13 @@
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
"license": "MIT"
},
"node_modules/@types/date-fns": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/@types/date-fns/-/date-fns-2.5.3.tgz",
"integrity": "sha512-4KVPD3g5RjSgZtdOjvI/TDFkLNUHhdoWxmierdQbDeEg17Rov0hbBYtIzNaQA67ORpteOhvR9YEMTb6xeDCang==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -3810,6 +3818,7 @@
"version": "2.6.13",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz",
"integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
@@ -3880,14 +3889,14 @@
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.23",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz",
"integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==",
"version": "18.3.27",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
"csstype": "^3.2.2"
}
},
"node_modules/@types/react-dom": {
@@ -4600,9 +4609,9 @@
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT"
},
"node_modules/d": {
+6 -5
View File
@@ -47,7 +47,6 @@
"@types/form-data": "^2.2.1",
"@types/memoizee": "^0.4.12",
"@types/multer": "^2.0.0",
"@types/node-fetch": "^2.6.13",
"@types/pg": "^8.15.4",
"bcrypt": "^6.0.0",
"busboy": "^1.6.0",
@@ -104,13 +103,15 @@
"@tailwindcss/typography": "^0.5.15",
"@tailwindcss/vite": "^4.1.3",
"@types/connect-pg-simple": "^7.0.3",
"@types/express": "4.17.21",
"@types/date-fns": "^2.5.3",
"@types/express": "^4.17.21",
"@types/express-session": "^1.18.0",
"@types/node": "20.16.11",
"@types/node": "^20.16.11",
"@types/node-fetch": "^2.6.13",
"@types/passport": "^1.0.16",
"@types/passport-local": "^1.0.38",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.1",
"@types/react": "^18.3.27",
"@types/react-dom": "^18.3.7",
"@types/ws": "^8.5.13",
"@vitejs/plugin-react": "^4.3.2",
"autoprefixer": "^10.4.20",
+473 -463
View File
File diff suppressed because it is too large. Load diff
+26 -13
View File
@@ -1,25 +1,38 @@
# Task: Redirection authentification si déconnecté
# Task: Correction blocage API Publicités
## Context
L'utilisateur signale que lorsqu'il est déconnecté et navigue ou se trouve sur une page, il n'est pas systématiquement renvoyé vers la page d'authentification. Il souhaite une redirection immédiate.
L'utilisateur rencontre une erreur `net::ERR_BLOCKED_BY_CLIENT` lors de l'appel à l'API `/api/publicities`. Cette erreur est typique des bloqueurs de publicité (AdBlock, uBlock) qui filtrent les requêtes contenant des mots-clés comme "publicity" ou "ads".
## Current Focus
Vérification terminée.
Renommer les endpoints API pour éviter le blocage par les bloqueurs de publicité.
## Master Plan
- [x] Analyser le fichier principal de routing (ex: `App.tsx` ou `main.tsx`).
- [x] Examiner le hook d'authentification `useAuthUnified` et les composants de protection de route (ex: `ProtectedRoute`).
- [x] Identifier pourquoi la redirection ne se fait pas ou pas correctement.
- [x] Implémenter/Corriger la logique de redirection vers `/auth` (ou la page de login).
- [x] Vérifier le comportement.
- [x] **Remplacement des API calls (Client-side)**
- [x] `client/src/pages/Calendar.tsx`
- [x] `client/src/pages/Dashboard.tsx`
- [x] `client/src/components/PublicityForm.tsx`
- [x] `client/src/pages/Publicities.tsx`
- [x] `client/src/pages/mobile/PublicitiesPage.tsx`
- [x] **Revue du sélecteur d'année Mobile**
- [x] Vérifier la logique de génération des années dans `client/src/pages/mobile/PublicitiesPage.tsx` pour inclure les années passées (ex: 2024 en 2026).
- [x] Aligner avec la correction desktop si nécessaire.
- [x] **Correction des erreurs Lint TypeScript**
- [x] Installer les paquets `@types` manquants (`node`, `express`, `react`, etc.).
- [x] Fixer les paramètres `req` et `res` (implicitement `any`) dans `server/routes.ts`.
- [x] Corriger l'erreur de propriété `invoiceAmountTTC` dans `server/routes.ts`.
- [x] Résoudre `Property 'env' does not exist on type 'ImportMeta'`.
- [x] Résoudre `JSX element implicitly has type 'any'`.
- [x] **Tests et Validation**
- [x] Vérifier que les appels API utilisent bien `/api/ad-campaigns`.
- [x] Tester le sélecteur d'année sur mobile.
- [x] Compiler le projet (`npm run check`) pour s'assurer que les erreurs critiques sont résolues.
## Progress Log
- Création de la tâche.
- Analyse de `App.tsx` et `useAuthUnified.ts`.
- Identification que `RouterProduction.tsx` renvoyait `NotFound` au lieu de rediriger.
- Modification de `RouterProduction.tsx` pour rediriger vers `/auth` toutes les routes inconnues si non authentifié.
- Correction d'une erreur de remplacement de code en réécrivant le fichier complet.
- Création de la tâche suite aux logs fournis.
+32 -7
View File
@@ -1,6 +1,15 @@
{
"include": ["client/src/**/*", "shared/**/*", "server/**/*"],
"exclude": ["node_modules", "build", "dist", "**/*.test.ts"],
"include": [
"client/src/**/*",
"shared/**/*",
"server/**/*"
],
"exclude": [
"node_modules",
"build",
"dist",
"**/*.test.ts"
],
"compilerOptions": {
"incremental": true,
"tsBuildInfoFile": "./node_modules/typescript/tsbuildinfo",
@@ -8,7 +17,11 @@
"target": "ES2020",
"module": "ES2022",
"strict": true,
"lib": ["ES2020", "dom", "dom.iterable"],
"lib": [
"ES2020",
"dom",
"dom.iterable"
],
"jsx": "preserve",
"esModuleInterop": true,
"skipLibCheck": true,
@@ -16,10 +29,22 @@
"moduleResolution": "bundler",
"downlevelIteration": true,
"baseUrl": ".",
"types": ["node", "vite/client"],
"types": [
"node",
"vite/client",
"express",
"react",
"react-dom",
"node-fetch",
"date-fns"
],
"paths": {
"@/*": ["./client/src/*"],
"@shared/*": ["./shared/*"]
"@/*": [
"./client/src/*"
],
"@shared/*": [
"./shared/*"
]
}
}
}
}