feat: comprehensive update with Signal, Copilot, themes, and PDF features
## Signal Messaging Integration - Added SignalService for sending messages to drivers via Signal - SignalMessage model for tracking message history - Driver chat modal for real-time messaging - Send schedule via Signal (ICS + PDF attachments) ## AI Copilot - Natural language interface for VIP Coordinator - Capabilities: create VIPs, schedule events, assign drivers - Help and guidance for users - Floating copilot button in UI ## Theme System - Dark/light/system theme support - Color scheme selection (blue, green, purple, orange, red) - ThemeContext for global state - AppearanceMenu in header ## PDF Schedule Export - VIPSchedulePDF component for schedule generation - PDF settings (header, footer, branding) - Preview PDF in browser - Settings stored in database ## Database Migrations - add_signal_messages: SignalMessage model - add_pdf_settings: Settings model for PDF config - add_reminder_tracking: lastReminderSent for events - make_driver_phone_optional: phone field nullable ## Event Management - Event status service for automated updates - IN_PROGRESS/COMPLETED status tracking - Reminder tracking for notifications ## UI/UX Improvements - Driver schedule modal - Improved My Schedule page - Better error handling and loading states - Responsive design improvements ## Other Changes - AGENT_TEAM.md documentation - Seed data improvements - Ability factory updates - Driver profile page Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ interface EventFormProps {
|
||||
onSubmit: (data: EventFormData) => void;
|
||||
onCancel: () => void;
|
||||
isSubmitting: boolean;
|
||||
extraActions?: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface EventFormData {
|
||||
@@ -36,7 +37,7 @@ interface ScheduleConflict {
|
||||
endTime: string;
|
||||
}
|
||||
|
||||
export function EventForm({ event, onSubmit, onCancel, isSubmitting }: EventFormProps) {
|
||||
export function EventForm({ event, onSubmit, onCancel, isSubmitting, extraActions }: EventFormProps) {
|
||||
// Helper to convert ISO datetime to datetime-local format
|
||||
const toDatetimeLocal = (isoString: string | null | undefined) => {
|
||||
if (!isoString) return '';
|
||||
@@ -199,15 +200,15 @@ export function EventForm({ event, onSubmit, onCancel, isSubmitting }: EventForm
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-white rounded-lg shadow-xl max-w-3xl w-full max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex items-center justify-between p-6 border-b sticky top-0 bg-white z-10">
|
||||
<h2 className="text-2xl font-bold text-gray-900">
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-card rounded-lg shadow-xl max-w-3xl w-full max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex items-center justify-between p-6 border-b border-border sticky top-0 bg-card z-10">
|
||||
<h2 className="text-2xl font-bold text-foreground">
|
||||
{event ? 'Edit Event' : 'Add New Event'}
|
||||
</h2>
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
style={{ minWidth: '44px', minHeight: '44px' }}
|
||||
>
|
||||
<X className="h-6 w-6" />
|
||||
@@ -217,42 +218,42 @@ export function EventForm({ event, onSubmit, onCancel, isSubmitting }: EventForm
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-4">
|
||||
{/* VIP Multi-Select */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
<Users className="inline h-4 w-4 mr-1" />
|
||||
VIPs * (select one or more)
|
||||
</label>
|
||||
|
||||
<div className="border border-gray-300 rounded-md p-3 max-h-48 overflow-y-auto">
|
||||
<div className="border border-input rounded-md p-3 max-h-48 overflow-y-auto bg-background">
|
||||
{vips?.map((vip) => (
|
||||
<label
|
||||
key={vip.id}
|
||||
className="flex items-center py-2 px-3 hover:bg-gray-50 rounded cursor-pointer"
|
||||
className="flex items-center py-2 px-3 hover:bg-accent rounded cursor-pointer"
|
||||
style={{ minHeight: '44px' }}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.vipIds.includes(vip.id)}
|
||||
onChange={() => handleVipToggle(vip.id)}
|
||||
className="h-4 w-4 text-primary rounded border-gray-300 focus:ring-primary"
|
||||
className="h-4 w-4 text-primary rounded border-input focus:ring-primary"
|
||||
/>
|
||||
<span className="ml-3 text-base text-gray-700">
|
||||
<span className="ml-3 text-base text-foreground">
|
||||
{vip.name}
|
||||
{vip.organization && (
|
||||
<span className="text-sm text-gray-500 ml-2">({vip.organization})</span>
|
||||
<span className="text-sm text-muted-foreground ml-2">({vip.organization})</span>
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-2 text-sm text-gray-600">
|
||||
<div className="mt-2 text-sm text-muted-foreground">
|
||||
<strong>Selected ({formData.vipIds.length}):</strong> {selectedVipNames}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
<label className="block text-sm font-medium text-foreground mb-1">
|
||||
Event Title *
|
||||
</label>
|
||||
<input
|
||||
@@ -262,14 +263,14 @@ export function EventForm({ event, onSubmit, onCancel, isSubmitting }: EventForm
|
||||
value={formData.title}
|
||||
onChange={handleChange}
|
||||
placeholder="e.g., Transport to Campfire Night"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
className="w-full px-3 py-2 bg-background text-foreground placeholder:text-muted-foreground border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Pickup & Dropoff Locations */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
<label className="block text-sm font-medium text-foreground mb-1">
|
||||
Pickup Location
|
||||
</label>
|
||||
<input
|
||||
@@ -278,11 +279,11 @@ export function EventForm({ event, onSubmit, onCancel, isSubmitting }: EventForm
|
||||
value={formData.pickupLocation}
|
||||
onChange={handleChange}
|
||||
placeholder="e.g., Grand Hotel Lobby"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
className="w-full px-3 py-2 bg-background text-foreground placeholder:text-muted-foreground border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
<label className="block text-sm font-medium text-foreground mb-1">
|
||||
Dropoff Location
|
||||
</label>
|
||||
<input
|
||||
@@ -291,7 +292,7 @@ export function EventForm({ event, onSubmit, onCancel, isSubmitting }: EventForm
|
||||
value={formData.dropoffLocation}
|
||||
onChange={handleChange}
|
||||
placeholder="e.g., Camp Amphitheater"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
className="w-full px-3 py-2 bg-background text-foreground placeholder:text-muted-foreground border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -299,7 +300,7 @@ export function EventForm({ event, onSubmit, onCancel, isSubmitting }: EventForm
|
||||
{/* Start & End Time */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
<label className="block text-sm font-medium text-foreground mb-1">
|
||||
Start Time *
|
||||
</label>
|
||||
<input
|
||||
@@ -308,11 +309,11 @@ export function EventForm({ event, onSubmit, onCancel, isSubmitting }: EventForm
|
||||
required
|
||||
value={formData.startTime}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
className="w-full px-3 py-2 bg-background text-foreground border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
<label className="block text-sm font-medium text-foreground mb-1">
|
||||
End Time *
|
||||
</label>
|
||||
<input
|
||||
@@ -321,14 +322,14 @@ export function EventForm({ event, onSubmit, onCancel, isSubmitting }: EventForm
|
||||
required
|
||||
value={formData.endTime}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
className="w-full px-3 py-2 bg-background text-foreground border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Vehicle Selection with Capacity */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
<label className="block text-sm font-medium text-foreground mb-1">
|
||||
<Car className="inline h-4 w-4 mr-1" />
|
||||
Assigned Vehicle
|
||||
</label>
|
||||
@@ -336,7 +337,7 @@ export function EventForm({ event, onSubmit, onCancel, isSubmitting }: EventForm
|
||||
name="vehicleId"
|
||||
value={formData.vehicleId}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
className="w-full px-3 py-2 bg-background text-foreground border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
>
|
||||
<option value="">No vehicle assigned</option>
|
||||
{vehicles?.map((vehicle) => (
|
||||
@@ -346,7 +347,7 @@ export function EventForm({ event, onSubmit, onCancel, isSubmitting }: EventForm
|
||||
))}
|
||||
</select>
|
||||
{selectedVehicle && (
|
||||
<div className={`mt-2 text-sm ${seatsUsed > seatsAvailable ? 'text-red-600 font-medium' : 'text-gray-600'}`}>
|
||||
<div className={`mt-2 text-sm ${seatsUsed > seatsAvailable ? 'text-red-600 font-medium' : 'text-muted-foreground'}`}>
|
||||
Capacity: {seatsUsed}/{seatsAvailable} seats used
|
||||
{seatsUsed > seatsAvailable && ' ⚠️ OVER CAPACITY'}
|
||||
</div>
|
||||
@@ -355,14 +356,14 @@ export function EventForm({ event, onSubmit, onCancel, isSubmitting }: EventForm
|
||||
|
||||
{/* Driver Selection */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
<label className="block text-sm font-medium text-foreground mb-1">
|
||||
Assigned Driver
|
||||
</label>
|
||||
<select
|
||||
name="driverId"
|
||||
value={formData.driverId}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
className="w-full px-3 py-2 bg-background text-foreground border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
>
|
||||
<option value="">No driver assigned</option>
|
||||
{drivers?.map((driver) => (
|
||||
@@ -376,7 +377,7 @@ export function EventForm({ event, onSubmit, onCancel, isSubmitting }: EventForm
|
||||
{/* Event Type & Status */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
<label className="block text-sm font-medium text-foreground mb-1">
|
||||
Event Type *
|
||||
</label>
|
||||
<select
|
||||
@@ -384,7 +385,7 @@ export function EventForm({ event, onSubmit, onCancel, isSubmitting }: EventForm
|
||||
required
|
||||
value={formData.type}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
className="w-full px-3 py-2 bg-background text-foreground border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
>
|
||||
<option value="TRANSPORT">Transport</option>
|
||||
<option value="MEETING">Meeting</option>
|
||||
@@ -394,7 +395,7 @@ export function EventForm({ event, onSubmit, onCancel, isSubmitting }: EventForm
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
<label className="block text-sm font-medium text-foreground mb-1">
|
||||
Status *
|
||||
</label>
|
||||
<select
|
||||
@@ -402,7 +403,7 @@ export function EventForm({ event, onSubmit, onCancel, isSubmitting }: EventForm
|
||||
required
|
||||
value={formData.status}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
className="w-full px-3 py-2 bg-background text-foreground border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
>
|
||||
<option value="SCHEDULED">Scheduled</option>
|
||||
<option value="IN_PROGRESS">In Progress</option>
|
||||
@@ -414,7 +415,7 @@ export function EventForm({ event, onSubmit, onCancel, isSubmitting }: EventForm
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
<label className="block text-sm font-medium text-foreground mb-1">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
@@ -423,7 +424,7 @@ export function EventForm({ event, onSubmit, onCancel, isSubmitting }: EventForm
|
||||
onChange={handleChange}
|
||||
rows={3}
|
||||
placeholder="Additional notes or instructions"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
className="w-full px-3 py-2 bg-background text-foreground placeholder:text-muted-foreground border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -440,20 +441,23 @@ export function EventForm({ event, onSubmit, onCancel, isSubmitting }: EventForm
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="flex-1 bg-gray-200 text-gray-800 py-2 px-4 rounded-md hover:bg-gray-300"
|
||||
className="flex-1 bg-muted text-foreground py-2 px-4 rounded-md hover:bg-muted/80"
|
||||
style={{ minHeight: '44px' }}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Extra Actions (e.g., Cancel Event, Delete Event) */}
|
||||
{extraActions}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Conflict Dialog */}
|
||||
{showConflictDialog && createPortal(
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-[60]">
|
||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-2xl mx-4">
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-[60]">
|
||||
<div className="bg-card rounded-lg shadow-xl w-full max-w-2xl mx-4">
|
||||
<div className="p-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex-shrink-0">
|
||||
@@ -462,10 +466,10 @@ export function EventForm({ event, onSubmit, onCancel, isSubmitting }: EventForm
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
<h3 className="text-lg font-semibold text-foreground mb-2">
|
||||
Scheduling Conflict Detected
|
||||
</h3>
|
||||
<p className="text-base text-gray-600 mb-4">
|
||||
<p className="text-base text-muted-foreground mb-4">
|
||||
This driver already has {conflicts.length} conflicting event{conflicts.length > 1 ? 's' : ''} scheduled during this time:
|
||||
</p>
|
||||
|
||||
@@ -483,14 +487,14 @@ export function EventForm({ event, onSubmit, onCancel, isSubmitting }: EventForm
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-base text-gray-700 font-medium mb-6">
|
||||
<p className="text-base text-foreground font-medium mb-6">
|
||||
Do you want to proceed with this assignment anyway?
|
||||
</p>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={handleCancelConflict}
|
||||
className="flex-1 px-4 py-3 border border-gray-300 rounded-md text-base font-medium text-gray-700 hover:bg-gray-50"
|
||||
className="flex-1 px-4 py-3 border border-input rounded-md text-base font-medium text-foreground hover:bg-accent"
|
||||
style={{ minHeight: '44px' }}
|
||||
>
|
||||
Cancel
|
||||
@@ -514,8 +518,8 @@ export function EventForm({ event, onSubmit, onCancel, isSubmitting }: EventForm
|
||||
|
||||
{/* Capacity Warning Dialog */}
|
||||
{showCapacityWarning && capacityExceeded && createPortal(
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-[60]">
|
||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-md mx-4">
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-[60]">
|
||||
<div className="bg-card rounded-lg shadow-xl w-full max-w-md mx-4">
|
||||
<div className="p-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex-shrink-0">
|
||||
@@ -524,21 +528,21 @@ export function EventForm({ event, onSubmit, onCancel, isSubmitting }: EventForm
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
<h3 className="text-lg font-semibold text-foreground mb-2">
|
||||
Vehicle Capacity Exceeded
|
||||
</h3>
|
||||
<p className="text-base text-gray-600 mb-4">
|
||||
<p className="text-base text-muted-foreground mb-4">
|
||||
You've assigned {capacityExceeded.requested} VIP{capacityExceeded.requested > 1 ? 's' : ''} to a vehicle with only {capacityExceeded.capacity} seat{capacityExceeded.capacity > 1 ? 's' : ''}.
|
||||
</p>
|
||||
|
||||
<p className="text-base text-gray-700 font-medium mb-6">
|
||||
<p className="text-base text-foreground font-medium mb-6">
|
||||
Do you want to proceed anyway?
|
||||
</p>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={handleCancelConflict}
|
||||
className="flex-1 px-4 py-3 border border-gray-300 rounded-md text-base font-medium text-gray-700 hover:bg-gray-50"
|
||||
className="flex-1 px-4 py-3 border border-input rounded-md text-base font-medium text-foreground hover:bg-accent"
|
||||
style={{ minHeight: '44px' }}
|
||||
>
|
||||
Cancel
|
||||
|
||||
Reference in New Issue
Block a user