feat: ticket system base implementation

This commit is contained in:
2026-06-19 01:19:01 +05:30
parent a18fff4d6f
commit 8c3d19a429
19 changed files with 1400 additions and 27 deletions

View 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]);
}

View 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'),
});
}