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
|
- name: Lint
|
||||||
run: pnpm lint
|
run: pnpm lint
|
||||||
|
|
||||||
|
- name: Typecheck
|
||||||
|
run: pnpm typecheck
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: pnpm test
|
run: pnpm test
|
||||||
|
|
||||||
|
|||||||
@@ -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,24 +125,23 @@ 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 client is reducing headcount and vendor has already assigned staff
|
if (!eventId) return;
|
||||||
if (!isVendor && totalRequested < originalRequested && assignedCount > totalRequested) {
|
|
||||||
setPendingUpdate({ ...eventData, requested: totalRequested });
|
|
||||||
setShowReductionAlert(true);
|
|
||||||
|
|
||||||
|
const teamHubId = getTeamHubId(eventData) ?? getTeamHubId(event);
|
||||||
|
if (!teamHubId) {
|
||||||
toast({
|
toast({
|
||||||
title: "⚠️ Headcount Reduced",
|
title: "Missing Team Hub",
|
||||||
description: "Assigned staff exceeds new headcount. Please resolve assignments.",
|
description: "Cannot update order because no team hub is associated.",
|
||||||
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (eventId) {
|
const updatePayload: UpdateOrderVariables = {
|
||||||
// Normal update
|
|
||||||
updateOrderMutation.mutate({
|
|
||||||
id: eventId,
|
id: eventId,
|
||||||
|
teamHubId,
|
||||||
eventName: eventData.event_name,
|
eventName: eventData.event_name,
|
||||||
date: eventData.date,
|
date: eventData.date,
|
||||||
startDate: eventData.startDate || eventData.date,
|
startDate: eventData.startDate || eventData.date,
|
||||||
@@ -113,40 +151,61 @@ export default function EditOrder() {
|
|||||||
requested: totalRequested,
|
requested: totalRequested,
|
||||||
total: eventData.total,
|
total: eventData.total,
|
||||||
poReference: eventData.po_reference,
|
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 () => {
|
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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user