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:
138
frontend/src/hooks/useSignalMessages.ts
Normal file
138
frontend/src/hooks/useSignalMessages.ts
Normal 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 })) || []
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user