Add web typecheck gate and remove order update any-casts

This commit is contained in:
zouantchaw
2026-02-13 10:42:47 -05:00
parent 38c2699fa2
commit 1269f7228e
4 changed files with 196 additions and 46 deletions

View File

@@ -39,6 +39,9 @@ jobs:
- name: Lint - name: Lint
run: pnpm lint run: pnpm lint
- name: Typecheck
run: pnpm typecheck
- name: Test - name: Test
run: pnpm test run: pnpm test

View File

@@ -10,9 +10,48 @@ import OrderReductionAlert from "./components/OrderReductionAlert";
import EventFormWizard from "./components/EventFormWizard"; import EventFormWizard from "./components/EventFormWizard";
import { useToast } from "@/common/components/ui/use-toast"; import { useToast } from "@/common/components/ui/use-toast";
import { useGetOrderById, useUpdateOrder, useListStaff } from "@/dataconnect-generated/react"; import { useGetOrderById, useUpdateOrder, useListStaff } from "@/dataconnect-generated/react";
import type { UpdateOrderVariables } from "@/dataconnect-generated";
import { dataConnect } from "@/features/auth/firebase"; import { dataConnect } from "@/features/auth/firebase";
import type { RootState } from "@/store/store"; import type { RootState } from "@/store/store";
const asRecord = (value: unknown): Record<string, unknown> | null => {
if (typeof value === "object" && value !== null) {
return value as Record<string, unknown>;
}
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() { export default function EditOrder() {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -22,7 +61,7 @@ export default function EditOrder() {
const { user } = useSelector((state: RootState) => state.auth); const { user } = useSelector((state: RootState) => state.auth);
const [showReductionAlert, setShowReductionAlert] = useState(false); const [showReductionAlert, setShowReductionAlert] = useState(false);
const [pendingUpdate, setPendingUpdate] = useState<any>(null); const [pendingUpdate, setPendingUpdate] = useState<UpdateOrderVariables | null>(null);
const [originalRequested, setOriginalRequested] = useState(0); const [originalRequested, setOriginalRequested] = useState(0);
const { data: orderData, isLoading: isOrderLoading } = useGetOrderById( const { data: orderData, isLoading: isOrderLoading } = useGetOrderById(
@@ -86,11 +125,37 @@ export default function EditOrder() {
const assignedStaff = Array.isArray(event?.assignedStaff) ? event!.assignedStaff : []; const assignedStaff = Array.isArray(event?.assignedStaff) ? event!.assignedStaff : [];
const assignedCount = assignedStaff.length; 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 client is reducing headcount and vendor has already assigned staff
if (!isVendor && totalRequested < originalRequested && assignedCount > totalRequested) { if (!isVendor && totalRequested < originalRequested && assignedCount > totalRequested) {
setPendingUpdate({ ...eventData, requested: totalRequested }); setPendingUpdate(updatePayload);
setShowReductionAlert(true); setShowReductionAlert(true);
toast({ toast({
@@ -100,53 +165,47 @@ export default function EditOrder() {
return; return;
} }
if (eventId) { updateOrderMutation.mutate(updatePayload);
// 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);
}
}; };
const handleAutoUnassign = async () => { const handleAutoUnassign = async () => {
if (!pendingUpdate || !event || !eventId) return; if (!pendingUpdate || !event || !eventId) return;
const assignedStaff = Array.isArray(event.assignedStaff) ? (event.assignedStaff as any[]) : []; const assignedStaff = Array.isArray(event.assignedStaff) ? event.assignedStaff : [];
const excessCount = assignedStaff.length - pendingUpdate.requested; const requestedCount = pendingUpdate.requested ?? 0;
const excessCount = Math.max(0, assignedStaff.length - requestedCount);
// Calculate reliability scores for assigned staff // Calculate reliability scores for assigned staff
const staffWithScores = assignedStaff.map(assigned => { const staffWithScores = assignedStaff.map((assigned) => {
const staffInfo = allStaff.find(s => s.id === assigned.staff_id || s.id === assigned.staffId); 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 { return {
...assigned, staffId,
staffName,
role,
reliability: staffInfo?.averageRating ? staffInfo.averageRating * 20 : 50, // Convert 0-5 to 0-100 reliability: staffInfo?.averageRating ? staffInfo.averageRating * 20 : 50, // Convert 0-5 to 0-100
}; };
}); });
// Sort by reliability (lowest first) // 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 // Remove lowest reliability staff
const staffToKeep = staffWithScores.slice(excessCount); 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({ await updateOrderMutation.mutateAsync({
id: eventId,
...pendingUpdate, ...pendingUpdate,
assignedStaff: staffToKeep.map((s: any) => ({ assignedStaff: assignedStaffPayload,
staffId: s.staffId || s.staff_id, });
staffName: s.staffName || s.staff_name,
role: s.role
}))
} as any);
setShowReductionAlert(false); setShowReductionAlert(false);
setPendingUpdate(null); setPendingUpdate(null);
@@ -203,14 +262,16 @@ export default function EditOrder() {
<div className="mb-6"> <div className="mb-6">
<OrderReductionAlert <OrderReductionAlert
originalRequested={originalRequested} originalRequested={originalRequested}
newRequested={pendingUpdate.requested} newRequested={pendingUpdate.requested ?? 0}
currentAssigned={Array.isArray(event.assignedStaff) ? event.assignedStaff.length : 0} currentAssigned={Array.isArray(event.assignedStaff) ? event.assignedStaff.length : 0}
onAutoUnassign={handleAutoUnassign} onAutoUnassign={handleAutoUnassign}
onManualUnassign={handleManualUnassign} onManualUnassign={handleManualUnassign}
lowReliabilityStaff={(Array.isArray(event.assignedStaff) ? event.assignedStaff : []).map((assigned: any) => { lowReliabilityStaff={(Array.isArray(event.assignedStaff) ? event.assignedStaff : []).map((assigned) => {
const staffInfo = allStaff.find(s => s.id === assigned.staffId || s.id === assigned.staff_id); 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 { return {
name: assigned.staffName || assigned.staff_name, name: staffName,
reliability: staffInfo?.averageRating ? staffInfo.averageRating * 20 : 50 reliability: staffInfo?.averageRating ? staffInfo.averageRating * 20 : 50
}; };
}).sort((a, b) => a.reliability - b.reliability)} }).sort((a, b) => a.reliability - b.reliability)}
@@ -219,10 +280,10 @@ export default function EditOrder() {
)} )}
<EventFormWizard <EventFormWizard
event={event as any} event={event}
onSubmit={handleSubmit} onSubmit={handleSubmit}
isSubmitting={updateOrderMutation.isPending} isSubmitting={updateOrderMutation.isPending}
currentUser={user as any} currentUser={user}
onCancel={() => navigate(createPageUrl("Events"))} onCancel={() => navigate(createPageUrl("Events"))}
/> />
</div> </div>

View File

@@ -9,7 +9,7 @@ import { Button } from "@/common/components/ui/button";
import { Badge } from "@/common/components/ui/badge"; import { Badge } from "@/common/components/ui/badge";
import DashboardLayout from "@/features/layouts/DashboardLayout"; import DashboardLayout from "@/features/layouts/DashboardLayout";
import { useGetOrderById, useUpdateOrder, useListShiftRolesByBusinessAndOrder } from "@/dataconnect-generated/react"; 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 { dataConnect } from "@/features/auth/firebase";
import { useToast } from "@/common/components/ui/use-toast"; import { useToast } from "@/common/components/ui/use-toast";
import AssignStaffModal from "./components/AssignStaffModal"; 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<string, unknown>;
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<string, unknown>;
const nestedValue = teamHubRecord.id;
if (typeof nestedValue === "string" && nestedValue.length > 0) {
return nestedValue;
}
return null;
};
const getStatusBadge = (status: OrderStatus) => { const getStatusBadge = (status: OrderStatus) => {
switch (status) { switch (status) {
case OrderStatus.FULLY_STAFFED: case OrderStatus.FULLY_STAFFED:
@@ -141,10 +166,22 @@ export default function OrderDetail() {
const handleCancel = () => { const handleCancel = () => {
if (!order || !id || !canModify) return; 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, id,
teamHubId,
status: OrderStatus.CANCELLED, status: OrderStatus.CANCELLED,
} as any); };
cancelMutation.mutate(payload);
}; };
const handleEdit = () => { const handleEdit = () => {
@@ -162,7 +199,7 @@ export default function OrderDetail() {
if (shiftRolesData?.shiftRoles && shiftRolesData.shiftRoles.length > 0) { if (shiftRolesData?.shiftRoles && shiftRolesData.shiftRoles.length > 0) {
return shiftRolesData.shiftRoles; return shiftRolesData.shiftRoles;
} }
return Array.isArray(order?.shifts) ? (order!.shifts as any[]) : []; return Array.isArray(order?.shifts) ? order.shifts : [];
}, [shiftRolesData, order?.shifts]); }, [shiftRolesData, order?.shifts]);
const totalRequested = order?.requested ?? 0; const totalRequested = order?.requested ?? 0;

View File

@@ -46,7 +46,7 @@ import { Tabs, TabsList, TabsTrigger } from "@/common/components/ui/tabs";
import DashboardLayout from "@/features/layouts/DashboardLayout"; import DashboardLayout from "@/features/layouts/DashboardLayout";
import { useListOrders, useUpdateOrder } from "@/dataconnect-generated/react"; import { useListOrders, useUpdateOrder } from "@/dataconnect-generated/react";
import { dataConnect } from "@/features/auth/firebase"; 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"; 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<string, unknown>;
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<string, unknown>;
const nestedValue = teamHubRecord.id;
if (typeof nestedValue === "string" && nestedValue.length > 0) {
return nestedValue;
}
return null;
};
export default function Schedule() { export default function Schedule() {
const navigate = useNavigate(); const navigate = useNavigate();
const { toast } = useToast(); const { toast } = useToast();
@@ -204,10 +233,22 @@ export default function Schedule() {
const confirmReschedule = () => { const confirmReschedule = () => {
if (rescheduleData) { 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, id: rescheduleData.order.id,
date: rescheduleData.newDate.toISOString() teamHubId,
} as any); date: rescheduleData.newDate.toISOString(),
};
updateOrderMutation.mutate(payload);
} }
}; };
@@ -287,7 +328,15 @@ export default function Schedule() {
</h2> </h2>
</div> </div>
<Tabs value={viewMode} onValueChange={(v) => setViewMode(v as any)} className="w-full md:w-auto"> <Tabs
value={viewMode}
onValueChange={(value) => {
if (isViewMode(value)) {
setViewMode(value);
}
}}
className="w-full md:w-auto"
>
<TabsList className="grid grid-cols-3 w-full md:w-[240px]"> <TabsList className="grid grid-cols-3 w-full md:w-[240px]">
<TabsTrigger value="day" className="text-xs font-bold uppercase tracking-wider">Day</TabsTrigger> <TabsTrigger value="day" className="text-xs font-bold uppercase tracking-wider">Day</TabsTrigger>
<TabsTrigger value="week" className="text-xs font-bold uppercase tracking-wider">Week</TabsTrigger> <TabsTrigger value="week" className="text-xs font-bold uppercase tracking-wider">Week</TabsTrigger>