Merge pull request #539 from R0m1k3/1.1

feat: add mobile calendar page to display orders and deliveries as ev…
This commit is contained in:
LogiFlow authored and GitHub committed 2025-12-10 14:26:08 +01:00
commit c943698e29
1 file changed
+127 -48
+127 -48
View File
@@ -1,5 +1,6 @@
/** /**
* CalendarPage Mobile - Calendrier simplifié pour mobile * CalendarPage Mobile - Calendrier simplifié pour mobile
* Affiche les commandes et livraisons comme événements
*/ */
import { useState } from "react"; import { useState } from "react";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
@@ -13,8 +14,8 @@ import {
Calendar as CalendarIcon, Calendar as CalendarIcon,
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
Clock, Package,
MapPin Truck
} from "lucide-react"; } from "lucide-react";
import { import {
format, format,
@@ -31,15 +32,14 @@ import {
} from "date-fns"; } from "date-fns";
import { fr } from "date-fns/locale"; import { fr } from "date-fns/locale";
// Type pour les événements // Type pour les événements (combinaison de commandes et livraisons)
type CalendarEvent = { type CalendarEvent = {
id: number; id: number;
title: string; title: string;
startDate: string; date: string;
endDate?: string; type: 'order' | 'delivery';
description?: string; status?: string;
location?: string; supplier?: { name: string };
type?: string;
}; };
export default function MobileCalendarPage() { export default function MobileCalendarPage() {
@@ -48,15 +48,20 @@ export default function MobileCalendarPage() {
const [currentMonth, setCurrentMonth] = useState(new Date()); const [currentMonth, setCurrentMonth] = useState(new Date());
const [selectedDate, setSelectedDate] = useState<Date | null>(new Date()); const [selectedDate, setSelectedDate] = useState<Date | null>(new Date());
// Fetch calendar events const monthStart = startOfMonth(currentMonth);
const { data: events = [], isLoading } = useQuery({ const monthEnd = endOfMonth(currentMonth);
queryKey: ["/api/calendar/events", selectedStoreId, format(currentMonth, 'yyyy-MM')],
queryFn: async () => {
const params = new URLSearchParams();
if (selectedStoreId) params.append('storeId', selectedStoreId.toString());
params.append('month', format(currentMonth, 'yyyy-MM'));
const response = await fetch(`/api/calendar/events?${params}`, { // Fetch orders for current month
const { data: orders = [] } = useQuery({
queryKey: ["/api/orders", selectedStoreId, format(currentMonth, 'yyyy-MM')],
queryFn: async () => {
const params = new URLSearchParams({
startDate: format(monthStart, 'yyyy-MM-dd'),
endDate: format(monthEnd, 'yyyy-MM-dd')
});
if (selectedStoreId) params.append('storeId', selectedStoreId.toString());
const response = await fetch(`/api/orders?${params}`, {
credentials: 'include' credentials: 'include'
}); });
if (!response.ok) return []; if (!response.ok) return [];
@@ -66,25 +71,67 @@ export default function MobileCalendarPage() {
enabled: !!user, enabled: !!user,
}); });
// Fetch deliveries for current month
const { data: deliveries = [] } = useQuery({
queryKey: ["/api/deliveries", selectedStoreId, format(currentMonth, 'yyyy-MM')],
queryFn: async () => {
const params = new URLSearchParams({
startDate: format(monthStart, 'yyyy-MM-dd'),
endDate: format(monthEnd, 'yyyy-MM-dd')
});
if (selectedStoreId) params.append('storeId', selectedStoreId.toString());
const response = await fetch(`/api/deliveries?${params}`, {
credentials: 'include'
});
if (!response.ok) return [];
const data = await response.json();
return Array.isArray(data) ? data : [];
},
enabled: !!user,
});
// Combine orders and deliveries into events
const events: CalendarEvent[] = [
...orders.map((order: any) => ({
id: order.id,
title: `Cmd #${order.orderNumber || order.id}`,
date: order.orderDate,
type: 'order' as const,
status: order.status,
supplier: order.supplier
})),
...deliveries.map((delivery: any) => ({
id: delivery.id,
title: `Liv #${delivery.deliveryNumber || delivery.id}`,
date: delivery.expectedDate,
type: 'delivery' as const,
status: delivery.status,
supplier: delivery.supplier
}))
];
// Get days for the calendar grid // Get days for the calendar grid
const monthStart = startOfMonth(currentMonth);
const monthEnd = endOfMonth(currentMonth);
const calendarStart = startOfWeek(monthStart, { locale: fr }); const calendarStart = startOfWeek(monthStart, { locale: fr });
const calendarEnd = endOfWeek(monthEnd, { locale: fr }); const calendarEnd = endOfWeek(monthEnd, { locale: fr });
const calendarDays = eachDayOfInterval({ start: calendarStart, end: calendarEnd }); const calendarDays = eachDayOfInterval({ start: calendarStart, end: calendarEnd });
// Get events for selected date // Get events for selected date
const selectedDateEvents = selectedDate const selectedDateEvents = selectedDate
? events.filter((event: CalendarEvent) => ? events.filter((event) => {
isSameDay(new Date(event.startDate), selectedDate) if (!event.date) return false;
) const eventDate = new Date(event.date);
return !isNaN(eventDate.getTime()) && isSameDay(eventDate, selectedDate);
})
: []; : [];
// Check if a day has events // Check if a day has events
const hasEvents = (day: Date) => { const getEventsForDay = (day: Date) => {
return events.some((event: CalendarEvent) => return events.filter((event) => {
isSameDay(new Date(event.startDate), day) if (!event.date) return false;
); const eventDate = new Date(event.date);
return !isNaN(eventDate.getTime()) && isSameDay(eventDate, day);
});
}; };
const navigateMonth = (direction: 'prev' | 'next') => { const navigateMonth = (direction: 'prev' | 'next') => {
@@ -103,6 +150,7 @@ export default function MobileCalendarPage() {
<CalendarIcon className="h-5 w-5 text-purple-600" /> <CalendarIcon className="h-5 w-5 text-purple-600" />
<h1 className="text-lg font-bold">Calendrier</h1> <h1 className="text-lg font-bold">Calendrier</h1>
</div> </div>
<Badge variant="secondary">{events.length} événements</Badge>
</div> </div>
{/* Month navigation */} {/* Month navigation */}
@@ -130,6 +178,18 @@ export default function MobileCalendarPage() {
</Button> </Button>
</div> </div>
{/* Legend */}
<div className="flex gap-4 text-xs">
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-blue-500 rounded-full" />
<span>Commandes</span>
</div>
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-green-500 rounded-full" />
<span>Livraisons</span>
</div>
</div>
{/* Calendar grid */} {/* Calendar grid */}
<Card> <Card>
<CardContent className="p-2"> <CardContent className="p-2">
@@ -151,14 +211,16 @@ export default function MobileCalendarPage() {
const isCurrentMonth = isSameMonth(day, currentMonth); const isCurrentMonth = isSameMonth(day, currentMonth);
const isTodayDate = isToday(day); const isTodayDate = isToday(day);
const isSelected = selectedDate && isSameDay(day, selectedDate); const isSelected = selectedDate && isSameDay(day, selectedDate);
const dayHasEvents = hasEvents(day); const dayEvents = getEventsForDay(day);
const hasOrders = dayEvents.some(e => e.type === 'order');
const hasDeliveries = dayEvents.some(e => e.type === 'delivery');
return ( return (
<button <button
key={i} key={i}
onClick={() => setSelectedDate(day)} onClick={() => setSelectedDate(day)}
className={` className={`
relative aspect-square flex items-center justify-center relative aspect-square flex flex-col items-center justify-center
text-sm rounded-lg transition-colors text-sm rounded-lg transition-colors
${!isCurrentMonth ? 'text-gray-300' : 'text-gray-700'} ${!isCurrentMonth ? 'text-gray-300' : 'text-gray-700'}
${isTodayDate ? 'font-bold' : ''} ${isTodayDate ? 'font-bold' : ''}
@@ -166,8 +228,11 @@ export default function MobileCalendarPage() {
`} `}
> >
{format(day, 'd')} {format(day, 'd')}
{dayHasEvents && !isSelected && ( {(hasOrders || hasDeliveries) && !isSelected && (
<span className="absolute bottom-0.5 left-1/2 -translate-x-1/2 w-1 h-1 bg-blue-600 rounded-full" /> <div className="absolute bottom-0.5 flex gap-0.5">
{hasOrders && <span className="w-1 h-1 bg-blue-500 rounded-full" />}
{hasDeliveries && <span className="w-1 h-1 bg-green-500 rounded-full" />}
</div>
)} )}
</button> </button>
); );
@@ -180,32 +245,46 @@ export default function MobileCalendarPage() {
{selectedDate && ( {selectedDate && (
<div className="space-y-2"> <div className="space-y-2">
<h3 className="font-semibold text-gray-900"> <h3 className="font-semibold text-gray-900">
{format(selectedDate, 'd MMMM', { locale: fr })} {format(selectedDate, 'EEEE d MMMM', { locale: fr })}
</h3> </h3>
{selectedDateEvents.length === 0 ? ( {selectedDateEvents.length === 0 ? (
<p className="text-sm text-gray-500 py-4 text-center"> <p className="text-sm text-gray-500 py-4 text-center">
Aucun événement Aucun événement ce jour
</p> </p>
) : ( ) : (
selectedDateEvents.map((event: CalendarEvent) => ( selectedDateEvents.map((event) => (
<Card key={event.id}> <Card key={`${event.type}-${event.id}`}>
<CardContent className="p-3"> <CardContent className="p-3">
<h4 className="font-medium text-gray-900 mb-1"> <div className="flex items-start gap-3">
{event.title} <div className={`p-2 rounded-lg ${event.type === 'order'
</h4> ? 'bg-blue-100'
{event.startDate && ( : 'bg-green-100'
<p className="text-sm text-gray-600 flex items-center gap-1"> }`}>
<Clock className="h-3 w-3" /> {event.type === 'order'
{format(new Date(event.startDate), 'HH:mm')} ? <Package className="h-4 w-4 text-blue-600" />
</p> : <Truck className="h-4 w-4 text-green-600" />
)} }
{event.location && ( </div>
<p className="text-sm text-gray-600 flex items-center gap-1 mt-1"> <div className="flex-1">
<MapPin className="h-3 w-3" /> <h4 className="font-medium text-gray-900">
{event.location} {event.title}
</p> </h4>
)} {event.supplier && (
<p className="text-sm text-gray-600">
{event.supplier.name}
</p>
)}
{event.status && (
<Badge
variant="secondary"
className="mt-1 text-xs"
>
{event.status}
</Badge>
)}
</div>
</div>
</CardContent> </CardContent>
</Card> </Card>
)) ))