diff --git a/apps/web/src/features/operations/orders/OrderDetail.tsx b/apps/web/src/features/operations/orders/OrderDetail.tsx index 9a9db41a..8c3c0388 100644 --- a/apps/web/src/features/operations/orders/OrderDetail.tsx +++ b/apps/web/src/features/operations/orders/OrderDetail.tsx @@ -1,9 +1,451 @@ -import React from 'react' +import React, { useMemo } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { format } from "date-fns"; +import { useSelector } from "react-redux"; +import { Calendar, MapPin, Users, DollarSign, Edit3, X, Copy, Clock } from "lucide-react"; + +import { Card, CardContent, CardHeader, CardTitle } from "@/common/components/ui/card"; +import { Button } from "@/common/components/ui/button"; +import { Badge } from "@/common/components/ui/badge"; +import DashboardLayout from "@/features/layouts/DashboardLayout"; +import { useGetOrderById, useUpdateOrder } from "@/dataconnect-generated/react"; +import { OrderStatus } from "@/dataconnect-generated"; +import { dataConnect } from "@/features/auth/firebase"; +import { useToast } from "@/common/components/ui/use-toast"; +import type { RootState } from "@/store/store"; + +const safeFormatDate = (value?: string | null): string => { + if (!value) return "—"; + try { + const d = new Date(value); + if (Number.isNaN(d.getTime())) return "—"; + return format(d, "MMM d, yyyy"); + } catch { + return "—"; + } +}; + +const safeFormatDateTime = (value?: string | null): string => { + if (!value) return "—"; + try { + const d = new Date(value); + if (Number.isNaN(d.getTime())) return "—"; + return format(d, "MMM d, yyyy • h:mm a"); + } catch { + return "—"; + } +}; + +const getStatusBadge = (status: OrderStatus) => { + switch (status) { + case OrderStatus.FULLY_STAFFED: + case OrderStatus.FILLED: + return ( + + Fully Staffed + + ); + case OrderStatus.PARTIAL_STAFFED: + return ( + + Partial Staffed + + ); + case OrderStatus.PENDING: + case OrderStatus.POSTED: + return ( + + {status} + + ); + case OrderStatus.CANCELLED: + return ( + + Cancelled + + ); + case OrderStatus.COMPLETED: + return ( + + Completed + + ); + case OrderStatus.DRAFT: + default: + return ( + + {status} + + ); + } +}; + +export default function OrderDetail() { + const navigate = useNavigate(); + const { id } = useParams<{ id: string }>(); + const { toast } = useToast(); + const { user } = useSelector((state: RootState) => state.auth); + + const { + data, + isLoading, + } = useGetOrderById( + dataConnect, + { id: id || "" }, + { + enabled: !!id, + }, + ); + + const order = data?.order; + + const cancelMutation = useUpdateOrder(dataConnect, { + onSuccess: () => { + toast({ + title: "Order cancelled", + description: "The order status has been updated to Cancelled.", + }); + }, + onError: () => { + toast({ + title: "Failed to cancel order", + description: "Please try again or contact support.", + variant: "destructive", + }); + }, + }); + + const canModify = useMemo(() => { + if (!order) return false; + const status = order.status as OrderStatus; + return status !== OrderStatus.CANCELLED && status !== OrderStatus.COMPLETED; + }, [order]); + + const handleCancel = () => { + if (!order || !id || !canModify) return; + cancelMutation.mutate({ + id, + status: OrderStatus.CANCELLED, + }); + }; + + const handleEdit = () => { + if (!order || !id) return; + // Placeholder: route can later be wired to an edit form + navigate(`/orders/create?edit=${id}`); + }; + + const handleDuplicate = () => { + if (!order || !id) return; + // Placeholder: route can later pre-fill a new order from this one + navigate(`/orders/create?duplicate=${id}`); + }; + + const shifts: any[] = Array.isArray(order?.shifts) ? (order!.shifts as any[]) : []; + + const totalRequested = order?.requested ?? 0; + const totalAssigned = Array.isArray(order?.assignedStaff) ? order!.assignedStaff.length : 0; + + if (isLoading) { + return ( + +
+
+
+ + ); + } + + if (!order) { + return ( + +
+

This order may have been deleted or the link is invalid.

+ +
+
+ ); + } + + const isClient = user?.userRole === "client"; + + const clientName = order.business?.businessName || "Unknown client"; + const eventDateLabel = safeFormatDate(order.date as string | null); + const locationLabel = order.business?.businessName || "—"; + + const timelineItems = [ + { + label: "Order created", + value: safeFormatDateTime(order.createdAt as string | null), + }, + { + label: "Event date", + value: eventDateLabel, + }, + { + label: "Current status", + value: (order.status as string) || "—", + }, + ]; -const OrderDetail = () => { return ( -
OrderDetail
- ) + + {getStatusBadge(order.status as OrderStatus)} + + + +
+ } + > +
+ {/* Header / Key Info */} + + + Order Overview + + +
+
+
+ +
+
+

+ Client Name +

+

{clientName}

+
+
+ +
+
+ +
+
+

+ Event Date +

+

{eventDateLabel}

+
+
+ +
+
+ +
+
+

+ Location +

+

{locationLabel}

+
+
+ +
+
+ +
+
+

+ Staffed / Requested +

+

+ {totalAssigned} / {totalRequested} +

+
+
+
+
+
+ + {/* Shifts Section */} + + + Shifts + + + {shifts.length === 0 ? ( +
+ +

No shifts defined for this order.

+

+ Add shifts when creating or editing the order to see them here. +

+
+ ) : ( +
+ {shifts.map((shift: any, index: number) => { + const start = safeFormatDateTime(shift.startTime || shift.start || shift.date); + const end = safeFormatDateTime(shift.endTime || shift.end); + const title = shift.title || shift.positionName || shift.roleName || `Shift #${index + 1}`; + const workersNeeded = shift.workersNeeded ?? shift.requested ?? 0; + const filled = + typeof shift.filled === "number" + ? shift.filled + : Array.isArray(shift.assignedStaff) + ? shift.assignedStaff.length + : 0; + const vacancies = Math.max(workersNeeded - filled, 0); + + return ( +
+
+

{title}

+
+ + + {start} {end !== "—" && `→ ${end}`} + +
+
+ +
+
+ + Required + + {workersNeeded || "—"} +
+
+ + Assigned + + {filled} +
+
+ + Vacancies + + {vacancies} +
+
+
+ ); + })} +
+ )} +
+
+ + {/* Timeline Section */} + + + + Order Status Timeline + + + +
+ {timelineItems.map((item, idx) => ( +
+
+

+ {item.label} +

+

{item.value}

+
+ ))} +
+ + + + {/* Financial Summary (optional helper, derived from existing fields) */} + + + Summary + + +
+
+ +
+
+

+ Estimated Total +

+

+ {typeof order.total === "number" ? `$${order.total.toLocaleString()}` : "—"} +

+
+
+ +
+
+ +
+
+

+ Total Positions +

+

{totalRequested}

+
+
+ +
+
+ +
+
+

+ Order Type +

+

+ {(order.orderType as string)?.replace("_", " ") || "—"} +

+
+
+
+
+
+ + ); } -export default OrderDetail \ No newline at end of file +const FileTextIcon: React.FC = () => ( + +);