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

@@ -10,12 +10,12 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@dataconnect/generated": "link:src/dataconnect-generated",
"@firebase/analytics": "^0.10.19", "@firebase/analytics": "^0.10.19",
"@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-slot": "^1.2.4", "@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/themes": "^3.2.1", "@radix-ui/themes": "^3.2.1",
"@reduxjs/toolkit": "^2.11.2", "@reduxjs/toolkit": "^2.11.2",

View File

@@ -5,17 +5,12 @@ settings:
excludeLinksFromLockfile: false excludeLinksFromLockfile: false
overrides: overrides:
'@firebasegen/example-connector': link:src/dataconnect-generated
dataconnect-generated: link:../../../../../AppData/Local/pnpm/global/5/node_modules/src/dataconnect-generated dataconnect-generated: link:../../../../../AppData/Local/pnpm/global/5/node_modules/src/dataconnect-generated
'@dataconnect/generated': link:src/dataconnect-generated
importers: importers:
.: .:
dependencies: dependencies:
'@dataconnect/generated':
specifier: link:src/dataconnect-generated
version: link:src/dataconnect-generated
'@firebase/analytics': '@firebase/analytics':
specifier: ^0.10.19 specifier: ^0.10.19
version: 0.10.19(@firebase/app@0.14.7) version: 0.10.19(@firebase/app@0.14.7)
@@ -31,6 +26,9 @@ importers:
'@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)
'@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': '@radix-ui/react-tabs':
specifier: ^1.1.13 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) 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==} resolution: {integrity: sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.57.0': '@rollup/rollup-linux-arm-musleabihf@4.57.0':
resolution: {integrity: sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA==} resolution: {integrity: sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.57.0': '@rollup/rollup-linux-arm64-gnu@4.57.0':
resolution: {integrity: sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw==} resolution: {integrity: sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.57.0': '@rollup/rollup-linux-arm64-musl@4.57.0':
resolution: {integrity: sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw==} resolution: {integrity: sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.57.0': '@rollup/rollup-linux-loong64-gnu@4.57.0':
resolution: {integrity: sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q==} resolution: {integrity: sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q==}
cpu: [loong64] cpu: [loong64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.57.0': '@rollup/rollup-linux-loong64-musl@4.57.0':
resolution: {integrity: sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ==} resolution: {integrity: sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ==}
cpu: [loong64] cpu: [loong64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.57.0': '@rollup/rollup-linux-ppc64-gnu@4.57.0':
resolution: {integrity: sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ==} resolution: {integrity: sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.57.0': '@rollup/rollup-linux-ppc64-musl@4.57.0':
resolution: {integrity: sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA==} resolution: {integrity: sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.57.0': '@rollup/rollup-linux-riscv64-gnu@4.57.0':
resolution: {integrity: sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ==} resolution: {integrity: sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.57.0': '@rollup/rollup-linux-riscv64-musl@4.57.0':
resolution: {integrity: sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ==} resolution: {integrity: sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.57.0': '@rollup/rollup-linux-s390x-gnu@4.57.0':
resolution: {integrity: sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg==} resolution: {integrity: sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.57.0': '@rollup/rollup-linux-x64-gnu@4.57.0':
resolution: {integrity: sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A==} resolution: {integrity: sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.57.0': '@rollup/rollup-linux-x64-musl@4.57.0':
resolution: {integrity: sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw==} resolution: {integrity: sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-openbsd-x64@4.57.0': '@rollup/rollup-openbsd-x64@4.57.0':
resolution: {integrity: sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw==} resolution: {integrity: sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw==}
@@ -1645,24 +1656,28 @@ packages:
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.1.18': '@tailwindcss/oxide-linux-arm64-musl@4.1.18':
resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.1.18': '@tailwindcss/oxide-linux-x64-gnu@4.1.18':
resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.1.18': '@tailwindcss/oxide-linux-x64-musl@4.1.18':
resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.1.18': '@tailwindcss/oxide-wasm32-wasi@4.1.18':
resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==}
@@ -2390,24 +2405,28 @@ packages:
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.30.2: lightningcss-linux-arm64-musl@1.30.2:
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.30.2: lightningcss-linux-x64-gnu@1.30.2:
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.30.2: lightningcss-linux-x64-musl@1.30.2:
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.30.2: lightningcss-win32-arm64-msvc@1.30.2:
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}

View File

@@ -1,4 +1,3 @@
overrides: 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 dataconnect-generated: link:../../../../../AppData/Local/pnpm/global/5/node_modules/src/dataconnect-generated

View 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 }

View 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 }

View 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,
}

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>
);
}

17
apps/web/src/lib/index.ts Normal file
View 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, '-');
}

View File

@@ -16,6 +16,8 @@ import ClientList from './features/business/clients/ClientList';
import EditClient from './features/business/clients/EditClient'; import EditClient from './features/business/clients/EditClient';
import AddClient from './features/business/clients/AddClient'; 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 OrderDetail from './features/operations/orders/OrderDetail';
/** /**
@@ -91,6 +93,10 @@ const AppRoutes: React.FC = () => {
<Route path="/clients/:id/edit" element={<EditClient />} /> <Route path="/clients/:id/edit" element={<EditClient />} />
<Route path="/clients/add" element={<AddClient />} /> <Route path="/clients/add" element={<AddClient />} />
<Route path="/rates" element={<ServiceRates />} /> <Route path="/rates" element={<ServiceRates />} />
{/* Operations Routes */}
<Route path="/orders" element={<OrderList />} />
<Route path="/orders/:id" element={<OrderDetail />} />
</Route> </Route>
<Route path="*" element={<Navigate to="/login" replace />} /> <Route path="*" element={<Navigate to="/login" replace />} />
</Routes> </Routes>