860 lines
35 KiB
JavaScript
860 lines
35 KiB
JavaScript
import React, { useState, useMemo } from "react";
|
|
import { base44 } from "@/api/base44Client";
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import { Link, useNavigate } from "react-router-dom";
|
|
import { createPageUrl } from "@/utils";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import {
|
|
Tabs, // New import
|
|
TabsList, // New import
|
|
TabsTrigger, // New import
|
|
} from "@/components/ui/tabs"; // New import
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { Calendar as CalendarComponent } from "@/components/ui/calendar";
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from "@/components/ui/popover";
|
|
import {
|
|
Command,
|
|
CommandEmpty,
|
|
CommandGroup,
|
|
CommandInput,
|
|
CommandItem,
|
|
} from "@/components/ui/command";
|
|
import {
|
|
Search, Calendar, MapPin, Users, Eye, Edit, X, Trash2, FileText, // Edit instead of Edit2
|
|
Clock, DollarSign, Package, CheckCircle, AlertTriangle, Grid, List, Zap, Plus, Building2, Bell, Edit3, Filter, CalendarIcon, Check, ChevronsUpDown
|
|
} from "lucide-react";
|
|
import { useToast } from "@/components/ui/use-toast";
|
|
import { format, parseISO, isValid } from "date-fns";
|
|
import OrderDetailModal from "@/components/orders/OrderDetailModal";
|
|
|
|
const safeParseDate = (dateString) => {
|
|
if (!dateString) return null;
|
|
try {
|
|
const date = typeof dateString === 'string' ? parseISO(dateString) : new Date(dateString);
|
|
return isValid(date) ? date : null;
|
|
} catch { return null; }
|
|
};
|
|
|
|
const safeFormatDate = (dateString, formatString) => {
|
|
const date = safeParseDate(dateString);
|
|
return date ? format(date, formatString) : '—';
|
|
};
|
|
|
|
const convertTo12Hour = (time24) => {
|
|
if (!time24) return "-";
|
|
try {
|
|
const [hours, minutes] = time24.split(':');
|
|
const hour = parseInt(hours);
|
|
const ampm = hour >= 12 ? 'PM' : 'AM';
|
|
const hour12 = hour % 12 || 12;
|
|
return `${hour12}:${minutes} ${ampm}`;
|
|
} catch {
|
|
return time24;
|
|
}
|
|
};
|
|
|
|
const getStatusBadge = (event) => {
|
|
if (event.is_rapid) {
|
|
return (
|
|
<div className="inline-flex items-center gap-2 bg-red-500 text-white px-4 py-2 rounded-lg font-semibold text-xs shadow-md">
|
|
<Zap className="w-3.5 h-3.5 fill-white" />
|
|
RAPID
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const statusConfig = {
|
|
'Draft': { bg: 'bg-slate-500', icon: FileText },
|
|
'Pending': { bg: 'bg-amber-500', icon: Clock },
|
|
'Partial Staffed': { bg: 'bg-orange-500', icon: AlertTriangle },
|
|
'Fully Staffed': { bg: 'bg-emerald-500', icon: CheckCircle },
|
|
'Active': { bg: 'bg-blue-500', icon: Users },
|
|
'Completed': { bg: 'bg-slate-400', icon: CheckCircle },
|
|
'Canceled': { bg: 'bg-red-500', icon: X },
|
|
};
|
|
|
|
const config = statusConfig[event.status] || { bg: 'bg-slate-400', icon: Clock };
|
|
const Icon = config.icon;
|
|
|
|
return (
|
|
<div className={`inline-flex items-center gap-2 ${config.bg} text-white px-4 py-2 rounded-lg font-semibold text-xs shadow-md`}>
|
|
<Icon className="w-3.5 h-3.5" />
|
|
{event.status}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default function ClientOrders() {
|
|
const navigate = useNavigate();
|
|
const queryClient = useQueryClient();
|
|
const { toast } = useToast();
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
const [statusFilter, setStatusFilter] = useState("all"); // Updated values for Tabs
|
|
const [dateFilter, setDateFilter] = useState("all");
|
|
const [specificDate, setSpecificDate] = useState(null);
|
|
const [tempDate, setTempDate] = useState(null);
|
|
const [locationFilter, setLocationFilter] = useState("all");
|
|
const [managerFilter, setManagerFilter] = useState("all");
|
|
const [locationOpen, setLocationOpen] = useState(false);
|
|
const [managerOpen, setManagerOpen] = useState(false);
|
|
const [cancelDialogOpen, setCancelDialogOpen] = useState(false); // Changed from cancelDialog.open
|
|
const [orderToCancel, setOrderToCancel] = useState(null); // Changed from cancelDialog.order
|
|
const [viewOrderModal, setViewOrderModal] = useState(false);
|
|
const [selectedOrder, setSelectedOrder] = useState(null);
|
|
const [calendarOpen, setCalendarOpen] = useState(false);
|
|
|
|
const { data: user } = useQuery({
|
|
queryKey: ['current-user-client-orders'],
|
|
queryFn: () => base44.auth.me(),
|
|
});
|
|
|
|
const { data: allEvents = [] } = useQuery({
|
|
queryKey: ['all-events-client'],
|
|
queryFn: () => base44.entities.Event.list('-date'),
|
|
});
|
|
|
|
const clientEvents = useMemo(() => {
|
|
return allEvents.filter(e =>
|
|
e.client_email === user?.email ||
|
|
e.business_name === user?.company_name ||
|
|
e.created_by === user?.email
|
|
);
|
|
}, [allEvents, user]);
|
|
|
|
const cancelOrderMutation = useMutation({
|
|
mutationFn: (orderId) => base44.entities.Event.update(orderId, { status: "Canceled" }),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['all-events-client'] });
|
|
toast({
|
|
title: "✅ Order Canceled",
|
|
description: "Your order has been canceled successfully",
|
|
});
|
|
setCancelDialogOpen(false); // Updated
|
|
setOrderToCancel(null); // Updated
|
|
},
|
|
onError: () => {
|
|
toast({
|
|
title: "❌ Failed to Cancel",
|
|
description: "Could not cancel order. Please try again.",
|
|
variant: "destructive",
|
|
});
|
|
},
|
|
});
|
|
|
|
// Get unique locations and managers for filters
|
|
const uniqueLocations = useMemo(() => {
|
|
const locations = new Set();
|
|
clientEvents.forEach(e => {
|
|
if (e.hub) locations.add(e.hub);
|
|
if (e.event_location) locations.add(e.event_location);
|
|
});
|
|
return Array.from(locations).sort();
|
|
}, [clientEvents]);
|
|
|
|
const uniqueManagers = useMemo(() => {
|
|
const managers = new Set();
|
|
clientEvents.forEach(e => {
|
|
if (e.manager_name) managers.add(e.manager_name);
|
|
// Also check in shifts for manager names
|
|
e.shifts?.forEach(shift => {
|
|
if (shift.manager_name) managers.add(shift.manager_name);
|
|
});
|
|
});
|
|
return Array.from(managers).sort();
|
|
}, [clientEvents]);
|
|
|
|
const filteredOrders = useMemo(() => { // Renamed from filteredEvents
|
|
let filtered = clientEvents;
|
|
|
|
if (searchTerm) {
|
|
const lower = searchTerm.toLowerCase();
|
|
filtered = filtered.filter(e =>
|
|
e.event_name?.toLowerCase().includes(lower) ||
|
|
e.business_name?.toLowerCase().includes(lower) ||
|
|
e.hub?.toLowerCase().includes(lower) ||
|
|
e.event_location?.toLowerCase().includes(lower) // Added event_location to search
|
|
);
|
|
}
|
|
|
|
const now = new Date();
|
|
// Reset time for comparison to only compare dates
|
|
now.setHours(0, 0, 0, 0);
|
|
|
|
filtered = filtered.filter(e => {
|
|
const eventDate = safeParseDate(e.date);
|
|
const isCompleted = e.status === "Completed";
|
|
const isCanceled = e.status === "Canceled";
|
|
const isFutureOrPresent = eventDate && eventDate >= now;
|
|
|
|
if (statusFilter === "active") {
|
|
return !isCompleted && !isCanceled && isFutureOrPresent;
|
|
} else if (statusFilter === "completed") {
|
|
return isCompleted;
|
|
}
|
|
return true; // For "all" or other statuses
|
|
});
|
|
|
|
// Specific date filter (from calendar)
|
|
if (specificDate) {
|
|
filtered = filtered.filter(e => {
|
|
const eventDate = safeParseDate(e.date);
|
|
if (!eventDate) return false;
|
|
const selectedDateNormalized = new Date(specificDate);
|
|
selectedDateNormalized.setHours(0, 0, 0, 0);
|
|
eventDate.setHours(0, 0, 0, 0);
|
|
return eventDate.getTime() === selectedDateNormalized.getTime();
|
|
});
|
|
}
|
|
// Date range filter
|
|
else if (dateFilter !== "all") {
|
|
filtered = filtered.filter(e => {
|
|
const eventDate = safeParseDate(e.date);
|
|
if (!eventDate) return false;
|
|
|
|
const now = new Date();
|
|
now.setHours(0, 0, 0, 0);
|
|
|
|
if (dateFilter === "today") {
|
|
return eventDate.toDateString() === now.toDateString();
|
|
} else if (dateFilter === "week") {
|
|
const weekFromNow = new Date(now);
|
|
weekFromNow.setDate(now.getDate() + 7);
|
|
return eventDate >= now && eventDate <= weekFromNow;
|
|
} else if (dateFilter === "month") {
|
|
const monthFromNow = new Date(now);
|
|
monthFromNow.setMonth(now.getMonth() + 1);
|
|
return eventDate >= now && eventDate <= monthFromNow;
|
|
} else if (dateFilter === "past") {
|
|
return eventDate < now;
|
|
}
|
|
return true;
|
|
});
|
|
}
|
|
|
|
// Location filter
|
|
if (locationFilter !== "all") {
|
|
filtered = filtered.filter(e =>
|
|
e.hub === locationFilter || e.event_location === locationFilter
|
|
);
|
|
}
|
|
|
|
// Manager filter
|
|
if (managerFilter !== "all") {
|
|
filtered = filtered.filter(e => {
|
|
if (e.manager_name === managerFilter) return true;
|
|
// Check shifts for manager
|
|
return e.shifts?.some(shift => shift.manager_name === managerFilter);
|
|
});
|
|
}
|
|
|
|
return filtered;
|
|
}, [clientEvents, searchTerm, statusFilter, dateFilter, specificDate, locationFilter, managerFilter]);
|
|
|
|
const activeOrders = clientEvents.filter(e =>
|
|
e.status !== "Completed" && e.status !== "Canceled"
|
|
).length;
|
|
const completedOrders = clientEvents.filter(e => e.status === "Completed").length;
|
|
const totalSpent = clientEvents
|
|
.filter(e => e.status === "Completed")
|
|
.reduce((sum, e) => sum + (e.total || 0), 0);
|
|
|
|
const handleCancelOrder = (order) => {
|
|
setOrderToCancel(order); // Updated
|
|
setCancelDialogOpen(true); // Updated
|
|
};
|
|
|
|
const handleViewOrder = (order) => {
|
|
setSelectedOrder(order);
|
|
setViewOrderModal(true);
|
|
};
|
|
|
|
const confirmCancel = () => {
|
|
if (orderToCancel) { // Updated
|
|
cancelOrderMutation.mutate(orderToCancel.id); // Updated
|
|
}
|
|
};
|
|
|
|
const canEditOrder = (order) => {
|
|
const eventDate = safeParseDate(order.date);
|
|
const now = new Date();
|
|
return order.status !== "Completed" &&
|
|
order.status !== "Canceled" &&
|
|
eventDate && eventDate > now; // Ensure eventDate is valid before comparison
|
|
};
|
|
|
|
const canCancelOrder = (order) => {
|
|
return order.status !== "Completed" && order.status !== "Canceled";
|
|
};
|
|
|
|
const getAssignmentStatus = (event) => {
|
|
const totalRequested = event.shifts?.reduce((accShift, shift) => {
|
|
return accShift + (shift.roles?.reduce((accRole, role) => accRole + (role.count || 0), 0) || 0);
|
|
}, 0) || 0;
|
|
|
|
const assigned = event.assigned_staff?.length || 0;
|
|
const percentage = totalRequested > 0 ? Math.round((assigned / totalRequested) * 100) : 0;
|
|
|
|
let badgeClass = 'bg-slate-100 text-slate-600'; // Default: no staff, or no roles requested
|
|
if (assigned > 0 && assigned < totalRequested) {
|
|
badgeClass = 'bg-orange-500 text-white'; // Partial Staffed
|
|
} else if (assigned >= totalRequested && totalRequested > 0) {
|
|
badgeClass = 'bg-emerald-500 text-white'; // Fully Staffed
|
|
} else if (assigned === 0 && totalRequested > 0) {
|
|
badgeClass = 'bg-red-500 text-white'; // Requested but 0 assigned
|
|
} else if (assigned > 0 && totalRequested === 0) {
|
|
badgeClass = 'bg-blue-500 text-white'; // Staff assigned but no roles explicitly requested (e.g., event set up, staff assigned, but roles not detailed or count is 0)
|
|
}
|
|
|
|
return {
|
|
badgeClass,
|
|
assigned,
|
|
requested: totalRequested,
|
|
percentage,
|
|
};
|
|
};
|
|
|
|
const getEventTimes = (event) => {
|
|
const firstShift = event.shifts?.[0];
|
|
const rolesInFirstShift = firstShift?.roles || [];
|
|
|
|
let startTime = null;
|
|
let endTime = null;
|
|
|
|
if (rolesInFirstShift.length > 0) {
|
|
startTime = rolesInFirstShift[0].start_time || null;
|
|
endTime = rolesInFirstShift[0].end_time || null;
|
|
}
|
|
|
|
return {
|
|
startTime: startTime ? convertTo12Hour(startTime) : "-",
|
|
endTime: endTime ? convertTo12Hour(endTime) : "-"
|
|
};
|
|
};
|
|
|
|
return (
|
|
<div className="p-4 md:p-8 bg-slate-50 min-h-screen">
|
|
<div className="max-w-[1800px] mx-auto space-y-6">
|
|
<div className=""> {/* Removed mb-6 */}
|
|
<h1 className="text-2xl font-bold text-slate-900">My Orders</h1>
|
|
<p className="text-sm text-slate-500 mt-1">View and manage all your orders</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4"> {/* Removed mb-6 from here as it's now part of space-y-6 */}
|
|
<Card className="border border-blue-200 bg-blue-50">
|
|
<CardContent className="p-5">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-blue-500 rounded-lg flex items-center justify-center">
|
|
<Package className="w-5 h-5 text-white" />
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-blue-600 font-semibold uppercase">TOTAL</p>
|
|
<p className="text-2xl font-bold text-blue-700">{clientEvents.length}</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="border border-orange-200 bg-orange-50">
|
|
<CardContent className="p-5">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-orange-500 rounded-lg flex items-center justify-center">
|
|
<Clock className="w-5 h-5 text-white" />
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-orange-600 font-semibold uppercase">ACTIVE</p>
|
|
<p className="text-2xl font-bold text-orange-700">{activeOrders}</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="border border-green-200 bg-green-50">
|
|
<CardContent className="p-5">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-green-500 rounded-lg flex items-center justify-center">
|
|
<CheckCircle className="w-5 h-5 text-white" />
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-green-600 font-semibold uppercase">COMPLETED</p>
|
|
<p className="text-2xl font-bold text-green-700">{completedOrders}</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="border border-purple-200 bg-purple-50">
|
|
<CardContent className="p-5">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-purple-500 rounded-lg flex items-center justify-center">
|
|
<DollarSign className="w-5 h-5 text-white" />
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-purple-600 font-semibold uppercase">TOTAL SPENT</p>
|
|
<p className="text-2xl font-bold text-purple-700">${Math.round(totalSpent / 1000)}k</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-xl p-4 border shadow-sm">
|
|
<div className="flex items-center gap-4 mb-4">
|
|
<div className="relative flex-1">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-slate-400" />
|
|
<Input
|
|
placeholder="Search orders..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="pl-10 border-slate-300 h-10"
|
|
/>
|
|
</div>
|
|
<Tabs value={statusFilter} onValueChange={setStatusFilter} className="w-fit">
|
|
<TabsList>
|
|
<TabsTrigger value="all">All</TabsTrigger>
|
|
<TabsTrigger value="active">Active</TabsTrigger>
|
|
<TabsTrigger value="completed">Completed</TabsTrigger>
|
|
</TabsList>
|
|
</Tabs>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex items-center gap-2">
|
|
<Filter className="w-4 h-4 text-slate-500" />
|
|
<span className="text-sm font-medium text-slate-700">Filters:</span>
|
|
</div>
|
|
|
|
<Popover open={calendarOpen} onOpenChange={setCalendarOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
className={`h-9 w-[160px] justify-start text-left font-normal ${specificDate ? 'bg-blue-50 border-blue-300' : ''}`}
|
|
>
|
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
|
{specificDate ? format(specificDate, 'MMM dd, yyyy') : 'Pick a date'}
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-auto p-0" align="start">
|
|
<div className="p-6">
|
|
<CalendarComponent
|
|
mode="single"
|
|
selected={tempDate || specificDate}
|
|
onSelect={(date) => setTempDate(date)}
|
|
numberOfMonths={2}
|
|
initialFocus
|
|
/>
|
|
|
|
<div className="mt-6 pt-6 border-t border-slate-200">
|
|
<div className="flex items-center justify-center gap-3 mb-6">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setTempDate(new Date())}
|
|
className={`px-6 h-10 font-medium ${!tempDate && !specificDate ? 'border-b-2 border-blue-600 rounded-none' : ''}`}
|
|
>
|
|
Today
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => {
|
|
const yesterday = new Date();
|
|
yesterday.setDate(yesterday.getDate() - 1);
|
|
setTempDate(yesterday);
|
|
}}
|
|
className="px-6 h-10 font-medium"
|
|
>
|
|
Yesterday
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => {
|
|
const tomorrow = new Date();
|
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
setTempDate(tomorrow);
|
|
}}
|
|
className="px-6 h-10 font-medium"
|
|
>
|
|
Tomorrow
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => {
|
|
setTempDate(null);
|
|
setDateFilter("week");
|
|
}}
|
|
className="px-6 h-10 font-medium"
|
|
>
|
|
This Week
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => {
|
|
setTempDate(null);
|
|
setDateFilter("month");
|
|
}}
|
|
className="px-6 h-10 font-medium"
|
|
>
|
|
This Month
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-end gap-3">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => {
|
|
setTempDate(null);
|
|
setSpecificDate(null);
|
|
setDateFilter("all");
|
|
setCalendarOpen(false);
|
|
}}
|
|
className="px-8 h-10 text-red-600 border-red-300 hover:bg-red-50 font-medium"
|
|
>
|
|
Reset
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => {
|
|
setTempDate(null);
|
|
setCalendarOpen(false);
|
|
}}
|
|
className="px-8 h-10 font-medium"
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
onClick={() => {
|
|
if (tempDate) {
|
|
setSpecificDate(tempDate);
|
|
setDateFilter("all");
|
|
}
|
|
setTempDate(null);
|
|
setCalendarOpen(false);
|
|
}}
|
|
className="px-10 h-10 bg-blue-600 hover:bg-blue-700 font-medium"
|
|
>
|
|
Apply
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
|
|
<Popover open={locationOpen} onOpenChange={setLocationOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={locationOpen}
|
|
className="w-[200px] h-9 justify-between text-sm"
|
|
>
|
|
<span className="truncate">{locationFilter === "all" ? "All Locations" : locationFilter}</span>
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-[200px] p-0">
|
|
<Command shouldFilter={true}>
|
|
<CommandInput placeholder="Type to search..." className="h-9" />
|
|
<CommandEmpty>No location found.</CommandEmpty>
|
|
<CommandGroup className="max-h-64 overflow-auto">
|
|
<CommandItem
|
|
value="all"
|
|
onSelect={() => {
|
|
setLocationFilter("all");
|
|
setLocationOpen(false);
|
|
}}
|
|
>
|
|
<Check className={`mr-2 h-4 w-4 ${locationFilter === "all" ? "opacity-100" : "opacity-0"}`} />
|
|
All Locations
|
|
</CommandItem>
|
|
{uniqueLocations.map((location) => (
|
|
<CommandItem
|
|
key={location}
|
|
value={location}
|
|
onSelect={(currentValue) => {
|
|
setLocationFilter(currentValue);
|
|
setLocationOpen(false);
|
|
}}
|
|
>
|
|
<Check className={`mr-2 h-4 w-4 ${locationFilter === location ? "opacity-100" : "opacity-0"}`} />
|
|
{location}
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
|
|
<Popover open={managerOpen} onOpenChange={setManagerOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={managerOpen}
|
|
className="w-[200px] h-9 justify-between text-sm"
|
|
>
|
|
<span className="truncate">{managerFilter === "all" ? "All Managers" : managerFilter}</span>
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-[200px] p-0">
|
|
<Command shouldFilter={true}>
|
|
<CommandInput placeholder="Type to search..." className="h-9" />
|
|
<CommandEmpty>No manager found.</CommandEmpty>
|
|
<CommandGroup className="max-h-64 overflow-auto">
|
|
<CommandItem
|
|
value="all"
|
|
onSelect={() => {
|
|
setManagerFilter("all");
|
|
setManagerOpen(false);
|
|
}}
|
|
>
|
|
<Check className={`mr-2 h-4 w-4 ${managerFilter === "all" ? "opacity-100" : "opacity-0"}`} />
|
|
All Managers
|
|
</CommandItem>
|
|
{uniqueManagers.map((manager) => (
|
|
<CommandItem
|
|
key={manager}
|
|
value={manager}
|
|
onSelect={(currentValue) => {
|
|
setManagerFilter(currentValue);
|
|
setManagerOpen(false);
|
|
}}
|
|
>
|
|
<Check className={`mr-2 h-4 w-4 ${managerFilter === manager ? "opacity-100" : "opacity-0"}`} />
|
|
{manager}
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
|
|
{(dateFilter !== "all" || specificDate || locationFilter !== "all" || managerFilter !== "all") && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => {
|
|
setDateFilter("all");
|
|
setSpecificDate(null);
|
|
setLocationFilter("all");
|
|
setManagerFilter("all");
|
|
}}
|
|
className="text-slate-600 hover:text-slate-900"
|
|
>
|
|
Clear Filters
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<Card className="border-slate-200 shadow-sm"> {/* Card class updated */}
|
|
<CardContent className="p-0"> {/* CardContent padding updated */}
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="bg-slate-50 hover:bg-slate-50">
|
|
<TableHead className="font-semibold text-slate-700 text-xs uppercase">Business</TableHead>
|
|
<TableHead className="font-semibold text-slate-700 text-xs uppercase">Hub</TableHead>
|
|
<TableHead className="font-semibold text-slate-700 text-xs uppercase">Event</TableHead>
|
|
<TableHead className="font-semibold text-slate-700 text-xs uppercase">Date & Time</TableHead>
|
|
<TableHead className="font-semibold text-slate-700 text-xs uppercase">Status</TableHead>
|
|
<TableHead className="font-semibold text-slate-700 text-xs uppercase">Requested</TableHead>
|
|
<TableHead className="font-semibold text-slate-700 text-xs uppercase">Assigned</TableHead>
|
|
<TableHead className="font-semibold text-slate-700 text-xs uppercase">Invoice</TableHead>
|
|
<TableHead className="font-semibold text-slate-700 text-xs uppercase text-right">Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{filteredOrders.length === 0 ? (
|
|
<TableRow>
|
|
<TableCell colSpan={9} className="text-center py-12 text-slate-500">
|
|
<Package className="w-12 h-12 mx-auto mb-3 text-slate-300" />
|
|
<p className="font-medium">No orders found</p>
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
filteredOrders.map((order) => {
|
|
const assignedCount = order.assigned_staff?.length || 0;
|
|
const requestedCount = order.requested || 0;
|
|
const assignmentProgress = requestedCount > 0 ? Math.round((assignedCount / requestedCount) * 100) : 0;
|
|
const { startTime, endTime } = getEventTimes(order);
|
|
|
|
return (
|
|
<TableRow key={order.id} className="hover:bg-slate-50">
|
|
<TableCell>
|
|
<div className="flex items-center gap-2">
|
|
<Building2 className="w-4 h-4 text-blue-600" />
|
|
<span className="text-sm font-medium text-slate-900">{order.business_name || "Primary Location"}</span>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="flex items-center gap-2">
|
|
<MapPin className="w-4 h-4 text-purple-600" />
|
|
<span className="text-sm text-slate-700">{order.hub || "Main Hub"}</span>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<p className="font-semibold text-slate-900">{order.event_name || "Untitled Event"}</p>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div>
|
|
<p className="font-medium text-slate-900">{safeFormatDate(order.date, 'MM.dd.yyyy')}</p>
|
|
<p className="text-xs text-slate-500 flex items-center gap-1">
|
|
<Clock className="w-3 h-3" />
|
|
{startTime} - {endTime}
|
|
</p>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
{getStatusBadge(order)}
|
|
</TableCell>
|
|
<TableCell>
|
|
<span className="text-lg font-bold text-slate-900">{requestedCount}</span>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="flex flex-col items-center gap-1">
|
|
<div className="w-10 h-10 bg-emerald-500 rounded-full flex items-center justify-center">
|
|
<span className="text-white font-bold text-sm">{assignedCount}</span>
|
|
</div>
|
|
<span className="text-xs text-emerald-600 font-semibold">{assignmentProgress}%</span>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<button className="w-8 h-8 flex items-center justify-center hover:bg-slate-100 rounded transition-colors">
|
|
<FileText className="w-5 h-5 text-slate-400" />
|
|
</button>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="flex items-center justify-end gap-2">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => handleViewOrder(order)}
|
|
className="h-8 w-8 p-0"
|
|
title="View"
|
|
>
|
|
<Eye className="w-4 h-4" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 p-0"
|
|
title="Notifications"
|
|
>
|
|
<Bell className="w-4 h-4" />
|
|
</Button>
|
|
{canEditOrder(order) && (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => navigate(createPageUrl(`EditEvent?id=${order.id}`))}
|
|
className="h-8 w-8 p-0"
|
|
title="Edit"
|
|
>
|
|
<Edit3 className="w-4 h-4" />
|
|
</Button>
|
|
)}
|
|
{canCancelOrder(order) && (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => handleCancelOrder(order)}
|
|
className="h-8 w-8 p-0 text-red-600 hover:text-red-700 hover:bg-red-50"
|
|
title="Cancel"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
})
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
<OrderDetailModal
|
|
open={viewOrderModal}
|
|
onClose={() => setViewOrderModal(false)}
|
|
order={selectedOrder}
|
|
onCancel={handleCancelOrder}
|
|
/>
|
|
|
|
<Dialog open={cancelDialogOpen} onOpenChange={setCancelDialogOpen}> {/* Updated open and onOpenChange */}
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2 text-red-600">
|
|
<AlertTriangle className="w-5 h-5" />
|
|
Cancel Order?
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
Are you sure you want to cancel this order? This action cannot be undone.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
{orderToCancel && ( // Using orderToCancel
|
|
<div className="bg-slate-50 rounded-lg p-4 space-y-2">
|
|
<p className="font-bold text-slate-900">{orderToCancel.event_name}</p>
|
|
<div className="flex items-center gap-2 text-sm text-slate-600">
|
|
<Calendar className="w-4 h-4" />
|
|
{orderToCancel.date ? format(new Date(orderToCancel.date), "MMMM d, yyyy") : "—"}
|
|
</div>
|
|
<div className="flex items-center gap-2 text-sm text-slate-600">
|
|
<MapPin className="w-4 h-4" />
|
|
{orderToCancel.hub || orderToCancel.event_location}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<DialogFooter>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setCancelDialogOpen(false)} // Updated
|
|
>
|
|
Keep Order
|
|
</Button>
|
|
<Button
|
|
variant="destructive"
|
|
onClick={confirmCancel}
|
|
disabled={cancelOrderMutation.isPending}
|
|
>
|
|
{cancelOrderMutation.isPending ? "Canceling..." : "Yes, Cancel Order"}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
} |