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

@@ -14,6 +14,7 @@
"@firebase/data-connect": "^0.3.12", "@firebase/data-connect": "^0.3.12",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.7", "@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tabs": "^1.1.13",
@@ -33,6 +34,7 @@
"lucide-react": "^0.563.0", "lucide-react": "^0.563.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-datepicker": "^9.1.0", "react-datepicker": "^9.1.0",
"react-day-picker": "^9.13.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-hook-form": "^7.71.1", "react-hook-form": "^7.71.1",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",

View File

@@ -23,6 +23,9 @@ importers:
'@radix-ui/react-label': '@radix-ui/react-label':
specifier: ^2.1.7 specifier: ^2.1.7
version: 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) version: 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-popover':
specifier: ^1.1.15
version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-slot': '@radix-ui/react-slot':
specifier: ^1.2.4 specifier: ^1.2.4
version: 1.2.4(@types/react@19.2.10)(react@19.2.4) version: 1.2.4(@types/react@19.2.10)(react@19.2.4)
@@ -80,6 +83,9 @@ importers:
react-datepicker: react-datepicker:
specifier: ^9.1.0 specifier: ^9.1.0
version: 9.1.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) version: 9.1.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react-day-picker:
specifier: ^9.13.0
version: 9.13.0(react@19.2.4)
react-dom: react-dom:
specifier: ^19.2.0 specifier: ^19.2.0
version: 19.2.4(react@19.2.4) version: 19.2.4(react@19.2.4)
@@ -236,6 +242,9 @@ packages:
resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==} resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@date-fns/tz@1.4.1':
resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==}
'@esbuild/aix-ppc64@0.27.2': '@esbuild/aix-ppc64@0.27.2':
resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -2016,6 +2025,9 @@ packages:
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
engines: {node: '>=12'} engines: {node: '>=12'}
date-fns-jalali@4.1.0-0:
resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==}
date-fns@4.1.0: date-fns@4.1.0:
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
@@ -2583,6 +2595,12 @@ packages:
date-fns-tz: date-fns-tz:
optional: true optional: true
react-day-picker@9.13.0:
resolution: {integrity: sha512-euzj5Hlq+lOHqI53NiuNhCP8HWgsPf/bBAVijR50hNaY1XwjKjShAnIe8jm8RD2W9IJUvihDIZ+KrmqfFzNhFQ==}
engines: {node: '>=18'}
peerDependencies:
react: '>=16.8.0'
react-dom@19.2.4: react-dom@19.2.4:
resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==}
peerDependencies: peerDependencies:
@@ -3042,6 +3060,8 @@ snapshots:
'@babel/helper-string-parser': 7.27.1 '@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5 '@babel/helper-validator-identifier': 7.28.5
'@date-fns/tz@1.4.1': {}
'@esbuild/aix-ppc64@0.27.2': '@esbuild/aix-ppc64@0.27.2':
optional: true optional: true
@@ -4857,6 +4877,8 @@ snapshots:
d3-timer@3.0.1: {} d3-timer@3.0.1: {}
date-fns-jalali@4.1.0-0: {}
date-fns@4.1.0: {} date-fns@4.1.0: {}
debug@4.4.3: debug@4.4.3:
@@ -5454,6 +5476,13 @@ snapshots:
react: 19.2.4 react: 19.2.4
react-dom: 19.2.4(react@19.2.4) react-dom: 19.2.4(react@19.2.4)
react-day-picker@9.13.0(react@19.2.4):
dependencies:
'@date-fns/tz': 1.4.1
date-fns: 4.1.0
date-fns-jalali: 4.1.0-0
react: 19.2.4
react-dom@19.2.4(react@19.2.4): react-dom@19.2.4(react@19.2.4):
dependencies: dependencies:
react: 19.2.4 react: 19.2.4

View File

@@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const buttonVariants = cva( export const buttonVariants = cva(
"inline-flex items-center justify-center transition-premium gap-2 whitespace-nowrap rounded-xl text-base font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 active:scale-[0.98]", "inline-flex items-center justify-center transition-premium gap-2 whitespace-nowrap rounded-xl text-base font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 active:scale-[0.98]",
{ {
variants: { variants: {

View File

@@ -0,0 +1,73 @@
import { ChevronLeft, ChevronRight } from "lucide-react"
import { DayPicker, type DayPickerProps } from "react-day-picker"
import { buttonVariants } from "@/common/components/ui/button"
import { cn } from "@/lib/utils"
export type CalendarProps = DayPickerProps & {
className?: string;
};
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex justify-center pt-1 relative items-center",
caption_label: "text-sm font-medium",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: cn(
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md",
props.mode === "range"
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
: "[&:has([aria-selected])]:rounded-md"
),
day: cn(
buttonVariants({ variant: "ghost" }),
"h-8 w-8 p-0 font-normal aria-selected:opacity-100"
),
day_range_start: "day-range-start",
day_range_end: "day-range-end",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
Chevron: ({ orientation }) => {
const Icon = orientation === "left" ? ChevronLeft : ChevronRight;
return <Icon className="h-4 w-4" />;
},
}}
{...props}
/>
);
}
Calendar.displayName = "Calendar";
export { Calendar }

View File

@@ -0,0 +1,152 @@
import * as React from "react"
import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react"
import { cn } from "@/lib/utils"
import { Dialog, DialogContent } from "@/common/components/ui/dialog"
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className
)}
{...props}
/>
))
Command.displayName = CommandPrimitive.displayName
interface CommandDialogProps extends React.ComponentPropsWithoutRef<typeof Dialog> {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
))
CommandInput.displayName = CommandPrimitive.Input.displayName
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
))
CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
))
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
/>
))
CommandGroup.displayName = CommandPrimitive.Group.displayName
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
className
)}
{...props}
/>
))
CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
CommandShortcut.displayName = "CommandShortcut"
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

View File

@@ -0,0 +1,29 @@
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }

View File

@@ -6,7 +6,7 @@ import {
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/common/components/ui/dailog"; } from "@/common/components/ui/dialog";
import { Input } from "@/common/components/ui/input"; import { Input } from "@/common/components/ui/input";
import { Label } from "@/common/components/ui/label"; import { Label } from "@/common/components/ui/label";
import { 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>
);
}

View File

@@ -18,7 +18,7 @@ import AddClient from './features/business/clients/AddClient';
import ServiceRates from './features/business/rates/ServiceRates'; import ServiceRates from './features/business/rates/ServiceRates';
import OrderList from './features/operations/orders/OrderList'; import OrderList from './features/operations/orders/OrderList';
import OrderDetail from './features/operations/orders/OrderDetail'; import OrderDetail from './features/operations/orders/OrderDetail';
import ClientOrderList from './features/operations/orders/ClientOrderList';
/** /**
* AppRoutes Component * AppRoutes Component
@@ -95,6 +95,7 @@ const AppRoutes: React.FC = () => {
<Route path="/rates" element={<ServiceRates />} /> <Route path="/rates" element={<ServiceRates />} />
{/* Operations Routes */} {/* Operations Routes */}
<Route path="/orders" element={<OrderList />} /> <Route path="/orders" element={<OrderList />} />
<Route path="/orders/client" element={<ClientOrderList />} />
<Route path="/orders/:id" element={<OrderDetail />} /> <Route path="/orders/:id" element={<OrderDetail />} />
</Route> </Route>