mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-17 20:27:10 +02:00
feat: add mobile pages for customer orders, publicities, and calendar, and introduce a production router component.
This commit is contained in:
1 parent
a7c3b2913e
commit
75be8e75b3
4 files changed
+383
-37
No files matched your search
@@ -36,6 +36,8 @@ import MobileOrdersPage from "@/pages/mobile/OrdersPage";
|
|||||||
import MobileDeliveriesPage from "@/pages/mobile/DeliveriesPage";
|
import MobileDeliveriesPage from "@/pages/mobile/DeliveriesPage";
|
||||||
import MobileCalendarPage from "@/pages/mobile/CalendarPage";
|
import MobileCalendarPage from "@/pages/mobile/CalendarPage";
|
||||||
import MobileTasksPage from "@/pages/mobile/TasksPage";
|
import MobileTasksPage from "@/pages/mobile/TasksPage";
|
||||||
|
import MobilePublicitiesPage from "@/pages/mobile/PublicitiesPage";
|
||||||
|
import MobileCustomerOrdersPage from "@/pages/mobile/CustomerOrdersPage";
|
||||||
|
|
||||||
function RouterProduction() {
|
function RouterProduction() {
|
||||||
const { isAuthenticated, isLoading, user, environment, error } = useAuthUnified();
|
const { isAuthenticated, isLoading, user, environment, error } = useAuthUnified();
|
||||||
@@ -102,8 +104,8 @@ function RouterProduction() {
|
|||||||
<Route path="/groups" component={Groups} />
|
<Route path="/groups" component={Groups} />
|
||||||
<Route path="/users" component={Users} />
|
<Route path="/users" component={Users} />
|
||||||
<Route path="/bl-reconciliation" component={BLReconciliation} />
|
<Route path="/bl-reconciliation" component={BLReconciliation} />
|
||||||
<Route path="/publicities" component={Publicities} />
|
<Route path="/publicities" component={MobilePublicitiesPage} />
|
||||||
<Route path="/customer-orders" component={CustomerOrders} />
|
<Route path="/customer-orders" component={MobileCustomerOrdersPage} />
|
||||||
<Route path="/dlc" component={DlcPage} />
|
<Route path="/dlc" component={DlcPage} />
|
||||||
<Route path="/utilities" component={Utilities} />
|
<Route path="/utilities" component={Utilities} />
|
||||||
<Route path="/sav" component={SavTickets} />
|
<Route path="/sav" component={SavTickets} />
|
||||||
|
|||||||
@@ -48,11 +48,8 @@ export default function MobileCalendarPage() {
|
|||||||
const [currentMonth, setCurrentMonth] = useState(new Date());
|
const [currentMonth, setCurrentMonth] = useState(new Date());
|
||||||
const [selectedDate, setSelectedDate] = useState<Date | null>(new Date());
|
const [selectedDate, setSelectedDate] = useState<Date | null>(new Date());
|
||||||
|
|
||||||
// Debug log - doit apparaître dans la console
|
|
||||||
console.log('🗓️ MobileCalendarPage RENDER', { user: !!user, selectedStoreId, currentMonth: currentMonth.toISOString() });
|
|
||||||
|
|
||||||
// Fetch orders for current month
|
// Fetch orders for current month
|
||||||
const { data: orders = [], isLoading: loadingOrders } = useQuery({
|
const { data: orders = [] } = useQuery({
|
||||||
queryKey: ["/api/orders", selectedStoreId, format(currentMonth, 'yyyy-MM')],
|
queryKey: ["/api/orders", selectedStoreId, format(currentMonth, 'yyyy-MM')],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const start = format(startOfMonth(currentMonth), 'yyyy-MM-dd');
|
const start = format(startOfMonth(currentMonth), 'yyyy-MM-dd');
|
||||||
@@ -64,24 +61,20 @@ export default function MobileCalendarPage() {
|
|||||||
});
|
});
|
||||||
if (selectedStoreId) params.append('storeId', selectedStoreId.toString());
|
if (selectedStoreId) params.append('storeId', selectedStoreId.toString());
|
||||||
|
|
||||||
console.log('📅 Mobile Calendar fetching orders:', { start, end, selectedStoreId });
|
|
||||||
|
|
||||||
const response = await fetch(`/api/orders?${params}`, {
|
const response = await fetch(`/api/orders?${params}`, {
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.error('❌ Orders fetch failed:', response.status);
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('📦 Orders received:', Array.isArray(data) ? data.length : 'NOT_ARRAY', data);
|
|
||||||
return Array.isArray(data) ? data : [];
|
return Array.isArray(data) ? data : [];
|
||||||
},
|
},
|
||||||
enabled: !!user,
|
enabled: !!user,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch deliveries for current month
|
// Fetch deliveries for current month
|
||||||
const { data: deliveries = [], isLoading: loadingDeliveries } = useQuery({
|
const { data: deliveries = [] } = useQuery({
|
||||||
queryKey: ["/api/deliveries", selectedStoreId, format(currentMonth, 'yyyy-MM')],
|
queryKey: ["/api/deliveries", selectedStoreId, format(currentMonth, 'yyyy-MM')],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const start = format(startOfMonth(currentMonth), 'yyyy-MM-dd');
|
const start = format(startOfMonth(currentMonth), 'yyyy-MM-dd');
|
||||||
@@ -93,17 +86,13 @@ export default function MobileCalendarPage() {
|
|||||||
});
|
});
|
||||||
if (selectedStoreId) params.append('storeId', selectedStoreId.toString());
|
if (selectedStoreId) params.append('storeId', selectedStoreId.toString());
|
||||||
|
|
||||||
console.log('📅 Mobile Calendar fetching deliveries:', { start, end, selectedStoreId });
|
|
||||||
|
|
||||||
const response = await fetch(`/api/deliveries?${params}`, {
|
const response = await fetch(`/api/deliveries?${params}`, {
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.error('❌ Deliveries fetch failed:', response.status);
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('🚚 Deliveries received:', Array.isArray(data) ? data.length : 'NOT_ARRAY', data);
|
|
||||||
return Array.isArray(data) ? data : [];
|
return Array.isArray(data) ? data : [];
|
||||||
},
|
},
|
||||||
enabled: !!user,
|
enabled: !!user,
|
||||||
@@ -279,8 +268,8 @@ export default function MobileCalendarPage() {
|
|||||||
<CardContent className="p-3">
|
<CardContent className="p-3">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<div className={`p-2 rounded-lg ${event.type === 'order'
|
<div className={`p-2 rounded-lg ${event.type === 'order'
|
||||||
? 'bg-blue-100'
|
? 'bg-blue-100'
|
||||||
: 'bg-green-100'
|
: 'bg-green-100'
|
||||||
}`}>
|
}`}>
|
||||||
{event.type === 'order'
|
{event.type === 'order'
|
||||||
? <Package className="h-4 w-4 text-blue-600" />
|
? <Package className="h-4 w-4 text-blue-600" />
|
||||||
@@ -312,26 +301,6 @@ export default function MobileCalendarPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* DEBUG SECTION - VISIBLE ON MOBILE */}
|
|
||||||
<div className="mt-8 p-4 bg-gray-100 rounded text-xs font-mono text-gray-600 break-all">
|
|
||||||
<p className="font-bold">DEBUG INFO:</p>
|
|
||||||
<p>Current Month: {format(currentMonth, 'yyyy-MM')}</p>
|
|
||||||
<p>Store ID: {selectedStoreId ? selectedStoreId : 'None'}</p>
|
|
||||||
<p>Orders: {loadingOrders ? 'Loading...' : orders?.length || 0}</p>
|
|
||||||
<p>Deliveries: {loadingDeliveries ? 'Loading...' : deliveries?.length || 0}</p>
|
|
||||||
<p>Total Events: {events.length}</p>
|
|
||||||
<p>User: {user ? user.username : 'No user'}</p>
|
|
||||||
|
|
||||||
{events.length > 0 && (
|
|
||||||
<div className="mt-2 text-[10px]">
|
|
||||||
<p className="font-bold">First Event Sample:</p>
|
|
||||||
<p>Title: {events[0].title}</p>
|
|
||||||
<p>Raw Date: {String(events[0].date)}</p>
|
|
||||||
<p>Parsed: {new Date(events[0].date).toString()}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</MobileLayout>
|
</MobileLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
/**
|
||||||
|
* MobileCustomerOrdersPage.tsx
|
||||||
|
* Version mobile de la page Commandes Clients
|
||||||
|
*/
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useQuery, useMutation, useQueryClient } 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 { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import {
|
||||||
|
Package,
|
||||||
|
Search,
|
||||||
|
Phone,
|
||||||
|
User,
|
||||||
|
ClipboardList,
|
||||||
|
Plus,
|
||||||
|
MoreVertical
|
||||||
|
} from "lucide-react";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import { fr } from "date-fns/locale";
|
||||||
|
import { apiRequest } from "@/lib/queryClient";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
|
||||||
|
export default function MobileCustomerOrdersPage() {
|
||||||
|
const { user } = useAuthUnified();
|
||||||
|
const { selectedStoreId } = useStore();
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const { toast } = useToast();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const { data: orders = [], isLoading } = useQuery({
|
||||||
|
queryKey: ["/api/customer-orders", selectedStoreId],
|
||||||
|
queryFn: async () => {
|
||||||
|
if (!selectedStoreId) return [];
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/customer-orders?storeId=${selectedStoreId}`,
|
||||||
|
{ credentials: 'include' }
|
||||||
|
);
|
||||||
|
if (!response.ok) throw new Error("Failed to fetch customer orders");
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
enabled: !!selectedStoreId && !!user,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mutation pour changer le statut
|
||||||
|
const statusMutation = useMutation({
|
||||||
|
mutationFn: ({ id, status }: { id: number; status: string }) =>
|
||||||
|
apiRequest(`/api/customer-orders/${id}`, 'PUT', { status }),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/customer-orders"] });
|
||||||
|
toast({ title: "Statut mis à jour" });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredOrders = orders.filter((order: any) =>
|
||||||
|
order.customerName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
order.productDesignation.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
(order.productReference && order.productReference.toLowerCase().includes(searchTerm.toLowerCase()))
|
||||||
|
);
|
||||||
|
|
||||||
|
const getStatusVariant = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case "Disponible": return "success"; // vert
|
||||||
|
case "Commande en Cours": return "default"; // bleu
|
||||||
|
case "Retiré": return "secondary"; // gris
|
||||||
|
case "Annulé": return "destructive"; // rouge
|
||||||
|
default: return "outline";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case "Disponible": return "bg-green-100 text-green-800 border-green-200";
|
||||||
|
case "Commande en Cours": return "bg-blue-100 text-blue-800 border-blue-200";
|
||||||
|
case "Retiré": return "bg-gray-100 text-gray-800 border-gray-200";
|
||||||
|
case "Annulé": return "bg-red-100 text-red-800 border-red-200";
|
||||||
|
default: return "bg-yellow-100 text-yellow-800 border-yellow-200";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MobileLayout>
|
||||||
|
<div className="p-4 space-y-4 pb-20">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Package className="h-6 w-6 text-blue-600" />
|
||||||
|
<h1 className="text-xl font-bold text-gray-900">Commandes Clients</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search */}
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-500" />
|
||||||
|
<Input
|
||||||
|
placeholder="Client, produit, rév..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="pl-9 h-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Orders List */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex justify-center p-8">
|
||||||
|
<div className="animate-spin h-8 w-8 border-4 border-blue-600 border-t-transparent rounded-full" />
|
||||||
|
</div>
|
||||||
|
) : filteredOrders.length === 0 ? (
|
||||||
|
<div className="text-center py-10 text-gray-500 bg-white rounded-lg border border-dashed border-gray-300">
|
||||||
|
<Package className="h-10 w-10 mx-auto text-gray-300 mb-2" />
|
||||||
|
<p>Aucune commande trouvée</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filteredOrders.map((order: any) => (
|
||||||
|
<Card key={order.id} className="overflow-hidden">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex justify-between items-start mb-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<Badge className={`${getStatusColor(order.status)} border px-2 py-0.5 text-xs`}>
|
||||||
|
{order.status}
|
||||||
|
</Badge>
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
{format(new Date(order.createdAt), "dd/MM", { locale: fr })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="font-bold text-gray-900 leading-tight">
|
||||||
|
{order.customerName}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 line-clamp-1 mt-1">
|
||||||
|
{order.productDesignation}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="sm" className="h-8 w-8 p-0 -mr-2">
|
||||||
|
<MoreVertical className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem onClick={() => statusMutation.mutate({ id: order.id, status: 'Disponible' })}>
|
||||||
|
Marquer Disponible
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => statusMutation.mutate({ id: order.id, status: 'Retiré' })}>
|
||||||
|
Marquer Retiré
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem className="text-red-600" onClick={() => statusMutation.mutate({ id: order.id, status: 'Annulé' })}>
|
||||||
|
Annuler
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 mt-3 pt-3 border-t border-gray-100 text-sm text-gray-600">
|
||||||
|
{order.customerPhone && (
|
||||||
|
<a
|
||||||
|
href={`tel:${order.customerPhone}`}
|
||||||
|
className="flex items-center gap-1.5 px-3 py-1.5 bg-green-50 text-green-700 rounded-full hover:bg-green-100 transition-colors"
|
||||||
|
>
|
||||||
|
<Phone className="h-3.5 w-3.5" />
|
||||||
|
<span className="font-medium">Appeler</span>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center gap-1.5 ml-auto">
|
||||||
|
<div className="bg-gray-100 px-2 py-1 rounded text-xs">
|
||||||
|
Qty: {order.quantity || 1}
|
||||||
|
</div>
|
||||||
|
{order.deposit && parseFloat(order.deposit) > 0 && (
|
||||||
|
<div className="bg-orange-100 text-orange-800 px-2 py-1 rounded text-xs font-medium">
|
||||||
|
Acompte: {parseFloat(order.deposit)}€
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* FAB */}
|
||||||
|
<Button
|
||||||
|
className="fixed bottom-20 right-4 h-14 w-14 rounded-full shadow-lg bg-blue-600 hover:bg-blue-700 p-0 flex items-center justify-center z-50"
|
||||||
|
onClick={() => {
|
||||||
|
// TODO: Create modal logic
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="h-6 w-6 text-white" />
|
||||||
|
</Button>
|
||||||
|
</MobileLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
/**
|
||||||
|
* MobilePublicitiesPage.tsx
|
||||||
|
* Version mobile de la page Publicités
|
||||||
|
*/
|
||||||
|
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 {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Megaphone,
|
||||||
|
Search,
|
||||||
|
Calendar,
|
||||||
|
User,
|
||||||
|
Plus
|
||||||
|
} from "lucide-react";
|
||||||
|
import { format, isWithinInterval, parseISO } from "date-fns";
|
||||||
|
import { fr } from "date-fns/locale";
|
||||||
|
|
||||||
|
export default function MobilePublicitiesPage() {
|
||||||
|
const { user } = useAuthUnified();
|
||||||
|
const { selectedStoreId } = useStore();
|
||||||
|
const [selectedYear, setSelectedYear] = useState<number>(new Date().getFullYear());
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
|
||||||
|
// Générer les années pour le select (année courante +/- 2 ans)
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
const years = Array.from({ length: 5 }, (_, i) => currentYear - 2 + i);
|
||||||
|
|
||||||
|
const { data: publicities = [], isLoading } = useQuery({
|
||||||
|
queryKey: ["/api/publicities", selectedStoreId, selectedYear],
|
||||||
|
queryFn: async () => {
|
||||||
|
if (!selectedStoreId) return [];
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/publicities?storeId=${selectedStoreId}&year=${selectedYear}`,
|
||||||
|
{ credentials: 'include' }
|
||||||
|
);
|
||||||
|
if (!response.ok) throw new Error("Failed to fetch publicities");
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
enabled: !!selectedStoreId && !!user,
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredPublicities = publicities.filter((pub: any) =>
|
||||||
|
pub.designation.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
pub.pubNumber.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
const getStatusBadge = (startDate: string, endDate: string) => {
|
||||||
|
const start = parseISO(startDate);
|
||||||
|
const end = parseISO(endDate);
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
if (isWithinInterval(now, { start, end })) {
|
||||||
|
return <Badge className="bg-green-500">En cours</Badge>;
|
||||||
|
} else if (now > end) {
|
||||||
|
return <Badge variant="secondary">Terminée</Badge>;
|
||||||
|
} else {
|
||||||
|
return <Badge variant="outline" className="text-blue-600 border-blue-200 bg-blue-50">À venir</Badge>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MobileLayout>
|
||||||
|
<div className="p-4 space-y-4 pb-20">
|
||||||
|
{/* Header & Filters */}
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Megaphone className="h-6 w-6 text-purple-600" />
|
||||||
|
<h1 className="text-xl font-bold text-gray-900">Publicités</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="flex-1 relative">
|
||||||
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-500" />
|
||||||
|
<Input
|
||||||
|
placeholder="Rechercher une pub..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="pl-9 h-10 w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
value={selectedYear.toString()}
|
||||||
|
onValueChange={(v) => setSelectedYear(parseInt(v))}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[100px]">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{years.map((y) => (
|
||||||
|
<SelectItem key={y} value={y.toString()}>
|
||||||
|
{y}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* List */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex justify-center p-8">
|
||||||
|
<div className="animate-spin h-8 w-8 border-4 border-purple-600 border-t-transparent rounded-full" />
|
||||||
|
</div>
|
||||||
|
) : filteredPublicities.length === 0 ? (
|
||||||
|
<div className="text-center py-10 text-gray-500 bg-white rounded-lg border border-dashed border-gray-300">
|
||||||
|
<Megaphone className="h-10 w-10 mx-auto text-gray-300 mb-2" />
|
||||||
|
<p>Aucune publicité trouvée pour {selectedYear}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filteredPublicities.map((pub: any) => (
|
||||||
|
<Card key={pub.id} className="overflow-hidden">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex justify-between items-start mb-2">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<Badge variant="outline" className="font-mono text-xs">
|
||||||
|
{pub.pubNumber}
|
||||||
|
</Badge>
|
||||||
|
{getStatusBadge(pub.startDate, pub.endDate)}
|
||||||
|
</div>
|
||||||
|
<h3 className="font-bold text-gray-900 leading-tight">
|
||||||
|
{pub.designation}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-sm text-gray-600 space-y-1.5 mt-3 pt-3 border-t border-gray-100">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Calendar className="h-4 w-4 text-gray-400" />
|
||||||
|
<span>
|
||||||
|
Du {format(parseISO(pub.startDate), "dd/MM", { locale: fr })} au {format(parseISO(pub.endDate), "dd/MM/yyyy", { locale: fr })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<User className="h-4 w-4 text-gray-400" />
|
||||||
|
<span>Créé par {pub.creator?.username || pub.createdBy}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* FAB Add Button */}
|
||||||
|
<Button
|
||||||
|
className="fixed bottom-20 right-4 h-14 w-14 rounded-full shadow-lg bg-purple-600 hover:bg-purple-700 p-0 flex items-center justify-center z-50"
|
||||||
|
onClick={() => {
|
||||||
|
// TODO: Ouvrir modal création mobile
|
||||||
|
// Pour l'instant on peut rediriger ou ouvrir un drawer
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="h-6 w-6 text-white" />
|
||||||
|
</Button>
|
||||||
|
</MobileLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user