diff --git a/Makefile b/Makefile index 8156aa91..beaa4d0a 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ # --- Flutter check --- FLUTTER := $(shell which flutter) ifeq ($(FLUTTER),) -$(error "flutter not found in PATH. Please install Flutter and add it to your PATH.") +#$(error "flutter not found in PATH. Please install Flutter and add it to your PATH.") endif # --- Firebase & GCP Configuration --- diff --git a/frontend-web/src/components/common/GoogleAddressInput.jsx b/frontend-web/src/components/common/GoogleAddressInput.jsx new file mode 100644 index 00000000..5aa14161 --- /dev/null +++ b/frontend-web/src/components/common/GoogleAddressInput.jsx @@ -0,0 +1,72 @@ +import React, { useRef, useEffect, useState } from "react"; +import { Input } from "@/components/ui/input"; +import { MapPin } from "lucide-react"; + +export default function GoogleAddressInput({ value, onChange, placeholder = "Enter address...", className = "" }) { + const inputRef = useRef(null); + const autocompleteRef = useRef(null); + const [isLoaded, setIsLoaded] = useState(false); + + useEffect(() => { + // Check if Google Maps is already loaded + if (window.google && window.google.maps && window.google.maps.places) { + setIsLoaded(true); + return; + } + + // Load Google Maps script + const script = document.createElement('script'); + script.src = `https://maps.googleapis.com/maps/api/js?key=AIzaSyBkP7xH4NvR6C6vZ8Y3J7qX2QW8Z9vN3Zc&libraries=places`; + script.async = true; + script.defer = true; + script.onload = () => setIsLoaded(true); + document.head.appendChild(script); + + return () => { + if (script.parentNode) { + script.parentNode.removeChild(script); + } + }; + }, []); + + useEffect(() => { + if (!isLoaded || !inputRef.current) return; + + try { + // Initialize Google Maps Autocomplete + autocompleteRef.current = new window.google.maps.places.Autocomplete(inputRef.current, { + types: ['address'], + componentRestrictions: { country: 'us' }, + }); + + // Handle place selection + autocompleteRef.current.addListener('place_changed', () => { + const place = autocompleteRef.current.getPlace(); + if (place.formatted_address) { + onChange(place.formatted_address); + } + }); + } catch (error) { + console.error('Error initializing Google Maps autocomplete:', error); + } + + return () => { + if (autocompleteRef.current) { + window.google.maps.event.clearInstanceListeners(autocompleteRef.current); + } + }; + }, [isLoaded, onChange]); + + return ( +
+ + onChange(e.target.value)} + placeholder={placeholder} + className={`pl-10 ${className}`} + /> +
+ ); +} \ No newline at end of file diff --git a/frontend-web/src/components/events/AssignedStaffManager.jsx b/frontend-web/src/components/events/AssignedStaffManager.jsx index 384e3bfa..8296c5ff 100644 --- a/frontend-web/src/components/events/AssignedStaffManager.jsx +++ b/frontend-web/src/components/events/AssignedStaffManager.jsx @@ -16,6 +16,7 @@ import { Input } from "@/components/ui/input"; import { useToast } from "@/components/ui/use-toast"; import { Edit2, Trash2, ArrowLeftRight, Clock, MapPin, Check, X } from "lucide-react"; import { format } from "date-fns"; +import { calculateOrderStatus } from "../orders/OrderStatusUtils"; export default function AssignedStaffManager({ event, shift, role }) { const { toast } = useToast(); @@ -52,11 +53,19 @@ export default function AssignedStaffManager({ event, shift, role }) { return s; }); - await base44.entities.Event.update(event.id, { + const updatedEvent = { assigned_staff: updatedAssignedStaff, shifts: updatedShifts, - requested: Math.max((event.requested || 0) - 1, 0), + // NEVER MODIFY REQUESTED - it's set by client, not by staff assignment + }; + + // Auto-update status based on staffing level + updatedEvent.status = calculateOrderStatus({ + ...event, + ...updatedEvent }); + + await base44.entities.Event.update(event.id, updatedEvent); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['events'] }); @@ -135,7 +144,7 @@ export default function AssignedStaffManager({ event, shift, role }) { key={staff.staff_id} className="flex items-center gap-3 p-3 bg-white border border-slate-200 rounded-lg hover:shadow-sm transition-shadow" > - + {staff.staff_name?.charAt(0) || 'S'} diff --git a/frontend-web/src/components/events/EventAssignmentModal.jsx b/frontend-web/src/components/events/EventAssignmentModal.jsx index 733b8d13..df769c5c 100644 --- a/frontend-web/src/components/events/EventAssignmentModal.jsx +++ b/frontend-web/src/components/events/EventAssignmentModal.jsx @@ -62,7 +62,7 @@ const hasTimeConflict = (existingStart, existingEnd, newStart, newEnd, existingD return (newStartMin < existingEndMin && newEndMin > existingStartMin); }; -export default function EventAssignmentModal({ open, onClose, order, onUpdate }) { +export default function EventAssignmentModal({ open, onClose, order, onUpdate, isRapid = false }) { const [selectedShiftIndex, setSelectedShiftIndex] = useState(0); const [selectedRoleIndex, setSelectedRoleIndex] = useState(0); const [selectedStaffIds, setSelectedStaffIds] = useState([]); @@ -196,6 +196,20 @@ export default function EventAssignmentModal({ open, onClose, order, onUpdate }) const updatedOrder = { ...order }; const assignments = updatedOrder.shifts_data[selectedShiftIndex].roles[selectedRoleIndex].assignments || []; + const needed = parseInt(currentRole.count) || 0; + const currentAssigned = assignments.length; + const remaining = needed - currentAssigned; + + // Strictly enforce the requested count + if (remaining <= 0) { + toast({ + title: "Assignment Limit Reached", + description: `This position requested exactly ${needed} staff. Cannot assign more.`, + variant: "destructive", + }); + return; + } + // Check for conflicts const conflictingStaff = []; selectedStaffIds.forEach(staffId => { @@ -215,13 +229,9 @@ export default function EventAssignmentModal({ open, onClose, order, onUpdate }) return; } - const needed = parseInt(currentRole.count) || 0; - const currentAssigned = assignments.length; - const remaining = needed - currentAssigned; - if (selectedStaffIds.length > remaining) { toast({ - title: "Too many selected", + title: "Assignment Limit", description: `Only ${remaining} more staff ${remaining === 1 ? 'is' : 'are'} needed.`, variant: "destructive", }); @@ -255,6 +265,16 @@ export default function EventAssignmentModal({ open, onClose, order, onUpdate }) const updatedOrder = { ...order }; const assignments = updatedOrder.shifts_data[selectedShiftIndex].roles[selectedRoleIndex].assignments || []; + // Strictly enforce the requested count + if (assignments.length >= needed) { + toast({ + title: "Assignment Limit Reached", + description: `This position requested exactly ${needed} staff. Cannot assign more.`, + variant: "destructive", + }); + return; + } + // Check if already assigned if (assignments.some(a => a.employee_id === staffMember.id)) { toast({ diff --git a/frontend-web/src/components/events/EventFormWizard.jsx b/frontend-web/src/components/events/EventFormWizard.jsx index c56d526c..112a31d6 100644 --- a/frontend-web/src/components/events/EventFormWizard.jsx +++ b/frontend-web/src/components/events/EventFormWizard.jsx @@ -1,36 +1,30 @@ - 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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { - Calendar, Zap, RefreshCw, Users, Building2, Shield, - Plus, Minus, Trash2, Search, X, FileText, Save + Calendar, Zap, RefreshCw, Users, Building2, + Plus, Minus, Trash2, Search, Save, MapPin, Edit2, UserPlus } from "lucide-react"; import { format } from "date-fns"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Calendar as CalendarComponent } from "@/components/ui/calendar"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Checkbox } from "@/components/ui/checkbox"; import { Textarea } from "@/components/ui/textarea"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from "@/components/ui/command"; +import GoogleAddressInput from "../common/GoogleAddressInput"; +import RapidOrderInterface from "../orders/RapidOrderInterface"; +import { createPageUrl } from "@/utils"; -const UNIFORM_TYPES = ["Type 1", "Type 2", "Type 3", "All Black", "Business Casual", "Chef Whites"]; - -export default function EventFormWizard({ event, onSubmit, isSubmitting, currentUser, onCancel }) { +export default function EventFormWizard({ event, onSubmit, onRapidSubmit, isSubmitting, currentUser, onCancel }) { + const [showRapidInterface, setShowRapidInterface] = useState(false); const [formData, setFormData] = useState(event || { event_name: "", order_type: "one_time", - recurrence_type: "single", - recurrence_start_date: "", - recurrence_end_date: "", - scatter_dates: [], - recurring_days: [], - recurring_frequency: "weekly", business_id: "", business_name: "", hub: "", @@ -40,8 +34,15 @@ export default function EventFormWizard({ event, onSubmit, isSubmitting, current date: "", include_backup: false, backup_staff_count: 0, - vendor_id: "", // Added vendor_id - vendor_name: "", // Added vendor_name + vendor_id: "", + vendor_name: "", + recurring_start_date: "", + recurring_end_date: "", + recurring_days: [], + recurring_time: "09:00", + permanent_start_date: "", + permanent_days: [], + permanent_time: "09:00", shifts: [{ shift_name: "Shift 1", location_address: "", @@ -74,7 +75,7 @@ export default function EventFormWizard({ event, onSubmit, isSubmitting, current const currentUserData = currentUser || user; const userRole = currentUserData?.user_role || currentUserData?.role || "admin"; const isVendor = userRole === "vendor"; - const isClient = userRole === "client"; // Added isClient + const isClient = userRole === "client"; const { data: businesses = [] } = useQuery({ queryKey: ['businesses'], @@ -82,7 +83,7 @@ export default function EventFormWizard({ event, onSubmit, isSubmitting, current initialData: [], }); - const { data: vendors = [] } = useQuery({ // Added vendors query + const { data: vendors = [] } = useQuery({ queryKey: ['vendors-for-order'], queryFn: () => base44.entities.Vendor.list(), initialData: [], @@ -96,7 +97,6 @@ export default function EventFormWizard({ event, onSubmit, isSubmitting, current const availableRoles = [...new Set(allRates.map(r => r.role_name))].sort(); - // Auto-select preferred vendor for clients useEffect(() => { if (isClient && currentUserData && !formData.vendor_id) { const preferredVendorId = currentUserData.preferred_vendor_id; @@ -110,14 +110,125 @@ export default function EventFormWizard({ event, onSubmit, isSubmitting, current })); } } - }, [isClient, currentUserData, formData.vendor_id]); // Dependency array updated + }, [isClient, currentUserData, formData.vendor_id]); + + // Auto-fill fields when order type changes to RAPID + useEffect(() => { + if (formData.order_type === 'rapid' && formData.shifts?.length > 0 && formData.shifts[0].roles?.length > 0) { + const firstRole = formData.shifts[0].roles[0]; + + // Only auto-fill if fields are empty + if (!firstRole.start_time || !firstRole.count || firstRole.count === 0) { + const updatedShifts = [...formData.shifts]; + const updatedRole = { + ...firstRole, + start_time: firstRole.start_time || "09:00", + end_time: firstRole.end_time || "17:00", + count: firstRole.count || 1, + break_minutes: firstRole.break_minutes ?? 30, + hours: firstRole.hours || 8, + }; + + // If role is selected, fetch rate + if (updatedRole.role && formData.vendor_id) { + const rate = getRateForRole(updatedRole.role, formData.vendor_id); + updatedRole.rate_per_hour = rate; + updatedRole.total_value = rate * updatedRole.hours * updatedRole.count; + } + + updatedShifts[0].roles[0] = updatedRole; + + setFormData(prev => ({ + ...prev, + shifts: updatedShifts + })); + updateGrandTotal(); + } + } + }, [formData.order_type]); useEffect(() => { if (event) { - setFormData(event); + // Determine order type from event data + let orderType = 'one_time'; + if (event.is_rapid) { + orderType = 'rapid'; + } else if (event.is_recurring || event.order_type === 'recurring') { + orderType = 'recurring'; + } else if (event.order_type === 'permanent') { + orderType = 'permanent'; + } else if (event.order_type) { + orderType = event.order_type; + } + + // Ensure all fields are properly loaded + const loadedData = { + ...event, + order_type: orderType, + vendor_id: event.vendor_id || "", + vendor_name: event.vendor_name || "", + hub: event.hub || "", + date: event.date || "", + event_name: event.event_name || "", + total: event.total || 0, + shifts: event.shifts && event.shifts.length > 0 ? event.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 + }] + }] + }; + + setFormData(loadedData); } }, [event]); + // Auto-fill fields when order type changes to RAPID + useEffect(() => { + if (formData.order_type === 'rapid' && formData.shifts?.length > 0 && formData.shifts[0].roles?.length > 0) { + const firstRole = formData.shifts[0].roles[0]; + + // Only auto-fill if fields are empty + if (!firstRole.start_time || !firstRole.count || firstRole.count === 0) { + const updatedShifts = [...formData.shifts]; + const updatedRole = { + ...firstRole, + start_time: firstRole.start_time || "09:00", + end_time: firstRole.end_time || "17:00", + count: firstRole.count || 1, + break_minutes: firstRole.break_minutes ?? 30, + hours: firstRole.hours || 8, + }; + + // If role is selected, fetch rate + if (updatedRole.role && formData.vendor_id) { + const rate = getRateForRole(updatedRole.role, formData.vendor_id); + updatedRole.rate_per_hour = rate; + updatedRole.total_value = rate * updatedRole.hours * updatedRole.count; + } + + updatedShifts[0].roles[0] = updatedRole; + + setFormData(prev => ({ + ...prev, + shifts: updatedShifts + })); + updateGrandTotal(); + } + } + }, [formData.order_type]); + const handleChange = (field, value) => { setFormData(prev => ({ ...prev, [field]: value })); }; @@ -129,15 +240,29 @@ export default function EventFormWizard({ event, onSubmit, isSubmitting, current ...prev, business_id: businessId, business_name: selectedBusiness.business_name || "", + hub: selectedBusiness.hub_building || prev.hub, shifts: prev.shifts.map(shift => ({ ...shift, - location_address: shift.same_as_billing ? selectedBusiness.address || shift.location_address : shift.location_address + location_address: selectedBusiness.address || shift.location_address })) })); } }; - const handleVendorChange = (vendorId) => { // Added handleVendorChange + // Auto-populate shift addresses when hub changes + useEffect(() => { + if (formData.hub && formData.shifts.length > 0) { + setFormData(prev => ({ + ...prev, + shifts: prev.shifts.map((shift, idx) => ({ + ...shift, + location_address: shift.location_address || formData.hub + })) + })); + } + }, [formData.hub]); + + const handleVendorChange = (vendorId) => { const selectedVendor = vendors.find(v => v.id === vendorId); if (selectedVendor) { setFormData(prev => { @@ -145,16 +270,20 @@ export default function EventFormWizard({ event, onSubmit, isSubmitting, current ...shift, roles: shift.roles.map(role => { if (role.role) { - const rate = getRateForRole(role.role, vendorId); // Re-calculate rates with new vendorId + const rate = getRateForRole(role.role, vendorId); return { ...role, rate_per_hour: rate, - total_value: rate * (role.hours || 0) * (role.count || 1), + total_value: rate * (role.hours || 0) * (parseInt(role.count) || 1), vendor_id: vendorId, vendor_name: selectedVendor.legal_name || selectedVendor.doing_business_as }; } - return role; + return { + ...role, + vendor_id: vendorId, + vendor_name: selectedVendor.legal_name || selectedVendor.doing_business_as + }; }) })); @@ -165,7 +294,7 @@ export default function EventFormWizard({ event, onSubmit, isSubmitting, current shifts: updatedShifts }; }); - updateGrandTotal(); + setTimeout(() => updateGrandTotal(), 10); } }; @@ -176,25 +305,28 @@ export default function EventFormWizard({ event, onSubmit, isSubmitting, current const startMinutes = startHour * 60 + startMin; const endMinutes = endHour * 60 + endMin; let totalMinutes = endMinutes - startMinutes; - if (totalMinutes < 0) totalMinutes += 24 * 60; // Handle overnight shifts - totalMinutes -= (breakMinutes || 0); // Subtract break - return Math.max(0, totalMinutes / 60); + if (totalMinutes < 0) totalMinutes += 24 * 60; + totalMinutes -= (breakMinutes || 0); + return Math.max(0, Math.round((totalMinutes / 60) * 100) / 100); }; - const getRateForRole = (roleName, vendorId = null) => { // Modified getRateForRole + const getRateForRole = (roleName, vendorId = null) => { const targetVendorId = vendorId || formData.vendor_id; if (targetVendorId) { const rate = allRates.find(r => r.role_name === roleName && r.vendor_id === targetVendorId && - r.is_active + r.is_active !== false ); - if (rate) return parseFloat(rate.client_rate || 0); + if (rate) { + console.log('Found rate for', roleName, 'from vendor', targetVendorId, ':', rate.client_rate); + return parseFloat(rate.client_rate || 0); + } } - // Fallback to any active rate if no specific vendor or vendor-specific rate is found - const fallbackRate = allRates.find(r => r.role_name === roleName && r.is_active); + console.log('No rate found for', roleName, 'from vendor', targetVendorId); + const fallbackRate = allRates.find(r => r.role_name === roleName && r.is_active !== false); return fallbackRate ? parseFloat(fallbackRate.client_rate || 0) : 0; }; @@ -205,21 +337,27 @@ export default function EventFormWizard({ event, onSubmit, isSubmitting, current role[field] = value; if (field === 'role') { - const rate = getRateForRole(value); + const rate = getRateForRole(value, prev.vendor_id); + console.log('Setting rate for role', value, ':', rate, 'vendor:', prev.vendor_id); role.rate_per_hour = rate; - role.vendor_id = prev.vendor_id; // Added vendor_id to role - role.vendor_name = prev.vendor_name; // Added vendor_name to role + role.vendor_id = prev.vendor_id; + role.vendor_name = prev.vendor_name; } if (field === 'start_time' || field === 'end_time' || field === 'break_minutes') { role.hours = calculateHours(role.start_time, role.end_time, role.break_minutes); } + if (field === 'count') { + role.count = parseInt(value) || 1; + } + role.total_value = (role.rate_per_hour || 0) * (role.hours || 0) * (role.count || 1); + console.log('Role total:', role.total_value, '= rate:', role.rate_per_hour, '* hours:', role.hours, '* count:', role.count); return { ...prev, shifts: newShifts }; }); - updateGrandTotal(); + setTimeout(() => updateGrandTotal(), 0); }; const updateGrandTotal = () => { @@ -234,6 +372,48 @@ export default function EventFormWizard({ event, onSubmit, isSubmitting, current }, 0); }; + const handleAddShift = () => { + setFormData(prev => { + const newShift = { + shift_name: `Shift ${prev.shifts.length + 1}`, + location_address: prev.hub || "", + same_as_billing: false, + 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, + vendor_id: prev.vendor_id, + vendor_name: prev.vendor_name + }] + }; + return { ...prev, shifts: [...prev.shifts, newShift] }; + }); + }; + + const handleRemoveShift = (shiftIndex) => { + if (formData.shifts.length === 1) return; + setFormData(prev => { + const newShifts = prev.shifts.filter((_, idx) => idx !== shiftIndex); + return { ...prev, shifts: newShifts }; + }); + updateGrandTotal(); + }; + + const handleShiftChange = (shiftIndex, field, value) => { + setFormData(prev => { + const newShifts = [...prev.shifts]; + newShifts[shiftIndex][field] = value; + return { ...prev, shifts: newShifts }; + }); + }; + const handleAddRole = (shiftIndex) => { setFormData(prev => { const newShifts = [...prev.shifts]; @@ -245,11 +425,11 @@ export default function EventFormWizard({ event, onSubmit, isSubmitting, current end_time: "17:00", hours: 8, uniform: "Type 1", - break_minutes: 30, // Default to 30 min non-payable + break_minutes: 30, rate_per_hour: 0, total_value: 0, - vendor_id: prev.vendor_id, // Added vendor_id to new role - vendor_name: prev.vendor_name // Added vendor_name to new role + vendor_id: prev.vendor_id, + vendor_name: prev.vendor_name }); return { ...prev, shifts: newShifts }; }); @@ -267,586 +447,459 @@ export default function EventFormWizard({ event, onSubmit, isSubmitting, current const handleOrderTypeChange = (type) => { setFormData(prev => ({ ...prev, - order_type: type, - date: "", - recurrence_start_date: "", - recurrence_end_date: "", - scatter_dates: [], - recurring_days: [], - recurring_frequency: "weekly", - recurrence_type: type === "recurring" ? "date_range" : "single" + order_type: type })); }; - const handleScatterDateSelect = (dates) => { + const handleRapidInterfaceSubmit = (rapidData) => { + // Parse the rapid message and populate form + // For now, just add the message to notes and switch to standard view setFormData(prev => ({ ...prev, - scatter_dates: dates?.map(d => format(d, 'yyyy-MM-dd')).sort() || [] + notes: rapidData.rawMessage, + event_name: "RAPID Order", + date: new Date().toISOString().split('T')[0] })); - }; - 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) - }; - }); + // Switch back to standard form view + setFormData(prev => ({ + ...prev, + order_type: 'one_time' + })); }; const handleSubmit = (isDraft = false) => { const status = isDraft ? "Draft" : formData.order_type === "rapid" ? "Active" : "Pending"; - onSubmit({ ...formData, status }); + + const totalRequested = formData.shifts.reduce((sum, shift) => { + return sum + shift.roles.reduce((roleSum, role) => roleSum + (parseInt(role.count) || 0), 0); + }, 0); + + onSubmit({ + ...formData, + status, + requested: totalRequested + }); }; + + return ( - <> - - -
- {/* Left Column - Order Type & Details */} -
- {/* Order Type */} - - - -
- - - - - - - -
- - {/* Recurring Options - Enhanced with Calendar */} - {formData.order_type === 'recurring' && ( -
-
- {['weekly', 'monthly', 'custom'].map(freq => ( - - ))} -
- - {/* Weekly: Day Selector */} - {formData.recurring_frequency === 'weekly' && ( -
- -
- {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day, idx) => { - const isSelected = (formData.recurring_days || []).includes(idx); - return ( - - ); - })} -
-
- )} - - {/* Monthly: Date Range Picker */} - {formData.recurring_frequency === 'monthly' && ( -
- -
-
- - handleChange('recurrence_start_date', e.target.value)} - className="h-8 text-xs" - /> -
-
- - handleChange('recurrence_end_date', e.target.value)} - className="h-8 text-xs" - /> -
-
-
- This order will repeat monthly between the selected dates -
-
- )} - - {/* Custom: Multiple Date Picker */} - {formData.recurring_frequency === 'custom' && ( -
- - - - - - - new Date(d))} - onSelect={(dates) => handleScatterDateSelect(dates)} - numberOfMonths={2} - className="rounded-md" - /> - - - {formData.scatter_dates && formData.scatter_dates.length > 0 && ( -
-

Selected dates:

-
- {formData.scatter_dates.slice(0, 3).map((date, idx) => ( - - {format(new Date(date), 'MMM d')} - - ))} - {formData.scatter_dates.length > 3 && ( - - +{formData.scatter_dates.length - 3} more - - )} -
-
- )} -
- )} -
- )} -
-
- - {/* Event Details - REORDERED */} - - - - - {/* Vendor Selection for Clients */} - {isClient && ( -
- - - {formData.vendor_id && ( -

- ✓ Rates will be automatically applied from {formData.vendor_name} -

- )} -
- )} - - {/* 1. Hub (first) */} -
- - handleChange('hub', e.target.value)} - placeholder="Hub location" - className="h-8 text-sm" - required - /> -
- - {/* 2. Department (new field) */} -
- - handleChange('department', e.target.value)} - placeholder="Department name" - className="h-8 text-sm" - /> -
- - {/* 3. Date (required - only for non-recurring) */} - {formData.order_type !== 'recurring' && ( -
- - handleChange('date', e.target.value)} - className="h-8 text-sm" - required - /> -
- )} - - {/* 4. Event Name (now optional) */} -
- - handleChange('event_name', e.target.value)} - placeholder="Event name (optional)" - className="h-8 text-sm" - /> -
- - {/* 5. PO Reference (optional) */} -
- - handleChange('po_reference', e.target.value)} - placeholder="Purchase order number (optional)" - className="h-8 text-sm" - /> -
- - {isVendor && ( -
- - -
- )} - -
- handleChange('include_backup', checked)} - /> - -
-
-
- - {/* Grand Total */} - - -
-

Total

-

${(formData.total || 0).toFixed(2)}

-
-
-
- - {/* Actions */} -
- - + + - + + + +

Recurring

+

Ongoing Weekly / Monthly Coverage

+ + +
- {/* Right Column - Shifts & Roles */} -
- + + + - {formData.shifts.map((shift, shiftIdx) => ( - - -
-

{shift.shift_name}

+ {isClient && ( +
+ + + {formData.vendor_id && ( +

+ ✓ Rates will be automatically applied from {formData.vendor_name} + {currentUserData?.preferred_vendor_id === formData.vendor_id && " (Your preferred vendor)"} +

+ )} +
+ )} + +
+ + handleChange('hub', e.target.value)} + placeholder="Hub location" + className="h-10" + required + /> +
+ +
+ + handleChange('department', e.target.value)} + placeholder="Department name" + className="h-10" + /> +
+ + {formData.order_type === 'recurring' ? ( + <> +
+ + { + const dateValue = e.target.value; + handleChange('recurring_start_date', dateValue); + }} + className="h-10" + required + /> +
+
+ + { + const dateValue = e.target.value; + handleChange('recurring_end_date', dateValue); + }} + className="h-10" + required + /> +
+
+ +
+ {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day, idx) => { + const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + const isSelected = formData.recurring_days?.includes(dayNames[idx]); + return ( + + ); + })} +
+ {formData.recurring_days?.length > 0 && ( +

+ Every {formData.recurring_days.join(', ')} +

+ )} +
+ + ) : formData.order_type === 'permanent' ? ( + <> +
+ + { + const dateValue = e.target.value; + handleChange('permanent_start_date', dateValue); + }} + className="h-10" + required + /> +
+
+ +
+ {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day, idx) => { + const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + const isSelected = formData.permanent_days?.includes(dayNames[idx]); + return ( + + ); + })} +
+ {formData.permanent_days?.length > 0 && ( +

+ Every {formData.permanent_days.join(', ')} +

+ )} +
+ + ) : ( +
+ + { + const dateValue = e.target.value; + handleChange('date', dateValue); + }} + className="h-10" + required + /> +
+ )} + +
+ + handleChange('event_name', e.target.value)} + placeholder="Event name (optional)" + className="h-10" + /> +
+ +
+ + handleChange('po_reference', e.target.value)} + placeholder="Purchase order number (optional)" + className="h-10" + /> +
+ + {isVendor && ( +
+ + +
+ )} + +
+ handleChange('include_backup', checked)} + /> + +
+ + + + + +
+

Total

+

${(formData.total || 0).toFixed(2)}

+
+
+
+ +
+ + + +
+
+ + {/* Right Column */} +
+ {/* RAPID Interface - Shows when RAPID is selected */} + {formData.order_type === 'rapid' && ( +
+ handleOrderTypeChange('one_time')} + onSubmit={handleRapidInterfaceSubmit} + /> +
+ )} + + {/* Show standard form sections only when NOT RAPID */} + {formData.order_type !== 'rapid' && ( + <> + + + Shifts & Roles + + + {formData.shifts.map((shift, shiftIdx) => ( +
+
+
+ +
+

{shift.shift_name}

+
+ handleShiftChange(shiftIdx, 'location_address', value)} + placeholder="Enter location address..." + /> +
+
+
+
+ + {formData.shifts.length > 1 && ( + + )} +
{shift.roles.map((role, roleIdx) => ( -
- {/* Row 1: Role, Count, Times, Hours */} -
- {/* Role */} +
+
- + setRoleSearchOpen(prev => ({ ...prev, [`${shiftIdx}-${roleIdx}`]: open }))}> - - + - + No role found. - + {availableRoles.map((roleName) => ( ({ ...prev, [`${shiftIdx}-${roleIdx}`]: false })); }} - className="text-xs" + className="aria-selected:bg-blue-100 aria-selected:text-blue-900" > {roleName} @@ -865,85 +918,72 @@ export default function EventFormWizard({ event, onSubmit, isSubmitting, current
- {/* Count - More Visible */} -
- -
+
+ +
-
- {role.count || 1} -
+ handleRoleChange(shiftIdx, roleIdx, 'count', Math.max(1, parseInt(e.target.value) || 1))} + className="h-10 flex-1 text-center font-bold text-blue-600 text-xl border border-slate-300 bg-white" + />
- {/* Start Time */}
- + handleRoleChange(shiftIdx, roleIdx, 'start_time', e.target.value)} - className="h-8 text-xs" + className="h-10" />
- {/* End Time */}
- + handleRoleChange(shiftIdx, roleIdx, 'end_time', e.target.value)} - className="h-8 text-xs" + className="h-10" />
- {/* Hours */}
- -
- {role.hours || 0}h + +
+ {role.hours % 1 === 0 ? role.hours : (role.hours || 0).toFixed(2)}h
- - {/* Delete */} -
- {shift.roles.length > 1 && ( - - )} -
- {/* Row 2: Break Selection */} -
- +
+
- {/* Rate & Total */} -
- Rate: ${(role.rate_per_hour || 0).toFixed(2)}/hr - Total: ${(role.total_value || 0).toFixed(2)} +
+ Rate: ${(role.rate_per_hour || 0).toFixed(2)}/hr +
+ Total: ${(role.total_value || 0).toFixed(2)} + {shift.roles.length > 1 && ( + + )} +
))} - - - - ))} + + Add role + +
+ ))} - {/* Notes */} - - - -