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:
2026-02-01 19:30:41 +01:00
parent 2d842ed294
commit 3b0b1205df
84 changed files with 12330 additions and 2103 deletions

View File

@@ -0,0 +1,163 @@
import { useState, useRef, useEffect } from 'react';
import { Settings, Sun, Moon, Monitor, Check } from 'lucide-react';
import { useTheme, ThemeMode, ColorScheme } from '@/hooks/useTheme';
const modes: { value: ThemeMode; label: string; icon: typeof Sun }[] = [
{ value: 'light', label: 'Light', icon: Sun },
{ value: 'dark', label: 'Dark', icon: Moon },
{ value: 'system', label: 'System', icon: Monitor },
];
const colorSchemes: { value: ColorScheme; label: string; color: string }[] = [
{ value: 'blue', label: 'Blue', color: 'bg-blue-500' },
{ value: 'purple', label: 'Purple', color: 'bg-purple-500' },
{ value: 'green', label: 'Green', color: 'bg-green-500' },
{ value: 'orange', label: 'Orange', color: 'bg-orange-500' },
];
interface AppearanceMenuProps {
/** Compact mode for mobile - shows inline instead of dropdown */
compact?: boolean;
}
export function AppearanceMenu({ compact = false }: AppearanceMenuProps) {
const { mode, colorScheme, resolvedTheme, setMode, setColorScheme } = useTheme();
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const CurrentIcon = resolvedTheme === 'dark' ? Moon : Sun;
// Compact inline mode for mobile drawer
if (compact) {
return (
<div className="space-y-3">
{/* Theme Mode */}
<div>
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2">
Theme
</p>
<div className="flex gap-1">
{modes.map(({ value, label, icon: Icon }) => (
<button
key={value}
onClick={() => setMode(value)}
className={`flex items-center justify-center gap-1.5 px-3 py-2 rounded-lg text-sm transition-colors ${
mode === value
? 'bg-primary text-primary-foreground'
: 'bg-muted hover:bg-accent text-foreground'
}`}
title={label}
>
<Icon className="h-4 w-4" />
<span className="sr-only sm:not-sr-only">{label}</span>
</button>
))}
</div>
</div>
{/* Color Scheme */}
<div>
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2">
Accent Color
</p>
<div className="flex gap-2">
{colorSchemes.map(({ value, label, color }) => (
<button
key={value}
onClick={() => setColorScheme(value)}
className={`relative w-8 h-8 rounded-full ${color} transition-transform hover:scale-110 ${
colorScheme === value ? 'ring-2 ring-offset-2 ring-offset-card ring-foreground' : ''
}`}
title={label}
aria-label={`${label} color scheme`}
>
{colorScheme === value && (
<Check className="absolute inset-0 m-auto h-4 w-4 text-white" />
)}
</button>
))}
</div>
</div>
</div>
);
}
// Standard dropdown mode for header
return (
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center justify-center w-9 h-9 rounded-lg hover:bg-accent transition-colors"
aria-label="Appearance settings"
aria-expanded={isOpen}
aria-haspopup="true"
>
<CurrentIcon className="h-5 w-5 text-muted-foreground" />
</button>
{isOpen && (
<div className="absolute right-0 mt-2 w-52 rounded-xl bg-popover border border-border shadow-elevated z-50 overflow-hidden animate-in fade-in slide-in-from-top-2 duration-150">
{/* Theme Mode Section */}
<div className="p-3 border-b border-border">
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2">
Theme
</p>
<div className="flex gap-1">
{modes.map(({ value, label, icon: Icon }) => (
<button
key={value}
onClick={() => setMode(value)}
className={`flex-1 flex items-center justify-center gap-1.5 px-2 py-2 rounded-lg text-xs font-medium transition-colors ${
mode === value
? 'bg-primary text-primary-foreground'
: 'bg-muted hover:bg-accent text-foreground'
}`}
title={label}
>
<Icon className="h-3.5 w-3.5" />
{label}
</button>
))}
</div>
</div>
{/* Color Scheme Section */}
<div className="p-3">
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2">
Accent Color
</p>
<div className="flex gap-2 justify-between">
{colorSchemes.map(({ value, label, color }) => (
<button
key={value}
onClick={() => setColorScheme(value)}
className={`relative w-9 h-9 rounded-full ${color} transition-all hover:scale-110 ${
colorScheme === value
? 'ring-2 ring-offset-2 ring-offset-popover ring-foreground scale-110'
: ''
}`}
title={label}
aria-label={`${label} color scheme`}
>
{colorScheme === value && (
<Check className="absolute inset-0 m-auto h-4 w-4 text-white drop-shadow-sm" />
)}
</button>
))}
</div>
</div>
</div>
)}
</div>
);
}