Files
Krow-workspace/frontend-web/src/pages/VendorOrders.jsx
bwnyasse 80cd49deb5 feat(Makefile): install frontend dependencies on dev command
feat(Makefile): patch Layout.jsx queryKey for local development
feat(frontend-web): mock base44 client for local development with role switching
feat(frontend-web): add event assignment modal with conflict detection and bulk assign
feat(frontend-web): add client dashboard with key metrics and quick actions
feat(frontend-web): add layout component with role-based navigation
feat(frontend-web): update various pages to use "@/components" alias
feat(frontend-web): update create event page with ai assistant toggle
feat(frontend-web): update dashboard page with new components
feat(frontend-web): update events page with quick assign popover
feat(frontend-web): update invite vendor page with hover card
feat(frontend-web): update messages page with conversation list and message thread
feat(frontend-web): update operator dashboard page with new components
feat(frontend-web): update partner management page with new components
feat(frontend-web): update permissions page with new components
feat(frontend-web): update procurement dashboard page with new components
feat(frontend-web): update smart vendor onboarding page with new components
feat(frontend-web): update staff directory page with new components
feat(frontend-web): update teams page with new components
feat(frontend-web): update user management page with new components
feat(frontend-web): update vendor compliance page with new components
feat(frontend-web): update main.jsx to include react query provider

feat: add vendor marketplace page
feat: add global import fix to prepare-export script
feat: add patch-layout-query-key script to fix query key
feat: update patch-base44-client script to use a more robust method
2025-11-13 14:56:31 -05:00

864 lines
35 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState } from "react";
import { base44 } from "@/api/base44Client";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Plus, Search, Filter, Download, LayoutGrid, List, Eye, Edit, Trash2, MoreHorizontal, Users, UserPlus, RefreshCw, Copy, Calendar as CalendarIcon, ArrowUpDown, Check, Bell, Send, FileText, Zap } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent } from "@/components/ui/card";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Calendar } from "@/components/ui/calendar";
import { format, isToday, addDays } from "date-fns";
import { useToast } from "@/components/ui/use-toast";
import { Link, useNavigate } from "react-router-dom";
import { createPageUrl } from "@/utils";
import EventAssignmentModal from "@/components/events/EventAssignmentModal";
const getStatusColor = (order) => {
// Check for Rapid Request first
if (order.is_rapid_request) {
return "bg-red-500 text-white";
}
if (!order.shifts_data || order.shifts_data.length === 0) {
return "bg-orange-500 text-white";
}
let totalNeeded = 0;
let totalAssigned = 0;
order.shifts_data.forEach(shift => {
shift.roles.forEach(role => {
const needed = parseInt(role.count) || 0;
const assigned = role.assignments?.length || 0;
totalNeeded += needed;
totalAssigned += assigned;
});
});
if (totalNeeded === 0) return "bg-orange-500 text-white";
if (totalAssigned === 0) return "bg-orange-500 text-white";
if (totalAssigned < totalNeeded) return "bg-blue-500 text-white";
if (totalAssigned >= totalNeeded) return "bg-green-500 text-white";
return "bg-slate-500 text-white";
};
const getStatusText = (order) => {
if (order.is_rapid_request) return "Rapid Request";
if (!order.shifts_data || order.shifts_data.length === 0) return "Pending";
let totalNeeded = 0;
let totalAssigned = 0;
order.shifts_data.forEach(shift => {
shift.roles.forEach(role => {
totalNeeded += parseInt(role.count) || 0;
totalAssigned += role.assignments?.length || 0;
});
});
if (totalAssigned === 0) return "Pending";
if (totalAssigned < totalNeeded) return "Partially Filled";
if (totalAssigned >= totalNeeded && totalNeeded > 0) return "Fully Staffed";
return "Pending";
};
const convertTo12Hour = (time24) => {
if (!time24) return '';
const [hours, minutes] = time24.split(':');
const hour = parseInt(hours);
const ampm = hour >= 12 ? 'PM' : 'AM';
const hour12 = hour % 12 || 12;
return `${hour12}:${minutes} ${ampm}`;
};
export default function VendorOrders() {
const [searchQuery, setSearchQuery] = useState("");
const [filters, setFilters] = useState({ status: "all", hub: "all" });
const [viewMode, setViewMode] = useState("list");
const [sortBy, setSortBy] = useState("date");
const [sortOrder, setSortOrder] = useState("desc");
const [selectedDate, setSelectedDate] = useState(null);
const [notifyingOrders, setNotifyingOrders] = useState(new Set());
const [assignmentModal, setAssignmentModal] = useState({ open: false, order: null });
const queryClient = useQueryClient();
const { toast } = useToast();
const navigate = useNavigate();
const { data: user } = useQuery({
queryKey: ['current-user-vendor-orders'],
queryFn: () => base44.auth.me(),
});
const { data: allOrders = [], isLoading } = useQuery({
queryKey: ['orders'],
queryFn: () => base44.entities.Order.list('-created_date'),
});
const { data: invoices = [] } = useQuery({
queryKey: ['invoices'],
queryFn: () => base44.entities.Invoice.list(),
});
// Filter orders for this vendor only
const orders = allOrders.filter(order =>
order.vendor_id === user?.id ||
order.vendor_name === user?.company_name ||
order.created_by === user?.email
);
const getInvoiceForOrder = (orderId) => {
return invoices.find(inv => inv.order_id === orderId);
};
const sendNotificationToStaff = async (order, assignment) => {
const employees = await base44.entities.Employee.filter({ id: assignment.employee_id });
if (employees.length === 0) return { success: false, method: 'none', reason: 'Employee not found' };
const employee = employees[0];
const formattedDate = assignment.shift_date ?
format(new Date(assignment.shift_date), 'EEEE, MMMM d, yyyy') :
'TBD';
const startTime = assignment.shift_start ? convertTo12Hour(assignment.shift_start) : 'TBD';
const endTime = assignment.shift_end ? convertTo12Hour(assignment.shift_end) : 'TBD';
const emailBody = `
Hi ${assignment.employee_name},
Great news! You've been assigned to a new shift.
EVENT DETAILS:
━━━━━━━━━━━━━━━━━━━━
📅 Event: ${order.event_name}
🏢 Client: ${order.client_business}
📍 Location: ${assignment.location || 'TBD'}
🏠 Hub: ${assignment.hub_location || order.hub_location}
SHIFT DETAILS:
━━━━━━━━━━━━━━━━━━━━
📆 Date: ${formattedDate}
🕐 Time: ${startTime} - ${endTime}
👔 Position: ${assignment.position}
${order.notes ? `\nADDITIONAL NOTES:\n${order.notes}\n` : ''}
Please confirm your availability as soon as possible.
If you have any questions, please contact your manager.
Thank you,
Legendary Event Staffing Team
`.trim();
const smsBody = `🎉 Legendary Event Staffing\n\nYou're assigned to ${order.event_name}!\n📆 ${formattedDate}\n🕐 ${startTime}-${endTime}\n👔 ${assignment.position}\n📍 ${assignment.location || order.hub_location}\n\nPlease confirm ASAP. Check email for full details.`;
let emailSent = false;
let smsSent = false;
if (employee.phone) {
try {
const cleanPhone = employee.phone.replace(/\D/g, '');
await base44.integrations.Core.InvokeLLM({
prompt: `Send an SMS text message to phone number ${cleanPhone} with the following content:\n\n${smsBody}\n\nUse a reliable SMS service to send this message. Return success status.`,
add_context_from_internet: false,
});
smsSent = true;
console.log(`✅ SMS sent to ${employee.phone}`);
} catch (error) {
console.error(`❌ Failed to send SMS to ${employee.phone}:`, error);
}
}
if (employee.email) {
try {
await base44.integrations.Core.SendEmail({
from_name: 'Legendary Event Staffing',
to: employee.email,
subject: `🎉 You've been assigned to ${order.event_name}`,
body: emailBody
});
emailSent = true;
console.log(`✅ Email sent to ${employee.email}`);
} catch (error) {
console.error(`❌ Failed to send email to ${employee.email}:`, error);
}
}
if (emailSent || smsSent) {
const methods = [];
if (smsSent) methods.push('SMS');
if (emailSent) methods.push('Email');
return { success: true, method: methods.join(' & '), employee: employee.full_name };
}
return { success: false, method: 'none', reason: 'No contact info or failed to send' };
};
const handleNotifyStaff = async (order) => {
if (notifyingOrders.has(order.id)) return;
setNotifyingOrders(prev => new Set(prev).add(order.id));
const allAssignments = [];
if (order.shifts_data) {
order.shifts_data.forEach((shift, shiftIdx) => {
shift.roles.forEach(role => {
if (role.assignments && role.assignments.length > 0) {
role.assignments.forEach(assignment => {
allAssignments.push({
...assignment,
shift_date: order.event_date,
shift_start: role.start_time,
shift_end: role.end_time,
position: role.service,
location: shift.address || order.event_address,
hub_location: order.hub_location,
});
});
}
});
});
}
if (allAssignments.length === 0) {
toast({
title: "No staff assigned",
description: "Please assign staff before sending notifications.",
variant: "destructive",
});
setNotifyingOrders(prev => {
const next = new Set(prev);
next.delete(order.id);
return next;
});
return;
}
toast({
title: "Sending notifications...",
description: `Notifying ${allAssignments.length} staff member${allAssignments.length > 1 ? 's' : ''} for "${order.event_name}"...`,
});
let successCount = 0;
const results = [];
for (const assignment of allAssignments) {
const result = await sendNotificationToStaff(order, assignment);
if (result.success) {
successCount++;
results.push(`${result.employee} (${result.method})`);
} else {
results.push(`${assignment.employee_name} (${result.reason})`);
}
}
toast({
title: `Notifications sent!`,
description: `Successfully notified ${successCount} of ${allAssignments.length} staff members for "${order.event_name}".`,
});
setNotifyingOrders(prev => {
const next = new Set(prev);
next.delete(order.id);
return next;
});
};
const handleSort = (field) => {
if (sortBy === field) {
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
} else {
setSortBy(field);
setSortOrder('asc');
}
};
const handleOpenAssignment = (order) => {
setAssignmentModal({ open: true, order });
};
const handleCloseAssignment = () => {
setAssignmentModal({ open: false, order: null });
};
const filteredOrders = orders.filter(order => {
const matchesSearch = order.event_name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
order.client_business?.toLowerCase().includes(searchQuery.toLowerCase()) ||
order.manager?.toLowerCase().includes(searchQuery.toLowerCase());
// Updated status filtering to include "Rapid Request" and "Partially Filled"
let matchesStatus = filters.status === 'all';
if (!matchesStatus) {
const statusText = getStatusText(order);
if (filters.status === 'Rapid Request') {
matchesStatus = order.is_rapid_request;
} else {
matchesStatus = statusText === filters.status;
}
}
const matchesHub = filters.hub === 'all' || order.hub_location === filters.hub;
const matchesDate = !selectedDate || order.event_date === format(selectedDate, 'yyyy-MM-dd');
return matchesSearch && matchesStatus && matchesHub && matchesDate;
}).sort((a, b) => {
let compareA, compareB;
switch(sortBy) {
case 'business':
compareA = a.client_business || '';
compareB = b.client_business || '';
break;
case 'status':
compareA = getStatusText(a);
compareB = getStatusText(b);
break;
case 'date':
compareA = a.event_date || '';
compareB = b.event_date || '';
break;
default:
return 0;
}
if (compareA < compareB) return sortOrder === 'asc' ? -1 : 1;
if (compareA > compareB) return sortOrder === 'asc' ? 1 : -1;
return 0;
});
const hubs = [...new Set(orders.map(o => o.hub_location).filter(Boolean))];
const today = format(new Date(), 'yyyy-MM-dd');
const todaysOrders = orders.filter(o => o.event_date === today);
const totalStaffToday = todaysOrders.reduce((sum, order) => {
if (!order.shifts_data) return sum;
return sum + order.shifts_data.reduce((shiftSum, shift) => {
return shiftSum + shift.roles.reduce((roleSum, role) => {
return roleSum + (role.assignments?.length || 0);
}, 0);
}, 0);
}, 0);
const rapidRequestToday = todaysOrders.filter(o => o.is_rapid_request).length;
const pendingToday = todaysOrders.filter(o => getStatusText(o) === 'Pending').length;
const partiallyFilledToday = todaysOrders.filter(o => getStatusText(o) === 'Partially Filled').length;
const fullyStaffedToday = todaysOrders.filter(o => getStatusText(o) === 'Fully Staffed').length;
return (
<div className="p-6 md:p-8 space-y-6 bg-slate-50 min-h-screen">
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
<div>
<h1 className="text-3xl font-bold text-slate-900 tracking-tight">Orders Dashboard</h1>
<p className="text-slate-500 mt-1">Manage and track all event orders</p>
</div>
<div className="flex gap-2">
<Button variant="outline" className="gap-2">
Draft
</Button>
<Link to={createPageUrl("CreateEvent")}>
<Button className="bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white shadow-lg gap-2">
<Plus className="w-5 h-5" />
Create Order
</Button>
</Link>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4">
<Card className="bg-gradient-to-br from-blue-500 to-blue-600 border-0 text-white">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-blue-100 text-sm mb-2">Total Staff Today</p>
<p className="text-4xl font-bold">{totalStaffToday}</p>
</div>
<Users className="w-12 h-12 text-blue-200 opacity-50" />
</div>
</CardContent>
</Card>
<Card className="bg-white border-slate-200 bg-gradient-to-br from-red-50 to-white">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500 mb-2">Rapid Request</p>
<p className="text-3xl font-bold text-red-600">{rapidRequestToday}</p>
</div>
<div className="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center">
<Zap className="w-6 h-6 text-red-600" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-white border-slate-200">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500 mb-2">Pending</p>
<p className="text-3xl font-bold text-orange-600">{pendingToday}</p>
</div>
<div className="w-12 h-12 bg-orange-100 rounded-full flex items-center justify-center">
<CalendarIcon className="w-6 h-6 text-orange-600" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-white border-slate-200">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500 mb-2">Partially Filled</p>
<p className="text-3xl font-bold text-blue-600">{partiallyFilledToday}</p>
</div>
<div className="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center">
<Users className="w-6 h-6 text-blue-600" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-white border-slate-200">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500 mb-2">Fully Staffed</p>
<p className="text-3xl font-bold text-green-600">{fullyStaffedToday}</p>
</div>
<div className="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center">
<Check className="w-6 h-6 text-green-600" />
</div>
</div>
</CardContent>
</Card>
</div>
<div className="flex flex-col sm:flex-row gap-4 items-center">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<Input
placeholder="Search by event, business, or manager..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 bg-white border-slate-200"
/>
</div>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="gap-2">
<CalendarIcon className="w-4 h-4" />
{selectedDate ? format(selectedDate, 'MMM d, yyyy') : 'Filter by Date'}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar
mode="single"
selected={selectedDate}
onSelect={setSelectedDate}
/>
{selectedDate && (
<div className="p-2 border-t">
<Button
variant="ghost"
size="sm"
className="w-full"
onClick={() => setSelectedDate(null)}
>
Clear Filter
</Button>
</div>
)}
</PopoverContent>
</Popover>
<Select value={filters.status} onValueChange={(value) => setFilters({...filters, status: value})}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="All Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="Rapid Request">
<div className="flex items-center gap-2">
<Zap className="w-4 h-4 text-red-600" />
Rapid Request
</div>
</SelectItem>
<SelectItem value="Pending">Pending</SelectItem>
<SelectItem value="Partially Filled">Partially Filled</SelectItem>
<SelectItem value="Fully Staffed">Fully Staffed</SelectItem>
</SelectContent>
</Select>
<div className="flex gap-2">
<Button
variant={viewMode === 'cards' ? 'default' : 'outline'}
size="icon"
onClick={() => setViewMode('cards')}
>
<LayoutGrid className="w-4 h-4" />
</Button>
<Button
variant={viewMode === 'list' ? 'default' : 'outline'}
size="icon"
onClick={() => setViewMode('list')}
>
<List className="w-4 h-4" />
</Button>
</div>
</div>
{viewMode === 'list' ? (
<Card className="bg-white border-slate-200 shadow-sm">
<CardContent className="p-0">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50 border-b border-slate-200">
<tr>
<th className="text-left px-4 py-3 text-xs font-semibold text-slate-700">
<button
onClick={() => handleSort('business')}
className="flex items-center gap-1 hover:text-blue-600"
>
Business
<ArrowUpDown className="w-3 h-3" />
</button>
</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-slate-700">Hub</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-slate-700">Event Name</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-slate-700">
<button
onClick={() => handleSort('status')}
className="flex items-center gap-1 hover:text-blue-600"
>
Status
<ArrowUpDown className="w-3 h-3" />
</button>
</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-slate-700">
<button
onClick={() => handleSort('date')}
className="flex items-center gap-1 hover:text-blue-600"
>
Date
<ArrowUpDown className="w-3 h-3" />
</button>
</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-slate-700">Requested</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-slate-700">Assigned</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-slate-700">Invoice</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-slate-700">Actions</th>
</tr>
</thead>
<tbody>
{filteredOrders.map((order) => {
let totalNeeded = 0;
let totalAssigned = 0;
if (order.shifts_data) {
order.shifts_data.forEach(shift => {
shift.roles.forEach(role => {
totalNeeded += parseInt(role.count) || 0;
totalAssigned += role.assignments?.length || 0;
});
});
}
const statusText = getStatusText(order);
const isNotifying = notifyingOrders.has(order.id);
const invoice = getInvoiceForOrder(order.id);
return (
<tr
key={order.id}
className="border-b border-slate-100 hover:bg-slate-50 transition-colors cursor-pointer"
onClick={() => handleOpenAssignment(order)}
>
<td className="px-4 py-3">
<div className="text-sm font-semibold text-slate-900">
{order.client_business || 'N/A'}
</div>
</td>
<td className="px-4 py-3">
<Badge variant="outline" className="text-xs">
{order.hub_location}
</Badge>
</td>
<td className="px-4 py-3">
<div className="text-sm font-medium text-slate-900">
{order.event_name}
</div>
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<Badge className={`${getStatusColor(order)} rounded-full px-3 text-xs`}>
{statusText}
</Badge>
{order.include_backup && order.backup_staff_count > 0 && (
<Badge className="bg-green-100 text-green-700 text-xs">
🛡 {order.backup_staff_count}
</Badge>
)}
</div>
</td>
<td className="px-4 py-3 text-sm text-slate-600">
{order.event_date ? format(new Date(order.event_date), 'MM/dd/yy') : 'N/A'}
</td>
<td className="px-4 py-3 text-sm text-slate-600">{totalNeeded}</td>
<td className="px-4 py-3">
<Badge className={`${totalAssigned >= totalNeeded && totalNeeded > 0 ? 'bg-green-100 text-green-700' : 'bg-orange-100 text-orange-700'} font-semibold`}>
{totalAssigned}/{totalNeeded}
</Badge>
</td>
<td className="px-4 py-3">
{invoice ? (
<Link to={`${createPageUrl("InvoiceDetail")}?id=${invoice.id}`} onClick={(e) => e.stopPropagation()}>
<Button variant="ghost" size="sm" className="gap-2 text-blue-600 hover:text-blue-700" title="View Invoice">
<FileText className="w-4 h-4" />
<span className="text-xs font-mono">{invoice.invoice_number}</span>
</Button>
</Link>
) : (
<span className="text-xs text-slate-400"></span>
)}
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={(e) => {
e.stopPropagation();
handleNotifyStaff(order);
}}
disabled={isNotifying || totalAssigned === 0}
title="Notify all assigned staff"
>
{isNotifying ? (
<RefreshCw className="w-4 h-4 animate-spin" />
) : (
<Send className="w-4 h-4" />
)}
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={(e) => {
e.stopPropagation();
handleOpenAssignment(order);
}}
title="Assign staff"
>
<Users className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
title="Edit order"
>
<Edit className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
title="Copy order"
>
<Copy className="w-4 h-4" />
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
<Button variant="ghost" size="icon" className="h-8 w-8">
<MoreHorizontal className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{invoice && (
<DropdownMenuItem onClick={() => navigate(`${createPageUrl("InvoiceDetail")}?id=${invoice.id}`)}>
<FileText className="w-4 h-4 mr-2" />
View Invoice
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={() => handleNotifyStaff(order)}
disabled={totalAssigned === 0 || isNotifying}
>
<Send className="w-4 h-4 mr-2" />
Notify All Staff
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
<div className="flex items-center justify-between px-4 py-3 border-t border-slate-200">
<p className="text-sm text-slate-600">Showing {filteredOrders.length} orders</p>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm">Previous</Button>
<Button variant="outline" size="sm">Next</Button>
</div>
</div>
</CardContent>
</Card>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{filteredOrders.map((order) => {
let totalNeeded = 0;
let totalAssigned = 0;
if (order.shifts_data) {
order.shifts_data.forEach(shift => {
shift.roles.forEach(role => {
totalNeeded += parseInt(role.count) || 0;
totalAssigned += role.assignments?.length || 0;
});
});
}
const statusText = getStatusText(order);
const invoice = getInvoiceForOrder(order.id);
return (
<Card
key={order.id}
className="relative hover:shadow-lg transition-all cursor-pointer"
onClick={() => handleOpenAssignment(order)}
>
{invoice && (
<Link to={`${createPageUrl("InvoiceDetail")}?id=${invoice.id}`} onClick={(e) => e.stopPropagation()}>
<Button
variant="ghost"
size="sm"
className="absolute top-2 right-2 z-10 gap-2 bg-white/90 hover:bg-white shadow-sm"
title="View Invoice"
>
<FileText className="w-4 h-4 text-blue-600" />
<span className="text-xs font-mono text-blue-600">{invoice.invoice_number}</span>
</Button>
</Link>
)}
<CardContent className="p-6">
<div className="space-y-4">
<div className="flex items-start justify-between">
<div className="flex-1">
<h3 className="text-xl font-bold text-slate-900 mb-1">
{order.event_name}
</h3>
<p className="text-sm text-slate-600">
{order.client_business}
</p>
</div>
<div className="flex flex-col gap-2">
<Badge className={`${getStatusColor(order)} rounded-full px-3`}>
{statusText}
</Badge>
{order.include_backup && order.backup_staff_count > 0 && (
<Badge className="bg-green-100 text-green-700">
🛡 {order.backup_staff_count} Backup
</Badge>
)}
</div>
</div>
<div className="flex items-center gap-2 text-slate-700">
<CalendarIcon className="w-5 h-5 text-blue-600" />
<span className="font-medium">
{order.event_date ? format(new Date(order.event_date), 'MMMM d, yyyy') : 'No date'}
</span>
</div>
<div className="flex items-center justify-between p-3 bg-slate-50 rounded-lg">
<div className="flex items-center gap-2">
<Users className="w-5 h-5 text-purple-600" />
<span className="font-semibold text-slate-700">Staff</span>
</div>
<Badge
className={`${totalAssigned >= totalNeeded && totalNeeded > 0 ? 'bg-green-100 text-green-700' : 'bg-orange-100 text-orange-700'} font-bold border-2`}
style={{
borderColor: totalAssigned >= totalNeeded && totalNeeded > 0 ? '#10b981' : '#f97316'
}}
>
{totalAssigned} / {totalNeeded}
</Badge>
</div>
<div className="flex gap-2">
<Button
variant="outline"
className="flex-1"
onClick={(e) => {
e.stopPropagation();
handleNotifyStaff(order);
}}
disabled={totalAssigned === 0}
>
<Send className="w-4 h-4 mr-2" />
Notify Staff
</Button>
{invoice && (
<Link to={`${createPageUrl("InvoiceDetail")}?id=${invoice.id}`} className="flex-1" onClick={(e) => e.stopPropagation()}>
<Button variant="outline" className="w-full">
<FileText className="w-4 h-4 mr-2" />
View Invoice
</Button>
</Link>
)}
</div>
</div>
</CardContent>
</Card>
);
})}
</div>
)}
{filteredOrders.length === 0 && (
<div className="text-center py-12">
<p className="text-slate-400 text-lg">No orders found</p>
<p className="text-sm text-slate-500 mt-2">Try adjusting your filters</p>
</div>
)}
{/* Event Assignment Modal */}
<EventAssignmentModal
open={assignmentModal.open}
onClose={handleCloseAssignment}
order={assignmentModal.order}
onUpdate={() => queryClient.invalidateQueries({ queryKey: ['orders'] })}
/>
</div>
);
}