feat: Implement Client Order List
This commit is contained in:
@@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
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]",
|
||||
{
|
||||
variants: {
|
||||
|
||||
73
apps/web/src/common/components/ui/calendar.tsx
Normal file
73
apps/web/src/common/components/ui/calendar.tsx
Normal 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 }
|
||||
152
apps/web/src/common/components/ui/command.tsx
Normal file
152
apps/web/src/common/components/ui/command.tsx
Normal 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,
|
||||
}
|
||||
29
apps/web/src/common/components/ui/popover.tsx
Normal file
29
apps/web/src/common/components/ui/popover.tsx
Normal 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 }
|
||||
@@ -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 {
|
||||
|
||||
319
apps/web/src/features/operations/orders/ClientOrderList.tsx
Normal file
319
apps/web/src/features/operations/orders/ClientOrderList.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import AddClient from './features/business/clients/AddClient';
|
||||
import ServiceRates from './features/business/rates/ServiceRates';
|
||||
import OrderList from './features/operations/orders/OrderList';
|
||||
import OrderDetail from './features/operations/orders/OrderDetail';
|
||||
|
||||
import ClientOrderList from './features/operations/orders/ClientOrderList';
|
||||
|
||||
/**
|
||||
* AppRoutes Component
|
||||
@@ -95,6 +95,7 @@ const AppRoutes: React.FC = () => {
|
||||
<Route path="/rates" element={<ServiceRates />} />
|
||||
{/* Operations Routes */}
|
||||
<Route path="/orders" element={<OrderList />} />
|
||||
<Route path="/orders/client" element={<ClientOrderList />} />
|
||||
<Route path="/orders/:id" element={<OrderDetail />} />
|
||||
|
||||
</Route>
|
||||
|
||||
Reference in New Issue
Block a user