feat: Implement Client Order List

This commit is contained in:
dhinesh-m24
2026-02-05 16:28:09 +05:30
parent 4f5b1c5e69
commit 9dab3fef05
10 changed files with 608 additions and 3 deletions

View File

@@ -6,7 +6,7 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/common/components/ui/dailog";
} from "@/common/components/ui/dialog";
import { Input } from "@/common/components/ui/input";
import { Label } from "@/common/components/ui/label";
import {

View File

@@ -0,0 +1,319 @@
import { useState, useMemo } from "react";
import { useNavigate } from "react-router-dom";
import { format, parseISO, isValid } from "date-fns";
import {
Search, MapPin, FileText,
Clock, Package, CheckCircle, Check, ChevronsUpDown,
Plus
} from "lucide-react";
import { Card, CardContent } from "@/common/components/ui/card";
import { Badge } from "@/common/components/ui/badge";
import { Button } from "@/common/components/ui/button";
import { Input } from "@/common/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/common/components/ui/table";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/common/components/ui/popover";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from "@/common/components/ui/command";
import DashboardLayout from "@/features/layouts/DashboardLayout";
import { useSelector } from "react-redux";
import type { RootState } from "@/store/store";
import { useGetBusinessesByUserId, useGetOrdersByBusinessId } from "@/dataconnect-generated/react";
import { OrderStatus } from "@/dataconnect-generated";
import { dataConnect } from "@/features/auth/firebase";
const safeParseDate = (dateString: any): Date | null => {
if (!dateString) return null;
try {
const date = typeof dateString === 'string' ? parseISO(dateString) : new Date(dateString);
return isValid(date) ? date : null;
} catch { return null; }
};
export default function ClientOrderList() {
const navigate = useNavigate();
const { user } = useSelector((state: RootState) => state.auth);
const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
const [locationFilter, setLocationFilter] = useState("all");
const [locationOpen, setLocationOpen] = useState(false);
// 1. Get businesses for the logged in user
const { data: businessData } = useGetBusinessesByUserId(dataConnect, { userId: user?.uid || "" });
const businesses = businessData?.businesses || [];
const primaryBusinessId = businesses[0]?.id;
// 2. Get orders for the primary business
const { data: orderData, isLoading } = useGetOrdersByBusinessId(dataConnect, {
businessId: primaryBusinessId || ""
}, {
enabled: !!primaryBusinessId
});
const orders = orderData?.orders || [];
const filteredOrders = useMemo(() => {
let filtered = [...orders];
if (searchTerm) {
const lower = searchTerm.toLowerCase();
filtered = filtered.filter(o =>
o.eventName?.toLowerCase().includes(lower) ||
o.teamHub?.hubName?.toLowerCase().includes(lower)
);
}
if (statusFilter !== "all") {
filtered = filtered.filter(o => o.status === statusFilter);
}
if (locationFilter !== "all") {
filtered = filtered.filter(o => o.teamHub?.hubName === locationFilter);
}
return filtered;
}, [orders, searchTerm, statusFilter, locationFilter]);
const uniqueLocations = useMemo(() => {
const locations = new Set<string>();
orders.forEach(o => {
if (o.teamHub?.hubName) locations.add(o.teamHub.hubName);
});
return Array.from(locations).sort();
}, [orders]);
const stats = useMemo(() => {
const total = orders.length;
const active = orders.filter(o => o.status !== OrderStatus.COMPLETED && o.status !== OrderStatus.CANCELLED).length;
const completed = orders.filter(o => o.status === OrderStatus.COMPLETED).length;
const filled = orders.filter(o => o.status === OrderStatus.FILLED || o.status === OrderStatus.FULLY_STAFFED).length;
return { total, active, completed, filled };
}, [orders]);
const getFillRate = (order: any) => {
const requested = order.requested || 0;
const assigned = Array.isArray(order.assignedStaff) ? order.assignedStaff.length : 0;
if (requested === 0) return 0;
return Math.round((assigned / requested) * 100);
};
const getStatusBadge = (status: OrderStatus) => {
switch (status) {
case OrderStatus.COMPLETED:
return <Badge className="bg-emerald-100 text-emerald-700 hover:bg-emerald-100 border-none">Completed</Badge>;
case OrderStatus.CANCELLED:
return <Badge className="bg-red-100 text-red-700 hover:bg-red-100 border-none">Cancelled</Badge>;
case OrderStatus.FULLY_STAFFED:
case OrderStatus.FILLED:
return <Badge className="bg-blue-100 text-blue-700 hover:bg-blue-100 border-none">Filled</Badge>;
case OrderStatus.PARTIAL_STAFFED:
return <Badge className="bg-amber-100 text-amber-700 hover:bg-amber-100 border-none">Partial</Badge>;
case OrderStatus.POSTED:
return <Badge className="bg-purple-100 text-purple-700 hover:bg-purple-100 border-none">Posted</Badge>;
default:
return <Badge className="bg-slate-100 text-slate-700 hover:bg-slate-100 border-none">{status}</Badge>;
}
};
return (
<DashboardLayout title="My Orders" subtitle="Manage and track your staffing requests">
<div className="space-y-6">
{/* Stats Section */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card className="bg-card border-border/50 shadow-sm">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-muted-foreground text-xs font-medium mb-1 uppercase tracking-wider">Total Orders</p>
<p className="text-2xl font-bold">{stats.total}</p>
</div>
<div className="w-10 h-10 bg-primary/10 rounded-lg flex items-center justify-center">
<Package className="w-5 h-5 text-primary" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-card border-border/50 shadow-sm">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-muted-foreground text-xs font-medium mb-1 uppercase tracking-wider">Active</p>
<p className="text-2xl font-bold">{stats.active}</p>
</div>
<div className="w-10 h-10 bg-amber-500/10 rounded-lg flex items-center justify-center">
<Clock className="w-5 h-5 text-amber-600" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-card border-border/50 shadow-sm">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-muted-foreground text-xs font-medium mb-1 uppercase tracking-wider">Filled</p>
<p className="text-2xl font-bold">{stats.filled}</p>
</div>
<div className="w-10 h-10 bg-blue-500/10 rounded-lg flex items-center justify-center">
<CheckCircle className="w-5 h-5 text-blue-600" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-card border-border/50 shadow-sm">
<CardContent className="p-6 text-center flex flex-col items-center justify-center bg-primary text-primary-foreground hover:bg-primary/90 cursor-pointer transition-colors" onClick={() => navigate('/orders/create')}>
<Plus className="w-6 h-6 mb-1" />
<p className="font-bold">Create New Order</p>
</CardContent>
</Card>
</div>
{/* Filters and Search */}
<div className="flex flex-col md:flex-row items-center gap-4">
<div className="relative flex-1 w-full">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
placeholder="Search orders..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-9"
/>
</div>
<div className="flex items-center gap-2 w-full md:w-auto">
<Popover open={locationOpen} onOpenChange={setLocationOpen}>
<PopoverTrigger asChild>
<Button variant="outline" className="w-full md:w-[200px] justify-between">
{locationFilter === "all" ? "All Locations" : locationFilter}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<Command>
<CommandInput placeholder="Search location..." />
<CommandEmpty>No location found.</CommandEmpty>
<CommandGroup>
<CommandItem onSelect={() => { setLocationFilter("all"); setLocationOpen(false); }}>
<Check className={`mr-2 h-4 w-4 ${locationFilter === "all" ? "opacity-100" : "opacity-0"}`} />
All Locations
</CommandItem>
{uniqueLocations.map((loc) => (
<CommandItem key={loc} onSelect={() => { setLocationFilter(loc); setLocationOpen(false); }}>
<Check className={`mr-2 h-4 w-4 ${locationFilter === loc ? "opacity-100" : "opacity-0"}`} />
{loc}
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
<Button
variant="outline"
onClick={() => {
setSearchTerm("");
setStatusFilter("all");
setLocationFilter("all");
}}
>
Reset
</Button>
</div>
</div>
{/* Orders Table */}
<div className="bg-card rounded-xl shadow-sm border border-border/50 overflow-hidden">
<Table>
<TableHeader>
<TableRow className="bg-muted/50">
<TableHead className="w-[120px]">Order #</TableHead>
<TableHead>Event Name</TableHead>
<TableHead>Event Date</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-center">Positions</TableHead>
<TableHead className="text-center">Fill Rate</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{isLoading ? (
<TableRow>
<TableCell colSpan={7} className="text-center py-10 text-muted-foreground">
Loading orders...
</TableCell>
</TableRow>
) : filteredOrders.length === 0 ? (
<TableRow>
<TableCell colSpan={7} className="text-center py-10 text-muted-foreground">
No orders found.
</TableCell>
</TableRow>
) : (
filteredOrders.map((order) => {
const eventDate = safeParseDate(order.date);
const fillRate = getFillRate(order);
return (
<TableRow key={order.id} className="cursor-pointer hover:bg-muted/30" onClick={() => navigate(`/orders/${order.id}`)}>
<TableCell className="font-mono text-xs text-muted-foreground uppercase">
{order.id.substring(0, 8)}
</TableCell>
<TableCell className="font-medium">
{order.eventName}
<div className="flex items-center gap-1 text-[10px] text-muted-foreground mt-0.5">
<MapPin className="w-3 h-3" />
{order.teamHub?.hubName || "No location"}
</div>
</TableCell>
<TableCell>
<div className="flex flex-col">
<span className="text-sm">{eventDate ? format(eventDate, 'MMM dd, yyyy') : 'No date'}</span>
<span className="text-[10px] text-muted-foreground uppercase">{eventDate ? format(eventDate, 'EEEE') : ''}</span>
</div>
</TableCell>
<TableCell>
{getStatusBadge(order.status)}
</TableCell>
<TableCell className="text-center font-semibold">
{order.requested || 0}
</TableCell>
<TableCell className="text-center">
<div className="flex flex-col items-center gap-1">
<div className="w-16 bg-slate-100 rounded-full h-1.5 overflow-hidden">
<div
className={`h-full rounded-full ${fillRate === 100 ? 'bg-emerald-500' : fillRate > 0 ? 'bg-blue-500' : 'bg-slate-300'}`}
style={{ width: `${fillRate}%` }}
/>
</div>
<span className="text-[10px] font-bold text-muted-foreground">{fillRate}%</span>
</div>
</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm" onClick={(e) => { e.stopPropagation(); navigate(`/orders/${order.id}`); }}>
<FileText className="w-4 h-4 mr-1" />
Details
</Button>
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
</div>
</div>
</DashboardLayout>
);
}