import React, { useState, useMemo } from "react"; import { base44 } from "@/api/base44Client"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Checkbox } from "@/components/ui/checkbox"; import { Calendar, Users, Check, Plus, X, Clock, MapPin, ChevronLeft, ChevronRight, AlertTriangle, RefreshCw, Search } from "lucide-react"; import { format, parseISO, isWithinInterval } from "date-fns"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Input } from "@/components/ui/input"; import { useToast } from "@/components/ui/use-toast"; 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}`; }; const getInitials = (name) => { if (!name) return 'S'; return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2); }; const avatarColors = [ 'bg-blue-500', 'bg-purple-500', 'bg-green-500', 'bg-orange-500', 'bg-pink-500', 'bg-indigo-500', 'bg-teal-500', 'bg-red-500', ]; // Helper to check if times overlap const hasTimeConflict = (existingStart, existingEnd, newStart, newEnd, existingDate, newDate) => { // If different dates, no conflict if (existingDate !== newDate) return false; if (!existingStart || !existingEnd || !newStart || !newEnd) return false; const parseTime = (time) => { const [hours, minutes] = time.split(':').map(Number); return hours * 60 + minutes; }; const existingStartMin = parseTime(existingStart); const existingEndMin = parseTime(existingEnd); const newStartMin = parseTime(newStart); const newEndMin = parseTime(newEnd); return (newStartMin < existingEndMin && newEndMin > existingStartMin); }; export default function EventAssignmentModal({ open, onClose, order, onUpdate }) { const [selectedShiftIndex, setSelectedShiftIndex] = useState(0); const [selectedRoleIndex, setSelectedRoleIndex] = useState(0); const [selectedStaffIds, setSelectedStaffIds] = useState([]); const [searchTerm, setSearchTerm] = useState(""); const [swapMode, setSwapMode] = useState(null); // { employeeId, assignmentIndex } const queryClient = useQueryClient(); const { toast } = useToast(); const { data: allStaff = [] } = useQuery({ queryKey: ['staff-for-assignment'], queryFn: () => base44.entities.Staff.list(), enabled: open, }); const { data: allOrders = [] } = useQuery({ queryKey: ['orders-for-conflict-check'], queryFn: () => base44.entities.Order.list(), enabled: open, }); const updateOrderMutation = useMutation({ mutationFn: (updatedOrder) => base44.entities.Order.update(order.id, updatedOrder), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['orders'] }); if (onUpdate) onUpdate(); setSelectedStaffIds([]); toast({ title: "✅ Staff assigned successfully", description: "The order has been updated with new assignments.", }); }, }); if (!order || !order.shifts_data || order.shifts_data.length === 0) { return null; } const currentShift = order.shifts_data[selectedShiftIndex]; const currentRole = currentShift?.roles[selectedRoleIndex]; if (!currentRole) return null; // Check for conflicts const getStaffConflicts = (staffId) => { const conflicts = []; allOrders.forEach(existingOrder => { if (existingOrder.id === order.id) return; // Skip current order if (!existingOrder.shifts_data) return; existingOrder.shifts_data.forEach(shift => { shift.roles.forEach(role => { if (!role.assignments) return; role.assignments.forEach(assignment => { if (assignment.employee_id === staffId) { const hasConflict = hasTimeConflict( role.start_time, role.end_time, currentRole.start_time, currentRole.end_time, existingOrder.event_date, order.event_date ); if (hasConflict) { conflicts.push({ orderName: existingOrder.event_name, role: role.service, time: `${convertTo12Hour(role.start_time)} - ${convertTo12Hour(role.end_time)}`, date: existingOrder.event_date }); } } }); }); }); }); return conflicts; }; // Role-based filtering const roleKeywords = { 'bartender': ['bartender', 'bar'], 'cook': ['cook', 'chef', 'kitchen'], 'server': ['server', 'waiter', 'waitress'], 'cashier': ['cashier', 'register'], 'host': ['host', 'hostess', 'greeter'], 'busser': ['busser', 'bus'], 'dishwasher': ['dishwasher', 'dish'], 'manager': ['manager', 'supervisor'], }; const getRoleCategory = (roleName) => { const lowerRole = roleName.toLowerCase(); for (const [category, keywords] of Object.entries(roleKeywords)) { if (keywords.some(keyword => lowerRole.includes(keyword))) { return category; } } return null; }; const matchesRole = (staffPosition, requiredRole) => { const staffLower = (staffPosition || '').toLowerCase(); const requiredLower = (requiredRole || '').toLowerCase(); // Direct match if (staffLower.includes(requiredLower) || requiredLower.includes(staffLower)) { return true; } // Category match const staffCategory = getRoleCategory(staffPosition || ''); const requiredCategory = getRoleCategory(requiredRole); return staffCategory && requiredCategory && staffCategory === requiredCategory; }; const handleBulkAssign = () => { if (selectedStaffIds.length === 0) { toast({ title: "No staff selected", description: "Please select staff members to assign.", variant: "destructive", }); return; } const updatedOrder = { ...order }; const assignments = updatedOrder.shifts_data[selectedShiftIndex].roles[selectedRoleIndex].assignments || []; // Check for conflicts const conflictingStaff = []; selectedStaffIds.forEach(staffId => { const conflicts = getStaffConflicts(staffId); if (conflicts.length > 0) { const staff = allStaff.find(s => s.id === staffId); conflictingStaff.push(staff?.employee_name); } }); if (conflictingStaff.length > 0) { toast({ title: "⚠️ Time Conflict Detected", description: `${conflictingStaff.join(', ')} ${conflictingStaff.length === 1 ? 'is' : 'are'} already assigned to overlapping shifts.`, variant: "destructive", }); return; } const needed = parseInt(currentRole.count) || 0; const currentAssigned = assignments.length; const remaining = needed - currentAssigned; if (selectedStaffIds.length > remaining) { toast({ title: "Too many selected", description: `Only ${remaining} more staff ${remaining === 1 ? 'is' : 'are'} needed.`, variant: "destructive", }); return; } // Add all selected staff const newAssignments = selectedStaffIds.map(staffId => { const staff = allStaff.find(s => s.id === staffId); return { employee_id: staff.id, employee_name: staff.employee_name, position: currentRole.service, shift_date: order.event_date, shift_start: currentRole.start_time, shift_end: currentRole.end_time, location: currentShift.address || order.event_address, hub_location: order.hub_location, }; }); updatedOrder.shifts_data[selectedShiftIndex].roles[selectedRoleIndex].assignments = [ ...assignments, ...newAssignments ]; updateOrderMutation.mutate(updatedOrder); }; const handleAssignStaff = (staffMember) => { const updatedOrder = { ...order }; const assignments = updatedOrder.shifts_data[selectedShiftIndex].roles[selectedRoleIndex].assignments || []; // Check if already assigned if (assignments.some(a => a.employee_id === staffMember.id)) { toast({ title: "Already assigned", description: `${staffMember.employee_name} is already assigned to this role.`, variant: "destructive", }); return; } // Check for conflicts const conflicts = getStaffConflicts(staffMember.id); if (conflicts.length > 0) { toast({ title: "⚠️ Time Conflict", description: `${staffMember.employee_name} is already assigned to ${conflicts[0].orderName} at ${conflicts[0].time}`, variant: "destructive", }); return; } // Add new assignment const newAssignment = { employee_id: staffMember.id, employee_name: staffMember.employee_name, position: currentRole.service, shift_date: order.event_date, shift_start: currentRole.start_time, shift_end: currentRole.end_time, location: currentShift.address || order.event_address, hub_location: order.hub_location, }; updatedOrder.shifts_data[selectedShiftIndex].roles[selectedRoleIndex].assignments = [ ...assignments, newAssignment ]; updateOrderMutation.mutate(updatedOrder); }; const handleRemoveStaff = (employeeId) => { const updatedOrder = { ...order }; const assignments = updatedOrder.shifts_data[selectedShiftIndex].roles[selectedRoleIndex].assignments || []; updatedOrder.shifts_data[selectedShiftIndex].roles[selectedRoleIndex].assignments = assignments.filter(a => a.employee_id !== employeeId); updateOrderMutation.mutate(updatedOrder); }; const handleSwapStaff = (newStaffMember) => { if (!swapMode) return; const updatedOrder = { ...order }; const assignments = updatedOrder.shifts_data[selectedShiftIndex].roles[selectedRoleIndex].assignments || []; // Check for conflicts const conflicts = getStaffConflicts(newStaffMember.id); if (conflicts.length > 0) { toast({ title: "⚠️ Time Conflict", description: `${newStaffMember.employee_name} is already assigned to ${conflicts[0].orderName} at ${conflicts[0].time}`, variant: "destructive", }); return; } // Replace the assignment assignments[swapMode.assignmentIndex] = { employee_id: newStaffMember.id, employee_name: newStaffMember.employee_name, position: currentRole.service, shift_date: order.event_date, shift_start: currentRole.start_time, shift_end: currentRole.end_time, location: currentShift.address || order.event_address, hub_location: order.hub_location, }; updatedOrder.shifts_data[selectedShiftIndex].roles[selectedRoleIndex].assignments = assignments; updateOrderMutation.mutate(updatedOrder); setSwapMode(null); }; const assignments = currentRole.assignments || []; const needed = parseInt(currentRole.count) || 0; const assigned = assignments.length; const isFullyStaffed = assigned >= needed; // Filter staff by role and exclude already assigned const assignedIds = new Set(assignments.map(a => a.employee_id)); const roleFilteredStaff = allStaff.filter(s => matchesRole(s.position, currentRole.service) || matchesRole(s.position_2, currentRole.service) ); const availableStaff = roleFilteredStaff .filter(s => !assignedIds.has(s.id)) .filter(s => { if (!searchTerm) return true; const lowerSearch = searchTerm.toLowerCase(); return ( s.employee_name?.toLowerCase().includes(lowerSearch) || s.position?.toLowerCase().includes(lowerSearch) || s.position_2?.toLowerCase().includes(lowerSearch) ); }); // Calculate total assignments across all roles in this shift let totalNeeded = 0; let totalAssigned = 0; currentShift.roles.forEach(role => { totalNeeded += parseInt(role.count) || 0; totalAssigned += role.assignments?.length || 0; }); const toggleStaffSelection = (staffId) => { setSelectedStaffIds(prev => prev.includes(staffId) ? prev.filter(id => id !== staffId) : [...prev, staffId] ); }; const selectAll = () => { const remaining = needed - assigned; const selectableStaff = availableStaff.slice(0, remaining); setSelectedStaffIds(selectableStaff.map(s => s.id)); }; const deselectAll = () => { setSelectedStaffIds([]); }; return (
{order.event_name}

{order.client_business}

= totalNeeded ? 'bg-emerald-500 text-white border-0 animate-pulse' : 'bg-orange-500 text-white border-0' } font-semibold px-3 py-1 shadow-md`} > {totalAssigned >= totalNeeded ? ( <> Fully Staffed ) : ( <> Needs Staff )}
{order.event_date ? format(new Date(order.event_date), 'EEEE, MMMM d, yyyy') : 'No date'}
{currentShift.address && (
{currentShift.address}
)}
{/* Staff Assignment Summary */}

Staff Assignment

= totalNeeded ? 'border-emerald-500 text-emerald-700 bg-emerald-50' : 'border-orange-500 text-orange-700 bg-orange-50' }`} > {totalAssigned} / {totalNeeded}
{totalAssigned >= totalNeeded && (
✨ Fully staffed - All positions filled!
)}
{/* Position Selection */} {currentShift.roles.length > 1 && (
)} {/* Current Position Details */}

{currentRole.service}

= needed ? 'bg-emerald-500 text-white' : 'bg-orange-500 text-white' } font-bold px-3 py-1 text-base`} > {assigned}/{needed}
{currentRole.start_time && (
{convertTo12Hour(currentRole.start_time)} - {convertTo12Hour(currentRole.end_time)}
)}
{/* Swap Mode Banner */} {swapMode && (
Swap Mode Active - Select replacement for {assignments[swapMode.assignmentIndex]?.employee_name}
)} {/* Assigned Staff List */} {assignments.length > 0 && (

✅ Assigned Staff:

{assignments.map((assignment, idx) => { const conflicts = getStaffConflicts(assignment.employee_id); return (
{getInitials(assignment.employee_name)}

{assignment.employee_name}

{currentRole.service}

{conflicts.length > 0 && (
Time conflict detected
)}
); })}
)} {/* Add Staff Section */} {(assigned < needed || swapMode) && (

{swapMode ? '🔄 Select Replacement:' : '➕ Add Staff:'}

{!swapMode && availableStaff.length > 0 && (
{selectedStaffIds.length > 0 && ( <> )}
)}
{/* Search */}
setSearchTerm(e.target.value)} className="pl-10" />

Showing {availableStaff.length} {currentRole.service.toLowerCase()}(s) {roleFilteredStaff.length !== allStaff.length && ( (filtered by role) )}

{availableStaff.length > 0 ? (
{availableStaff.map((staff, idx) => { const isSelected = selectedStaffIds.includes(staff.id); const conflicts = getStaffConflicts(staff.id); const hasConflict = conflicts.length > 0; return (
{!swapMode && ( toggleStaffSelection(staff.id)} disabled={hasConflict} className="border-2" /> )} {getInitials(staff.employee_name)}

{staff.employee_name}

{staff.position || 'Staff Member'}

{staff.position_2 && ( {staff.position_2} )}
{hasConflict && (
Conflict: {conflicts[0].orderName} ({conflicts[0].time})
)}
); })}
) : (

{searchTerm ? 'No staff match your search' : `No available ${currentRole.service.toLowerCase()}s found` }

{searchTerm ? 'Try a different search term' : 'All matching staff have been assigned'}

)}
)}
{selectedShiftIndex > 0 && ( )} {selectedShiftIndex < order.shifts_data.length - 1 && ( )}
= totalNeeded ? 'bg-emerald-50 border-emerald-500 text-emerald-700' : 'bg-orange-50 border-orange-500 text-orange-700' }`} > {totalAssigned}/{totalNeeded} Filled
); }