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 Tasks from "@/pages/Tasks";
|
||||
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 Avoirs from "@/pages/Avoirs";
|
||||
import WeatherSettings from "@/pages/WeatherSettings";
|
||||
@@ -90,10 +94,10 @@ function RouterProduction() {
|
||||
return (
|
||||
<Layout>
|
||||
<Switch>
|
||||
<Route path="/calendar" component={Calendar} />
|
||||
<Route path="/dashboard" component={Dashboard} />
|
||||
<Route path="/orders" component={Orders} />
|
||||
<Route path="/deliveries" component={Deliveries} />
|
||||
<Route path="/calendar" component={ResponsiveCalendar} />
|
||||
<Route path="/dashboard" component={ResponsiveDashboard} />
|
||||
<Route path="/orders" component={ResponsiveOrders} />
|
||||
<Route path="/deliveries" component={ResponsiveDeliveries} />
|
||||
<Route path="/suppliers" component={Suppliers} />
|
||||
<Route path="/groups" component={Groups} />
|
||||
<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 { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import MobileLayout from "./MobileLayout";
|
||||
import {
|
||||
ListTodo,
|
||||
Plus,
|
||||
|
||||
Reference in new issue
Block a user