feat: Implement Edit Orders

This commit is contained in:
dhinesh-m24
2026-02-09 11:31:18 +05:30
parent 0ebb76b3a7
commit 1c8541cb1d
9 changed files with 800 additions and 3 deletions

View File

@@ -12,6 +12,7 @@
"dependencies": { "dependencies": {
"@firebase/analytics": "^0.10.19", "@firebase/analytics": "^0.10.19",
"@firebase/data-connect": "^0.3.12", "@firebase/data-connect": "^0.3.12",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.7", "@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-popover": "^1.1.15",

View File

@@ -17,6 +17,9 @@ importers:
'@firebase/data-connect': '@firebase/data-connect':
specifier: ^0.3.12 specifier: ^0.3.12
version: 0.3.12(@firebase/app@0.14.7) version: 0.3.12(@firebase/app@0.14.7)
'@radix-ui/react-avatar':
specifier: ^1.1.11
version: 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-dialog': '@radix-ui/react-dialog':
specifier: ^1.1.15 specifier: ^1.1.15
version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -828,6 +831,19 @@ packages:
'@types/react-dom': '@types/react-dom':
optional: true optional: true
'@radix-ui/react-avatar@1.1.11':
resolution: {integrity: sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-checkbox@1.3.3': '@radix-ui/react-checkbox@1.3.3':
resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==}
peerDependencies: peerDependencies:
@@ -898,6 +914,15 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
'@radix-ui/react-context@1.1.3':
resolution: {integrity: sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-dialog@1.1.15': '@radix-ui/react-dialog@1.1.15':
resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==}
peerDependencies: peerDependencies:
@@ -3671,6 +3696,19 @@ snapshots:
'@types/react': 19.2.10 '@types/react': 19.2.10
'@types/react-dom': 19.2.3(@types/react@19.2.10) '@types/react-dom': 19.2.3(@types/react@19.2.10)
'@radix-ui/react-avatar@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/react-context': 1.1.3(@types/react@19.2.10)(react@19.2.4)
'@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.10)(react@19.2.4)
'@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.10)(react@19.2.4)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.10)(react@19.2.4)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
optionalDependencies:
'@types/react': 19.2.10
'@types/react-dom': 19.2.3(@types/react@19.2.10)
'@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies: dependencies:
'@radix-ui/primitive': 1.1.3 '@radix-ui/primitive': 1.1.3
@@ -3741,6 +3779,12 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 19.2.10 '@types/react': 19.2.10
'@radix-ui/react-context@1.1.3(@types/react@19.2.10)(react@19.2.4)':
dependencies:
react: 19.2.4
optionalDependencies:
'@types/react': 19.2.10
'@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies: dependencies:
'@radix-ui/primitive': 1.1.3 '@radix-ui/primitive': 1.1.3

View File

@@ -0,0 +1,48 @@
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

View 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>
);
}

View File

@@ -131,8 +131,7 @@ export default function OrderDetail() {
const handleEdit = () => { const handleEdit = () => {
if (!order || !id) return; if (!order || !id) return;
// Placeholder: route can later be wired to an edit form navigate(`/orders/${id}/edit`);
navigate(`/orders/create?edit=${id}`);
}; };
const handleDuplicate = () => { const handleDuplicate = () => {

View File

@@ -0,0 +1,339 @@
import React, { useState } from "react";
import { useForm, useFieldArray } from "react-hook-form";
import { Button } from "@/common/components/ui/button";
import { Input } from "@/common/components/ui/input";
import { Label } from "@/common/components/ui/label";
import { Textarea } from "@/common/components/ui/textarea";
import { Card, CardContent } from "@/common/components/ui/card";
import {
Plus,
Trash2,
Info,
ChevronRight,
ChevronLeft,
Save,
X,
PlusCircle
} from "lucide-react";
import { format } from "date-fns";
interface Role {
name: string;
count: number;
rate?: number;
}
interface Shift {
id?: string;
title: string;
startTime: string;
endTime: string;
roles: Role[];
}
interface EventFormData {
event_name: string;
date: string;
startDate: string;
endDate: string;
notes: string;
po_reference: string;
total: number;
shifts: Shift[];
}
interface EventFormWizardProps {
event: any;
onSubmit: (data: any) => void;
isSubmitting: boolean;
currentUser: any;
onCancel: () => void;
}
export default function EventFormWizard({
event,
onSubmit,
isSubmitting,
onCancel
}: EventFormWizardProps) {
const [step, setStep] = useState(1);
const { register, control, handleSubmit, formState: { errors } } = useForm<EventFormData>({
defaultValues: {
event_name: event?.eventName || "",
date: event?.date ? format(new Date(event.date), "yyyy-MM-dd") : "",
startDate: event?.startDate ? format(new Date(event.startDate), "yyyy-MM-dd'T'HH:mm") : "",
endDate: event?.endDate ? format(new Date(event.endDate), "yyyy-MM-dd'T'HH:mm") : "",
notes: event?.notes || "",
po_reference: event?.poReference || "",
total: event?.total || 0,
shifts: event?.shifts?.map((s: any) => ({
...s,
roles: Array.isArray(s.roles) ? s.roles : []
})) || []
}
});
const { fields: shiftFields, append: appendShift, remove: removeShift } = useFieldArray({
control,
name: "shifts"
});
const nextStep = () => setStep(prev => prev + 1);
const prevStep = () => setStep(prev => prev - 1);
return (
<div className="space-y-6">
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-4">
{[1, 2].map((i) => (
<React.Fragment key={i}>
<div
className={`w-10 h-10 rounded-full flex items-center justify-center font-bold transition-colors ${
step >= i ? "bg-primary text-white" : "bg-muted text-muted-foreground"
}`}
>
{i}
</div>
{i < 2 && <div className={`w-12 h-0.5 ${step > i ? "bg-primary" : "bg-muted"}`} />}
</React.Fragment>
))}
</div>
<div className="text-right">
<p className="text-xs font-black text-muted-foreground uppercase tracking-widest">
Step {step} of 2
</p>
<p className="text-sm font-bold text-primary-text">
{step === 1 ? "Order Details" : "Shift Management"}
</p>
</div>
</div>
<form onSubmit={handleSubmit(onSubmit)}>
{step === 1 && (
<div className="space-y-6 animate-in fade-in slide-in-from-right-4 duration-300">
<Card className="border-border shadow-sm">
<CardContent className="p-8 space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label className="text-xs font-black uppercase tracking-widest text-muted-foreground">Event Name</Label>
<Input
{...register("event_name", { required: "Event name is required" })}
placeholder="e.g., Annual Gala 2024"
className="rounded-xl h-12"
/>
{errors.event_name && <p className="text-xs text-destructive font-bold">{errors.event_name.message}</p>}
</div>
<div className="space-y-2">
<Label className="text-xs font-black uppercase tracking-widest text-muted-foreground">PO Reference (Optional)</Label>
<Input
{...register("po_reference")}
placeholder="PO-12345"
className="rounded-xl h-12"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="space-y-2">
<Label className="text-xs font-black uppercase tracking-widest text-muted-foreground">Primary Date</Label>
<Input
type="date"
{...register("date", { required: "Date is required" })}
className="rounded-xl h-12"
/>
</div>
<div className="space-y-2">
<Label className="text-xs font-black uppercase tracking-widest text-muted-foreground">Start Date & Time</Label>
<Input
type="datetime-local"
{...register("startDate", { required: "Start time is required" })}
className="rounded-xl h-12"
/>
</div>
<div className="space-y-2">
<Label className="text-xs font-black uppercase tracking-widest text-muted-foreground">End Date & Time</Label>
<Input
type="datetime-local"
{...register("endDate", { required: "End time is required" })}
className="rounded-xl h-12"
/>
</div>
</div>
<div className="space-y-2">
<Label className="text-xs font-black uppercase tracking-widest text-muted-foreground">Additional Notes</Label>
<Textarea
{...register("notes")}
placeholder="Enter any special instructions or requirements..."
className="rounded-xl min-h-[120px]"
/>
</div>
</CardContent>
</Card>
<div className="flex justify-between pt-4">
<Button type="button" variant="outline" onClick={onCancel} className="rounded-xl px-8 font-bold">
Cancel
</Button>
<Button type="button" onClick={nextStep} className="rounded-xl px-8 font-bold" trailingIcon={<ChevronRight className="w-4 h-4" />}>
Next: Manage Shifts
</Button>
</div>
</div>
)}
{step === 2 && (
<div className="space-y-6 animate-in fade-in slide-in-from-right-4 duration-300">
<div className="flex items-center justify-between">
<h3 className="text-xl font-bold text-primary-text">Shifts & Staffing</h3>
<Button
type="button"
onClick={() => appendShift({ title: "New Shift", startTime: "", endTime: "", roles: [] })}
variant="outline"
className="rounded-xl font-bold"
leadingIcon={<PlusCircle className="w-4 h-4" />}
>
Add Shift
</Button>
</div>
{shiftFields.map((field, shiftIndex) => (
<Card key={field.id} className="border-border shadow-sm overflow-hidden">
<div className="bg-muted/30 px-6 py-4 border-b border-border flex items-center justify-between">
<div className="flex items-center gap-4 flex-1">
<div className="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center text-primary font-bold">
{shiftIndex + 1}
</div>
<Input
{...register(`shifts.${shiftIndex}.title`)}
className="bg-transparent border-none font-bold text-lg p-0 focus-visible:ring-0"
/>
</div>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => removeShift(shiftIndex)}
className="text-destructive hover:bg-destructive/10"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
<CardContent className="p-6 space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label className="text-[10px] font-black uppercase tracking-widest text-muted-foreground">Shift Start</Label>
<Input
type="datetime-local"
{...register(`shifts.${shiftIndex}.startTime`)}
className="rounded-xl"
/>
</div>
<div className="space-y-2">
<Label className="text-[10px] font-black uppercase tracking-widest text-muted-foreground">Shift End</Label>
<Input
type="datetime-local"
{...register(`shifts.${shiftIndex}.endTime`)}
className="rounded-xl"
/>
</div>
</div>
<div className="space-y-4">
<Label className="text-[10px] font-black uppercase tracking-widest text-muted-foreground">Roles & Headcount</Label>
<ShiftRoles
shiftIndex={shiftIndex}
control={control}
register={register}
/>
</div>
</CardContent>
</Card>
))}
{shiftFields.length === 0 && (
<div className="text-center py-12 bg-muted/20 rounded-2xl border-2 border-dashed border-border">
<Info className="w-12 h-12 text-muted-foreground/30 mx-auto mb-4" />
<p className="font-bold text-muted-foreground">No shifts added yet.</p>
<p className="text-sm text-muted-foreground mb-6">Every order needs at least one shift to request staff.</p>
<Button
type="button"
onClick={() => appendShift({ title: "Primary Shift", startTime: "", endTime: "", roles: [] })}
className="rounded-xl font-bold"
>
Add Your First Shift
</Button>
</div>
)}
<div className="flex justify-between pt-4">
<Button type="button" variant="outline" onClick={prevStep} className="rounded-xl px-8 font-bold" leadingIcon={<ChevronLeft className="w-4 h-4" />}>
Back
</Button>
<Button
type="submit"
disabled={isSubmitting}
className="rounded-xl px-12 font-bold shadow-lg shadow-primary/20"
leadingIcon={isSubmitting ? <Plus className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
>
{isSubmitting ? "Saving Changes..." : "Save Order Changes"}
</Button>
</div>
</div>
)}
</form>
</div>
);
}
function ShiftRoles({ shiftIndex, control, register }: { shiftIndex: number, control: any, register: any }) {
const { fields, append, remove } = useFieldArray({
control,
name: `shifts.${shiftIndex}.roles`
});
return (
<div className="space-y-3">
{fields.map((role, roleIndex) => (
<div key={role.id} className="flex items-center gap-3 bg-muted/20 p-3 rounded-xl border border-border/40">
<div className="flex-1">
<Input
{...register(`shifts.${shiftIndex}.roles.${roleIndex}.name`)}
placeholder="Role (e.g. Bartender)"
className="bg-transparent border-none font-bold h-8 focus-visible:ring-0"
/>
</div>
<div className="flex items-center gap-2">
<Label className="text-[10px] font-black text-muted-foreground uppercase">Count</Label>
<Input
type="number"
{...register(`shifts.${shiftIndex}.roles.${roleIndex}.count`)}
className="w-20 h-8 rounded-lg font-bold"
min="1"
/>
</div>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => remove(roleIndex)}
className="h-8 w-8 text-muted-foreground hover:text-destructive"
>
<X className="w-3 h-3" />
</Button>
</div>
))}
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => append({ name: "", count: 1 })}
className="text-xs font-bold text-primary hover:bg-primary/5"
leadingIcon={<Plus className="w-3 h-3" />}
>
Add Role
</Button>
</div>
);
}

View File

@@ -0,0 +1,117 @@
import { Alert, AlertDescription } from "@/common/components/ui/alert";
import { Badge } from "@/common/components/ui/badge";
import { Button } from "@/common/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/common/components/ui/card";
import { AlertTriangle, CheckCircle, TrendingDown, UserMinus } from "lucide-react";
interface OrderReductionAlertProps {
originalRequested: number;
newRequested: number;
currentAssigned: number;
onAutoUnassign: () => void;
onManualUnassign: () => void;
lowReliabilityStaff?: Array<{ name: string; reliability: number }>;
}
export default function OrderReductionAlert({
originalRequested,
newRequested,
currentAssigned,
onAutoUnassign,
onManualUnassign,
lowReliabilityStaff = []
}: OrderReductionAlertProps) {
const excessStaff = currentAssigned - newRequested;
if (excessStaff <= 0) return null;
return (
<Card className="border-2 border-orange-500 bg-orange-50 shadow-lg">
<CardHeader className="bg-gradient-to-r from-orange-100 to-red-50 border-b border-orange-200">
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-orange-500 rounded-xl flex items-center justify-center">
<AlertTriangle className="w-6 h-6 text-white" />
</div>
<div>
<CardTitle className="text-xl font-bold text-orange-900">
Order Size Reduction Detected
</CardTitle>
<p className="text-sm text-orange-700 mt-1">
Client reduced headcount from {originalRequested} to {newRequested}
</p>
</div>
</div>
</CardHeader>
<CardContent className="p-6 space-y-4">
<Alert className="bg-white border-orange-300">
<AlertDescription className="text-slate-900">
<div className="flex items-center gap-2 mb-2">
<TrendingDown className="w-5 h-5 text-orange-600" />
<span className="font-bold">Action Required:</span>
</div>
<p className="text-sm">
You have <strong className="text-orange-700">{excessStaff} staff member{excessStaff !== 1 ? 's' : ''}</strong> assigned
that exceed{excessStaff === 1 ? 's' : ''} the new request.
You must unassign {excessStaff} worker{excessStaff !== 1 ? 's' : ''} to match the new headcount.
</p>
</AlertDescription>
</Alert>
<div className="grid grid-cols-3 gap-4">
<div className="bg-white border-2 border-slate-200 rounded-xl p-4 text-center">
<p className="text-xs text-slate-500 mb-1">Original Request</p>
<p className="text-2xl font-bold text-slate-900">{originalRequested}</p>
</div>
<div className="bg-white border-2 border-orange-300 rounded-xl p-4 text-center">
<p className="text-xs text-orange-600 mb-1">New Request</p>
<p className="text-2xl font-bold text-orange-700">{newRequested}</p>
</div>
<div className="bg-white border-2 border-red-300 rounded-xl p-4 text-center">
<p className="text-xs text-red-600 mb-1">Must Remove</p>
<p className="text-2xl font-bold text-red-700">{excessStaff}</p>
</div>
</div>
<div className="space-y-3">
<Button
onClick={onManualUnassign}
variant="outline"
className="w-full border-2 border-slate-300 hover:bg-slate-50"
>
<UserMinus className="w-4 h-4 mr-2" />
Manually Select Which Staff to Remove
</Button>
{lowReliabilityStaff.length > 0 && (
<Button
onClick={onAutoUnassign}
className="w-full bg-orange-600 hover:bg-orange-700 text-white"
>
<CheckCircle className="w-4 h-4 mr-2" />
Auto-Remove {excessStaff} Lowest Reliability Staff
</Button>
)}
</div>
{lowReliabilityStaff.length > 0 && (
<div className="bg-white border border-orange-200 rounded-lg p-4">
<p className="text-xs font-bold text-slate-700 mb-3 uppercase">
Suggested for Auto-Removal (Lowest Reliability):
</p>
<div className="space-y-2">
{lowReliabilityStaff.slice(0, excessStaff).map((staff, idx) => (
<div key={idx} className="flex items-center justify-between p-2 bg-red-50 rounded-lg border border-red-200">
<span className="text-sm font-medium text-slate-900">{staff.name}</span>
<Badge variant="outline" className="border-red-400 text-red-700">
Reliability: {staff.reliability}%
</Badge>
</div>
))}
</div>
</div>
)}
</CardContent>
</Card>
);
}

View File

@@ -60,6 +60,10 @@ export interface User {
user_role?: string; // MVP uses both sometimes user_role?: string; // MVP uses both sometimes
company_name?: string; company_name?: string;
name: string; name: string;
preferred_vendor_id?: string;
preferredVendorId?: string;
preferred_vendor_name?: string;
preferredVendorName?: string;
} }
export interface Event { export interface Event {
@@ -68,4 +72,17 @@ export interface Event {
business_name?: string; business_name?: string;
created_by?: string; created_by?: string;
assigned_staff?: { staff_id: string }[]; assigned_staff?: { staff_id: string }[];
// Fields used in EventFormWizard
event_name?: string;
vendor_id?: string;
vendor_name?: string;
hub?: string;
date?: string;
total?: number;
shifts?: any[];
order_type?: string;
is_rapid?: boolean;
is_recurring?: boolean;
status?: string;
} }

View File

@@ -20,6 +20,7 @@ import OrderList from './features/operations/orders/OrderList';
import OrderDetail from './features/operations/orders/OrderDetail'; import OrderDetail from './features/operations/orders/OrderDetail';
import ClientOrderList from './features/operations/orders/ClientOrderList'; import ClientOrderList from './features/operations/orders/ClientOrderList';
import VendorOrderList from './features/operations/orders/VendorOrderList'; import VendorOrderList from './features/operations/orders/VendorOrderList';
import EditOrder from './features/operations/orders/EditOrder';
/** /**
* AppRoutes Component * AppRoutes Component
@@ -99,7 +100,7 @@ const AppRoutes: React.FC = () => {
<Route path="/orders/client" element={<ClientOrderList />} /> <Route path="/orders/client" element={<ClientOrderList />} />
<Route path="/orders/:id" element={<OrderDetail />} /> <Route path="/orders/:id" element={<OrderDetail />} />
<Route path="/orders/vendor" element={<VendorOrderList />} /> <Route path="/orders/vendor" element={<VendorOrderList />} />
<Route path="/orders/:id/edit" element={<EditOrder />} />
</Route> </Route>
<Route path="*" element={<Navigate to="/login" replace />} /> <Route path="*" element={<Navigate to="/login" replace />} />
</Routes> </Routes>