diff --git a/bun.lock b/bun.lock index 6ac2527..bee4ccf 100644 --- a/bun.lock +++ b/bun.lock @@ -66,7 +66,9 @@ "cmdk": "^1.1.1", "date-fns": "^4.1.0", "embla-carousel-react": "^8.6.0", + "fuse.js": "^7.1.0", "lucide-react": "^0.563.0", + "motion": "^12.23.24", "react": "^19.2.4", "react-day-picker": "^9.13.1", "react-dom": "^19.2.4", @@ -944,6 +946,8 @@ "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + "framer-motion": ["framer-motion@12.33.0", "", { "dependencies": { "motion-dom": "^12.33.0", "motion-utils": "^12.29.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-ca8d+rRPcDP5iIF+MoT3WNc0KHJMjIyFAbtVLvM9eA7joGSpeqDfiNH/kCs1t4CHi04njYvWyj0jS4QlEK/rJQ=="], + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], "fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="], @@ -956,6 +960,8 @@ "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], + "fuse.js": ["fuse.js@7.1.0", "", {}, "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ=="], + "fuzzysort": ["fuzzysort@3.1.0", "", {}, "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ=="], "fzf": ["fzf@0.5.2", "", {}, "sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q=="], @@ -1224,6 +1230,12 @@ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + "motion": ["motion@12.33.0", "", { "dependencies": { "framer-motion": "^12.33.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-TcND7PijsrTeIA9SRVUB8TOJQ+6mJnJ5K4a9oAJZvyI0Zy47Gq5oofU+VkTxbLcvDoKXnHspQcII2mnk3TbFsQ=="], + + "motion-dom": ["motion-dom@12.33.0", "", { "dependencies": { "motion-utils": "^12.29.2" } }, "sha512-XRPebVypsl0UM+7v0Hr8o9UAj0S2djsQWRdHBd5iVouVpMrQqAI0C/rDAT3QaYnXnHuC5hMcwDHCboNeyYjPoQ=="], + + "motion-utils": ["motion-utils@12.29.2", "", {}, "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "msw": ["msw@2.12.9", "", { "dependencies": { "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.41.2", "@open-draft/deferred-promise": "^2.2.0", "@types/statuses": "^2.0.6", "cookie": "^1.0.2", "graphql": "^16.12.0", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.10.1", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.0", "type-fest": "^5.2.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-NYbi51C6M3dujGmcmuGemu68jy12KqQPoVWGeroKToLGsBgrwG5ErM8WctoIIg49/EV49SEvYM9WSqO4G7kNeQ=="], diff --git a/frontend/index.html b/frontend/index.html index 0c91d52..172e908 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -14,7 +14,7 @@ -
+
diff --git a/frontend/package.json b/frontend/package.json index 4440675..22621c6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -38,7 +38,9 @@ "cmdk": "^1.1.1", "date-fns": "^4.1.0", "embla-carousel-react": "^8.6.0", + "fuse.js": "^7.1.0", "lucide-react": "^0.563.0", + "motion": "^12.23.24", "react": "^19.2.4", "react-day-picker": "^9.13.1", "react-dom": "^19.2.4", diff --git a/frontend/src/components/layout/Navbar.tsx b/frontend/src/components/layout/Header.tsx similarity index 98% rename from frontend/src/components/layout/Navbar.tsx rename to frontend/src/components/layout/Header.tsx index d3fc326..1c4e297 100644 --- a/frontend/src/components/layout/Navbar.tsx +++ b/frontend/src/components/layout/Header.tsx @@ -22,6 +22,7 @@ import { type HTMLAttributes, type ImgHTMLAttributes, type ReactElement, + type SVGAttributes, useCallback, type ReactNode } from "react"; @@ -87,7 +88,7 @@ const Logo = (props: ImgHTMLAttributes) => { ); }; -const HamburgerIcon = ({ className, ...props }: React.SVGAttributes) => ( +const HamburgerIcon = ({ className, ...props }: SVGAttributes) => ( - onItemClick?.("dashboard")}> + onItemClick?.("/dashboard")}> View all notifications @@ -286,7 +287,7 @@ const NavbarComponent = forwardRef( )} {...props} > -
+
+ ); } diff --git a/frontend/src/components/pages/orders/OrderBadge.tsx b/frontend/src/components/pages/orders/OrderBadge.tsx new file mode 100644 index 0000000..366dbd6 --- /dev/null +++ b/frontend/src/components/pages/orders/OrderBadge.tsx @@ -0,0 +1,27 @@ +import type { ReactElement } from "react"; + +import { Badge } from "@/components/ui/badge"; + +import { OrderStatus } from "./orders"; + +export default function OrderBadge (props: { status: OrderStatus }): ReactElement<{ status: OrderStatus }> | null { + const color = props.status === OrderStatus.Denied + ? "bg-red-500" + : props.status === OrderStatus.Pending + ? "bg-gray-500" + : props.status === OrderStatus.Delivered + ? "bg-green-500" + : "bg-amber-500"; + + return ( + + { + props.status === OrderStatus.Denied + ? "Denied" + : props.status === OrderStatus.Approved + ? "Approved" + : "Pending" + } + + ); +} diff --git a/frontend/src/components/pages/orders/OrderCard.tsx b/frontend/src/components/pages/orders/OrderCard.tsx new file mode 100644 index 0000000..3c01c78 --- /dev/null +++ b/frontend/src/components/pages/orders/OrderCard.tsx @@ -0,0 +1,77 @@ +import { useRef, type ReactElement } from "react"; +import { + Check, + CircleSmall, + Ellipsis, + Pencil, + X +} from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle +} from "@/components/ui/card"; +import { Separator } from "@/components/ui/separator"; +import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"; + +import OrderBadge from "./OrderBadge"; + +import { + OrderColors, + OrderStatus, + type Order, + OrderAction +} from "./orders"; + +export default function OrderEntry (props: { order: Order, admin: boolean, orderTask: (order: Order, action: OrderAction) => void }): ReactElement<{ order: Order, isAdmin: boolean, orderTask: (order: Order, action: OrderAction) => void }> | null { + const viewDetails = useRef(null); + return ( + (e.stopPropagation(), props.orderTask(props.order, OrderAction.View))}> + +
+ {props.order.id} + +
+ {props.order.reviews.map((x, i) => )} +
+
+
+ + +

Subsystem - {props.order.requester.subsystem}

+

Subtotal - ${props.order.price} ({props.order.length} {props.order.length === 1 ? "item" : "items"})

+ +

Requester - {props.order.requester.displayName}

+

Deadline - {new Date(props.order.deadline).toLocaleDateString()}

+
+ + + + + + View Details + + + + Edit + + {props.admin && ( + <> + + + Approve + + + + Deny + + + )} + +
+ ); +} diff --git a/frontend/src/components/pages/orders/OrderDetails.tsx b/frontend/src/components/pages/orders/OrderDetails.tsx new file mode 100644 index 0000000..a99e39d --- /dev/null +++ b/frontend/src/components/pages/orders/OrderDetails.tsx @@ -0,0 +1,11 @@ +import type { ReactElement } from "react"; + +import type { DetailedOrder } from "./orders"; + +export default function OrderDetails (props: { order: DetailedOrder | undefined }): ReactElement<{ order: DetailedOrder | undefined }> | null { + return props.order + ? ( +
+ ) + : null; +}; diff --git a/frontend/src/components/pages/orders/OrderForm.tsx b/frontend/src/components/pages/orders/OrderForm.tsx new file mode 100644 index 0000000..4b0d023 --- /dev/null +++ b/frontend/src/components/pages/orders/OrderForm.tsx @@ -0,0 +1,319 @@ +import { + AlertCircleIcon, + ChevronDownIcon, + ChevronLeftIcon, + ChevronRightIcon, + MinusIcon, + PlusIcon +} from "lucide-react"; +import { + Fragment, + useDeferredValue, + useState, + type Dispatch, + type ReactElement, + type SetStateAction +} from "react"; + +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator +} from "@/components/ui/breadcrumb"; +import { Button } from "@/components/ui/button"; +import { ButtonGroup, ButtonGroupText } from "@/components/ui/button-group"; +import { Calendar } from "@/components/ui/calendar"; +import { + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { InputGroup, InputGroupInput } from "@/components/ui/input-group"; +import { Label } from "@/components/ui/label"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue +} from "@/components/ui/select"; +import { + Pagination, + PaginationContent, + PaginationItem, + PaginationLink +} from "@/components/ui/pagination"; +import { Separator } from "@/components/ui/separator"; +import { Textarea } from "@/components/ui/textarea"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; + +import type { DetailedOrder } from "./orders"; + +import { config } from "../../../../../shared/config/config"; +import type { Subsystem } from "../../../../../shared/config/enums"; + +interface OrderFormProps { + deadlineDate: Date | undefined + deadlineOpen: boolean + order: DetailedOrder | undefined + setDeadlineDate: Dispatch> + setDeadlineOpen: Dispatch> +} + +enum OrderFormState { + BasicInformation, + Parts, + Notes +} + +const defaultPart = { + name: "", + number: "", + supplier: "", + url: "", + quantity: -1, + price: -1 +}; + +export default function OrderForm (props: OrderFormProps): ReactElement | null { + const { deadlineDate, deadlineOpen, order, setDeadlineDate, setDeadlineOpen } = props; + + const [requester, setRequester] = useState(order?.requester.displayName ?? ""); + const [subsystem, setSubsystem] = useState(order?.requester.subsystem); + const [supplier, setSupplier] = useState(order?.supplier ?? ""); + const [parts, setParts] = useState( + order?.parts + ? Array.from(order.parts) + : [defaultPart] + ); + const [deadline, setDeadline] = useState(order?.deadline ?? ""); + const [notes, setNotes] = useState(order?.notes ?? ""); + + const [tab, setTab] = useState(OrderFormState.BasicInformation); + const [partIndex, setPartIndex] = useState(0); + + const totalPrice = useDeferredValue(parts.length > 0 ? parts.map(x => x.price * x.quantity).reduce((a, b) => a + b) : 0); + + return ( +
+ + + Order Request Form + + Request parts for your subsystem. + + + + + + There was an error processing your order request. + +

Ricky, you've been playing near the server again!

+
+
+ + + + + + + + + + + {tab === OrderFormState.BasicInformation && ( +
+
+ + setRequester(e.target.value)} required /> +
+
+ + +
+
+ + + + + + + { + setDeadlineDate(date); + setDeadlineOpen(false); + }} + required + /> + + +
+
+ + setSupplier(e.target.value)} required /> +
+
+ + + + + + + + + +
+
+ )} + {tab === OrderFormState.Parts && ( +
+
+ + +
+
+ + +
+
+ + + + + + + + + +
+
+ + +
+
+ + + + + + + + + +
+
+ + + + + + + + + +
+
+ )} + {tab === OrderFormState.Notes && ( +
+
+ +