Backend: - Extract shared hard-delete authorization utility (#9) - Extract Prisma include constants per entity (#11) - Fix N+1 query pattern in events findAll (#12) - Extract shared date utility functions (#13) - Move vehicle utilization filtering to DB query (#15) - Add ParseBooleanPipe for query params - Add CurrentDriver decorator + ResolveDriverInterceptor (#20) Frontend: - Extract shared form utilities (toDatetimeLocal) and enum labels (#17) - Replace browser confirm() with styled ConfirmModal (#18) - Add centralized query-keys.ts constants (#19) - Clean up unused imports, add useMemo where needed (#19) - Standardize filter button styling across list pages Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
142 lines
4.0 KiB
TypeScript
142 lines
4.0 KiB
TypeScript
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { api } from '../lib/api';
|
|
import { queryKeys } from '../lib/query-keys';
|
|
|
|
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: driverId ? queryKeys.signal.messages(driverId) : ['signal-messages', null],
|
|
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: queryKeys.signal.unreadCounts,
|
|
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);
|
|
|
|
const eventIds = eventsWithDrivers.map((e) => e.id).join(',');
|
|
|
|
return useQuery({
|
|
queryKey: queryKeys.signal.driverResponses(eventIds),
|
|
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[]>(
|
|
queryKeys.signal.messages(variables.driverId),
|
|
(old) => [...(old || []), data]
|
|
);
|
|
// Also invalidate to ensure consistency
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.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>(
|
|
queryKeys.signal.unreadCounts,
|
|
(old) => {
|
|
if (!old) return {};
|
|
const updated = { ...old };
|
|
delete updated[driverId];
|
|
return updated;
|
|
}
|
|
);
|
|
// Mark messages as read in the messages cache
|
|
queryClient.setQueryData<SignalMessage[]>(
|
|
queryKeys.signal.messages(driverId),
|
|
(old) => old?.map((msg) => ({ ...msg, isRead: true })) || []
|
|
);
|
|
},
|
|
});
|
|
}
|