feat: Initialize monorepo structure and comprehensive documentation

This commit establishes the new monorepo architecture for the KROW Workforce platform.

Key changes include:
- Reorganized project into `frontend-web`, `mobile-apps`, `firebase`, `scripts`, and `secrets` directories.
- Updated `Makefile` to support the new monorepo layout and automate Base44 export integration.
- Fixed `scripts/prepare-export.js` for ES module compatibility and global component import resolution.
- Created and updated `CONTRIBUTING.md` for developer onboarding.
- Restructured, renamed, and translated all `docs/` files for clarity and consistency.
- Implemented an interactive internal launchpad with diagram viewing capabilities.
- Configured base Firebase project files (`firebase.json`, security rules).
- Updated `README.md` to reflect the new project structure and documentation overview.
This commit is contained in:
bwnyasse
2025-11-12 12:50:55 -05:00
parent 92fd0118be
commit 554dc9f9e3
203 changed files with 1414 additions and 732 deletions

View File

@@ -0,0 +1,863 @@
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>
);
}