Files
LogiFlow/client/src/pages/Calendar.tsx
T
michaelschal 3b46f945a4 Stabilize calendar display by refining data caching and invalidation
Fix an issue where closing an order or delivery card caused all calendar entries to disappear by implementing selective cache invalidation and adjusting cache timings.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 7c468936-2a88-4686-ac0a-84921a45222d
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/7c468936-2a88-4686-ac0a-84921a45222d/B7k6m01
2025-08-13 20:34:02 +00:00

337 lines
11 KiB
TypeScript

import { useState } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { ChevronLeft, ChevronRight, Plus, RefreshCw, Calendar as CalendarIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import CalendarGrid from "@/components/CalendarGrid";
import QuickCreateMenu from "@/components/modals/QuickCreateMenu";
import OrderDetailModal from "@/components/modals/OrderDetailModal";
import CreateOrderModal from "@/components/modals/CreateOrderModal";
import CreateDeliveryModal from "@/components/modals/CreateDeliveryModal";
import StatsPanel from "@/components/StatsPanel";
import { useAuthUnified } from "@/hooks/useAuthUnified";
import { useStore } from "@/components/Layout";
import { apiRequest } from "@/lib/queryClient";
import { useToast } from "@/hooks/use-toast";
import { format, startOfMonth, endOfMonth } from "date-fns";
import { fr } from "date-fns/locale";
import { hasPermission } from "@/lib/permissions";
export default function Calendar() {
const { user } = useAuthUnified();
const { selectedStoreId } = useStore();
const queryClient = useQueryClient();
const { toast } = useToast();
const [currentDate, setCurrentDate] = useState(new Date()); // Current date
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
const [showQuickCreate, setShowQuickCreate] = useState(false);
const [showOrderDetail, setShowOrderDetail] = useState(false);
const [showCreateOrder, setShowCreateOrder] = useState(false);
const [showCreateDelivery, setShowCreateDelivery] = useState(false);
const [selectedItem, setSelectedItem] = useState<any>(null);
const monthStart = startOfMonth(currentDate);
const monthEnd = endOfMonth(currentDate);
// 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')
}],
queryFn: async () => {
const params = new URLSearchParams({
startDate: format(monthStart, 'yyyy-MM-dd'),
endDate: format(monthEnd, 'yyyy-MM-dd')
});
if (selectedStoreId && user?.role === 'admin') {
params.append('storeId', selectedStoreId.toString());
}
const url = `/api/orders?${params.toString()}`;
if (import.meta.env.DEV) {
console.log('📅 Calendar fetching orders:', {
url,
selectedStoreId,
userRole: user?.role,
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');
return [];
}
return data;
},
staleTime: 5 * 60 * 1000, // 5 minutes de cache pour éviter la disparition des données
gcTime: 10 * 60 * 1000, // 10 minutes en cache
});
const { data: deliveries = [], isLoading: loadingDeliveries } = useQuery({
queryKey: ['/api/deliveries', selectedStoreId, {
startDate: format(monthStart, 'yyyy-MM-dd'),
endDate: format(monthEnd, 'yyyy-MM-dd')
}],
queryFn: async () => {
const params = new URLSearchParams({
startDate: format(monthStart, 'yyyy-MM-dd'),
endDate: format(monthEnd, 'yyyy-MM-dd')
});
if (selectedStoreId && user?.role === 'admin') {
params.append('storeId', selectedStoreId.toString());
}
const url = `/api/deliveries?${params.toString()}`;
if (import.meta.env.DEV) {
console.log('📅 Calendar fetching deliveries:', {
url,
selectedStoreId,
userRole: user?.role,
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');
return [];
}
return data;
},
staleTime: 5 * 60 * 1000, // 5 minutes de cache pour éviter la disparition des données
gcTime: 10 * 60 * 1000, // 10 minutes en cache
});
// Fetch publicities for the current year
const { data: publicities = [], isLoading: loadingPublicities } = useQuery({
queryKey: ['/api/publicities', 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()}`;
if (import.meta.env.DEV) {
console.log('📅 Calendar fetching publicities:', {
url,
selectedStoreId,
userRole: user?.role,
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');
}
return Array.isArray(data) ? data : [];
},
});
const navigateMonth = (direction: 'prev' | 'next') => {
const newDate = new Date(currentDate);
if (direction === 'prev') {
newDate.setMonth(newDate.getMonth() - 1);
} else {
newDate.setMonth(newDate.getMonth() + 1);
}
setCurrentDate(newDate);
};
const handleDateClick = (date: Date) => {
setSelectedDate(date);
setShowQuickCreate(true);
};
const handleItemClick = (item: any, type: 'order' | 'delivery') => {
// Ne pas invalider le cache à l'ouverture pour éviter la disparition des données
setSelectedItem({ ...item, type });
setShowOrderDetail(true);
};
const handleCreateOrder = () => {
setShowQuickCreate(false);
setShowCreateOrder(true);
};
const handleCreateDelivery = () => {
setShowQuickCreate(false);
setShowCreateDelivery(true);
};
const isLoading = loadingOrders || loadingDeliveries || loadingPublicities;
return (
<div className="flex-1 flex flex-col overflow-hidden">
{/* Header */}
<div className="bg-white shadow-sm border-b border-gray-200 p-6">
<div className="flex items-center justify-between">
<div className="flex items-center">
<CalendarIcon className="w-8 h-8 text-blue-600 mr-3" />
<div>
<h2 className="text-2xl font-bold text-gray-900">
Calendrier des Commandes & Livraisons
</h2>
<p className="text-gray-600">
{format(currentDate, 'MMMM yyyy', { locale: fr })}
</p>
</div>
</div>
<div className="flex items-center space-x-4">
{/* Legend */}
<div className="flex items-center space-x-6 text-sm">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-primary rounded"></div>
<span className="text-gray-600">Commandes</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-secondary rounded"></div>
<span className="text-gray-600">Livraisons</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-delivered rounded"></div>
<span className="text-gray-600">Livré</span>
</div>
</div>
</div>
</div>
</div>
{/* Calendar Navigation */}
<div className="p-6 pb-0">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-4">
<Button
variant="outline"
size="sm"
onClick={() => navigateMonth('prev')}
className="p-2"
>
<ChevronLeft className="w-4 h-4" />
</Button>
<h3 className="text-xl font-semibold text-gray-900">
{format(currentDate, 'MMMM yyyy', { locale: fr })}
</h3>
<Button
variant="outline"
size="sm"
onClick={() => navigateMonth('next')}
className="p-2"
>
<ChevronRight className="w-4 h-4" />
</Button>
</div>
<div className="flex items-center space-x-2">
{(hasPermission(user?.role || '', 'orders', 'create') || hasPermission(user?.role || '', 'deliveries', 'create')) && (
<Button
onClick={() => setShowQuickCreate(true)}
className="bg-accent hover:bg-orange-600 text-white"
>
<Plus className="w-4 h-4 mr-2" />
Nouveau
</Button>
)}
</div>
</div>
</div>
{/* Calendar Grid */}
<div className="flex-1 p-6 overflow-auto">
{isLoading ? (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
</div>
) : (
<CalendarGrid
currentDate={currentDate}
orders={orders || []}
deliveries={deliveries || []}
publicities={publicities || []}
selectedStoreId={selectedStoreId}
userGroups={user?.userGroups || []}
onDateClick={handleDateClick}
onItemClick={handleItemClick}
/>
)}
</div>
{/* Modals */}
{showQuickCreate && (
<QuickCreateMenu
isOpen={showQuickCreate}
onClose={() => setShowQuickCreate(false)}
onCreateOrder={handleCreateOrder}
onCreateDelivery={handleCreateDelivery}
/>
)}
{showOrderDetail && selectedItem && (
<OrderDetailModal
isOpen={showOrderDetail}
onClose={() => setShowOrderDetail(false)}
item={selectedItem}
/>
)}
{showCreateOrder && (
<CreateOrderModal
isOpen={showCreateOrder}
onClose={() => setShowCreateOrder(false)}
selectedDate={selectedDate}
/>
)}
{showCreateDelivery && (
<CreateDeliveryModal
isOpen={showCreateDelivery}
onClose={() => setShowCreateDelivery(false)}
selectedDate={selectedDate}
/>
)}
{/* Stats Panel */}
<StatsPanel currentDate={currentDate} />
</div>
);
}