feat: Implement Edit Orders
This commit is contained in:
231
apps/web/src/features/operations/orders/EditOrder.tsx
Normal file
231
apps/web/src/features/operations/orders/EditOrder.tsx
Normal file
@@ -0,0 +1,231 @@
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import { createPageUrl } from "@/lib/index";
|
||||
import { Button } from "@/common/components/ui/button";
|
||||
import { Loader2, AlertTriangle } from "lucide-react";
|
||||
import DashboardLayout from "@/features/layouts/DashboardLayout";
|
||||
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 { dataConnect } from "@/features/auth/firebase";
|
||||
import type { RootState } from "@/store/store";
|
||||
|
||||
|
||||
export default function EditOrder() {
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const { toast } = useToast();
|
||||
const { id: eventId } = useParams<{ id: string }>();
|
||||
const { user } = useSelector((state: RootState) => state.auth);
|
||||
|
||||
const [showReductionAlert, setShowReductionAlert] = useState(false);
|
||||
const [pendingUpdate, setPendingUpdate] = useState<any>(null);
|
||||
const [originalRequested, setOriginalRequested] = useState(0);
|
||||
|
||||
const { data: orderData, isLoading: isOrderLoading } = useGetOrderById(
|
||||
dataConnect,
|
||||
{ id: eventId || "" },
|
||||
{ enabled: !!eventId }
|
||||
);
|
||||
|
||||
const event = orderData?.order;
|
||||
|
||||
const { data: staffData } = useListStaff(dataConnect);
|
||||
const allStaff = staffData?.staffs || [];
|
||||
|
||||
useEffect(() => {
|
||||
if (event) {
|
||||
setOriginalRequested(event.requested || 0);
|
||||
}
|
||||
}, [event]);
|
||||
|
||||
const updateOrderMutation = useUpdateOrder(dataConnect, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["listOrders"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["getOrderById", { id: eventId }] });
|
||||
toast({
|
||||
title: "✅ Order Updated",
|
||||
description: "Your changes have been saved successfully.",
|
||||
});
|
||||
navigate(createPageUrl("Events"));
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: "❌ Update Failed",
|
||||
description: error.message || "Could not update the order. Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const canModify = useMemo(() => {
|
||||
if (!event) return false;
|
||||
if (!event.startDate) return true;
|
||||
const startTime = new Date(event.startDate);
|
||||
return startTime > new Date();
|
||||
}, [event]);
|
||||
|
||||
const handleSubmit = (eventData: any) => {
|
||||
if (!canModify) {
|
||||
toast({
|
||||
title: "Cannot Edit Order",
|
||||
description: "This order has already started and cannot be modified.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// CRITICAL: Recalculate requested count from current roles
|
||||
const totalRequested = eventData.shifts.reduce((sum: number, shift: any) => {
|
||||
const roles = Array.isArray(shift.roles) ? shift.roles : [];
|
||||
return sum + roles.reduce((roleSum: number, role: any) => roleSum + (parseInt(role.count) || 0), 0);
|
||||
}, 0);
|
||||
|
||||
const assignedStaff = Array.isArray(event?.assignedStaff) ? event!.assignedStaff : [];
|
||||
const assignedCount = assignedStaff.length;
|
||||
const isVendor = user?.userRole === 'vendor' || (user as any)?.role === 'vendor';
|
||||
|
||||
// If client is reducing headcount and vendor has already assigned staff
|
||||
if (!isVendor && totalRequested < originalRequested && assignedCount > totalRequested) {
|
||||
setPendingUpdate({ ...eventData, requested: totalRequested });
|
||||
setShowReductionAlert(true);
|
||||
|
||||
toast({
|
||||
title: "⚠️ Headcount Reduced",
|
||||
description: "Assigned staff exceeds new headcount. Please resolve assignments.",
|
||||
});
|
||||
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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleAutoUnassign = async () => {
|
||||
if (!pendingUpdate || !event || !eventId) return;
|
||||
|
||||
const assignedStaff = Array.isArray(event.assignedStaff) ? (event.assignedStaff as any[]) : [];
|
||||
const excessCount = assignedStaff.length - pendingUpdate.requested;
|
||||
|
||||
// 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);
|
||||
return {
|
||||
...assigned,
|
||||
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);
|
||||
|
||||
// Remove lowest reliability staff
|
||||
const staffToKeep = staffWithScores.slice(excessCount);
|
||||
|
||||
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
|
||||
}))
|
||||
});
|
||||
|
||||
setShowReductionAlert(false);
|
||||
setPendingUpdate(null);
|
||||
|
||||
toast({
|
||||
title: "✅ Staff Auto-Unassigned",
|
||||
description: `Removed ${excessCount} lowest reliability staff members`,
|
||||
});
|
||||
};
|
||||
|
||||
const handleManualUnassign = () => {
|
||||
setShowReductionAlert(false);
|
||||
toast({
|
||||
title: "Manual Adjustment Required",
|
||||
description: "Please manually remove excess staff from the order",
|
||||
});
|
||||
};
|
||||
|
||||
if (isOrderLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-primary" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!event) {
|
||||
return (
|
||||
<div className="p-12 text-center">
|
||||
<h2 className="text-2xl font-bold text-primary-text mb-4">Event Not Found</h2>
|
||||
<Button onClick={() => navigate(createPageUrl("Events"))}>
|
||||
Back to Events
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DashboardLayout
|
||||
title={`Edit ${event.eventName || "Order"}`}
|
||||
subtitle="Update information for your order"
|
||||
>
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{!canModify && (
|
||||
<div className="mb-6 p-4 bg-amber-50 border border-amber-200 rounded-xl flex items-center gap-3 text-amber-800">
|
||||
<AlertTriangle className="w-5 h-5 flex-shrink-0" />
|
||||
<p className="text-sm font-medium">
|
||||
This order has already started. Some details may no longer be editable for security and tracking purposes.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showReductionAlert && pendingUpdate && (
|
||||
<div className="mb-6">
|
||||
<OrderReductionAlert
|
||||
originalRequested={originalRequested}
|
||||
newRequested={pendingUpdate.requested}
|
||||
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);
|
||||
return {
|
||||
name: assigned.staffName || assigned.staff_name,
|
||||
reliability: staffInfo?.averageRating ? staffInfo.averageRating * 20 : 50
|
||||
};
|
||||
}).sort((a, b) => a.reliability - b.reliability)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<EventFormWizard
|
||||
event={event as any}
|
||||
onSubmit={handleSubmit}
|
||||
isSubmitting={updateOrderMutation.isPending}
|
||||
currentUser={user as any}
|
||||
onCancel={() => navigate(createPageUrl("Events"))}
|
||||
/>
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user