feat: Implement calendar view to manage orders and events
This commit is contained in:
487
apps/web/src/features/operations/schedule/Schedule.tsx
Normal file
487
apps/web/src/features/operations/schedule/Schedule.tsx
Normal file
@@ -0,0 +1,487 @@
|
|||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
addDays,
|
||||||
|
addWeeks,
|
||||||
|
addMonths,
|
||||||
|
format,
|
||||||
|
isSameDay,
|
||||||
|
isToday,
|
||||||
|
isValid,
|
||||||
|
parseISO,
|
||||||
|
startOfWeek,
|
||||||
|
startOfMonth,
|
||||||
|
endOfMonth,
|
||||||
|
endOfWeek,
|
||||||
|
eachDayOfInterval,
|
||||||
|
subWeeks,
|
||||||
|
subMonths,
|
||||||
|
subDays
|
||||||
|
} from "date-fns";
|
||||||
|
import {
|
||||||
|
Calendar as CalendarIcon,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
Clock,
|
||||||
|
DollarSign,
|
||||||
|
Plus,
|
||||||
|
CalendarDays,
|
||||||
|
Users,
|
||||||
|
AlertTriangle
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useState, useMemo } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { Badge } from "@/common/components/ui/badge";
|
||||||
|
import { Button } from "@/common/components/ui/button";
|
||||||
|
import { Card, CardContent } from "@/common/components/ui/card";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
DialogDescription
|
||||||
|
} from "@/common/components/ui/dialog";
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from "@/common/components/ui/alert";
|
||||||
|
import { Tabs, TabsList, TabsTrigger } from "@/common/components/ui/tabs";
|
||||||
|
import DashboardLayout from "@/features/layouts/DashboardLayout";
|
||||||
|
import { useListOrders, useUpdateOrder } from "@/dataconnect-generated/react";
|
||||||
|
import { dataConnect } from "@/features/auth/firebase";
|
||||||
|
import { OrderStatus } from "@/dataconnect-generated";
|
||||||
|
import { useToast } from "@/common/components/ui/use-toast";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps OrderStatus to appropriate Tailwind color classes
|
||||||
|
*/
|
||||||
|
const getStatusColor = (status: OrderStatus | undefined) => {
|
||||||
|
switch (status) {
|
||||||
|
case OrderStatus.DRAFT:
|
||||||
|
case OrderStatus.CANCELLED:
|
||||||
|
return "bg-slate-100 text-slate-600 border-slate-200";
|
||||||
|
case OrderStatus.FILLED:
|
||||||
|
case OrderStatus.FULLY_STAFFED:
|
||||||
|
case OrderStatus.COMPLETED:
|
||||||
|
return "bg-emerald-50 text-emerald-700 border-emerald-200";
|
||||||
|
case OrderStatus.POSTED:
|
||||||
|
case OrderStatus.PENDING:
|
||||||
|
case OrderStatus.PARTIAL_STAFFED:
|
||||||
|
return "bg-blue-50 text-blue-700 border-blue-200";
|
||||||
|
default:
|
||||||
|
return "bg-slate-50 text-slate-600 border-slate-200";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely parses various date formats into a Date object
|
||||||
|
*/
|
||||||
|
const safeParseDate = (dateString: any) => {
|
||||||
|
if (!dateString) return null;
|
||||||
|
try {
|
||||||
|
const date = typeof dateString === 'string' ? parseISO(dateString) : new Date(dateString);
|
||||||
|
return isValid(date) ? date : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Schedule() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [currentDate, setCurrentDate] = useState(new Date());
|
||||||
|
const [viewMode, setViewMode] = useState<'day' | 'week' | 'month'>('week');
|
||||||
|
const [selectedOrder, setSelectedOrder] = useState<any>(null);
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
|
// State for rescheduling confirmation
|
||||||
|
const [rescheduleData, setRescheduleData] = useState<{ order: any; newDate: Date } | null>(null);
|
||||||
|
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
|
||||||
|
|
||||||
|
// Fetch real data from Data Connect
|
||||||
|
const { data, isLoading } = useListOrders(dataConnect);
|
||||||
|
const orders = data?.orders || [];
|
||||||
|
|
||||||
|
// Mutation for drag-and-drop rescheduling
|
||||||
|
const updateOrderMutation = useUpdateOrder(dataConnect, {
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["listOrders"] });
|
||||||
|
toast({
|
||||||
|
title: "Order Rescheduled",
|
||||||
|
description: "The order date has been updated successfully.",
|
||||||
|
});
|
||||||
|
setIsConfirmOpen(false);
|
||||||
|
setRescheduleData(null);
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast({
|
||||||
|
title: "Update Failed",
|
||||||
|
description: "Could not reschedule the order. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate days to display based on current view mode
|
||||||
|
const calendarDays = useMemo(() => {
|
||||||
|
if (viewMode === 'month') {
|
||||||
|
const start = startOfWeek(startOfMonth(currentDate));
|
||||||
|
const end = endOfWeek(endOfMonth(currentDate));
|
||||||
|
return eachDayOfInterval({ start, end });
|
||||||
|
} else if (viewMode === 'week') {
|
||||||
|
const start = startOfWeek(currentDate);
|
||||||
|
const end = endOfWeek(currentDate);
|
||||||
|
return eachDayOfInterval({ start, end });
|
||||||
|
} else {
|
||||||
|
return [currentDate];
|
||||||
|
}
|
||||||
|
}, [currentDate, viewMode]);
|
||||||
|
|
||||||
|
const getOrdersForDay = (date: Date) => {
|
||||||
|
return orders.filter((order) => {
|
||||||
|
const orderDate = safeParseDate(order.date);
|
||||||
|
return orderDate && isSameDay(orderDate, date);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate metrics for the current visible range
|
||||||
|
const metrics = useMemo(() => {
|
||||||
|
const visibleOrders = orders.filter(order => {
|
||||||
|
const orderDate = safeParseDate(order.date);
|
||||||
|
if (!orderDate) return false;
|
||||||
|
return orderDate >= calendarDays[0] && orderDate <= calendarDays[calendarDays.length - 1];
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalHours = visibleOrders.reduce((sum, order) => {
|
||||||
|
const shifts = Array.isArray(order.shifts) ? order.shifts : [];
|
||||||
|
const orderHours = shifts.reduce((shiftSum: number, shift: any) => {
|
||||||
|
const roles = Array.isArray(shift.roles) ? shift.roles : [];
|
||||||
|
return shiftSum + roles.reduce((roleSum: number, role: any) => roleSum + (Number(role.hours) || 0), 0);
|
||||||
|
}, 0);
|
||||||
|
return sum + orderHours;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
const totalCost = visibleOrders.reduce((sum, order) => sum + (order.total || 0), 0);
|
||||||
|
const totalShifts = visibleOrders.reduce((sum, order) => sum + (Array.isArray(order.shifts) ? order.shifts.length : 0), 0);
|
||||||
|
|
||||||
|
return { totalHours, totalCost, totalShifts };
|
||||||
|
}, [orders, calendarDays]);
|
||||||
|
|
||||||
|
// Navigation handlers
|
||||||
|
const handlePrev = () => {
|
||||||
|
if (viewMode === 'month') setCurrentDate(subMonths(currentDate, 1));
|
||||||
|
else if (viewMode === 'week') setCurrentDate(subWeeks(currentDate, 1));
|
||||||
|
else setCurrentDate(subDays(currentDate, 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
if (viewMode === 'month') setCurrentDate(addMonths(currentDate, 1));
|
||||||
|
else if (viewMode === 'week') setCurrentDate(addWeeks(currentDate, 1));
|
||||||
|
else setCurrentDate(addDays(currentDate, 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToday = () => setCurrentDate(new Date());
|
||||||
|
|
||||||
|
const handleEventClick = (order: any, e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setSelectedOrder(order);
|
||||||
|
setIsModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Drag and Drop Logic
|
||||||
|
const onDragStart = (e: React.DragEvent, order: any) => {
|
||||||
|
e.dataTransfer.setData("orderId", order.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDrop = async (e: React.DragEvent, date: Date) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const orderId = e.dataTransfer.getData("orderId");
|
||||||
|
const order = orders.find(o => o.id === orderId);
|
||||||
|
|
||||||
|
if (order && !isSameDay(safeParseDate(order.date)!, date)) {
|
||||||
|
setRescheduleData({ order, newDate: date });
|
||||||
|
setIsConfirmOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmReschedule = () => {
|
||||||
|
if (rescheduleData) {
|
||||||
|
updateOrderMutation.mutate({
|
||||||
|
id: rescheduleData.order.id,
|
||||||
|
date: rescheduleData.newDate.toISOString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<DashboardLayout title="Schedule" subtitle="Loading shifts...">
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||||
|
</div>
|
||||||
|
</DashboardLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardLayout
|
||||||
|
title="Schedule"
|
||||||
|
subtitle="Plan and manage staff shifts"
|
||||||
|
actions={
|
||||||
|
<Button onClick={() => navigate('/orders/new')}>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
New Order
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Metrics Section */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<Card className="bg-white border-border/50 shadow-sm">
|
||||||
|
<CardContent className="p-6 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-secondary-text text-xs font-bold uppercase tracking-wider mb-1">Total Hours</p>
|
||||||
|
<p className="text-2xl font-bold text-primary-text">{metrics.totalHours.toFixed(1)}</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-blue-50 rounded-xl text-blue-600">
|
||||||
|
<Clock className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className="bg-white border-border/50 shadow-sm">
|
||||||
|
<CardContent className="p-6 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-secondary-text text-xs font-bold uppercase tracking-wider mb-1">Labor Cost</p>
|
||||||
|
<p className="text-2xl font-bold text-primary-text">${metrics.totalCost.toLocaleString()}</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-emerald-50 rounded-xl text-emerald-600">
|
||||||
|
<DollarSign className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className="bg-white border-border/50 shadow-sm">
|
||||||
|
<CardContent className="p-6 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-secondary-text text-xs font-bold uppercase tracking-wider mb-1">Total Shifts</p>
|
||||||
|
<p className="text-2xl font-bold text-primary-text">{metrics.totalShifts}</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-teal-50 rounded-xl text-teal-600">
|
||||||
|
<CalendarIcon className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Calendar Controls */}
|
||||||
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 bg-white p-4 rounded-xl border border-border/50 shadow-sm">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button variant="outline" size="icon" onClick={handlePrev} className="h-9 w-9">
|
||||||
|
<ChevronLeft className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" onClick={handleToday} className="h-9 px-4 text-xs font-bold uppercase tracking-wider">
|
||||||
|
Today
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="icon" onClick={handleNext} className="h-9 w-9">
|
||||||
|
<ChevronRight className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<h2 className="ml-4 text-lg font-bold text-primary-text">
|
||||||
|
{format(currentDate, viewMode === 'month' ? 'MMMM yyyy' : 'MMMM d, yyyy')}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs value={viewMode} onValueChange={(v) => setViewMode(v as any)} className="w-full md:w-auto">
|
||||||
|
<TabsList className="grid grid-cols-3 w-full md:w-[240px]">
|
||||||
|
<TabsTrigger value="day" className="text-xs font-bold uppercase tracking-wider">Day</TabsTrigger>
|
||||||
|
<TabsTrigger value="week" className="text-xs font-bold uppercase tracking-wider">Week</TabsTrigger>
|
||||||
|
<TabsTrigger value="month" className="text-xs font-bold uppercase tracking-wider">Month</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Calendar Grid */}
|
||||||
|
<div className={`grid gap-4 ${
|
||||||
|
viewMode === 'month' ? 'grid-cols-7' :
|
||||||
|
viewMode === 'week' ? 'grid-cols-1 md:grid-cols-7' :
|
||||||
|
'grid-cols-1'
|
||||||
|
}`}>
|
||||||
|
{/* Day Names */}
|
||||||
|
{(viewMode === 'month' || (viewMode === 'week' && window.innerWidth > 768)) &&
|
||||||
|
['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(day => (
|
||||||
|
<div key={day} className="text-center py-2 text-[10px] font-bold uppercase tracking-widest text-secondary-text">
|
||||||
|
{day}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
{calendarDays.map((day) => {
|
||||||
|
const dayOrders = getOrdersForDay(day);
|
||||||
|
const isTodayDay = isToday(day);
|
||||||
|
const isDifferentMonth = viewMode === 'month' && day.getMonth() !== currentDate.getMonth();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={day.toISOString()}
|
||||||
|
onDragOver={(e) => e.preventDefault()}
|
||||||
|
onDrop={(e) => onDrop(e, day)}
|
||||||
|
className={`min-h-[140px] flex flex-col p-2 rounded-xl border transition-all ${
|
||||||
|
isTodayDay ? 'bg-primary/5 border-primary ring-1 ring-primary/20' :
|
||||||
|
isDifferentMonth ? 'bg-slate-50/50 border-border/30 opacity-60' :
|
||||||
|
'bg-white border-border/50 hover:border-border hover:shadow-sm'
|
||||||
|
} ${viewMode === 'day' ? 'min-h-[500px]' : ''}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-2 px-1">
|
||||||
|
<span className={`text-sm font-bold ${isTodayDay ? 'text-primary' : 'text-primary-text'}`}>
|
||||||
|
{format(day, viewMode === 'day' ? 'EEEE, MMMM d' : 'd')}
|
||||||
|
</span>
|
||||||
|
{dayOrders.length > 0 && (
|
||||||
|
<span className="text-[10px] font-bold text-secondary-text bg-slate-100 px-1.5 py-0.5 rounded">
|
||||||
|
{dayOrders.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 space-y-1 overflow-y-auto max-h-[300px] scrollbar-hide">
|
||||||
|
{dayOrders.map((order: any) => (
|
||||||
|
<div
|
||||||
|
key={order.id}
|
||||||
|
draggable
|
||||||
|
onDragStart={(e) => onDragStart(e, order)}
|
||||||
|
onClick={(e) => handleEventClick(order, e)}
|
||||||
|
className={`p-2 rounded-lg border text-left cursor-pointer transition-all hover:scale-[1.02] active:scale-95 shadow-sm ${getStatusColor(order.status)}`}
|
||||||
|
>
|
||||||
|
<p className="text-[11px] font-bold truncate leading-tight mb-1">
|
||||||
|
{order.eventName || "Unnamed Order"}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-1 opacity-80">
|
||||||
|
<Clock className="w-3 h-3" />
|
||||||
|
<span className="text-[9px] font-medium">
|
||||||
|
{order.shifts?.[0]?.roles?.[0]?.start_time || "00:00"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{dayOrders.length === 0 && viewMode !== 'month' && (
|
||||||
|
<div className="flex-1 flex items-center justify-center opacity-30 border border-dashed rounded-lg py-8">
|
||||||
|
<p className="text-[10px] font-bold uppercase tracking-wider">No Orders</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Order Detail Modal */}
|
||||||
|
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
||||||
|
<DialogContent className="sm:max-w-[500px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<div className="flex items-center justify-between pr-8">
|
||||||
|
<DialogTitle className="text-xl font-bold">{selectedOrder?.eventName || "Order Details"}</DialogTitle>
|
||||||
|
<Badge className={getStatusColor(selectedOrder?.status)}>
|
||||||
|
{selectedOrder?.status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{selectedOrder && (
|
||||||
|
<div className="space-y-6 py-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-[10px] font-bold text-secondary-text uppercase tracking-wider">Date</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CalendarDays className="w-4 h-4 text-primary" />
|
||||||
|
<span className="text-sm font-medium">{format(safeParseDate(selectedOrder.date)!, 'PPPP')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-[10px] font-bold text-secondary-text uppercase tracking-wider">Total Value</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<DollarSign className="w-4 h-4 text-emerald-600" />
|
||||||
|
<span className="text-sm font-bold text-emerald-700">${selectedOrder.total?.toLocaleString() || '0.00'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-[10px] font-bold text-secondary-text uppercase tracking-wider">Business</p>
|
||||||
|
<div className="flex items-center gap-2 bg-slate-50 p-2 rounded-lg border border-slate-100">
|
||||||
|
<Users className="w-4 h-4 text-blue-600" />
|
||||||
|
<span className="text-sm font-medium">{selectedOrder.business?.businessName || "Krow Workforce"}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<p className="text-[10px] font-bold text-secondary-text uppercase tracking-wider">Shifts & Staffing</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{Array.isArray(selectedOrder.shifts) && selectedOrder.shifts.map((shift: any, i: number) => (
|
||||||
|
<div key={i} className="p-3 rounded-lg border border-border/60 bg-white shadow-sm flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-8 h-8 rounded-full bg-slate-100 flex items-center justify-center text-xs font-bold">
|
||||||
|
{i + 1}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-bold text-primary-text">{shift.shiftName || `Shift ${i+1}`}</p>
|
||||||
|
<p className="text-[10px] text-secondary-text font-medium">
|
||||||
|
{shift.roles?.[0]?.start_time} - {shift.roles?.[0]?.end_time}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge variant="outline" className="text-[9px] font-bold uppercase tracking-wider">
|
||||||
|
{Array.isArray(shift.assignedStaff) ? shift.assignedStaff.length : 0} Assigned
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DialogFooter className="gap-2 sm:gap-0">
|
||||||
|
<Button variant="outline" onClick={() => navigate(`/orders/${selectedOrder?.id}`)} className="w-full sm:w-auto">
|
||||||
|
View Full Details
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => navigate(`/orders/${selectedOrder?.id}/edit`)} className="w-full sm:w-auto">
|
||||||
|
Edit Order
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Reschedule Confirmation Dialog */}
|
||||||
|
<Dialog open={isConfirmOpen} onOpenChange={setIsConfirmOpen}>
|
||||||
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Confirm Reschedule</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Are you sure you want to move this order to a different date?
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{rescheduleData && (
|
||||||
|
<div className="py-4">
|
||||||
|
<Alert variant="default" className="bg-amber-50 border-amber-200">
|
||||||
|
<AlertTriangle className="h-4 w-4 text-amber-600" />
|
||||||
|
<AlertTitle className="text-amber-800">Confirm Date Change</AlertTitle>
|
||||||
|
<AlertDescription className="text-amber-700">
|
||||||
|
Moving <strong>{rescheduleData.order.eventName}</strong> to <strong>{format(rescheduleData.newDate, 'PPPP')}</strong>.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setIsConfirmOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={confirmReschedule}
|
||||||
|
disabled={updateOrderMutation.isPending}
|
||||||
|
>
|
||||||
|
{updateOrderMutation.isPending ? "Updating..." : "Confirm Move"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</DashboardLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ 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';
|
import EditOrder from './features/operations/orders/EditOrder';
|
||||||
|
import Schedule from './features/operations/schedule/Schedule';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AppRoutes Component
|
* AppRoutes Component
|
||||||
@@ -101,6 +102,10 @@ const AppRoutes: React.FC = () => {
|
|||||||
<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 path="/orders/:id/edit" element={<EditOrder />} />
|
||||||
|
|
||||||
|
<Route path="/schedule" element={<Schedule />} />
|
||||||
|
|
||||||
|
|
||||||
</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