Add web typecheck gate and remove order update any-casts
This commit is contained in:
3
.github/workflows/web-quality.yml
vendored
3
.github/workflows/web-quality.yml
vendored
@@ -39,6 +39,9 @@ jobs:
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm typecheck
|
||||
|
||||
- name: Test
|
||||
run: pnpm test
|
||||
|
||||
|
||||
@@ -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,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() {
|
||||
<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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user