1import type { TicketAnalytics } from "@/components/ticket-agent/analytics/types";
2import type { CommentExtras } from "@/components/ticket-agent/timeline/TimelineCommentRow.vue";
3import { useNotificationStore } from "@/stores/notification";
4import { __ } from "@/translation";
5import type {
6 DocumentResource,
7 RecentSimilarTicket,
8 Resource,
9 TicketAssignee,
10 TicketContact,
11} from "@/types";
12import type { ODTicket } from "@/types/doctypes";
13import { createDocumentResource, createResource, toast } from "frappe-ui";
14import { reactive } from "vue";
15
16interface MapValue {
17 ticket: DocumentResource<ODTicket>;
18 assignees: Resource<TicketAssignee[]>;
19 contact: Resource<TicketContact>;
20 recentSimilarTickets: Resource<RecentSimilarTicket>;
21 analytics: Resource<TicketAnalytics>;
22 // shared by every timeline tab instance, fetched once per ticket
23 calls: Resource<Record<string, any>[]>;
24 commentExtras: Resource<TicketCommentExtras>;
25 // lent by the mounted timeline; see registerTicketFeed
26 reloadFeed?: () => void;
27}
28
29export interface TicketCommentExtras {
30 comments: Record<string, CommentExtras>;
31 // oldest first, so a pin's position is its "N:" in the pinned bar
32 pinned_comments: string[];
33}
34
35export interface PinnedComment {
36 name: string;
37 content: string;
38}
39
40const ticketMap: Record<string, MapValue> = reactive({});
41
42export const useTicket = (ticketId: string): MapValue => {
43 if (!ticketMap[ticketId]) {
44 ticketMap[ticketId] = {
45 ticket: createDocumentResource<ODTicket>({
46 doctype: "OD Ticket",
47 name: ticketId,
48 whitelistedMethods: {
49 markSeen: {
50 method: "mark_seen",
51 onSuccess: (cleared: number) =>
52 cleared && useNotificationStore().controller.reload(),
53 },
54 },
55 setValue: {
56 onSuccess: () => {
57 toast.success(__("Ticket updated successfully."));
58 },
59 onError: (error) => {
60 const msg = error.exc_type
61 ? (error.messages || error.message || []).join(", ")
62 : error.message;
63 toast.error(msg);
64 },
65 },
66 }),
67 assignees: createResource({
68 url: "opendesk.opendesk.doctype.od_ticket.api.get_ticket_assignees",
69 params: { ticket: ticketId },
70 auto: true,
71 }),
72 contact: createResource({
73 url: "opendesk.opendesk.doctype.od_ticket.api.get_ticket_contact",
74 params: { ticket: ticketId },
75 auto: true,
76 }),
77 recentSimilarTickets: createResource({
78 url: "opendesk.opendesk.doctype.od_ticket.api.get_recent_similar_tickets",
79 params: { ticket: ticketId },
80 auto: true,
81 }),
82 // fetched by the analytics tab, not on ticket open
83 analytics: createResource({
84 url: "opendesk.api.ticket_analytics.get_ticket_analytics",
85 params: { ticket: ticketId },
86 cache: ["Ticket", ticketId, "analytics"],
87 }),
88 calls: createResource({
89 url: "opendesk.api.timeline.get_ticket_calls",
90 params: { ticket: ticketId },
91 auto: true,
92 }),
93 commentExtras: createResource({
94 url: "opendesk.api.timeline.get_comment_extras",
95 params: { ticket: ticketId },
96 auto: true,
97 }),
98 };
99 }
100
101 return ticketMap[ticketId];
102};
103
104export function reloadTicket(ticketId: string) {
105 const ticketData = ticketMap[ticketId];
106 if (!ticketData) return;
107 ticketData.ticket.reload();
108 ticketData.assignees.reload();
109}
110
111// The timeline owns its feed through useActivityTimeline, so anything outside it
112// (a saved reply applying actions, say) reloads through the mounted component.
113export function registerTicketFeed(ticketId: string, reload: () => void) {
114 const ticketData = ticketMap[ticketId];
115 if (!ticketData) return () => {};
116 ticketData.reloadFeed = reload;
117 // identity check: on ticket switch the new instance mounts before the old unmounts
118 return () => {
119 if (ticketData.reloadFeed === reload) delete ticketData.reloadFeed;
120 };
121}
122
123export function reloadTicketFeed(ticketId: string) {
124 ticketMap[ticketId]?.reloadFeed?.();
125}
126
127// Refresh a ticket that may have gone stale
128export function revalidateTicket(ticketId: string | number) {
129 const ticketData = ticketMap[ticketId];
130 if (ticketData?.ticket.get.fetched && !ticketData.ticket.get.loading) {
131 reloadTicket(ticketId as string);
132 }
133}
134