feat: Implement Edit Orders
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
44
apps/web/pnpm-lock.yaml
generated
44
apps/web/pnpm-lock.yaml
generated
@@ -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
|
||||||
|
|||||||
48
apps/web/src/common/components/ui/avatar.tsx
Normal file
48
apps/web/src/common/components/ui/avatar.tsx
Normal 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 }
|
||||||
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 = () => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user