mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-11 17:27:31 +02:00
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:
1 file changed
+127
-48
@@ -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>
|
||||||
))
|
))
|
||||||
|
|||||||
Reference in new issue
Block a user