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
run: pnpm lint
- name: Typecheck
run: pnpm typecheck
- name: Test
run: pnpm test

View File

@@ -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<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() {
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<any>(null);
const [pendingUpdate, setPendingUpdate] = useState<UpdateOrderVariables | null>(null);
const [originalRequested, setOriginalRequested] = useState(0);
const { data: orderData, isLoading: isOrderLoading } = useGetOrderById(
@@ -86,24 +125,23 @@ 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 client is reducing headcount and vendor has already assigned staff
if (!isVendor && totalRequested < originalRequested && assignedCount > totalRequested) {
setPendingUpdate({ ...eventData, requested: totalRequested });
setShowReductionAlert(true);
if (!eventId) return;
const teamHubId = getTeamHubId(eventData) ?? getTeamHubId(event);
if (!teamHubId) {
toast({
title: "⚠️ Headcount Reduced",
description: "Assigned staff exceeds new headcount. Please resolve assignments.",
title: "Missing Team Hub",
description: "Cannot update order because no team hub is associated.",
variant: "destructive",
});
return;
}
if (eventId) {
// Normal update
updateOrderMutation.mutate({
const updatePayload: UpdateOrderVariables = {
id: eventId,
teamHubId,
eventName: eventData.event_name,
date: eventData.date,
startDate: eventData.startDate || eventData.date,
@@ -113,40 +151,61 @@ export default function EditOrder() {
requested: totalRequested,
total: eventData.total,
poReference: eventData.po_reference,
} as any);
};
// If client is reducing headcount and vendor has already assigned staff
if (!isVendor && totalRequested < originalRequested && assignedCount > totalRequested) {
setPendingUpdate(updatePayload);
setShowReductionAlert(true);
toast({
title: "⚠️ Headcount Reduced",
description: "Assigned staff exceeds new headcount. Please resolve assignments.",
});
return;
}
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() {
<div className="mb-6">
<OrderReductionAlert
originalRequested={originalRequested}
newRequested={pendingUpdate.requested}
newRequested={pendingUpdate.requested ?? 0}
currentAssigned={Array.isArray(event.assignedStaff) ? event.assignedStaff.length : 0}
onAutoUnassign={handleAutoUnassign}
onManualUnassign={handleManualUnassign}
lowReliabilityStaff={(Array.isArray(event.assignedStaff) ? event.assignedStaff : []).map((assigned: any) => {
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() {
)}
<EventFormWizard
event={event as any}
event={event}
onSubmit={handleSubmit}
isSubmitting={updateOrderMutation.isPending}
currentUser={user as any}
currentUser={user}
onCancel={() => navigate(createPageUrl("Events"))}
/>
</div>

View File

@@ -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<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) => {
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;

View File

@@ -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<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() {
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() {
</h2>
</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]">
<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>