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,74 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '../lib/api';
import { PdfSettings, UpdatePdfSettingsDto } from '../types/settings';
/**
* Fetch PDF settings
*/
export function usePdfSettings() {
return useQuery<PdfSettings>({
queryKey: ['settings', 'pdf'],
queryFn: async () => {
const { data } = await api.get('/settings/pdf');
return data;
},
});
}
/**
* Update PDF settings
*/
export function useUpdatePdfSettings() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (dto: UpdatePdfSettingsDto) => {
const { data } = await api.patch('/settings/pdf', dto);
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['settings', 'pdf'] });
},
});
}
/**
* Upload logo
*/
export function useUploadLogo() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (file: File) => {
const formData = new FormData();
formData.append('logo', file);
const { data } = await api.post('/settings/pdf/logo', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['settings', 'pdf'] });
},
});
}
/**
* Delete logo
*/
export function useDeleteLogo() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async () => {
const { data } = await api.delete('/settings/pdf/logo');
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['settings', 'pdf'] });
},
});
}

View File

@@ -0,0 +1,138 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '../lib/api';
export interface SignalMessage {
id: string;
driverId: string;
direction: 'INBOUND' | 'OUTBOUND';
content: string;
timestamp: string;
isRead: boolean;
}
export interface UnreadCounts {
[driverId: string]: number;
}
/**
* Fetch messages for a specific driver
*/
export function useDriverMessages(driverId: string | null, enabled = true) {
return useQuery({
queryKey: ['signal-messages', driverId],
queryFn: async () => {
if (!driverId) return [];
const { data } = await api.get<SignalMessage[]>(`/signal/messages/driver/${driverId}`);
return data;
},
enabled: enabled && !!driverId,
refetchInterval: 5000, // Poll for new messages every 5 seconds
});
}
/**
* Fetch unread message counts for all drivers
*/
export function useUnreadCounts() {
return useQuery({
queryKey: ['signal-unread-counts'],
queryFn: async () => {
const { data } = await api.get<UnreadCounts>('/signal/messages/unread');
return data;
},
refetchInterval: 10000, // Poll every 10 seconds
});
}
/**
* Check which events have driver responses since the event started
* @param events Array of events with id, driverId, and startTime
*/
export function useDriverResponseCheck(
events: Array<{ id: string; driver?: { id: string } | null; startTime: string }>
) {
// Only include events that have a driver
const eventsWithDrivers = events.filter((e) => e.driver?.id);
return useQuery({
queryKey: ['signal-driver-responses', eventsWithDrivers.map((e) => e.id).join(',')],
queryFn: async () => {
if (eventsWithDrivers.length === 0) {
return new Set<string>();
}
const payload = {
events: eventsWithDrivers.map((e) => ({
eventId: e.id,
driverId: e.driver!.id,
startTime: e.startTime,
})),
};
const { data } = await api.post<{ respondedEventIds: string[] }>(
'/signal/messages/check-responses',
payload
);
return new Set(data.respondedEventIds);
},
enabled: eventsWithDrivers.length > 0,
refetchInterval: 10000, // Poll every 10 seconds
});
}
/**
* Send a message to a driver
*/
export function useSendMessage() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ driverId, content }: { driverId: string; content: string }) => {
const { data } = await api.post<SignalMessage>('/signal/messages/send', {
driverId,
content,
});
return data;
},
onSuccess: (data, variables) => {
// Add the new message to the cache immediately
queryClient.setQueryData<SignalMessage[]>(
['signal-messages', variables.driverId],
(old) => [...(old || []), data]
);
// Also invalidate to ensure consistency
queryClient.invalidateQueries({ queryKey: ['signal-messages', variables.driverId] });
},
});
}
/**
* Mark messages as read for a driver
*/
export function useMarkMessagesAsRead() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (driverId: string) => {
const { data } = await api.post(`/signal/messages/driver/${driverId}/read`);
return data;
},
onSuccess: (_, driverId) => {
// Update the unread counts cache
queryClient.setQueryData<UnreadCounts>(
['signal-unread-counts'],
(old) => {
if (!old) return {};
const updated = { ...old };
delete updated[driverId];
return updated;
}
);
// Mark messages as read in the messages cache
queryClient.setQueryData<SignalMessage[]>(
['signal-messages', driverId],
(old) => old?.map((msg) => ({ ...msg, isRead: true })) || []
);
},
});
}

View File

@@ -0,0 +1,3 @@
// Re-export useTheme from ThemeContext for convenience
export { useTheme } from '@/contexts/ThemeContext';
export type { ThemeMode, ColorScheme } from '@/contexts/ThemeContext';