Merge pull request #533 from R0m1k3/1.1

feat: add mobile layout and bottom navigation components
This commit is contained in:
LogiFlow authored and GitHub committed 2025-12-10 13:42:30 +01:00
commit 4149202c6a
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 />;
}
+8 -4
View File
@@ -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} />
+218
View File
@@ -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>
);
}
+234
View File
@@ -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>
);
}
+196
View File
@@ -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>
);
}
+137
View File
@@ -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>
</>
);
}
+180
View File
@@ -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>
);
}
+181
View File
@@ -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>
);
}
+1
View File
@@ -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,