import type { TicketAnalytics } from "@/components/ticket-agent/analytics/types"; import type { CommentExtras } from "@/components/ticket-agent/timeline/TimelineCommentRow.vue"; import { useNotificationStore } from "@/stores/notification"; import { __ } from "@/translation"; import type { DocumentResource, RecentSimilarTicket, Resource, TicketAssignee, TicketContact, } from "@/types"; import type { ODTicket } from "@/types/doctypes"; import { createDocumentResource, createResource, toast } from "frappe-ui"; import { reactive } from "vue"; interface MapValue { ticket: DocumentResource; assignees: Resource; contact: Resource; recentSimilarTickets: Resource; analytics: Resource; // shared by every timeline tab instance, fetched once per ticket calls: Resource[]>; commentExtras: Resource; // lent by the mounted timeline; see registerTicketFeed reloadFeed?: () => void; } export interface TicketCommentExtras { comments: Record; // oldest first, so a pin's position is its "N:" in the pinned bar pinned_comments: string[]; } export interface PinnedComment { name: string; content: string; } const ticketMap: Record = reactive({}); export const useTicket = (ticketId: string): MapValue => { if (!ticketMap[ticketId]) { ticketMap[ticketId] = { ticket: createDocumentResource({ doctype: "OD Ticket", name: ticketId, whitelistedMethods: { markSeen: { method: "mark_seen", onSuccess: (cleared: number) => cleared && useNotificationStore().controller.reload(), }, }, setValue: { onSuccess: () => { toast.success(__("Ticket updated successfully.")); }, onError: (error) => { const msg = error.exc_type ? (error.messages || error.message || []).join(", ") : error.message; toast.error(msg); }, }, }), assignees: createResource({ url: "opendesk.opendesk.doctype.od_ticket.api.get_ticket_assignees", params: { ticket: ticketId }, auto: true, }), contact: createResource({ url: "opendesk.opendesk.doctype.od_ticket.api.get_ticket_contact", params: { ticket: ticketId }, auto: true, }), recentSimilarTickets: createResource({ url: "opendesk.opendesk.doctype.od_ticket.api.get_recent_similar_tickets", params: { ticket: ticketId }, auto: true, }), // fetched by the analytics tab, not on ticket open analytics: createResource({ url: "opendesk.api.ticket_analytics.get_ticket_analytics", params: { ticket: ticketId }, cache: ["Ticket", ticketId, "analytics"], }), calls: createResource({ url: "opendesk.api.timeline.get_ticket_calls", params: { ticket: ticketId }, auto: true, }), commentExtras: createResource({ url: "opendesk.api.timeline.get_comment_extras", params: { ticket: ticketId }, auto: true, }), }; } return ticketMap[ticketId]; }; export function reloadTicket(ticketId: string) { const ticketData = ticketMap[ticketId]; if (!ticketData) return; ticketData.ticket.reload(); ticketData.assignees.reload(); } // The timeline owns its feed through useActivityTimeline, so anything outside it // (a saved reply applying actions, say) reloads through the mounted component. export function registerTicketFeed(ticketId: string, reload: () => void) { const ticketData = ticketMap[ticketId]; if (!ticketData) return () => {}; ticketData.reloadFeed = reload; // identity check: on ticket switch the new instance mounts before the old unmounts return () => { if (ticketData.reloadFeed === reload) delete ticketData.reloadFeed; }; } export function reloadTicketFeed(ticketId: string) { ticketMap[ticketId]?.reloadFeed?.(); } // Refresh a ticket that may have gone stale export function revalidateTicket(ticketId: string | number) { const ticketData = ticketMap[ticketId]; if (ticketData?.ticket.get.fetched && !ticketData.ticket.get.loading) { reloadTicket(ticketId as string); } }