feat: ticket system base implementation
This commit is contained in:
157
src/app/(modules)/ticket/hooks/useTicketEvents.ts
Normal file
157
src/app/(modules)/ticket/hooks/useTicketEvents.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { API_ROUTES } from '@/constants/apiRoutes';
|
||||
import { ticketService } from '@/services/api';
|
||||
import { sseService } from '@/services/sse';
|
||||
import type {
|
||||
TicketDetail,
|
||||
TicketListResponse,
|
||||
TicketProgressEvent,
|
||||
TicketStatusEvent,
|
||||
} from '@/types';
|
||||
|
||||
import { ticketKeys } from '../queries/ticketKeys';
|
||||
|
||||
function getTicketEventId(event: TicketStatusEvent) {
|
||||
return event.id ?? event.ticket_id;
|
||||
}
|
||||
|
||||
function mergeTicketDetail(
|
||||
current: TicketDetail | undefined,
|
||||
event: TicketStatusEvent,
|
||||
) {
|
||||
if (!current) return current;
|
||||
|
||||
const eventId = getTicketEventId(event);
|
||||
if (eventId && eventId !== current.id) return current;
|
||||
|
||||
return {
|
||||
...current,
|
||||
...event,
|
||||
id: current.id,
|
||||
history: event.history ?? current.history,
|
||||
};
|
||||
}
|
||||
|
||||
function patchTicketLists(
|
||||
current: TicketListResponse | undefined,
|
||||
event: TicketStatusEvent,
|
||||
) {
|
||||
const eventId = getTicketEventId(event);
|
||||
if (!current || !eventId) return current;
|
||||
|
||||
return {
|
||||
...current,
|
||||
items: current.items.map((ticket) =>
|
||||
ticket.id === eventId
|
||||
? {
|
||||
...ticket,
|
||||
...event,
|
||||
id: ticket.id,
|
||||
}
|
||||
: ticket,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function useTenantTicketTableEvents(enabled = true) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
|
||||
let closed = false;
|
||||
let closeConnection: (() => void) | undefined;
|
||||
|
||||
ticketService
|
||||
.createTenantTicketEventsToken()
|
||||
.then(({ sse_token }) => {
|
||||
if (closed) return;
|
||||
|
||||
const connection = sseService.connect(
|
||||
API_ROUTES.TICKET_EVENTS.TENANT(sse_token),
|
||||
{
|
||||
ticket_status: (event: TicketStatusEvent) => {
|
||||
queryClient.setQueriesData<TicketListResponse>(
|
||||
{ queryKey: ticketKeys.lists() },
|
||||
(current) => patchTicketLists(current, event),
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
closeConnection = connection.close;
|
||||
})
|
||||
.catch(() => {
|
||||
queryClient.invalidateQueries({ queryKey: ticketKeys.lists() });
|
||||
});
|
||||
|
||||
return () => {
|
||||
closed = true;
|
||||
closeConnection?.();
|
||||
};
|
||||
}, [enabled, queryClient]);
|
||||
}
|
||||
|
||||
export function useTicketDetailEvents(
|
||||
ticketId: string | undefined,
|
||||
enabled = true,
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled || !ticketId) return;
|
||||
|
||||
let closed = false;
|
||||
let closeConnection: (() => void) | undefined;
|
||||
|
||||
ticketService
|
||||
.createTicketEventsToken(ticketId)
|
||||
.then(({ sse_token }) => {
|
||||
if (closed) return;
|
||||
|
||||
const connection = sseService.connect(
|
||||
API_ROUTES.TICKETS.DETAIL_EVENTS(ticketId, sse_token),
|
||||
{
|
||||
ticket_status: (event: TicketStatusEvent) => {
|
||||
queryClient.setQueryData<TicketDetail>(
|
||||
ticketKeys.detail(ticketId),
|
||||
(current) => mergeTicketDetail(current, event),
|
||||
);
|
||||
queryClient.invalidateQueries({ queryKey: ticketKeys.lists() });
|
||||
},
|
||||
progress: (event: TicketProgressEvent) => {
|
||||
queryClient.setQueryData<TicketDetail>(
|
||||
ticketKeys.detail(ticketId),
|
||||
(current) =>
|
||||
current && event.status
|
||||
? { ...current, status: current.status }
|
||||
: current,
|
||||
);
|
||||
},
|
||||
complete: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ticketKeys.detail(ticketId),
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ticketKeys.lists() });
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
closeConnection = connection.close;
|
||||
})
|
||||
.catch(() => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ticketKeys.detail(ticketId),
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
closed = true;
|
||||
closeConnection?.();
|
||||
};
|
||||
}, [enabled, queryClient, ticketId]);
|
||||
}
|
||||
141
src/app/(modules)/ticket/hooks/useTicketQueries.ts
Normal file
141
src/app/(modules)/ticket/hooks/useTicketQueries.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { ticketService } from '@/services/api';
|
||||
import type {
|
||||
AssignTicketPayload,
|
||||
ReviewRepairPayload,
|
||||
SubmitRepairPayload,
|
||||
TicketListParams,
|
||||
TicketStatus,
|
||||
} from '@/types';
|
||||
|
||||
import { ticketKeys } from '../queries/ticketKeys';
|
||||
|
||||
interface UseTicketsQueryParams {
|
||||
skip: number;
|
||||
limit: number;
|
||||
status?: TicketStatus | 'all';
|
||||
chainageId?: string;
|
||||
}
|
||||
|
||||
function buildTicketListParams({
|
||||
skip,
|
||||
limit,
|
||||
status,
|
||||
chainageId,
|
||||
}: UseTicketsQueryParams): TicketListParams {
|
||||
return {
|
||||
skip,
|
||||
limit,
|
||||
status: status === 'all' ? undefined : status,
|
||||
chainage_id: chainageId || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function useTicketsQuery(params: UseTicketsQueryParams) {
|
||||
const { skip, limit, status, chainageId } = params;
|
||||
const listParams = useMemo(
|
||||
() => buildTicketListParams({ skip, limit, status, chainageId }),
|
||||
[chainageId, limit, skip, status],
|
||||
);
|
||||
|
||||
return useQuery({
|
||||
queryKey: ticketKeys.list(listParams),
|
||||
queryFn: () => ticketService.getTickets(listParams),
|
||||
});
|
||||
}
|
||||
|
||||
export function useTicketDetailQuery(ticketId: string | undefined) {
|
||||
return useQuery({
|
||||
queryKey: ticketKeys.detail(ticketId ?? ''),
|
||||
queryFn: () => ticketService.getTicketDetail(ticketId as string),
|
||||
enabled: Boolean(ticketId),
|
||||
});
|
||||
}
|
||||
|
||||
export function useAssignableTicketUsersQuery(enabled: boolean) {
|
||||
return useQuery({
|
||||
queryKey: ticketKeys.assignableUsers(),
|
||||
queryFn: ticketService.getAssignableUsers,
|
||||
enabled,
|
||||
});
|
||||
}
|
||||
|
||||
export function useAssignTicketMutation(ticketId: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (payload: AssignTicketPayload) =>
|
||||
ticketService.assignTicket(ticketId, payload),
|
||||
onSuccess: (ticket) => {
|
||||
toast.success('Ticket assigned');
|
||||
queryClient.setQueryData(ticketKeys.detail(ticketId), ticket);
|
||||
queryClient.invalidateQueries({ queryKey: ticketKeys.lists() });
|
||||
},
|
||||
onError: () => toast.error('Failed to assign ticket'),
|
||||
});
|
||||
}
|
||||
|
||||
export function useStartTicketMutation(ticketId: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => ticketService.startTicket(ticketId),
|
||||
onSuccess: (ticket) => {
|
||||
toast.success('Ticket started');
|
||||
queryClient.setQueryData(ticketKeys.detail(ticketId), ticket);
|
||||
queryClient.invalidateQueries({ queryKey: ticketKeys.lists() });
|
||||
},
|
||||
onError: () => toast.error('Failed to start ticket'),
|
||||
});
|
||||
}
|
||||
|
||||
export function useSubmitRepairMutation(ticketId: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (payload: SubmitRepairPayload) =>
|
||||
ticketService.submitRepair(ticketId, payload),
|
||||
onSuccess: (ticket) => {
|
||||
toast.success('Repair submitted');
|
||||
queryClient.setQueryData(ticketKeys.detail(ticketId), ticket);
|
||||
queryClient.invalidateQueries({ queryKey: ticketKeys.lists() });
|
||||
},
|
||||
onError: () => toast.error('Failed to submit repair'),
|
||||
});
|
||||
}
|
||||
|
||||
export function useReviewRepairMutation(ticketId: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (payload: ReviewRepairPayload) =>
|
||||
ticketService.reviewRepair(ticketId, payload),
|
||||
onSuccess: (ticket, payload) => {
|
||||
toast.success(
|
||||
payload.action === 'approve' ? 'Repair approved' : 'Repair rejected',
|
||||
);
|
||||
queryClient.setQueryData(ticketKeys.detail(ticketId), ticket);
|
||||
queryClient.invalidateQueries({ queryKey: ticketKeys.lists() });
|
||||
},
|
||||
onError: () => toast.error('Failed to review repair'),
|
||||
});
|
||||
}
|
||||
|
||||
export function useCloseTicketMutation(ticketId: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => ticketService.closeTicket(ticketId),
|
||||
onSuccess: (ticket) => {
|
||||
toast.success('Ticket closed');
|
||||
queryClient.setQueryData(ticketKeys.detail(ticketId), ticket);
|
||||
queryClient.invalidateQueries({ queryKey: ticketKeys.lists() });
|
||||
},
|
||||
onError: () => toast.error('Failed to close ticket'),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user