mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-11 17:27:31 +02:00
feat: add mobile layout and bottom navigation components
This commit is contained in:
1 parent
69773e3d2f
commit
946b5f30bd
12 files changed
+1219
-4
No files matched your search
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* ResponsiveCalendar - Wrapper for automatic mobile/desktop detection
|
||||||
|
*/
|
||||||
|
import { useScreenSize } from "@/hooks/use-screen-size";
|
||||||
|
import Calendar from "@/pages/Calendar";
|
||||||
|
import MobileCalendarPage from "@/pages/mobile/CalendarPage";
|
||||||
|
|
||||||
|
export default function ResponsiveCalendar() {
|
||||||
|
const { isMobile } = useScreenSize();
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
return <MobileCalendarPage />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Calendar />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* ResponsiveDashboard - Wrapper for automatic mobile/desktop detection
|
||||||
|
*/
|
||||||
|
import { useScreenSize } from "@/hooks/use-screen-size";
|
||||||
|
import Dashboard from "@/pages/Dashboard";
|
||||||
|
import MobileDashboardPage from "@/pages/mobile/DashboardPage";
|
||||||
|
|
||||||
|
export default function ResponsiveDashboard() {
|
||||||
|
const { isMobile } = useScreenSize();
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
return <MobileDashboardPage />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Dashboard />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* ResponsiveDeliveries - Wrapper for automatic mobile/desktop detection
|
||||||
|
*/
|
||||||
|
import { useScreenSize } from "@/hooks/use-screen-size";
|
||||||
|
import Deliveries from "@/pages/Deliveries";
|
||||||
|
import MobileDeliveriesPage from "@/pages/mobile/DeliveriesPage";
|
||||||
|
|
||||||
|
export default function ResponsiveDeliveries() {
|
||||||
|
const { isMobile } = useScreenSize();
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
return <MobileDeliveriesPage />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Deliveries />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* ResponsiveOrders - Wrapper for automatic mobile/desktop detection
|
||||||
|
*/
|
||||||
|
import { useScreenSize } from "@/hooks/use-screen-size";
|
||||||
|
import Orders from "@/pages/Orders";
|
||||||
|
import MobileOrdersPage from "@/pages/mobile/OrdersPage";
|
||||||
|
|
||||||
|
export default function ResponsiveOrders() {
|
||||||
|
const { isMobile } = useScreenSize();
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
return <MobileOrdersPage />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Orders />;
|
||||||
|
}
|
||||||
@@ -21,6 +21,10 @@ import BackupManager from "@/pages/BackupManager";
|
|||||||
import Utilities from "@/pages/Utilities";
|
import Utilities from "@/pages/Utilities";
|
||||||
import Tasks from "@/pages/Tasks";
|
import Tasks from "@/pages/Tasks";
|
||||||
import ResponsiveTasks from "@/components/ResponsiveTasks";
|
import ResponsiveTasks from "@/components/ResponsiveTasks";
|
||||||
|
import ResponsiveDashboard from "@/components/ResponsiveDashboard";
|
||||||
|
import ResponsiveOrders from "@/components/ResponsiveOrders";
|
||||||
|
import ResponsiveDeliveries from "@/components/ResponsiveDeliveries";
|
||||||
|
import ResponsiveCalendar from "@/components/ResponsiveCalendar";
|
||||||
import SavTickets from "@/pages/SavTickets";
|
import SavTickets from "@/pages/SavTickets";
|
||||||
import Avoirs from "@/pages/Avoirs";
|
import Avoirs from "@/pages/Avoirs";
|
||||||
import WeatherSettings from "@/pages/WeatherSettings";
|
import WeatherSettings from "@/pages/WeatherSettings";
|
||||||
@@ -90,10 +94,10 @@ function RouterProduction() {
|
|||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/calendar" component={Calendar} />
|
<Route path="/calendar" component={ResponsiveCalendar} />
|
||||||
<Route path="/dashboard" component={Dashboard} />
|
<Route path="/dashboard" component={ResponsiveDashboard} />
|
||||||
<Route path="/orders" component={Orders} />
|
<Route path="/orders" component={ResponsiveOrders} />
|
||||||
<Route path="/deliveries" component={Deliveries} />
|
<Route path="/deliveries" component={ResponsiveDeliveries} />
|
||||||
<Route path="/suppliers" component={Suppliers} />
|
<Route path="/suppliers" component={Suppliers} />
|
||||||
<Route path="/groups" component={Groups} />
|
<Route path="/groups" component={Groups} />
|
||||||
<Route path="/users" component={Users} />
|
<Route path="/users" component={Users} />
|
||||||
|
|||||||
@@ -0,0 +1,218 @@
|
|||||||
|
/**
|
||||||
|
* CalendarPage Mobile - Calendrier simplifié pour mobile
|
||||||
|
*/
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useAuthUnified } from "@/hooks/useAuthUnified";
|
||||||
|
import { useStore } from "@/contexts/StoreContext";
|
||||||
|
import MobileLayout from "./MobileLayout";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import {
|
||||||
|
Calendar as CalendarIcon,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
Clock,
|
||||||
|
MapPin
|
||||||
|
} from "lucide-react";
|
||||||
|
import {
|
||||||
|
format,
|
||||||
|
startOfMonth,
|
||||||
|
endOfMonth,
|
||||||
|
eachDayOfInterval,
|
||||||
|
isSameMonth,
|
||||||
|
isToday,
|
||||||
|
addMonths,
|
||||||
|
subMonths,
|
||||||
|
startOfWeek,
|
||||||
|
endOfWeek,
|
||||||
|
isSameDay
|
||||||
|
} from "date-fns";
|
||||||
|
import { fr } from "date-fns/locale";
|
||||||
|
|
||||||
|
// Type pour les événements
|
||||||
|
type CalendarEvent = {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
startDate: string;
|
||||||
|
endDate?: string;
|
||||||
|
description?: string;
|
||||||
|
location?: string;
|
||||||
|
type?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function MobileCalendarPage() {
|
||||||
|
const { user } = useAuthUnified();
|
||||||
|
const { selectedStoreId } = useStore();
|
||||||
|
const [currentMonth, setCurrentMonth] = useState(new Date());
|
||||||
|
const [selectedDate, setSelectedDate] = useState<Date | null>(new Date());
|
||||||
|
|
||||||
|
// Fetch calendar events
|
||||||
|
const { data: events = [], isLoading } = useQuery({
|
||||||
|
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}`, {
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
if (!response.ok) return [];
|
||||||
|
const data = await response.json();
|
||||||
|
return Array.isArray(data) ? data : [];
|
||||||
|
},
|
||||||
|
enabled: !!user,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get days for the calendar grid
|
||||||
|
const monthStart = startOfMonth(currentMonth);
|
||||||
|
const monthEnd = endOfMonth(currentMonth);
|
||||||
|
const calendarStart = startOfWeek(monthStart, { locale: fr });
|
||||||
|
const calendarEnd = endOfWeek(monthEnd, { locale: fr });
|
||||||
|
const calendarDays = eachDayOfInterval({ start: calendarStart, end: calendarEnd });
|
||||||
|
|
||||||
|
// Get events for selected date
|
||||||
|
const selectedDateEvents = selectedDate
|
||||||
|
? events.filter((event: CalendarEvent) =>
|
||||||
|
isSameDay(new Date(event.startDate), selectedDate)
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// Check if a day has events
|
||||||
|
const hasEvents = (day: Date) => {
|
||||||
|
return events.some((event: CalendarEvent) =>
|
||||||
|
isSameDay(new Date(event.startDate), day)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateMonth = (direction: 'prev' | 'next') => {
|
||||||
|
setCurrentMonth(direction === 'prev'
|
||||||
|
? subMonths(currentMonth, 1)
|
||||||
|
: addMonths(currentMonth, 1)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MobileLayout>
|
||||||
|
<div className="p-3 space-y-3">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CalendarIcon className="h-5 w-5 text-purple-600" />
|
||||||
|
<h1 className="text-lg font-bold">Calendrier</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Month navigation */}
|
||||||
|
<div className="flex items-center justify-between bg-white rounded-lg p-2 shadow-sm">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => navigateMonth('prev')}
|
||||||
|
className="h-10 w-10 p-0"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<h2 className="font-semibold text-gray-900 capitalize">
|
||||||
|
{format(currentMonth, 'MMMM yyyy', { locale: fr })}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => navigateMonth('next')}
|
||||||
|
className="h-10 w-10 p-0"
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Calendar grid */}
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-2">
|
||||||
|
{/* Weekday headers */}
|
||||||
|
<div className="grid grid-cols-7 gap-1 mb-1">
|
||||||
|
{['L', 'M', 'M', 'J', 'V', 'S', 'D'].map((day, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="text-center text-xs font-medium text-gray-500 py-1"
|
||||||
|
>
|
||||||
|
{day}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Days grid */}
|
||||||
|
<div className="grid grid-cols-7 gap-1">
|
||||||
|
{calendarDays.map((day, i) => {
|
||||||
|
const isCurrentMonth = isSameMonth(day, currentMonth);
|
||||||
|
const isTodayDate = isToday(day);
|
||||||
|
const isSelected = selectedDate && isSameDay(day, selectedDate);
|
||||||
|
const dayHasEvents = hasEvents(day);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={i}
|
||||||
|
onClick={() => setSelectedDate(day)}
|
||||||
|
className={`
|
||||||
|
relative aspect-square flex items-center justify-center
|
||||||
|
text-sm rounded-lg transition-colors
|
||||||
|
${!isCurrentMonth ? 'text-gray-300' : 'text-gray-700'}
|
||||||
|
${isTodayDate ? 'font-bold' : ''}
|
||||||
|
${isSelected ? 'bg-blue-600 text-white' : 'hover:bg-gray-100'}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{format(day, 'd')}
|
||||||
|
{dayHasEvents && !isSelected && (
|
||||||
|
<span className="absolute bottom-0.5 left-1/2 -translate-x-1/2 w-1 h-1 bg-blue-600 rounded-full" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Events for selected date */}
|
||||||
|
{selectedDate && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="font-semibold text-gray-900">
|
||||||
|
{format(selectedDate, 'd MMMM', { locale: fr })}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{selectedDateEvents.length === 0 ? (
|
||||||
|
<p className="text-sm text-gray-500 py-4 text-center">
|
||||||
|
Aucun événement
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
selectedDateEvents.map((event: CalendarEvent) => (
|
||||||
|
<Card key={event.id}>
|
||||||
|
<CardContent className="p-3">
|
||||||
|
<h4 className="font-medium text-gray-900 mb-1">
|
||||||
|
{event.title}
|
||||||
|
</h4>
|
||||||
|
{event.startDate && (
|
||||||
|
<p className="text-sm text-gray-600 flex items-center gap-1">
|
||||||
|
<Clock className="h-3 w-3" />
|
||||||
|
{format(new Date(event.startDate), 'HH:mm')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{event.location && (
|
||||||
|
<p className="text-sm text-gray-600 flex items-center gap-1 mt-1">
|
||||||
|
<MapPin className="h-3 w-3" />
|
||||||
|
{event.location}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</MobileLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
/**
|
||||||
|
* DashboardPage Mobile - Tableau de bord simplifié pour mobile
|
||||||
|
*/
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useAuthUnified } from "@/hooks/useAuthUnified";
|
||||||
|
import { useStore } from "@/contexts/StoreContext";
|
||||||
|
import MobileLayout from "./MobileLayout";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Package,
|
||||||
|
Truck,
|
||||||
|
CheckSquare,
|
||||||
|
AlertTriangle,
|
||||||
|
TrendingUp,
|
||||||
|
Clock,
|
||||||
|
Calendar
|
||||||
|
} from "lucide-react";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import { fr } from "date-fns/locale";
|
||||||
|
|
||||||
|
// Stats card component
|
||||||
|
function StatCard({
|
||||||
|
icon: Icon,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
trend,
|
||||||
|
color
|
||||||
|
}: {
|
||||||
|
icon: React.ComponentType<{ className?: string }>;
|
||||||
|
label: string;
|
||||||
|
value: string | number;
|
||||||
|
trend?: string;
|
||||||
|
color: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Card className="overflow-hidden">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className={`p-2 rounded-lg ${color}`}>
|
||||||
|
<Icon className="h-5 w-5 text-white" />
|
||||||
|
</div>
|
||||||
|
{trend && (
|
||||||
|
<span className="text-xs text-green-600 font-medium flex items-center">
|
||||||
|
<TrendingUp className="h-3 w-3 mr-1" />
|
||||||
|
{trend}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mt-3">
|
||||||
|
<p className="text-2xl font-bold text-gray-900">{value}</p>
|
||||||
|
<p className="text-sm text-gray-500">{label}</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MobileDashboardPage() {
|
||||||
|
const { user } = useAuthUnified();
|
||||||
|
const { selectedStoreId } = useStore();
|
||||||
|
|
||||||
|
// Fetch stats
|
||||||
|
const { data: stats, isLoading } = useQuery({
|
||||||
|
queryKey: ["/api/stats/dashboard", selectedStoreId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (selectedStoreId) params.append('storeId', selectedStoreId.toString());
|
||||||
|
|
||||||
|
const response = await fetch(`/api/stats/dashboard?${params}`, {
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
if (!response.ok) return null;
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
enabled: !!user,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch pending tasks count
|
||||||
|
const { data: tasks = [] } = useQuery({
|
||||||
|
queryKey: ["/api/tasks", selectedStoreId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (selectedStoreId) params.append('storeId', selectedStoreId.toString());
|
||||||
|
|
||||||
|
const response = await fetch(`/api/tasks?${params}`, {
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
if (!response.ok) return [];
|
||||||
|
const data = await response.json();
|
||||||
|
return Array.isArray(data) ? data : [];
|
||||||
|
},
|
||||||
|
enabled: !!user,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch today's orders
|
||||||
|
const { data: orders = [] } = useQuery({
|
||||||
|
queryKey: ["/api/orders/today", selectedStoreId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (selectedStoreId) params.append('storeId', selectedStoreId.toString());
|
||||||
|
params.append('date', format(new Date(), 'yyyy-MM-dd'));
|
||||||
|
|
||||||
|
const response = await fetch(`/api/orders?${params}`, {
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
if (!response.ok) return [];
|
||||||
|
const data = await response.json();
|
||||||
|
return Array.isArray(data) ? data : [];
|
||||||
|
},
|
||||||
|
enabled: !!user,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch today's deliveries
|
||||||
|
const { data: deliveries = [] } = useQuery({
|
||||||
|
queryKey: ["/api/deliveries/today", selectedStoreId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (selectedStoreId) params.append('storeId', selectedStoreId.toString());
|
||||||
|
params.append('date', format(new Date(), 'yyyy-MM-dd'));
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pendingTasks = tasks.filter((t: any) => t.status !== 'completed').length;
|
||||||
|
const urgentTasks = tasks.filter((t: any) => t.priority === 'high' && t.status !== 'completed').length;
|
||||||
|
const todayDate = format(new Date(), "EEEE d MMMM", { locale: fr });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MobileLayout>
|
||||||
|
<div className="p-4 space-y-4">
|
||||||
|
{/* Welcome */}
|
||||||
|
<div className="bg-gradient-to-r from-blue-500 to-blue-600 rounded-xl p-4 text-white">
|
||||||
|
<p className="text-blue-100 text-sm">Bonjour,</p>
|
||||||
|
<h2 className="text-xl font-bold">
|
||||||
|
{user?.firstName || user?.username || 'Utilisateur'}
|
||||||
|
</h2>
|
||||||
|
<p className="text-blue-100 text-sm mt-1 capitalize">{todayDate}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Grid */}
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<StatCard
|
||||||
|
icon={Package}
|
||||||
|
label="Commandes"
|
||||||
|
value={orders.length}
|
||||||
|
color="bg-blue-500"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
icon={Truck}
|
||||||
|
label="Livraisons"
|
||||||
|
value={deliveries.length}
|
||||||
|
color="bg-green-500"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
icon={CheckSquare}
|
||||||
|
label="Tâches en cours"
|
||||||
|
value={pendingTasks}
|
||||||
|
color="bg-purple-500"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
icon={AlertTriangle}
|
||||||
|
label="Urgentes"
|
||||||
|
value={urgentTasks}
|
||||||
|
color="bg-red-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Actions */}
|
||||||
|
<div className="bg-white rounded-xl p-4 shadow-sm">
|
||||||
|
<h3 className="font-semibold text-gray-900 mb-3">Accès rapide</h3>
|
||||||
|
<div className="grid grid-cols-4 gap-2">
|
||||||
|
{[
|
||||||
|
{ icon: Package, label: "Commande", path: "/orders" },
|
||||||
|
{ icon: Truck, label: "Livraison", path: "/deliveries" },
|
||||||
|
{ icon: CheckSquare, label: "Tâche", path: "/tasks" },
|
||||||
|
{ icon: Calendar, label: "Agenda", path: "/calendar" },
|
||||||
|
].map((item) => {
|
||||||
|
const Icon = item.icon;
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
key={item.path}
|
||||||
|
href={item.path}
|
||||||
|
className="flex flex-col items-center p-3 rounded-lg bg-gray-50 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<Icon className="h-6 w-6 text-gray-600 mb-1" />
|
||||||
|
<span className="text-xs text-gray-600">{item.label}</span>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Recent Tasks */}
|
||||||
|
{pendingTasks > 0 && (
|
||||||
|
<div className="bg-white rounded-xl p-4 shadow-sm">
|
||||||
|
<h3 className="font-semibold text-gray-900 mb-3">Tâches à faire</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{tasks
|
||||||
|
.filter((t: any) => t.status !== 'completed')
|
||||||
|
.slice(0, 3)
|
||||||
|
.map((task: any) => (
|
||||||
|
<div
|
||||||
|
key={task.id}
|
||||||
|
className={`p-3 rounded-lg border-l-4 ${task.priority === 'high'
|
||||||
|
? 'border-red-500 bg-red-50'
|
||||||
|
: 'border-gray-300 bg-gray-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<p className="font-medium text-gray-900 text-sm truncate">
|
||||||
|
{task.title}
|
||||||
|
</p>
|
||||||
|
{task.dueDate && (
|
||||||
|
<p className="text-xs text-gray-500 flex items-center mt-1">
|
||||||
|
<Clock className="h-3 w-3 mr-1" />
|
||||||
|
{format(new Date(task.dueDate), 'dd/MM')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</MobileLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
/**
|
||||||
|
* DeliveriesPage Mobile - Liste des livraisons simplifiée pour mobile
|
||||||
|
*/
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useAuthUnified } from "@/hooks/useAuthUnified";
|
||||||
|
import { useStore } from "@/contexts/StoreContext";
|
||||||
|
import MobileLayout from "./MobileLayout";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import {
|
||||||
|
Truck,
|
||||||
|
Search,
|
||||||
|
Calendar,
|
||||||
|
Building,
|
||||||
|
ChevronRight,
|
||||||
|
Package,
|
||||||
|
CheckCircle,
|
||||||
|
Clock
|
||||||
|
} from "lucide-react";
|
||||||
|
import { format, isToday, isTomorrow, isPast } from "date-fns";
|
||||||
|
import { fr } from "date-fns/locale";
|
||||||
|
|
||||||
|
// Type pour les livraisons
|
||||||
|
type Delivery = {
|
||||||
|
id: number;
|
||||||
|
deliveryNumber?: string;
|
||||||
|
supplier?: { id: number; name: string; };
|
||||||
|
expectedDate: string;
|
||||||
|
status: string;
|
||||||
|
order?: { id: number; orderNumber: string; };
|
||||||
|
group?: { id: number; name: string; color?: string; };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function MobileDeliveriesPage() {
|
||||||
|
const { user } = useAuthUnified();
|
||||||
|
const { selectedStoreId } = useStore();
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [statusFilter, setStatusFilter] = useState<'all' | 'pending' | 'received'>('all');
|
||||||
|
|
||||||
|
// Fetch deliveries
|
||||||
|
const { data: deliveries = [], isLoading } = useQuery({
|
||||||
|
queryKey: ["/api/deliveries", selectedStoreId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter deliveries
|
||||||
|
const filteredDeliveries = deliveries.filter((delivery: Delivery) => {
|
||||||
|
// Status filter
|
||||||
|
if (statusFilter === 'pending' && delivery.status === 'received') return false;
|
||||||
|
if (statusFilter === 'received' && delivery.status !== 'received') return false;
|
||||||
|
|
||||||
|
// Search filter
|
||||||
|
if (searchTerm) {
|
||||||
|
const search = searchTerm.toLowerCase();
|
||||||
|
const matchNumber = delivery.deliveryNumber?.toLowerCase().includes(search);
|
||||||
|
const matchSupplier = delivery.supplier?.name?.toLowerCase().includes(search);
|
||||||
|
if (!matchNumber && !matchSupplier) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getDateBadge = (dateStr: string) => {
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
if (isToday(date)) {
|
||||||
|
return <Badge className="bg-blue-100 text-blue-700">Aujourd'hui</Badge>;
|
||||||
|
}
|
||||||
|
if (isTomorrow(date)) {
|
||||||
|
return <Badge className="bg-purple-100 text-purple-700">Demain</Badge>;
|
||||||
|
}
|
||||||
|
if (isPast(date)) {
|
||||||
|
return <Badge className="bg-red-100 text-red-700">En retard</Badge>;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusIcon = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'received':
|
||||||
|
return <CheckCircle className="h-4 w-4 text-green-600" />;
|
||||||
|
case 'pending':
|
||||||
|
return <Clock className="h-4 w-4 text-yellow-600" />;
|
||||||
|
default:
|
||||||
|
return <Package className="h-4 w-4 text-gray-600" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<MobileLayout>
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" />
|
||||||
|
</div>
|
||||||
|
</MobileLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MobileLayout>
|
||||||
|
<div className="p-3 space-y-3">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Truck className="h-5 w-5 text-green-600" />
|
||||||
|
<h1 className="text-lg font-bold">Livraisons</h1>
|
||||||
|
</div>
|
||||||
|
<Badge variant="secondary">{filteredDeliveries.length}</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search */}
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||||
|
<Input
|
||||||
|
placeholder="Rechercher..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="pl-10 h-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filter tabs */}
|
||||||
|
<div className="grid grid-cols-3 gap-1 bg-gray-100 p-1 rounded-lg">
|
||||||
|
{(['all', 'pending', 'received'] as const).map((status) => (
|
||||||
|
<Button
|
||||||
|
key={status}
|
||||||
|
variant={statusFilter === status ? 'default' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setStatusFilter(status)}
|
||||||
|
className="h-8 text-xs"
|
||||||
|
>
|
||||||
|
{status === 'all' && 'Toutes'}
|
||||||
|
{status === 'pending' && 'En attente'}
|
||||||
|
{status === 'received' && 'Reçues'}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Deliveries list */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
{filteredDeliveries.length === 0 ? (
|
||||||
|
<div className="text-center py-12 text-gray-500">
|
||||||
|
<Truck className="h-12 w-12 mx-auto mb-3 opacity-50" />
|
||||||
|
<p>Aucune livraison trouvée</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filteredDeliveries.map((delivery: Delivery) => (
|
||||||
|
<Card key={delivery.id} className="overflow-hidden">
|
||||||
|
<CardContent className="p-3">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
{getStatusIcon(delivery.status)}
|
||||||
|
<span className="font-semibold text-gray-900">
|
||||||
|
#{delivery.deliveryNumber || delivery.id}
|
||||||
|
</span>
|
||||||
|
{getDateBadge(delivery.expectedDate)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{delivery.supplier && (
|
||||||
|
<p className="text-sm text-gray-600 flex items-center gap-1 truncate">
|
||||||
|
<Building className="h-3 w-3 flex-shrink-0" />
|
||||||
|
{delivery.supplier.name}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p className="text-xs text-gray-500 flex items-center gap-1 mt-1">
|
||||||
|
<Calendar className="h-3 w-3" />
|
||||||
|
{format(new Date(delivery.expectedDate), 'dd/MM/yyyy', { locale: fr })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ChevronRight className="h-5 w-5 text-gray-400 flex-shrink-0" />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MobileLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
/**
|
||||||
|
* MobileBottomNav - Navigation bottom fixe pour l'application mobile
|
||||||
|
* 5 boutons: Accueil, Commandes, Livraisons, Tâches, Plus
|
||||||
|
*/
|
||||||
|
import { Link, useLocation } from "wouter";
|
||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
Home,
|
||||||
|
Package,
|
||||||
|
Truck,
|
||||||
|
CheckSquare,
|
||||||
|
MoreHorizontal,
|
||||||
|
Calendar,
|
||||||
|
Megaphone,
|
||||||
|
ShoppingCart,
|
||||||
|
Clock,
|
||||||
|
Wrench,
|
||||||
|
Receipt
|
||||||
|
} from "lucide-react";
|
||||||
|
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet";
|
||||||
|
import { useAuthUnified } from "@/hooks/useAuthUnified";
|
||||||
|
|
||||||
|
interface NavItem {
|
||||||
|
path: string;
|
||||||
|
label: string;
|
||||||
|
icon: React.ComponentType<{ className?: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const primaryItems: NavItem[] = [
|
||||||
|
{ path: "/dashboard", label: "Accueil", icon: Home },
|
||||||
|
{ path: "/orders", label: "Commandes", icon: Package },
|
||||||
|
{ path: "/deliveries", label: "Livraisons", icon: Truck },
|
||||||
|
{ path: "/tasks", label: "Tâches", icon: CheckSquare },
|
||||||
|
];
|
||||||
|
|
||||||
|
const moreItems: NavItem[] = [
|
||||||
|
{ path: "/calendar", label: "Calendrier", icon: Calendar },
|
||||||
|
{ path: "/publicities", label: "Publicités", icon: Megaphone },
|
||||||
|
{ path: "/customer-orders", label: "Cmd Client", icon: ShoppingCart },
|
||||||
|
{ path: "/dlc", label: "DLC", icon: Clock },
|
||||||
|
{ path: "/sav", label: "SAV", icon: Wrench },
|
||||||
|
{ path: "/avoirs", label: "Avoirs", icon: Receipt },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function MobileBottomNav() {
|
||||||
|
const [location] = useLocation();
|
||||||
|
const { user } = useAuthUnified();
|
||||||
|
const [moreOpen, setMoreOpen] = useState(false);
|
||||||
|
|
||||||
|
const isActive = (path: string) => {
|
||||||
|
if (path === "/dashboard" && (location === "/" || location === "/dashboard")) return true;
|
||||||
|
return location.startsWith(path);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if any "more" item is active
|
||||||
|
const isMoreActive = moreItems.some(item => isActive(item.path));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Navigation bottom fixe */}
|
||||||
|
<nav
|
||||||
|
className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 z-50"
|
||||||
|
style={{ paddingBottom: 'env(safe-area-inset-bottom)' }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-around h-16">
|
||||||
|
{/* Items principaux */}
|
||||||
|
{primaryItems.map((item) => {
|
||||||
|
const Icon = item.icon;
|
||||||
|
const active = isActive(item.path);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link key={item.path} href={item.path}>
|
||||||
|
<div className={`flex flex-col items-center justify-center min-w-[60px] py-1 ${active ? 'text-blue-600' : 'text-gray-500'
|
||||||
|
}`}>
|
||||||
|
<Icon className={`h-5 w-5 ${active ? 'text-blue-600' : 'text-gray-500'}`} />
|
||||||
|
<span className={`text-[10px] mt-0.5 font-medium ${active ? 'text-blue-600' : 'text-gray-500'
|
||||||
|
}`}>
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Bouton "Plus" */}
|
||||||
|
<div
|
||||||
|
className={`flex flex-col items-center justify-center min-w-[60px] py-1 cursor-pointer ${isMoreActive || moreOpen ? 'text-blue-600' : 'text-gray-500'
|
||||||
|
}`}
|
||||||
|
onClick={() => setMoreOpen(true)}
|
||||||
|
>
|
||||||
|
<MoreHorizontal className={`h-5 w-5 ${isMoreActive || moreOpen ? 'text-blue-600' : 'text-gray-500'
|
||||||
|
}`} />
|
||||||
|
<span className={`text-[10px] mt-0.5 font-medium ${isMoreActive || moreOpen ? 'text-blue-600' : 'text-gray-500'
|
||||||
|
}`}>
|
||||||
|
Plus
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Sheet "Plus" */}
|
||||||
|
<Sheet open={moreOpen} onOpenChange={setMoreOpen}>
|
||||||
|
<SheetContent side="bottom" className="h-auto max-h-[50vh] rounded-t-xl">
|
||||||
|
<SheetHeader className="pb-4">
|
||||||
|
<SheetTitle>Autres pages</SheetTitle>
|
||||||
|
</SheetHeader>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-3 pb-6">
|
||||||
|
{moreItems.map((item) => {
|
||||||
|
const Icon = item.icon;
|
||||||
|
const active = isActive(item.path);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={item.path}
|
||||||
|
href={item.path}
|
||||||
|
onClick={() => setMoreOpen(false)}
|
||||||
|
>
|
||||||
|
<div className={`flex flex-col items-center justify-center p-4 rounded-xl min-h-[80px] ${active
|
||||||
|
? 'bg-blue-100 text-blue-700'
|
||||||
|
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||||
|
}`}>
|
||||||
|
<Icon className={`h-6 w-6 ${active ? 'text-blue-600' : 'text-gray-600'}`} />
|
||||||
|
<span className={`text-xs mt-2 font-medium text-center ${active ? 'text-blue-700' : 'text-gray-700'
|
||||||
|
}`}>
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
/**
|
||||||
|
* MobileLayout - Layout dédié pour l'application mobile
|
||||||
|
* Affiche le magasin sélectionné de manière visible et une navigation bottom
|
||||||
|
*/
|
||||||
|
import { ReactNode, useState } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { Menu, X, Store, ChevronDown, LogOut } from "lucide-react";
|
||||||
|
import { useAuthUnified } from "@/hooks/useAuthUnified";
|
||||||
|
import { useStore } from "@/contexts/StoreContext";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import MobileBottomNav from "./MobileBottomNav";
|
||||||
|
import type { Group } from "@shared/schema";
|
||||||
|
|
||||||
|
interface MobileLayoutProps {
|
||||||
|
children: ReactNode;
|
||||||
|
title?: string;
|
||||||
|
showBackButton?: boolean;
|
||||||
|
onBack?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MobileLayout({ children, title }: MobileLayoutProps) {
|
||||||
|
const { user } = useAuthUnified();
|
||||||
|
const { selectedStoreId, setSelectedStoreId, stores } = useStore();
|
||||||
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
// Get selected store name
|
||||||
|
const selectedStore = stores?.find((s: Group) => s.id === selectedStoreId);
|
||||||
|
const storeName = selectedStore?.name || "Tous les magasins";
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
try {
|
||||||
|
await fetch('/api/logout', { method: 'POST', credentials: 'include' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logout error:', error);
|
||||||
|
} finally {
|
||||||
|
window.location.href = "/auth";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStoreChange = (value: string) => {
|
||||||
|
const newStoreId = value === "all" ? null : parseInt(value);
|
||||||
|
if (newStoreId) {
|
||||||
|
localStorage.setItem('selectedStoreId', newStoreId.toString());
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('selectedStoreId');
|
||||||
|
}
|
||||||
|
setSelectedStoreId(newStoreId);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="min-h-screen bg-gray-50 flex flex-col"
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: '100vw',
|
||||||
|
overflowX: 'hidden',
|
||||||
|
paddingBottom: '80px' // Space for bottom nav
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Header fixe */}
|
||||||
|
<header className="bg-white border-b border-gray-200 sticky top-0 z-50">
|
||||||
|
<div className="flex items-center justify-between h-14 px-3">
|
||||||
|
{/* Menu hamburger */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setMenuOpen(true)}
|
||||||
|
className="h-10 w-10 p-0"
|
||||||
|
>
|
||||||
|
<Menu className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Logo */}
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Store className="h-5 w-5 text-blue-600" />
|
||||||
|
<span className="font-semibold text-gray-900">LogiFlow</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Magasin sélectionné - bien visible */}
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-1 bg-blue-50 px-2 py-1 rounded-lg cursor-pointer"
|
||||||
|
onClick={() => setMenuOpen(true)}
|
||||||
|
>
|
||||||
|
<span className="text-xs font-medium text-blue-700 truncate max-w-[80px]">
|
||||||
|
{storeName}
|
||||||
|
</span>
|
||||||
|
<ChevronDown className="h-3 w-3 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Titre de page optionnel */}
|
||||||
|
{title && (
|
||||||
|
<div className="px-3 pb-2">
|
||||||
|
<h1 className="text-lg font-bold text-gray-900">{title}</h1>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Contenu principal */}
|
||||||
|
<main className="flex-1 overflow-y-auto overflow-x-hidden">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Navigation bottom */}
|
||||||
|
<MobileBottomNav />
|
||||||
|
|
||||||
|
{/* Menu latéral (Sheet) */}
|
||||||
|
<Sheet open={menuOpen} onOpenChange={setMenuOpen}>
|
||||||
|
<SheetContent side="left" className="w-[280px] p-0">
|
||||||
|
<SheetHeader className="p-4 border-b">
|
||||||
|
<SheetTitle className="flex items-center gap-2">
|
||||||
|
<Store className="h-5 w-5 text-blue-600" />
|
||||||
|
LogiFlow
|
||||||
|
</SheetTitle>
|
||||||
|
</SheetHeader>
|
||||||
|
|
||||||
|
<div className="p-4 space-y-6">
|
||||||
|
{/* Utilisateur */}
|
||||||
|
{user && (
|
||||||
|
<div className="bg-gray-50 rounded-lg p-3">
|
||||||
|
<p className="font-medium text-gray-900">
|
||||||
|
{user.firstName} {user.lastName}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500 capitalize">{user.role}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Sélecteur de magasin */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium text-gray-700">
|
||||||
|
Magasin
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
value={selectedStoreId?.toString() || (user?.role === 'admin' ? "all" : "")}
|
||||||
|
onValueChange={handleStoreChange}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full h-12">
|
||||||
|
<SelectValue placeholder="Sélectionner un magasin" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{user?.role === 'admin' && (
|
||||||
|
<SelectItem value="all">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-3 h-3 bg-gray-400 rounded-full" />
|
||||||
|
Tous les magasins
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
{stores?.map((store: Group) => (
|
||||||
|
<SelectItem key={store.id} value={store.id.toString()}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
className="w-3 h-3 rounded-full"
|
||||||
|
style={{ backgroundColor: store.color || '#gray' }}
|
||||||
|
/>
|
||||||
|
{store.name}
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Déconnexion */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full h-12 text-red-600 border-red-200 hover:bg-red-50"
|
||||||
|
onClick={handleLogout}
|
||||||
|
>
|
||||||
|
<LogOut className="h-4 w-4 mr-2" />
|
||||||
|
Déconnexion
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
/**
|
||||||
|
* OrdersPage Mobile - Liste des commandes simplifiée pour mobile
|
||||||
|
*/
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useAuthUnified } from "@/hooks/useAuthUnified";
|
||||||
|
import { useStore } from "@/contexts/StoreContext";
|
||||||
|
import MobileLayout from "./MobileLayout";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import {
|
||||||
|
Package,
|
||||||
|
Search,
|
||||||
|
Plus,
|
||||||
|
Calendar,
|
||||||
|
Building,
|
||||||
|
ChevronRight
|
||||||
|
} from "lucide-react";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import { fr } from "date-fns/locale";
|
||||||
|
|
||||||
|
// Type pour les commandes
|
||||||
|
type Order = {
|
||||||
|
id: number;
|
||||||
|
orderNumber?: string;
|
||||||
|
supplier?: { id: number; name: string; };
|
||||||
|
orderDate: string;
|
||||||
|
status: string;
|
||||||
|
totalAmount?: number;
|
||||||
|
group?: { id: number; name: string; color?: string; };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function MobileOrdersPage() {
|
||||||
|
const { user } = useAuthUnified();
|
||||||
|
const { selectedStoreId } = useStore();
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [statusFilter, setStatusFilter] = useState<'all' | 'pending' | 'delivered'>('all');
|
||||||
|
|
||||||
|
// Fetch orders
|
||||||
|
const { data: orders = [], isLoading } = useQuery({
|
||||||
|
queryKey: ["/api/orders", selectedStoreId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (selectedStoreId) params.append('storeId', selectedStoreId.toString());
|
||||||
|
|
||||||
|
const response = await fetch(`/api/orders?${params}`, {
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
if (!response.ok) return [];
|
||||||
|
const data = await response.json();
|
||||||
|
return Array.isArray(data) ? data : [];
|
||||||
|
},
|
||||||
|
enabled: !!user,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter orders
|
||||||
|
const filteredOrders = orders.filter((order: Order) => {
|
||||||
|
// Status filter
|
||||||
|
if (statusFilter === 'pending' && order.status === 'delivered') return false;
|
||||||
|
if (statusFilter === 'delivered' && order.status !== 'delivered') return false;
|
||||||
|
|
||||||
|
// Search filter
|
||||||
|
if (searchTerm) {
|
||||||
|
const search = searchTerm.toLowerCase();
|
||||||
|
const matchNumber = order.orderNumber?.toLowerCase().includes(search);
|
||||||
|
const matchSupplier = order.supplier?.name?.toLowerCase().includes(search);
|
||||||
|
if (!matchNumber && !matchSupplier) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getStatusBadge = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'delivered':
|
||||||
|
return <Badge className="bg-green-100 text-green-700">Livrée</Badge>;
|
||||||
|
case 'pending':
|
||||||
|
return <Badge className="bg-yellow-100 text-yellow-700">En attente</Badge>;
|
||||||
|
case 'cancelled':
|
||||||
|
return <Badge className="bg-red-100 text-red-700">Annulée</Badge>;
|
||||||
|
default:
|
||||||
|
return <Badge className="bg-gray-100 text-gray-700">{status}</Badge>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<MobileLayout>
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" />
|
||||||
|
</div>
|
||||||
|
</MobileLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MobileLayout>
|
||||||
|
<div className="p-3 space-y-3">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Package className="h-5 w-5 text-blue-600" />
|
||||||
|
<h1 className="text-lg font-bold">Commandes</h1>
|
||||||
|
</div>
|
||||||
|
<Badge variant="secondary">{filteredOrders.length}</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search */}
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||||
|
<Input
|
||||||
|
placeholder="Rechercher..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="pl-10 h-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filter tabs */}
|
||||||
|
<div className="grid grid-cols-3 gap-1 bg-gray-100 p-1 rounded-lg">
|
||||||
|
{(['all', 'pending', 'delivered'] as const).map((status) => (
|
||||||
|
<Button
|
||||||
|
key={status}
|
||||||
|
variant={statusFilter === status ? 'default' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setStatusFilter(status)}
|
||||||
|
className="h-8 text-xs"
|
||||||
|
>
|
||||||
|
{status === 'all' && 'Toutes'}
|
||||||
|
{status === 'pending' && 'En attente'}
|
||||||
|
{status === 'delivered' && 'Livrées'}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Orders list */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
{filteredOrders.length === 0 ? (
|
||||||
|
<div className="text-center py-12 text-gray-500">
|
||||||
|
<Package className="h-12 w-12 mx-auto mb-3 opacity-50" />
|
||||||
|
<p>Aucune commande trouvée</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filteredOrders.map((order: Order) => (
|
||||||
|
<Card key={order.id} className="overflow-hidden">
|
||||||
|
<CardContent className="p-3">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<span className="font-semibold text-gray-900">
|
||||||
|
#{order.orderNumber || order.id}
|
||||||
|
</span>
|
||||||
|
{getStatusBadge(order.status)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{order.supplier && (
|
||||||
|
<p className="text-sm text-gray-600 flex items-center gap-1 truncate">
|
||||||
|
<Building className="h-3 w-3 flex-shrink-0" />
|
||||||
|
{order.supplier.name}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p className="text-xs text-gray-500 flex items-center gap-1 mt-1">
|
||||||
|
<Calendar className="h-3 w-3" />
|
||||||
|
{format(new Date(order.orderDate), 'dd/MM/yyyy', { locale: fr })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ChevronRight className="h-5 w-5 text-gray-400 flex-shrink-0" />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MobileLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import MobileLayout from "./MobileLayout";
|
||||||
import {
|
import {
|
||||||
ListTodo,
|
ListTodo,
|
||||||
Plus,
|
Plus,
|
||||||
|
|||||||
Reference in new issue
Block a user