feat: Implement Order List for Admins

This commit is contained in:
dhinesh-m24
2026-02-05 15:30:05 +05:30
parent 7265f8db9e
commit 122159a62c
10 changed files with 605 additions and 8 deletions

View File

@@ -0,0 +1,9 @@
import React from 'react'
const OrderDetail = () => {
return (
<div>OrderDetail</div>
)
}
export default OrderDetail

View File

@@ -0,0 +1,343 @@
import { Badge } from "@/common/components/ui/badge";
import { Button } from "@/common/components/ui/button";
import { Card, CardContent } from "@/common/components/ui/card";
import { Input } from "@/common/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/common/components/ui/select";
import DashboardLayout from "@/features/layouts/DashboardLayout";
import {
Search,
Calendar,
Filter,
ArrowRight,
Clock,
CheckCircle,
AlertTriangle,
XCircle,
FileText
} from "lucide-react";
import React, { useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
import type { RootState } from "@/store/store";
import { useListOrders, useListBusinesses } from "@/dataconnect-generated/react";
import { dataConnect } from "@/features/auth/firebase";
import { format, isWithinInterval, parseISO, startOfDay, endOfDay } from "date-fns";
import { OrderStatus } from "@/dataconnect-generated";
export default function OrderList() {
const navigate = useNavigate();
const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
const [clientFilter, setClientFilter] = useState("all");
const [dateRange, setDateRange] = useState<{ start: string; end: string }>({ start: "", end: "" });
const { user } = useSelector((state: RootState) => state.auth);
const isAdmin = user?.userRole === 'admin' || user?.userRole === 'ADMIN';
const { data: orderData, isLoading: loadingOrders } = useListOrders(dataConnect);
const { data: businessData, isLoading: loadingBusinesses } = useListBusinesses(dataConnect);
const isLoading = loadingOrders || loadingBusinesses;
const orders = orderData?.orders || [];
const businesses = businessData?.businesses || [];
const filteredOrders = useMemo(() => {
return orders.filter(order => {
// Search by Order # (ID) or Event Name
const matchesSearch =
order.id.toLowerCase().includes(searchTerm.toLowerCase()) ||
(order.eventName?.toLowerCase().includes(searchTerm.toLowerCase()) ?? false);
// Filter by Status
const matchesStatus = statusFilter === "all" || order.status === statusFilter;
// Filter by Client
const matchesClient = clientFilter === "all" || order.businessId === clientFilter;
// Filter by Date Range
let matchesDate = true;
if (order.date) {
const orderDate = new Date(order.date);
if (dateRange.start && dateRange.end) {
matchesDate = isWithinInterval(orderDate, {
start: startOfDay(new Date(dateRange.start)),
end: endOfDay(new Date(dateRange.end))
});
} else if (dateRange.start) {
matchesDate = orderDate >= startOfDay(new Date(dateRange.start));
} else if (dateRange.end) {
matchesDate = orderDate <= endOfDay(new Date(dateRange.end));
}
}
return matchesSearch && matchesStatus && matchesClient && matchesDate;
});
}, [orders, searchTerm, statusFilter, clientFilter, dateRange]);
const getStatusBadge = (status: OrderStatus) => {
switch (status) {
case OrderStatus.FULLY_STAFFED:
case OrderStatus.FILLED:
return <Badge className="bg-emerald-500 hover:bg-emerald-600 text-white border-none font-bold uppercase text-[10px]">Fully Staffed</Badge>;
case OrderStatus.PARTIAL_STAFFED:
return <Badge className="bg-orange-500 hover:bg-orange-600 text-white border-none font-bold uppercase text-[10px]">Partial Staffed</Badge>;
case OrderStatus.PENDING:
case OrderStatus.POSTED:
return <Badge className="bg-blue-500 hover:bg-blue-600 text-white border-none font-bold uppercase text-[10px]">{status}</Badge>;
case OrderStatus.CANCELLED:
return <Badge className="bg-red-500 hover:bg-red-600 text-white border-none font-bold uppercase text-[10px]">Cancelled</Badge>;
case OrderStatus.COMPLETED:
return <Badge className="bg-green-500 hover:bg-slate-600 text-white border-none font-bold uppercase text-[10px]">Completed</Badge>;
case OrderStatus.DRAFT:
return <Badge variant="outline" className="text-muted-foreground font-bold uppercase text-[10px]">Draft</Badge>;
default:
return <Badge variant="secondary" className="font-bold uppercase text-[10px]">{status}</Badge>;
}
};
const calculateFillRate = (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);
};
if (!isAdmin) {
return (
<DashboardLayout title="Access Denied" subtitle="Unauthorized Access">
<div className="flex flex-col items-center justify-center min-h-[40vh] text-center">
<div className="w-16 h-16 bg-destructive/10 rounded-full flex items-center justify-center text-destructive mb-4">
<AlertTriangle className="w-8 h-8" />
</div>
<h2 className="text-2xl font-bold">Restricted Access</h2>
<p className="text-muted-foreground mt-2 max-w-sm">Only administrators are authorized to view the master order list.</p>
<Button onClick={() => navigate("/")} variant="outline" className="mt-6 rounded-xl font-bold">
Return to Dashboard
</Button>
</div>
</DashboardLayout>
);
}
return (
<DashboardLayout
title="Master Order List"
subtitle="Monitor and manage all client orders across the platform"
>
<div className="space-y-6">
{/* KPI Summary Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card className="bg-card/50 border-border/50 shadow-sm">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground font-bold uppercase tracking-wider mb-1">Total Orders</p>
<p className="text-2xl font-bold">{orders.length}</p>
</div>
<div className="p-2 bg-primary/10 rounded-lg text-primary">
<FileText className="w-5 h-5" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-card/50 border-border/50 shadow-sm">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground font-bold uppercase tracking-wider mb-1">Pending/Posted</p>
<p className="text-2xl font-bold text-blue-600">
{orders.filter(o => o.status === OrderStatus.PENDING || o.status === OrderStatus.POSTED).length}
</p>
</div>
<div className="p-2 bg-blue-500/10 rounded-lg text-blue-600">
<Clock className="w-5 h-5" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-card/50 border-border/50 shadow-sm">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground font-bold uppercase tracking-wider mb-1">Partial</p>
<p className="text-2xl font-bold text-orange-600">
{orders.filter(o => o.status === OrderStatus.PARTIAL_STAFFED).length}
</p>
</div>
<div className="p-2 bg-orange-500/10 rounded-lg text-orange-600">
<AlertTriangle className="w-5 h-5" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-card/50 border-border/50 shadow-sm">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground font-bold uppercase tracking-wider mb-1">Filled</p>
<p className="text-2xl font-bold text-emerald-600">
{orders.filter(o => o.status === OrderStatus.FULLY_STAFFED || o.status === OrderStatus.FILLED).length}
</p>
</div>
<div className="p-2 bg-emerald-500/10 rounded-lg text-emerald-600">
<CheckCircle className="w-5 h-5" />
</div>
</div>
</CardContent>
</Card>
</div>
{/* Filters Section */}
<div className="bg-card/30 p-4 rounded-2xl border border-border/50 flex flex-col lg:flex-row gap-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
placeholder="Search by Order # or Event Name..."
className="pl-10 h-11"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="h-11 min-w-[140px]">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Statuses</SelectItem>
{Object.values(OrderStatus).map(status => (
<SelectItem key={status} value={status}>{status.replace('_', ' ')}</SelectItem>
))}
</SelectContent>
</Select>
<Select value={clientFilter} onValueChange={setClientFilter}>
<SelectTrigger className="h-11 min-w-[160px]">
<SelectValue placeholder="Client" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Clients</SelectItem>
{businesses.map(business => (
<SelectItem key={business.id} value={business.id}>{business.businessName}</SelectItem>
))}
</SelectContent>
</Select>
<div className="flex gap-2 col-span-2">
<Input
type="date"
className="h-11"
value={dateRange.start}
onChange={(e) => setDateRange(prev => ({ ...prev, start: e.target.value }))}
/>
<div className="flex items-center text-muted-foreground">to</div>
<Input
type="date"
className="h-11"
value={dateRange.end}
onChange={(e) => setDateRange(prev => ({ ...prev, end: e.target.value }))}
/>
</div>
</div>
</div>
{/* Master Table */}
<div className="bg-card border border-border rounded-2xl overflow-hidden shadow-sm">
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className="border-b border-border bg-muted/30">
<th className="px-6 py-4 text-xs font-bold uppercase tracking-wider text-muted-foreground">Order #</th>
<th className="px-6 py-4 text-xs font-bold uppercase tracking-wider text-muted-foreground">Client Name</th>
<th className="px-6 py-4 text-xs font-bold uppercase tracking-wider text-muted-foreground">Event Date</th>
<th className="px-6 py-4 text-xs font-bold uppercase tracking-wider text-muted-foreground">Status</th>
<th className="px-6 py-4 text-xs font-bold uppercase tracking-wider text-muted-foreground text-center">Positions</th>
<th className="px-6 py-4 text-xs font-bold uppercase tracking-wider text-muted-foreground">Fill Rate</th>
<th className="px-6 py-4 text-xs font-bold uppercase tracking-wider text-muted-foreground text-right"></th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{isLoading ? (
<tr>
<td colSpan={7} className="px-6 py-12 text-center">
<div className="flex flex-col items-center gap-3">
<div className="w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin" />
<p className="text-sm text-muted-foreground font-medium">Fetching orders...</p>
</div>
</td>
</tr>
) : filteredOrders.length > 0 ? (
filteredOrders.map((order) => {
const fillRate = calculateFillRate(order);
return (
<tr
key={order.id}
className="hover:bg-muted/40 cursor-pointer transition-colors group"
onClick={() => navigate(`/orders/${order.id}`)}
>
<td className="px-6 py-4">
<div className="font-bold text-foreground group-hover:text-primary transition-colors truncate max-w-[120px]">
#{order.id.split('-')[0].toUpperCase()}
</div>
<div className="text-[10px] text-muted-foreground uppercase">{order.eventName || 'Unnamed Event'}</div>
</td>
<td className="px-6 py-4">
<div className="text-sm font-medium">{order.business.businessName}</div>
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2 text-sm">
<Calendar className="w-3.5 h-3.5 text-muted-foreground" />
{order.date ? format(new Date(order.date), 'MMM d, yyyy') : 'TBD'}
</div>
</td>
<td className="px-6 py-4">
{getStatusBadge(order.status)}
</td>
<td className="px-6 py-4 text-center">
<div className="text-sm font-bold bg-muted/50 px-2 py-1 rounded border border-border inline-block min-w-[30px]">
{order.requested || 0}
</div>
</td>
<td className="px-6 py-4">
<div className="flex flex-col gap-1.5 w-full max-w-[100px]">
<div className="flex justify-between text-[10px] font-bold">
<span>{fillRate}%</span>
</div>
<div className="w-full bg-muted rounded-full h-1.5 overflow-hidden">
<div
className={`h-full rounded-full transition-all ${
fillRate === 100 ? 'bg-emerald-500' : fillRate > 0 ? 'bg-orange-500' : 'bg-slate-300'
}`}
style={{ width: `${fillRate}%` }}
/>
</div>
</div>
</td>
<td className="px-6 py-4 text-right">
<ArrowRight className="w-4 h-4 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" />
</td>
</tr>
);
})
) : (
<tr>
<td colSpan={7} className="px-6 py-12 text-center text-muted-foreground">
No orders found matching your criteria.
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</div>
</DashboardLayout>
);
}