Files
Krow-workspace/frontend-web/src/components/events/EventAssignmentModal.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

817 lines
31 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="max-w-3xl max-h-[90vh] overflow-hidden flex flex-col">
<DialogHeader className="border-b pb-4">
<div className="flex items-start justify-between">
<div className="flex-1">
<DialogTitle className="text-2xl font-bold text-slate-900 mb-1">
{order.event_name}
</DialogTitle>
<p className="text-sm text-slate-600">{order.client_business}</p>
</div>
<Badge
className={`${
totalAssigned >= 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 ? (
<>
<Check className="w-3 h-3 mr-1" />
Fully Staffed
</>
) : (
<>
<AlertTriangle className="w-3 h-3 mr-1" />
Needs Staff
</>
)}
</Badge>
</div>
<div className="flex items-center gap-2 text-slate-700 mt-3">
<Calendar className="w-4 h-4 text-blue-600" />
<span className="text-sm font-medium">
{order.event_date ? format(new Date(order.event_date), 'EEEE, MMMM d, yyyy') : 'No date'}
</span>
</div>
{currentShift.address && (
<div className="flex items-center gap-2 text-slate-700 mt-2">
<MapPin className="w-4 h-4 text-blue-600" />
<span className="text-sm">{currentShift.address}</span>
</div>
)}
</DialogHeader>
<div className="flex-1 overflow-y-auto py-4">
{/* Staff Assignment Summary */}
<div className="mb-6">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Users className="w-5 h-5 text-purple-600" />
<h3 className="font-semibold text-slate-900">Staff Assignment</h3>
</div>
<Badge
variant="outline"
className={`text-sm font-bold border-2 ${
totalAssigned >= totalNeeded
? 'border-emerald-500 text-emerald-700 bg-emerald-50'
: 'border-orange-500 text-orange-700 bg-orange-50'
}`}
>
{totalAssigned} / {totalNeeded}
</Badge>
</div>
{totalAssigned >= totalNeeded && (
<div className="mb-3 p-3 rounded-lg bg-gradient-to-r from-emerald-50 to-green-50 border-2 border-emerald-200">
<div className="flex items-center gap-2 text-emerald-700 text-sm font-semibold">
<Check className="w-5 h-5" />
Fully staffed - All positions filled!
</div>
</div>
)}
</div>
{/* Position Selection */}
{currentShift.roles.length > 1 && (
<div className="mb-6">
<label className="text-sm font-semibold text-slate-700 mb-2 block">Select Position:</label>
<Select
value={selectedRoleIndex.toString()}
onValueChange={(value) => {
setSelectedRoleIndex(parseInt(value));
setSelectedStaffIds([]);
setSwapMode(null);
}}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{currentShift.roles.map((role, idx) => {
const roleAssigned = role.assignments?.length || 0;
const roleNeeded = parseInt(role.count) || 0;
const roleFilled = roleAssigned >= roleNeeded;
return (
<SelectItem key={idx} value={idx.toString()}>
<div className="flex items-center justify-between gap-4 w-full">
<span className="font-medium">{role.service}</span>
<Badge
className={`${
roleFilled
? 'bg-emerald-100 text-emerald-700'
: 'bg-orange-100 text-orange-700'
} text-xs font-bold ml-2`}
>
{roleAssigned}/{roleNeeded}
</Badge>
</div>
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
)}
{/* Current Position Details */}
<div className="mb-6 p-4 bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl border-2 border-blue-200">
<div className="flex items-center justify-between mb-3">
<h4 className="font-bold text-slate-900 text-lg">{currentRole.service}</h4>
<Badge
className={`${
assigned >= needed
? 'bg-emerald-500 text-white'
: 'bg-orange-500 text-white'
} font-bold px-3 py-1 text-base`}
>
{assigned}/{needed}
</Badge>
</div>
{currentRole.start_time && (
<div className="flex items-center gap-2 text-sm text-slate-700 font-medium">
<Clock className="w-4 h-4 text-blue-600" />
<span>
{convertTo12Hour(currentRole.start_time)} - {convertTo12Hour(currentRole.end_time)}
</span>
</div>
)}
</div>
{/* Swap Mode Banner */}
{swapMode && (
<div className="mb-4 p-4 bg-purple-50 border-2 border-purple-300 rounded-xl">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<RefreshCw className="w-5 h-5 text-purple-600" />
<span className="font-semibold text-purple-900">
Swap Mode Active - Select replacement for {assignments[swapMode.assignmentIndex]?.employee_name}
</span>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => setSwapMode(null)}
className="text-purple-600 hover:bg-purple-100"
>
Cancel
</Button>
</div>
</div>
)}
{/* Assigned Staff List */}
{assignments.length > 0 && (
<div className="mb-6">
<h4 className="text-sm font-bold text-slate-700 mb-3 uppercase tracking-wide"> Assigned Staff:</h4>
<div className="space-y-2">
{assignments.map((assignment, idx) => {
const conflicts = getStaffConflicts(assignment.employee_id);
return (
<div
key={idx}
className="flex items-center justify-between p-3 bg-white rounded-xl border-2 border-slate-200 hover:border-blue-300 transition-all shadow-sm"
>
<div className="flex items-center gap-3 flex-1">
<Avatar className={`w-10 h-10 ${avatarColors[idx % avatarColors.length]}`}>
<AvatarFallback className="text-white font-bold">
{getInitials(assignment.employee_name)}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<p className="font-bold text-slate-900">{assignment.employee_name}</p>
<p className="text-xs text-slate-500">{currentRole.service}</p>
{conflicts.length > 0 && (
<div className="flex items-center gap-1 mt-1">
<AlertTriangle className="w-3 h-3 text-red-500" />
<span className="text-xs text-red-600 font-medium">Time conflict detected</span>
</div>
)}
</div>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setSwapMode({ employeeId: assignment.employee_id, assignmentIndex: idx })}
className="text-purple-600 hover:bg-purple-50 border-purple-300"
>
<RefreshCw className="w-3 h-3 mr-1" />
Swap
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => handleRemoveStaff(assignment.employee_id)}
className="text-red-600 hover:text-red-700 hover:bg-red-50"
>
<X className="w-4 h-4" />
</Button>
</div>
</div>
);
})}
</div>
</div>
)}
{/* Add Staff Section */}
{(assigned < needed || swapMode) && (
<div>
<div className="flex items-center justify-between mb-3">
<h4 className="text-sm font-bold text-slate-700 uppercase tracking-wide">
{swapMode ? '🔄 Select Replacement:' : ' Add Staff:'}
</h4>
{!swapMode && availableStaff.length > 0 && (
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={selectAll}
disabled={needed - assigned === 0}
>
Select All ({Math.min(availableStaff.length, needed - assigned)})
</Button>
{selectedStaffIds.length > 0 && (
<>
<Button
variant="outline"
size="sm"
onClick={deselectAll}
>
Clear
</Button>
<Button
size="sm"
onClick={handleBulkAssign}
className="bg-blue-600 hover:bg-blue-700"
>
<Plus className="w-4 h-4 mr-1" />
Assign {selectedStaffIds.length}
</Button>
</>
)}
</div>
)}
</div>
{/* Search */}
<div className="mb-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-slate-400" />
<Input
placeholder="Search staff by name or position..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
<p className="text-xs text-slate-500 mt-2">
Showing {availableStaff.length} {currentRole.service.toLowerCase()}(s)
{roleFilteredStaff.length !== allStaff.length && (
<span className="text-blue-600 font-medium"> (filtered by role)</span>
)}
</p>
</div>
{availableStaff.length > 0 ? (
<div className="space-y-2 max-h-80 overflow-y-auto pr-2">
{availableStaff.map((staff, idx) => {
const isSelected = selectedStaffIds.includes(staff.id);
const conflicts = getStaffConflicts(staff.id);
const hasConflict = conflicts.length > 0;
return (
<div
key={staff.id}
className={`flex items-center justify-between p-3 bg-white rounded-xl border-2 transition-all ${
hasConflict
? 'border-red-200 bg-red-50'
: isSelected
? 'border-blue-400 bg-blue-50'
: 'border-slate-200 hover:border-blue-300'
}`}
>
<div className="flex items-center gap-3 flex-1">
{!swapMode && (
<Checkbox
checked={isSelected}
onCheckedChange={() => toggleStaffSelection(staff.id)}
disabled={hasConflict}
className="border-2"
/>
)}
<Avatar className={`w-10 h-10 ${avatarColors[idx % avatarColors.length]}`}>
<AvatarFallback className="text-white font-bold">
{getInitials(staff.employee_name)}
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<p className="font-bold text-slate-900 truncate">{staff.employee_name}</p>
<div className="flex items-center gap-2 flex-wrap">
<p className="text-xs text-slate-500">{staff.position || 'Staff Member'}</p>
{staff.position_2 && (
<Badge variant="outline" className="text-[10px]">{staff.position_2}</Badge>
)}
</div>
{hasConflict && (
<div className="flex items-center gap-1 mt-1">
<AlertTriangle className="w-3 h-3 text-red-500" />
<span className="text-xs text-red-600 font-medium">
Conflict: {conflicts[0].orderName} ({conflicts[0].time})
</span>
</div>
)}
</div>
</div>
<Button
size="sm"
onClick={() => swapMode ? handleSwapStaff(staff) : handleAssignStaff(staff)}
disabled={hasConflict}
className={`${
swapMode
? 'bg-purple-600 hover:bg-purple-700'
: 'bg-blue-600 hover:bg-blue-700'
} ${hasConflict ? 'opacity-50 cursor-not-allowed' : ''}`}
>
{swapMode ? (
<>
<RefreshCw className="w-4 h-4 mr-1" />
Swap
</>
) : (
<>
<Plus className="w-4 h-4 mr-1" />
Assign
</>
)}
</Button>
</div>
);
})}
</div>
) : (
<div className="text-center py-12 bg-slate-50 rounded-xl border-2 border-dashed border-slate-200">
<Users className="w-16 h-16 mx-auto mb-3 text-slate-300" />
<p className="font-medium text-slate-600">
{searchTerm
? 'No staff match your search'
: `No available ${currentRole.service.toLowerCase()}s found`
}
</p>
<p className="text-sm text-slate-400 mt-1">
{searchTerm ? 'Try a different search term' : 'All matching staff have been assigned'}
</p>
</div>
)}
</div>
)}
</div>
<div className="border-t pt-4 flex items-center justify-between bg-slate-50 -mx-6 px-6 -mb-6 pb-6">
<div className="flex gap-2">
{selectedShiftIndex > 0 && (
<Button
variant="outline"
onClick={() => {
setSelectedShiftIndex(selectedShiftIndex - 1);
setSelectedRoleIndex(0);
setSelectedStaffIds([]);
setSwapMode(null);
}}
>
<ChevronLeft className="w-4 h-4 mr-1" />
Previous Shift
</Button>
)}
{selectedShiftIndex < order.shifts_data.length - 1 && (
<Button
variant="outline"
onClick={() => {
setSelectedShiftIndex(selectedShiftIndex + 1);
setSelectedRoleIndex(0);
setSelectedStaffIds([]);
setSwapMode(null);
}}
>
Next Shift
<ChevronRight className="w-4 h-4 ml-1" />
</Button>
)}
</div>
<div className="flex gap-2">
<Badge
variant="outline"
className={`px-4 py-2 text-base font-bold ${
totalAssigned >= totalNeeded
? 'bg-emerald-50 border-emerald-500 text-emerald-700'
: 'bg-orange-50 border-orange-500 text-orange-700'
}`}
>
{totalAssigned}/{totalNeeded} Filled
</Badge>
<Button onClick={onClose} className="bg-blue-600 hover:bg-blue-700">
Done
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
}