feat: Implement Order List for Admins
This commit is contained in:
@@ -10,12 +10,12 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dataconnect/generated": "link:src/dataconnect-generated",
|
||||
"@firebase/analytics": "^0.10.19",
|
||||
"@firebase/data-connect": "^0.3.12",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/themes": "^3.2.1",
|
||||
"@reduxjs/toolkit": "^2.11.2",
|
||||
|
||||
29
apps/web/pnpm-lock.yaml
generated
29
apps/web/pnpm-lock.yaml
generated
@@ -5,17 +5,12 @@ settings:
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
overrides:
|
||||
'@firebasegen/example-connector': link:src/dataconnect-generated
|
||||
dataconnect-generated: link:../../../../../AppData/Local/pnpm/global/5/node_modules/src/dataconnect-generated
|
||||
'@dataconnect/generated': link:src/dataconnect-generated
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@dataconnect/generated':
|
||||
specifier: link:src/dataconnect-generated
|
||||
version: link:src/dataconnect-generated
|
||||
'@firebase/analytics':
|
||||
specifier: ^0.10.19
|
||||
version: 0.10.19(@firebase/app@0.14.7)
|
||||
@@ -31,6 +26,9 @@ importers:
|
||||
'@radix-ui/react-slot':
|
||||
specifier: ^1.2.4
|
||||
version: 1.2.4(@types/react@19.2.10)(react@19.2.4)
|
||||
'@radix-ui/react-switch':
|
||||
specifier: ^1.2.6
|
||||
version: 1.2.6(@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-tabs':
|
||||
specifier: ^1.1.13
|
||||
version: 1.1.13(@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)
|
||||
@@ -1510,66 +1508,79 @@ packages:
|
||||
resolution: {integrity: sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.57.0':
|
||||
resolution: {integrity: sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.57.0':
|
||||
resolution: {integrity: sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.57.0':
|
||||
resolution: {integrity: sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.57.0':
|
||||
resolution: {integrity: sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-loong64-musl@4.57.0':
|
||||
resolution: {integrity: sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.57.0':
|
||||
resolution: {integrity: sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-musl@4.57.0':
|
||||
resolution: {integrity: sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.57.0':
|
||||
resolution: {integrity: sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.57.0':
|
||||
resolution: {integrity: sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.57.0':
|
||||
resolution: {integrity: sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.57.0':
|
||||
resolution: {integrity: sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.57.0':
|
||||
resolution: {integrity: sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openbsd-x64@4.57.0':
|
||||
resolution: {integrity: sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw==}
|
||||
@@ -1645,24 +1656,28 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.1.18':
|
||||
resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.1.18':
|
||||
resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.1.18':
|
||||
resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-wasm32-wasi@4.1.18':
|
||||
resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==}
|
||||
@@ -2390,24 +2405,28 @@ packages:
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.30.2:
|
||||
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.30.2:
|
||||
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-x64-musl@1.30.2:
|
||||
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.30.2:
|
||||
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
overrides:
|
||||
'@dataconnect/generated': link:src/dataconnect-generated
|
||||
'@firebasegen/example-connector': link:src/dataconnect-generated
|
||||
|
||||
dataconnect-generated: link:../../../../../AppData/Local/pnpm/global/5/node_modules/src/dataconnect-generated
|
||||
|
||||
59
apps/web/src/common/components/ui/alert.tsx
Normal file
59
apps/web/src/common/components/ui/alert.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
destructive:
|
||||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Alert = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||
>(({ className, variant, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Alert.displayName = "Alert"
|
||||
|
||||
const AlertTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertTitle.displayName = "AlertTitle"
|
||||
|
||||
const AlertDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDescription.displayName = "AlertDescription"
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
||||
25
apps/web/src/common/components/ui/switch.tsx
Normal file
25
apps/web/src/common/components/ui/switch.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Switch = React.forwardRef<React.ElementRef<typeof SwitchPrimitives.Root>, React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
)
|
||||
)
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||
|
||||
export { Switch }
|
||||
120
apps/web/src/common/components/ui/table.tsx
Normal file
120
apps/web/src/common/components/ui/table.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
Table.displayName = "Table"
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||
))
|
||||
TableHeader.displayName = "TableHeader"
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableBody.displayName = "TableBody"
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableFooter.displayName = "TableFooter"
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableRow.displayName = "TableRow"
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableHead.displayName = "TableHead"
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"p-4 align-middle [&:has([role=checkbox])]:pr-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCell.displayName = "TableCell"
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCaption.displayName = "TableCaption"
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
9
apps/web/src/features/operations/orders/OrderDetail.tsx
Normal file
9
apps/web/src/features/operations/orders/OrderDetail.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
|
||||
const OrderDetail = () => {
|
||||
return (
|
||||
<div>OrderDetail</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OrderDetail
|
||||
343
apps/web/src/features/operations/orders/OrderList.tsx
Normal file
343
apps/web/src/features/operations/orders/OrderList.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
17
apps/web/src/lib/index.ts
Normal file
17
apps/web/src/lib/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export function createPageUrl(pageName: string) {
|
||||
// Basic implementation based on MVP usage: navigate(createPageUrl('Events'))
|
||||
// Assuming mapping based on pageName
|
||||
if (pageName === 'Events') return '/orders';
|
||||
if (pageName === 'ClientOrders') return '/orders'; // Assuming same route for now
|
||||
if (pageName === 'Invoices') return '/invoices'; // Assuming route exists
|
||||
if (pageName.startsWith('EventDetail?id=')) {
|
||||
const id = pageName.split('=')[1];
|
||||
return `/orders/${id}`;
|
||||
}
|
||||
if (pageName.startsWith('EditEvent?id=')) {
|
||||
const id = pageName.split('=')[1];
|
||||
return `/orders/${id}/edit`;
|
||||
}
|
||||
|
||||
return '/' + pageName.toLowerCase().replace(/ /g, '-');
|
||||
}
|
||||
@@ -16,6 +16,8 @@ import ClientList from './features/business/clients/ClientList';
|
||||
import EditClient from './features/business/clients/EditClient';
|
||||
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';
|
||||
|
||||
|
||||
/**
|
||||
@@ -91,6 +93,10 @@ const AppRoutes: React.FC = () => {
|
||||
<Route path="/clients/:id/edit" element={<EditClient />} />
|
||||
<Route path="/clients/add" element={<AddClient />} />
|
||||
<Route path="/rates" element={<ServiceRates />} />
|
||||
{/* Operations Routes */}
|
||||
<Route path="/orders" element={<OrderList />} />
|
||||
<Route path="/orders/:id" element={<OrderDetail />} />
|
||||
|
||||
</Route>
|
||||
<Route path="*" element={<Navigate to="/login" replace />} />
|
||||
</Routes>
|
||||
|
||||
Reference in New Issue
Block a user