diff --git a/.github/workflows/web-quality.yml b/.github/workflows/web-quality.yml index 69e5eddd..62b67d28 100644 --- a/.github/workflows/web-quality.yml +++ b/.github/workflows/web-quality.yml @@ -39,6 +39,9 @@ jobs: - name: Lint run: pnpm lint + - name: Typecheck + run: pnpm typecheck + - name: Test run: pnpm test diff --git a/apps/web/src/features/operations/orders/EditOrder.tsx b/apps/web/src/features/operations/orders/EditOrder.tsx index 3ff199af..cb4ff7bb 100644 --- a/apps/web/src/features/operations/orders/EditOrder.tsx +++ b/apps/web/src/features/operations/orders/EditOrder.tsx @@ -10,9 +10,48 @@ import OrderReductionAlert from "./components/OrderReductionAlert"; import EventFormWizard from "./components/EventFormWizard"; import { useToast } from "@/common/components/ui/use-toast"; import { useGetOrderById, useUpdateOrder, useListStaff } from "@/dataconnect-generated/react"; +import type { UpdateOrderVariables } from "@/dataconnect-generated"; import { dataConnect } from "@/features/auth/firebase"; import type { RootState } from "@/store/store"; +const asRecord = (value: unknown): Record | null => { + if (typeof value === "object" && value !== null) { + return value as Record; + } + return null; +}; + +const getTeamHubId = (value: unknown): string | null => { + const record = asRecord(value); + if (!record) return null; + + const directValue = record.teamHubId; + if (typeof directValue === "string" && directValue.length > 0) { + return directValue; + } + + const teamHub = asRecord(record.teamHub); + const nestedValue = teamHub?.id; + if (typeof nestedValue === "string" && nestedValue.length > 0) { + return nestedValue; + } + + return null; +}; + +const getStringField = (value: unknown, keys: string[]): string | null => { + const record = asRecord(value); + if (!record) return null; + + for (const key of keys) { + const fieldValue = record[key]; + if (typeof fieldValue === "string" && fieldValue.length > 0) { + return fieldValue; + } + } + + return null; +}; export default function EditOrder() { const navigate = useNavigate(); @@ -22,7 +61,7 @@ export default function EditOrder() { const { user } = useSelector((state: RootState) => state.auth); const [showReductionAlert, setShowReductionAlert] = useState(false); - const [pendingUpdate, setPendingUpdate] = useState(null); + const [pendingUpdate, setPendingUpdate] = useState(null); const [originalRequested, setOriginalRequested] = useState(0); const { data: orderData, isLoading: isOrderLoading } = useGetOrderById( @@ -86,11 +125,37 @@ export default function EditOrder() { const assignedStaff = Array.isArray(event?.assignedStaff) ? event!.assignedStaff : []; const assignedCount = assignedStaff.length; - const isVendor = user?.userRole === 'vendor' || (user as any)?.role === 'vendor'; + const isVendor = user?.userRole?.toLowerCase() === 'vendor'; + + if (!eventId) return; + + const teamHubId = getTeamHubId(eventData) ?? getTeamHubId(event); + if (!teamHubId) { + toast({ + title: "Missing Team Hub", + description: "Cannot update order because no team hub is associated.", + variant: "destructive", + }); + return; + } + + const updatePayload: UpdateOrderVariables = { + id: eventId, + teamHubId, + eventName: eventData.event_name, + date: eventData.date, + startDate: eventData.startDate || eventData.date, + endDate: eventData.endDate, + notes: eventData.notes, + shifts: eventData.shifts, + requested: totalRequested, + total: eventData.total, + poReference: eventData.po_reference, + }; // If client is reducing headcount and vendor has already assigned staff if (!isVendor && totalRequested < originalRequested && assignedCount > totalRequested) { - setPendingUpdate({ ...eventData, requested: totalRequested }); + setPendingUpdate(updatePayload); setShowReductionAlert(true); toast({ @@ -100,53 +165,47 @@ export default function EditOrder() { return; } - if (eventId) { - // Normal update - updateOrderMutation.mutate({ - id: eventId, - eventName: eventData.event_name, - date: eventData.date, - startDate: eventData.startDate || eventData.date, - endDate: eventData.endDate, - notes: eventData.notes, - shifts: eventData.shifts, - requested: totalRequested, - total: eventData.total, - poReference: eventData.po_reference, - } as any); - } + updateOrderMutation.mutate(updatePayload); }; const handleAutoUnassign = async () => { if (!pendingUpdate || !event || !eventId) return; - const assignedStaff = Array.isArray(event.assignedStaff) ? (event.assignedStaff as any[]) : []; - const excessCount = assignedStaff.length - pendingUpdate.requested; + const assignedStaff = Array.isArray(event.assignedStaff) ? event.assignedStaff : []; + const requestedCount = pendingUpdate.requested ?? 0; + const excessCount = Math.max(0, assignedStaff.length - requestedCount); // Calculate reliability scores for assigned staff - const staffWithScores = assignedStaff.map(assigned => { - const staffInfo = allStaff.find(s => s.id === assigned.staff_id || s.id === assigned.staffId); + const staffWithScores = assignedStaff.map((assigned) => { + const staffId = getStringField(assigned, ["staff_id", "staffId"]); + const staffName = getStringField(assigned, ["staffName", "staff_name"]); + const role = getStringField(assigned, ["role"]) ?? ""; + const staffInfo = staffId ? allStaff.find((s) => s.id === staffId) : undefined; return { - ...assigned, + staffId, + staffName, + role, reliability: staffInfo?.averageRating ? staffInfo.averageRating * 20 : 50, // Convert 0-5 to 0-100 }; }); // Sort by reliability (lowest first) - staffWithScores.sort((a: any, b: any) => a.reliability - b.reliability); + staffWithScores.sort((a, b) => a.reliability - b.reliability); // Remove lowest reliability staff const staffToKeep = staffWithScores.slice(excessCount); + const assignedStaffPayload = staffToKeep + .filter((s) => typeof s.staffId === "string" && s.staffId.length > 0) + .map((s) => ({ + staffId: s.staffId as string, + staffName: s.staffName ?? "Unknown", + role: s.role, + })); await updateOrderMutation.mutateAsync({ - id: eventId, ...pendingUpdate, - assignedStaff: staffToKeep.map((s: any) => ({ - staffId: s.staffId || s.staff_id, - staffName: s.staffName || s.staff_name, - role: s.role - })) - } as any); + assignedStaff: assignedStaffPayload, + }); setShowReductionAlert(false); setPendingUpdate(null); @@ -203,14 +262,16 @@ export default function EditOrder() {
{ - const staffInfo = allStaff.find(s => s.id === assigned.staffId || s.id === assigned.staff_id); + lowReliabilityStaff={(Array.isArray(event.assignedStaff) ? event.assignedStaff : []).map((assigned) => { + const staffId = getStringField(assigned, ["staffId", "staff_id"]); + const staffName = getStringField(assigned, ["staffName", "staff_name"]) ?? "Unknown"; + const staffInfo = staffId ? allStaff.find((s) => s.id === staffId) : undefined; return { - name: assigned.staffName || assigned.staff_name, + name: staffName, reliability: staffInfo?.averageRating ? staffInfo.averageRating * 20 : 50 }; }).sort((a, b) => a.reliability - b.reliability)} @@ -219,10 +280,10 @@ export default function EditOrder() { )} navigate(createPageUrl("Events"))} />
diff --git a/apps/web/src/features/operations/orders/OrderDetail.tsx b/apps/web/src/features/operations/orders/OrderDetail.tsx index 899d2311..b5251c74 100644 --- a/apps/web/src/features/operations/orders/OrderDetail.tsx +++ b/apps/web/src/features/operations/orders/OrderDetail.tsx @@ -9,7 +9,7 @@ import { Button } from "@/common/components/ui/button"; import { Badge } from "@/common/components/ui/badge"; import DashboardLayout from "@/features/layouts/DashboardLayout"; import { useGetOrderById, useUpdateOrder, useListShiftRolesByBusinessAndOrder } from "@/dataconnect-generated/react"; -import { OrderStatus } from "@/dataconnect-generated"; +import { OrderStatus, type UpdateOrderVariables } from "@/dataconnect-generated"; import { dataConnect } from "@/features/auth/firebase"; import { useToast } from "@/common/components/ui/use-toast"; import AssignStaffModal from "./components/AssignStaffModal"; @@ -37,6 +37,31 @@ const safeFormatDateTime = (value?: string | null): string => { } }; +const resolveTeamHubId = (value: unknown): string | null => { + if (typeof value !== "object" || value === null) { + return null; + } + + const record = value as Record; + const directValue = record.teamHubId; + if (typeof directValue === "string" && directValue.length > 0) { + return directValue; + } + + const teamHub = record.teamHub; + if (typeof teamHub !== "object" || teamHub === null) { + return null; + } + + const teamHubRecord = teamHub as Record; + const nestedValue = teamHubRecord.id; + if (typeof nestedValue === "string" && nestedValue.length > 0) { + return nestedValue; + } + + return null; +}; + const getStatusBadge = (status: OrderStatus) => { switch (status) { case OrderStatus.FULLY_STAFFED: @@ -141,10 +166,22 @@ export default function OrderDetail() { const handleCancel = () => { if (!order || !id || !canModify) return; - cancelMutation.mutate({ + const teamHubId = resolveTeamHubId(order); + if (!teamHubId) { + toast({ + title: "Missing Team Hub", + description: "Cannot cancel this order because team hub information is missing.", + variant: "destructive", + }); + return; + } + + const payload: UpdateOrderVariables = { id, + teamHubId, status: OrderStatus.CANCELLED, - } as any); + }; + cancelMutation.mutate(payload); }; const handleEdit = () => { @@ -162,7 +199,7 @@ export default function OrderDetail() { if (shiftRolesData?.shiftRoles && shiftRolesData.shiftRoles.length > 0) { return shiftRolesData.shiftRoles; } - return Array.isArray(order?.shifts) ? (order!.shifts as any[]) : []; + return Array.isArray(order?.shifts) ? order.shifts : []; }, [shiftRolesData, order?.shifts]); const totalRequested = order?.requested ?? 0; diff --git a/apps/web/src/features/operations/schedule/Schedule.tsx b/apps/web/src/features/operations/schedule/Schedule.tsx index 8af747d4..dffd5c20 100644 --- a/apps/web/src/features/operations/schedule/Schedule.tsx +++ b/apps/web/src/features/operations/schedule/Schedule.tsx @@ -46,7 +46,7 @@ import { Tabs, TabsList, TabsTrigger } from "@/common/components/ui/tabs"; import DashboardLayout from "@/features/layouts/DashboardLayout"; import { useListOrders, useUpdateOrder } from "@/dataconnect-generated/react"; import { dataConnect } from "@/features/auth/firebase"; -import { OrderStatus } from "@/dataconnect-generated"; +import { OrderStatus, type UpdateOrderVariables } from "@/dataconnect-generated"; import { useToast } from "@/common/components/ui/use-toast"; /** @@ -83,6 +83,35 @@ const safeParseDate = (dateString: any) => { } }; +const isViewMode = (value: string): value is "day" | "week" | "month" => { + return value === "day" || value === "week" || value === "month"; +}; + +const resolveTeamHubId = (value: unknown): string | null => { + if (typeof value !== "object" || value === null) { + return null; + } + + const record = value as Record; + const directValue = record.teamHubId; + if (typeof directValue === "string" && directValue.length > 0) { + return directValue; + } + + const teamHub = record.teamHub; + if (typeof teamHub !== "object" || teamHub === null) { + return null; + } + + const teamHubRecord = teamHub as Record; + const nestedValue = teamHubRecord.id; + if (typeof nestedValue === "string" && nestedValue.length > 0) { + return nestedValue; + } + + return null; +}; + export default function Schedule() { const navigate = useNavigate(); const { toast } = useToast(); @@ -204,10 +233,22 @@ export default function Schedule() { const confirmReschedule = () => { if (rescheduleData) { - updateOrderMutation.mutate({ + const teamHubId = resolveTeamHubId(rescheduleData.order); + if (!teamHubId) { + toast({ + title: "Missing Team Hub", + description: "Cannot reschedule this order because team hub information is missing.", + variant: "destructive", + }); + return; + } + + const payload: UpdateOrderVariables = { id: rescheduleData.order.id, - date: rescheduleData.newDate.toISOString() - } as any); + teamHubId, + date: rescheduleData.newDate.toISOString(), + }; + updateOrderMutation.mutate(payload); } }; @@ -287,7 +328,15 @@ export default function Schedule() { - setViewMode(v as any)} className="w-full md:w-auto"> + { + if (isViewMode(value)) { + setViewMode(value); + } + }} + className="w-full md:w-auto" + > Day Week