1103 lines
46 KiB
JavaScript
1103 lines
46 KiB
JavaScript
|
||
import React, { useState, useEffect } from "react";
|
||
import { base44 } from "@/api/base44Client";
|
||
import { useQuery } from "@tanstack/react-query";
|
||
import { Card, CardContent } from "@/components/ui/card";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Label } from "@/components/ui/label";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Switch } from "@/components/ui/switch";
|
||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||
import { Plus, Save, FileText, Building2, Calendar, Zap, Shield, Search, Minus, Trash2, MapPin, X, RefreshCw, Users, CheckCircle2 } from "lucide-react";
|
||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||
import { Calendar as CalendarComponent } from "@/components/ui/calendar";
|
||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||
import { format } from "date-fns";
|
||
import { Checkbox } from "@/components/ui/checkbox";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import { Textarea } from "@/components/ui/textarea";
|
||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from "@/components/ui/command";
|
||
|
||
const UNIFORM_TYPES = ["Type 1", "Type 2", "Type 3", "All Black", "Business Casual", "Chef Whites"];
|
||
|
||
export default function EventForm({ event, onSubmit, isSubmitting, currentUser }) {
|
||
const [formData, setFormData] = useState(event || {
|
||
event_name: "",
|
||
order_type: "one_time", // rapid, one_time, recurring, permanent
|
||
recurrence_type: "single", // This field is largely superseded by recurring_frequency in the new UI, but kept for compatibility.
|
||
recurrence_start_date: "",
|
||
recurrence_end_date: "",
|
||
scatter_dates: [],
|
||
recurring_days: [], // Added for weekly recurring
|
||
recurring_frequency: "weekly", // weekly, monthly, custom - default for recurring events
|
||
business_id: "",
|
||
business_name: "",
|
||
hub: "",
|
||
po_reference: "",
|
||
status: "Draft",
|
||
date: "",
|
||
include_backup: false,
|
||
backup_staff_count: 0,
|
||
shifts: [{
|
||
shift_name: "Shift 1",
|
||
location_address: "",
|
||
same_as_billing: true,
|
||
roles: [{
|
||
role: "",
|
||
department: "",
|
||
count: 1,
|
||
start_time: "09:00",
|
||
end_time: "17:00",
|
||
hours: 8,
|
||
uniform: "Type 1",
|
||
break_minutes: 30,
|
||
rate_per_hour: 0,
|
||
total_value: 0
|
||
}]
|
||
}],
|
||
notes: "",
|
||
total: 0
|
||
});
|
||
|
||
const [roleSearchOpen, setRoleSearchOpen] = useState({});
|
||
|
||
const { data: user } = useQuery({
|
||
queryKey: ['current-user-form'],
|
||
queryFn: () => base44.auth.me(),
|
||
enabled: !currentUser,
|
||
});
|
||
|
||
const currentUserData = currentUser || user;
|
||
const userRole = currentUserData?.user_role || currentUserData?.role || "admin";
|
||
const isVendor = userRole === "vendor";
|
||
|
||
const { data: businesses = [] } = useQuery({
|
||
queryKey: ['businesses'],
|
||
queryFn: () => base44.entities.Business.list(),
|
||
initialData: [],
|
||
});
|
||
|
||
const { data: allRates = [] } = useQuery({
|
||
queryKey: ['vendor-rates-all'],
|
||
queryFn: () => base44.entities.VendorRate.list(),
|
||
initialData: [],
|
||
});
|
||
|
||
const { data: vendorSettings = [] } = useQuery({
|
||
queryKey: ['vendor-settings'],
|
||
queryFn: () => base44.entities.VendorDefaultSettings.list(),
|
||
initialData: [],
|
||
});
|
||
|
||
// Get unique roles for dropdown
|
||
const availableRoles = [...new Set(allRates.map(r => r.role_name))].sort();
|
||
|
||
// Get hubs for current vendor
|
||
const vendorHubs = isVendor
|
||
? [...new Set(businesses.filter(b => b.business_name === currentUserData?.company_name).map(b => b.hub_building))].filter(Boolean)
|
||
: [];
|
||
|
||
// Auto-fill client details if current user is a client
|
||
useEffect(() => {
|
||
if (currentUserData && userRole === "client" && !event) {
|
||
const clientBusiness = businesses.find(b =>
|
||
b.email === currentUserData.email || b.contact_name === currentUserData.full_name
|
||
);
|
||
|
||
if (clientBusiness) {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
business_id: clientBusiness.id,
|
||
business_name: clientBusiness.business_name,
|
||
hub: clientBusiness.hub_building || prev.hub,
|
||
shifts: prev.shifts.map(shift => ({
|
||
...shift,
|
||
location_address: clientBusiness.address || shift.location_address
|
||
}))
|
||
}));
|
||
}
|
||
}
|
||
}, [currentUserData, businesses, event, userRole]);
|
||
|
||
useEffect(() => {
|
||
if (event) {
|
||
setFormData(event);
|
||
}
|
||
}, [event]);
|
||
|
||
const handleChange = (field, value) => {
|
||
setFormData(prev => ({ ...prev, [field]: value }));
|
||
};
|
||
|
||
const handleBusinessChange = (businessId) => {
|
||
const selectedBusiness = businesses.find(b => b.id === businessId);
|
||
if (selectedBusiness) {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
business_id: businessId,
|
||
business_name: selectedBusiness.business_name || "",
|
||
shifts: prev.shifts.map(shift => ({
|
||
...shift,
|
||
location_address: shift.same_as_billing ? selectedBusiness.address || shift.location_address : shift.location_address
|
||
}))
|
||
}));
|
||
}
|
||
};
|
||
|
||
const calculateHours = (startTime, endTime, breakMinutes = 0) => {
|
||
if (!startTime || !endTime) return 0;
|
||
|
||
const [startHour, startMin] = startTime.split(':').map(Number);
|
||
const [endHour, endMin] = endTime.split(':').map(Number);
|
||
|
||
const startMinutes = startHour * 60 + startMin;
|
||
const endMinutes = endHour * 60 + endMin;
|
||
|
||
let totalMinutes = endMinutes - startMinutes;
|
||
if (totalMinutes < 0) totalMinutes += 24 * 60;
|
||
|
||
totalMinutes -= (breakMinutes || 0);
|
||
|
||
return Math.max(0, totalMinutes / 60);
|
||
};
|
||
|
||
const getRateForRole = (roleName) => {
|
||
const rate = allRates.find(r => r.role_name === roleName && r.is_active);
|
||
return rate ? parseFloat(rate.client_rate || 0) : 0;
|
||
};
|
||
|
||
const handleRoleChange = (shiftIndex, roleIndex, field, value) => {
|
||
setFormData(prev => {
|
||
const newShifts = [...prev.shifts];
|
||
const role = newShifts[shiftIndex].roles[roleIndex];
|
||
|
||
role[field] = value;
|
||
|
||
// Auto-populate rate when role is selected
|
||
if (field === 'role') {
|
||
const rate = getRateForRole(value);
|
||
role.rate_per_hour = rate;
|
||
}
|
||
|
||
// Recalculate hours when time changes
|
||
if (field === 'start_time' || field === 'end_time' || field === 'break_minutes') {
|
||
role.hours = calculateHours(role.start_time, role.end_time, role.break_minutes);
|
||
}
|
||
|
||
// Recalculate total
|
||
role.total_value = (role.rate_per_hour || 0) * (role.hours || 0) * (role.count || 1);
|
||
|
||
return { ...prev, shifts: newShifts };
|
||
});
|
||
|
||
// Recalculate grand total
|
||
updateGrandTotal();
|
||
};
|
||
|
||
const updateGrandTotal = () => {
|
||
setTimeout(() => {
|
||
setFormData(prev => {
|
||
const total = prev.shifts.reduce((sum, shift) => {
|
||
const shiftTotal = shift.roles.reduce((roleSum, role) => roleSum + (role.total_value || 0), 0);
|
||
return sum + shiftTotal;
|
||
}, 0);
|
||
return { ...prev, total };
|
||
});
|
||
}, 0);
|
||
};
|
||
|
||
const handleAddRole = (shiftIndex) => {
|
||
setFormData(prev => {
|
||
const newShifts = [...prev.shifts];
|
||
newShifts[shiftIndex].roles.push({
|
||
role: "",
|
||
department: "",
|
||
count: 1,
|
||
start_time: "09:00",
|
||
end_time: "17:00",
|
||
hours: 8,
|
||
uniform: "Type 1",
|
||
break_minutes: 30,
|
||
rate_per_hour: 0,
|
||
total_value: 0
|
||
});
|
||
return { ...prev, shifts: newShifts };
|
||
});
|
||
};
|
||
|
||
const handleRemoveRole = (shiftIndex, roleIndex) => {
|
||
setFormData(prev => {
|
||
const newShifts = [...prev.shifts];
|
||
newShifts[shiftIndex].roles.splice(roleIndex, 1);
|
||
return { ...prev, shifts: newShifts };
|
||
});
|
||
updateGrandTotal();
|
||
};
|
||
|
||
const handleAddShift = () => {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
shifts: [...prev.shifts, {
|
||
shift_name: `Shift ${prev.shifts.length + 1}`,
|
||
location_address: "",
|
||
same_as_billing: true,
|
||
roles: [{
|
||
role: "",
|
||
department: "",
|
||
count: 1,
|
||
start_time: "09:00",
|
||
end_time: "17:00",
|
||
hours: 8,
|
||
uniform: "Type 1",
|
||
break_minutes: 30,
|
||
rate_per_hour: 0,
|
||
total_value: 0
|
||
}]
|
||
}]
|
||
}));
|
||
};
|
||
|
||
const handleOrderTypeChange = (type) => {
|
||
setFormData(prev => {
|
||
const newState = {
|
||
...prev,
|
||
order_type: type,
|
||
date: "", // Always clear the single 'date' field on order type change, it will be set by rapid/one_time/permanent if needed.
|
||
recurrence_start_date: "", // Clear these when order type changes
|
||
recurrence_end_date: "",
|
||
scatter_dates: [], // Clear these when order type changes
|
||
recurring_days: [], // Clear these when order type changes
|
||
recurring_frequency: "weekly" // Default for recurring
|
||
};
|
||
|
||
// Set recurrence_type for backend compatibility if it's still used there, but new UI uses recurring_frequency
|
||
if (type === "recurring") {
|
||
newState.recurrence_type = "date_range"; // Default to 'date_range' to maintain previous behavior if possible
|
||
} else {
|
||
newState.recurrence_type = "single"; // For one_time, rapid, permanent
|
||
}
|
||
|
||
if (type === "rapid" && !prev.date) { // Only set if not already set, or if changing from another type
|
||
newState.date = format(new Date(), 'yyyy-MM-dd'); // Default rapid to today if no date
|
||
} else if (type === "one_time" || type === "permanent") {
|
||
newState.date = prev.date; // Preserve date if user had one for one_time/permanent
|
||
}
|
||
// If type is recurring, date is already cleared. Other recurring fields default/cleared.
|
||
|
||
return newState;
|
||
});
|
||
};
|
||
|
||
const handleScatterDateSelect = (dates) => {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
scatter_dates: dates?.map(d => format(d, 'yyyy-MM-dd')).sort() || []
|
||
}));
|
||
};
|
||
|
||
const handleDayToggle = (day) => {
|
||
setFormData(prev => {
|
||
const days = prev.recurring_days || [];
|
||
const exists = days.includes(day);
|
||
return {
|
||
...prev,
|
||
recurring_days: exists
|
||
? days.filter(d => d !== day)
|
||
: [...days, day].sort((a,b) => a-b) // Ensure days are sorted
|
||
};
|
||
});
|
||
};
|
||
|
||
const handleSubmit = (e, isDraft = false) => {
|
||
e.preventDefault();
|
||
let status;
|
||
if (isDraft) {
|
||
status = "DRAFT";
|
||
} else {
|
||
switch (formData.order_type) {
|
||
case "rapid":
|
||
status = "ACTIVE"; // Rapid requests are active immediately upon submission
|
||
break;
|
||
case "one_time":
|
||
case "recurring":
|
||
case "permanent":
|
||
default: // In case of an unexpected order_type, default to Pending
|
||
status = "PENDING"; // These types typically need approval/processing
|
||
break;
|
||
}
|
||
}
|
||
|
||
const dataToSubmit = {
|
||
...formData,
|
||
status: status
|
||
};
|
||
onSubmit(dataToSubmit);
|
||
};
|
||
|
||
return (
|
||
<form onSubmit={(e) => handleSubmit(e, false)} className="space-y-6">
|
||
{/* Order Type Selection - 4 Options */}
|
||
<Card className="border-slate-200 shadow-sm bg-white">
|
||
<CardContent className="p-6">
|
||
<h3 className="text-lg font-bold text-[#1C323E] mb-4 text-center">Select Order Type</h3>
|
||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||
{/* Rapid */}
|
||
<button
|
||
type="button"
|
||
onClick={() => handleOrderTypeChange('rapid')}
|
||
className={`p-6 rounded-2xl border-3 transition-all duration-300 hover:scale-105 ${
|
||
formData.order_type === 'rapid'
|
||
? 'border-red-500 bg-gradient-to-br from-red-50 to-orange-50 shadow-lg'
|
||
: 'border-slate-200 bg-white hover:border-red-300'
|
||
}`}
|
||
>
|
||
<div className="text-center">
|
||
<Zap className={`w-10 h-10 mx-auto mb-3 ${formData.order_type === 'rapid' ? 'text-red-600' : 'text-slate-400'}`} />
|
||
<h4 className={`text-2xl font-bold mb-2 ${formData.order_type === 'rapid' ? 'text-red-600' : 'text-slate-700'}`}>
|
||
RAPID
|
||
</h4>
|
||
<p className="text-xs text-slate-600 font-medium">URGENT same-day Coverage</p>
|
||
</div>
|
||
</button>
|
||
|
||
{/* One-Time */}
|
||
<button
|
||
type="button"
|
||
onClick={() => handleOrderTypeChange('one_time')}
|
||
className={`p-6 rounded-2xl border-3 transition-all duration-300 hover:scale-105 ${
|
||
formData.order_type === 'one_time'
|
||
? 'border-[#0A39DF] bg-gradient-to-br from-blue-50 to-indigo-50 shadow-lg'
|
||
: 'border-slate-200 bg-white hover:border-[#0A39DF]/30'
|
||
}`}
|
||
>
|
||
<div className="text-center">
|
||
<Calendar className={`w-10 h-10 mx-auto mb-3 ${formData.order_type === 'one_time' ? 'text-[#0A39DF]' : 'text-slate-400'}`} />
|
||
<h4 className={`text-2xl font-bold mb-2 ${formData.order_type === 'one_time' ? 'text-[#0A39DF]' : 'text-slate-700'}`}>
|
||
One-Time
|
||
</h4>
|
||
<p className="text-xs text-slate-600 font-medium">Single Event or Shift Request</p>
|
||
</div>
|
||
</button>
|
||
|
||
{/* Recurring */}
|
||
<button
|
||
type="button"
|
||
onClick={() => handleOrderTypeChange('recurring')}
|
||
className={`p-6 rounded-2xl border-3 transition-all duration-300 hover:scale-105 ${
|
||
formData.order_type === 'recurring'
|
||
? 'border-purple-500 bg-gradient-to-br from-purple-50 to-pink-50 shadow-lg'
|
||
: 'border-slate-200 bg-white hover:border-purple-300'
|
||
}`}
|
||
>
|
||
<div className="text-center">
|
||
<RefreshCw className={`w-10 h-10 mx-auto mb-3 ${formData.order_type === 'recurring' ? 'text-purple-600' : 'text-slate-400'}`} />
|
||
<h4 className={`text-2xl font-bold mb-2 ${formData.order_type === 'recurring' ? 'text-purple-600' : 'text-slate-700'}`}>
|
||
Recurring
|
||
</h4>
|
||
<p className="text-xs text-slate-600 font-medium">Ongoing Weekly / Monthly Coverage</p>
|
||
</div>
|
||
</button>
|
||
|
||
{/* Permanent */}
|
||
<button
|
||
type="button"
|
||
onClick={() => handleOrderTypeChange('permanent')}
|
||
className={`p-6 rounded-2xl border-3 transition-all duration-300 hover:scale-105 ${
|
||
formData.order_type === 'permanent'
|
||
? 'border-green-500 bg-gradient-to-br from-green-50 to-emerald-50 shadow-lg'
|
||
: 'border-slate-200 bg-white hover:border-green-300'
|
||
}`}
|
||
>
|
||
<div className="text-center">
|
||
<Users className={`w-10 h-10 mx-auto mb-3 ${formData.order_type === 'permanent' ? 'text-green-600' : 'text-slate-400'}`} />
|
||
<h4 className={`text-2xl font-bold mb-2 ${formData.order_type === 'permanent' ? 'text-green-600' : 'text-slate-700'}`}>
|
||
Permanent
|
||
</h4>
|
||
<p className="text-xs text-slate-600 font-medium">Long-Term Staffing Placement</p>
|
||
</div>
|
||
</button>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Recurring Options - Android Style */}
|
||
{formData.order_type === 'recurring' && (
|
||
<Card className="border-[#0A39DF]/20 shadow-md bg-white">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center gap-2 mb-5">
|
||
<Calendar className="w-5 h-5 text-[#0A39DF]" />
|
||
<Label className="text-base font-bold text-[#1C323E]">Recurrence Pattern</Label>
|
||
</div>
|
||
|
||
{/* Frequency Selection */}
|
||
<div className="space-y-4 mb-6">
|
||
<Label className="text-sm font-semibold text-slate-700">Repeat Frequency</Label>
|
||
<div className="grid grid-cols-3 gap-3">
|
||
<button
|
||
type="button"
|
||
onClick={() => setFormData(prev => ({ ...prev, recurring_frequency: 'weekly' }))}
|
||
className={`p-4 rounded-xl border-2 transition-all text-center ${
|
||
formData.recurring_frequency === 'weekly'
|
||
? 'border-[#0A39DF] bg-blue-50 text-[#0A39DF] font-semibold'
|
||
: 'border-slate-200 hover:border-slate-300'
|
||
}`}
|
||
>
|
||
<RefreshCw className="w-5 h-5 mx-auto mb-2" />
|
||
<span className="text-sm">Weekly</span>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => setFormData(prev => ({ ...prev, recurring_frequency: 'monthly' }))}
|
||
className={`p-4 rounded-xl border-2 transition-all text-center ${
|
||
formData.recurring_frequency === 'monthly'
|
||
? 'border-[#0A39DF] bg-blue-50 text-[#0A39DF] font-semibold'
|
||
: 'border-slate-200 hover:border-slate-300'
|
||
}`}
|
||
>
|
||
<Calendar className="w-5 h-5 mx-auto mb-2" />
|
||
<span className="text-sm">Monthly</span>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => setFormData(prev => ({ ...prev, recurring_frequency: 'custom' }))}
|
||
className={`p-4 rounded-xl border-2 transition-all text-center ${
|
||
formData.recurring_frequency === 'custom'
|
||
? 'border-[#0A39DF] bg-blue-50 text-[#0A39DF] font-semibold'
|
||
: 'border-slate-200 hover:border-slate-300'
|
||
}`}
|
||
>
|
||
<Users className="w-5 h-5 mx-auto mb-2" />
|
||
<span className="text-sm">Custom</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Weekly - Android Alarm Style Day Selection */}
|
||
{formData.recurring_frequency === 'weekly' && (
|
||
<div className="space-y-4 p-5 bg-gradient-to-br from-blue-50 to-white rounded-xl border border-blue-100">
|
||
<Label className="text-sm font-semibold text-slate-700">Repeat on Days</Label>
|
||
<div className="flex items-center justify-center gap-2">
|
||
{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map((day, index) => {
|
||
const isSelected = (formData.recurring_days || []).includes(index);
|
||
return (
|
||
<button
|
||
key={day}
|
||
type="button"
|
||
onClick={() => handleDayToggle(index)}
|
||
className={`w-12 h-12 rounded-full flex items-center justify-center transition-all text-xs font-medium ${
|
||
isSelected
|
||
? 'bg-[#0A39DF] text-white shadow-md scale-105 font-bold'
|
||
: 'bg-white border-2 border-slate-200 text-slate-600 hover:border-[#0A39DF]/50 hover:scale-105'
|
||
}`}
|
||
>
|
||
{day}
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
{formData.recurring_days && formData.recurring_days.length > 0 && (
|
||
<div className="text-center text-sm text-slate-600 mt-3">
|
||
<span className="font-semibold text-[#0A39DF]">{formData.recurring_days.length}</span> day{formData.recurring_days.length > 1 ? 's' : ''} selected
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Monthly - Date Range */}
|
||
{formData.recurring_frequency === 'monthly' && (
|
||
<div className="space-y-4 p-5 bg-gradient-to-br from-purple-50 to-white rounded-xl border border-purple-100">
|
||
<Label className="text-sm font-semibold text-slate-700">Select Monthly Pattern</Label>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<Label htmlFor="recurrence_start_date" className="text-sm font-medium text-slate-700 mb-2 block">
|
||
Start Date
|
||
</Label>
|
||
<Input
|
||
id="recurrence_start_date"
|
||
type="date"
|
||
value={formData.recurrence_start_date || ""}
|
||
onChange={(e) => handleChange('recurrence_start_date', e.target.value)}
|
||
className="border-slate-300 bg-white h-12"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<Label htmlFor="recurrence_end_date" className="text-sm font-medium text-slate-700 mb-2 block">
|
||
End Date
|
||
</Label>
|
||
<Input
|
||
id="recurrence_end_date"
|
||
type="date"
|
||
value={formData.recurrence_end_date || ""}
|
||
onChange={(e) => handleChange('recurrence_end_date', e.target.value)}
|
||
className="border-slate-300 bg-white h-12"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Custom - Multiple Specific Dates */}
|
||
{formData.recurring_frequency === 'custom' && (
|
||
<div className="space-y-4 p-5 bg-gradient-to-br from-green-50 to-white rounded-xl border border-green-100">
|
||
<Label className="text-sm font-semibold text-slate-700">Select Specific Dates</Label>
|
||
<Popover>
|
||
<PopoverTrigger asChild>
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
className="w-full justify-start text-left h-12 border-2 border-slate-300 hover:border-[#0A39DF] bg-white"
|
||
>
|
||
<Calendar className="w-5 h-5 mr-2 text-[#0A39DF]" />
|
||
<span className="font-medium">
|
||
{formData.scatter_dates && formData.scatter_dates.length > 0
|
||
? `${formData.scatter_dates.length} date${formData.scatter_dates.length > 1 ? 's' : ''} selected`
|
||
: "Click to select dates"}
|
||
</span>
|
||
</Button>
|
||
</PopoverTrigger>
|
||
<PopoverContent className="w-auto p-0" align="start">
|
||
<CalendarComponent
|
||
mode="multiple"
|
||
selected={formData.scatter_dates?.map(d => new Date(d)) || []}
|
||
onSelect={handleScatterDateSelect}
|
||
initialFocus
|
||
className="rounded-lg border-0"
|
||
/>
|
||
</PopoverContent>
|
||
</Popover>
|
||
{formData.scatter_dates && formData.scatter_dates.length > 0 && (
|
||
<div className="mt-4 flex flex-wrap gap-2">
|
||
{formData.scatter_dates.map(date => (
|
||
<Badge
|
||
key={date}
|
||
className="bg-[#0A39DF] text-white px-4 py-2 text-sm flex items-center gap-2 hover:bg-[#0A39DF]/90"
|
||
>
|
||
{format(new Date(date), 'MMM d, yyyy')}
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
scatter_dates: prev.scatter_dates.filter(d => d !== date)
|
||
}));
|
||
}}
|
||
className="hover:bg-white/20 rounded-full p-0.5 ml-1"
|
||
>
|
||
<X className="w-3.5 h-3.5" />
|
||
</button>
|
||
</Badge>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Summary Section */}
|
||
{((formData.recurring_frequency === 'weekly' && formData.recurring_days?.length > 0) ||
|
||
(formData.recurring_frequency === 'monthly' && formData.recurrence_start_date && formData.recurrence_end_date) ||
|
||
(formData.recurring_frequency === 'custom' && formData.scatter_dates?.length > 0)) && (
|
||
<div className="mt-6 p-4 bg-gradient-to-r from-[#0A39DF]/10 to-purple-500/10 rounded-lg border border-[#0A39DF]/30">
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<CheckCircle2 className="w-5 h-5 text-[#0A39DF]" />
|
||
<span className="font-semibold text-[#1C323E]">Recurrence Summary</span>
|
||
</div>
|
||
<div className="text-sm text-slate-700 space-y-1">
|
||
{formData.recurring_frequency === 'weekly' && formData.recurring_days?.length > 0 && (
|
||
<p>• Repeats every <strong>
|
||
{['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
|
||
.filter((_, i) => formData.recurring_days.includes(i))
|
||
.join(', ')}
|
||
</strong></p>
|
||
)}
|
||
{formData.recurring_frequency === 'monthly' && formData.recurrence_start_date && formData.recurrence_end_date && (
|
||
<p>• Repeats monthly from <strong>{format(new Date(formData.recurrence_start_date), 'MMM d, yyyy')}</strong> to <strong>{format(new Date(formData.recurrence_end_date), 'MMM d, yyyy')}</strong></p>
|
||
)}
|
||
{formData.recurring_frequency === 'custom' && formData.scatter_dates?.length > 0 && (
|
||
<p>• <strong>{formData.scatter_dates.length}</strong> specific date{formData.scatter_dates.length > 1 ? 's' : ''} selected</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* Event Details */}
|
||
<Card className="border-slate-200">
|
||
<CardContent className="p-6">
|
||
<h3 className="text-lg font-bold text-slate-900 mb-6">Event</h3>
|
||
|
||
<div className="space-y-4">
|
||
<div>
|
||
<Label htmlFor="event_name" className="text-sm mb-2 block">Event Name *</Label>
|
||
<Input
|
||
id="event_name"
|
||
value={formData.event_name || ""}
|
||
onChange={(e) => handleChange('event_name', e.target.value)}
|
||
placeholder="Event Name"
|
||
required
|
||
className="border-slate-200"
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
{(formData.order_type === 'one_time' || formData.order_type === 'rapid' || formData.order_type === 'permanent') && (
|
||
<div>
|
||
<Label htmlFor="date" className="text-sm mb-2 block">Date *</Label>
|
||
<Input
|
||
id="date"
|
||
type="date"
|
||
value={formData.date || ""}
|
||
onChange={(e) => handleChange('date', e.target.value)}
|
||
required
|
||
className="border-slate-200"
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{/* Hub Selection - Show if vendor has multiple locations */}
|
||
{isVendor && vendorHubs.length > 1 ? (
|
||
<div>
|
||
<Label htmlFor="hub" className="text-sm mb-2 block flex items-center gap-2">
|
||
<MapPin className="w-4 h-4" />
|
||
Hub Location *
|
||
</Label>
|
||
<Select value={formData.hub || ""} onValueChange={(value) => handleChange('hub', value)}>
|
||
<SelectTrigger className="border-slate-200">
|
||
<SelectValue placeholder="Select hub" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{vendorHubs.map(hub => (
|
||
<SelectItem key={hub} value={hub}>{hub}</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
) : (
|
||
<div>
|
||
<Label htmlFor="hub" className="text-sm mb-2 block">Hub</Label>
|
||
<Input
|
||
id="hub"
|
||
value={formData.hub || ""}
|
||
onChange={(e) => handleChange('hub', e.target.value)}
|
||
placeholder="Hub location"
|
||
className="border-slate-200"
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* PO Reference - Now Optional */}
|
||
<div>
|
||
<Label htmlFor="po_reference" className="text-sm mb-2 block">Purchase Order (Optional)</Label>
|
||
<Input
|
||
id="po_reference"
|
||
value={formData.po_reference || ""}
|
||
onChange={(e) => handleChange('po_reference', e.target.value)}
|
||
placeholder="PO reference"
|
||
className="border-slate-200"
|
||
/>
|
||
</div>
|
||
|
||
{/* Client/Business Selection */}
|
||
{isVendor && (
|
||
<div>
|
||
<Label htmlFor="business_id" className="text-sm mb-2 block">Client / Business</Label>
|
||
<Select value={formData.business_id || ""} onValueChange={handleBusinessChange}>
|
||
<SelectTrigger className="border-slate-200">
|
||
<SelectValue placeholder="Select a client">
|
||
{formData.business_name && (
|
||
<div className="flex items-center gap-2">
|
||
<Building2 className="w-4 h-4 text-slate-500" />
|
||
<span>{formData.business_name}</span>
|
||
</div>
|
||
)}
|
||
</SelectValue>
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{businesses.map((business) => (
|
||
<SelectItem key={business.id} value={business.id}>
|
||
<div className="flex flex-col">
|
||
<span className="font-medium">{business.business_name || "Unnamed Business"}</span>
|
||
{business.contact_name && (
|
||
<span className="text-xs text-slate-500">{business.contact_name}</span>
|
||
)}
|
||
</div>
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
{formData.business_id && businesses.find(b => b.id === formData.business_id)?.rate_group && (
|
||
<p className="text-xs text-blue-600 mt-2 flex items-center gap-1">
|
||
ℹ️ Rate Group: {businesses.find(b => b.id === formData.business_id)?.rate_group} (Auto-detected from location)
|
||
</p>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Zero Risk Backup Staff */}
|
||
<div className="p-4 bg-gradient-to-r from-green-50 to-emerald-50 rounded-lg border-2 border-green-200">
|
||
<div className="flex items-start gap-3">
|
||
<Checkbox
|
||
id="include_backup"
|
||
checked={formData.include_backup}
|
||
onCheckedChange={(checked) => {
|
||
handleChange('include_backup', checked);
|
||
if (!checked) handleChange('backup_staff_count', 0);
|
||
}}
|
||
className="mt-1"
|
||
/>
|
||
<div className="flex-1">
|
||
<Label htmlFor="include_backup" className="font-semibold text-green-900 cursor-pointer flex items-center gap-2">
|
||
<Shield className="w-5 h-5" />
|
||
Zero Risk - Include Backup Staff
|
||
</Label>
|
||
<p className="text-sm text-green-700 mt-1">
|
||
Extra pool of staff in case of call-outs or no-shows
|
||
</p>
|
||
{formData.include_backup && (
|
||
<div className="mt-3">
|
||
<Label className="text-sm text-green-800">Number of backup staff</Label>
|
||
<Input
|
||
type="number"
|
||
min="1"
|
||
max="5"
|
||
value={formData.backup_staff_count || 1}
|
||
onChange={(e) => handleChange('backup_staff_count', parseInt(e.target.value) || 1)}
|
||
className="w-24 mt-1 border-green-300"
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Shifts Section */}
|
||
{formData.shifts.map((shift, shiftIndex) => (
|
||
<Card key={shiftIndex} className="border-slate-200">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between mb-6">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-12 h-12 bg-gradient-to-br from-purple-500 to-purple-700 rounded-xl flex items-center justify-center text-white font-bold text-lg shadow-md">
|
||
{shiftIndex + 1}
|
||
</div>
|
||
<div>
|
||
<h3 className="text-lg font-bold text-[#1C323E]">{shift.shift_name}</h3>
|
||
{shift.location_address && (
|
||
<p className="text-xs text-slate-500 flex items-center gap-1">
|
||
<MapPin className="w-3 h-3" />
|
||
{shift.location_address}
|
||
</p>
|
||
)}
|
||
</div>
|
||
</div >
|
||
<Button type="button" variant="ghost" size="sm">
|
||
Add contact
|
||
</Button>
|
||
</div>
|
||
|
||
{/* Roles */}
|
||
<div className="space-y-4">
|
||
{shift.roles.map((role, roleIndex) => (
|
||
<div key={roleIndex} className="bg-slate-50 rounded-lg p-4 border border-slate-200">
|
||
<div className="flex items-center gap-2 mb-4">
|
||
<div className="w-8 h-8 bg-[#1C323E] rounded-full flex items-center justify-center text-white font-bold text-sm">
|
||
{roleIndex + 1}
|
||
</div>
|
||
<div className="flex-1 grid grid-cols-4 gap-3">
|
||
{/* Service/Role with Search */}
|
||
<div>
|
||
<Label className="text-xs text-slate-600 mb-1 block">Service / Role</Label>
|
||
<Popover
|
||
open={roleSearchOpen[`${shiftIndex}-${roleIndex}`]}
|
||
onOpenChange={(open) => setRoleSearchOpen(prev => ({ ...prev, [`${shiftIndex}-${roleIndex}`]: open }))}
|
||
>
|
||
<PopoverTrigger asChild>
|
||
<Button
|
||
variant="outline"
|
||
role="combobox"
|
||
className="w-full justify-between text-left font-normal"
|
||
>
|
||
{role.role || "Select service..."}
|
||
<Search className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||
</Button>
|
||
</PopoverTrigger>
|
||
<PopoverContent className="w-[300px] p-0">
|
||
<Command>
|
||
<CommandInput placeholder="Search roles..." />
|
||
<CommandEmpty>No role found.</CommandEmpty>
|
||
<CommandGroup className="max-h-64 overflow-auto">
|
||
{availableRoles.map((roleName) => (
|
||
<CommandItem
|
||
key={roleName}
|
||
value={roleName}
|
||
onSelect={() => {
|
||
handleRoleChange(shiftIndex, roleIndex, 'role', roleName);
|
||
setRoleSearchOpen(prev => ({ ...prev, [`${shiftIndex}-${roleIndex}`]: false }));
|
||
}}
|
||
>
|
||
{roleName}
|
||
</CommandItem>
|
||
))}
|
||
</CommandGroup>
|
||
</Command>
|
||
</PopoverContent>
|
||
</Popover>
|
||
</div>
|
||
|
||
{/* Count */}
|
||
<div>
|
||
<Label className="text-xs text-slate-600 mb-1 block">Count</Label>
|
||
<div className="flex items-center gap-1">
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
size="icon"
|
||
className="h-9 w-9"
|
||
onClick={() => handleRoleChange(shiftIndex, roleIndex, 'count', Math.max(1, (role.count || 1) - 1))}
|
||
>
|
||
<Minus className="w-4 h-4" />
|
||
</Button>
|
||
<Input
|
||
type="number"
|
||
min="1"
|
||
value={role.count || 1}
|
||
onChange={(e) => handleRoleChange(shiftIndex, roleIndex, 'count', parseInt(e.target.value) || 1)}
|
||
className="w-16 text-center"
|
||
/>
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
size="icon"
|
||
className="h-9 w-9"
|
||
onClick={() => handleRoleChange(shiftIndex, roleIndex, 'count', (role.count || 1) + 1)}
|
||
>
|
||
<Plus className="w-4 h-4" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Start Time */}
|
||
<div>
|
||
<Label className="text-xs text-slate-600 mb-1 block">Start Time</Label>
|
||
<Input
|
||
type="time"
|
||
value={role.start_time || "09:00"}
|
||
onChange={(e) => handleRoleChange(shiftIndex, roleIndex, 'start_time', e.target.value)}
|
||
className="border-slate-200"
|
||
/>
|
||
</div>
|
||
|
||
{/* End Time */}
|
||
<div>
|
||
<Label className="text-xs text-slate-600 mb-1 block">End Time</Label>
|
||
<Input
|
||
type="time"
|
||
value={role.end_time || "17:00"}
|
||
onChange={(e) => handleRoleChange(shiftIndex, roleIndex, 'end_time', e.target.value)}
|
||
className="border-slate-200"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{shift.roles.length > 1 && (
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
size="icon"
|
||
onClick={() => handleRemoveRole(shiftIndex, roleIndex)}
|
||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||
>
|
||
<Trash2 className="w-4 h-4" />
|
||
</Button>
|
||
)}
|
||
</div>
|
||
|
||
<div className="grid grid-cols-4 gap-3">
|
||
{/* Department */}
|
||
<div>
|
||
<Label className="text-xs text-slate-600 mb-1 block">Department</Label>
|
||
<Input
|
||
value={role.department || ""}
|
||
onChange={(e) => handleRoleChange(shiftIndex, roleIndex, 'department', e.target.value)}
|
||
placeholder="Department"
|
||
className="border-slate-200"
|
||
/>
|
||
</div>
|
||
|
||
{/* Hours (Auto-calculated) */}
|
||
<div>
|
||
<Label className="text-xs text-slate-600 mb-1 block">Hours</Label>
|
||
<div className="flex items-center justify-center h-9">
|
||
<Badge className="bg-[#0A39DF] text-white text-lg px-4 py-1 rounded-full">
|
||
{role.hours || 0}
|
||
</Badge>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Break (min) */}
|
||
<div>
|
||
<Label className="text-xs text-slate-600 mb-1 block">Break (min)</Label>
|
||
<Input
|
||
type="number"
|
||
min="0"
|
||
value={role.break_minutes || 30}
|
||
onChange={(e) => handleRoleChange(shiftIndex, roleIndex, 'break_minutes', parseInt(e.target.value) || 0)}
|
||
className="border-slate-200"
|
||
/>
|
||
</div>
|
||
|
||
{/* Uniform */}
|
||
<div>
|
||
<Label className="text-xs text-slate-600 mb-1 block">Uniform</Label>
|
||
<Select
|
||
value={role.uniform || "Type 1"}
|
||
onValueChange={(value) => handleRoleChange(shiftIndex, roleIndex, 'uniform', value)}
|
||
>
|
||
<SelectTrigger className="border-slate-200">
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{UNIFORM_TYPES.map(type => (
|
||
<SelectItem key={type} value={type}>{type}</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Rate and Total */}
|
||
<div className="grid grid-cols-2 gap-3 mt-3">
|
||
<div>
|
||
<Label className="text-xs text-slate-600 mb-1 block flex items-center gap-1">
|
||
Rate/hr
|
||
{role.role && (
|
||
<span className="text-blue-600">ℹ️ FoodBuy</span>
|
||
)}
|
||
</Label>
|
||
<div className="flex items-center gap-1">
|
||
<span className="text-slate-500">$</span>
|
||
<Input
|
||
type="number"
|
||
step="0.01"
|
||
value={role.rate_per_hour || 0}
|
||
readOnly
|
||
className="border-slate-200 bg-slate-50 font-semibold"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<Label className="text-xs text-slate-600 mb-1 block">Total</Label>
|
||
<div className="text-2xl font-bold text-[#0A39DF]">
|
||
${(role.total_value || 0).toFixed(2)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
|
||
{/* Add Role Button */}
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
className="w-full border-dashed border-2 border-slate-300 hover:border-[#0A39DF] hover:bg-blue-50"
|
||
onClick={() => handleAddRole(shiftIndex)}
|
||
>
|
||
<Plus className="w-4 h-4 mr-2" />
|
||
Add Role
|
||
</Button>
|
||
</div>
|
||
|
||
{/* Shift Total */}
|
||
<div className="mt-4 pt-4 border-t border-slate-200 flex items-center justify-between">
|
||
<span className="font-semibold text-slate-700">Shift Total:</span>
|
||
<span className="text-2xl font-bold text-[#0A39DF]">
|
||
${shift.roles.reduce((sum, r) => sum + (r.total_value || 0), 0).toFixed(2)}
|
||
</span>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
|
||
{/* Add Another Shift Button */}
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
className="w-full border-dashed border-2 border-slate-300 hover:border-purple-500 hover:bg-purple-50"
|
||
onClick={handleAddShift}
|
||
>
|
||
<Plus className="w-4 h-4 mr-2" />
|
||
Add Another Shift (Different Location)
|
||
</Button>
|
||
|
||
{/* Other Instructions */}
|
||
<Card className="border-slate-200">
|
||
<CardContent className="p-6">
|
||
<Label className="text-sm font-semibold mb-3 block">Other Instructions</Label>
|
||
<Textarea
|
||
value={formData.notes || ""}
|
||
onChange={(e) => handleChange('notes', e.target.value)}
|
||
placeholder="Add any additional instructions..."
|
||
rows={3}
|
||
maxLength={300}
|
||
className="border-slate-200"
|
||
/>
|
||
<p className="text-xs text-slate-500 text-right mt-1">
|
||
{(formData.notes || "").length} / 300
|
||
</p>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Grand Total */}
|
||
<Card className="border-2 border-[#0A39DF] bg-gradient-to-br from-blue-50 to-white">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-slate-600">Grand Total</p>
|
||
<p className="text-xs text-slate-500">
|
||
{formData.shifts.reduce((sum, s) => sum + s.roles.reduce((roleSum, r) => roleSum + (r.count || 1), 0), 0)} roles across {formData.shifts.length} shift(s)
|
||
</p>
|
||
</div>
|
||
<div className="text-4xl font-bold text-[#0A39DF]">
|
||
${(formData.total || 0).toFixed(2)}
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Action Buttons */}
|
||
<div className="flex items-center justify-end gap-3">
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
className="border-slate-300"
|
||
onClick={() => window.history.back()}
|
||
>
|
||
Cancel
|
||
</Button>
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
onClick={(e) => handleSubmit(e, true)}
|
||
disabled={isSubmitting}
|
||
className="border-slate-300"
|
||
>
|
||
<FileText className="w-4 h-4 mr-2" />
|
||
Draft Event
|
||
</Button>
|
||
<Button
|
||
type="submit"
|
||
disabled={isSubmitting}
|
||
className="bg-gradient-to-r from-[#0A39DF] to-[#1C323E] hover:from-[#0829B0] hover:to-[#0F1D26] text-white px-8"
|
||
>
|
||
<Save className="w-4 h-4 mr-2" />
|
||
Create Event
|
||
</Button>
|
||
</div>
|
||
</form>
|
||
);
|
||
}
|