feat(tickets): deliver canonical operational lifecycle
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
import {
|
||||
apiFetch,
|
||||
apiPath,
|
||||
type ApiSettings
|
||||
} from "@govoplan/core-webui";
|
||||
|
||||
|
||||
export type TicketSubject = {
|
||||
kind: string;
|
||||
id: string;
|
||||
label?: string | null;
|
||||
role?: string | null;
|
||||
};
|
||||
|
||||
export type TicketLink = {
|
||||
link_id: string;
|
||||
kind: string;
|
||||
owner_module: string;
|
||||
resource_type: string;
|
||||
resource_id: string;
|
||||
relation: string;
|
||||
label?: string | null;
|
||||
url?: string | null;
|
||||
metadata: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type TicketRecord = {
|
||||
tenant_id: string;
|
||||
ticket_id: string;
|
||||
ticket_number: string;
|
||||
revision: number;
|
||||
ticket_type: "request" | "incident" | "problem" | "report";
|
||||
priority: "low" | "normal" | "high" | "urgent";
|
||||
status: string;
|
||||
title: string;
|
||||
description: string;
|
||||
visibility: "tenant" | "restricted";
|
||||
queue_ref?: string | null;
|
||||
assignee?: TicketSubject | null;
|
||||
reporter?: TicketSubject | null;
|
||||
requester?: TicketSubject | null;
|
||||
participants: TicketSubject[];
|
||||
links: TicketLink[];
|
||||
service_target_at?: string | null;
|
||||
received_at: string;
|
||||
recorded_at: string;
|
||||
resolved_at?: string | null;
|
||||
resolution_summary?: string | null;
|
||||
deleted_at?: string | null;
|
||||
change_reason: string;
|
||||
metadata: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type TicketListResponse = {
|
||||
tickets: TicketRecord[];
|
||||
total: number;
|
||||
offset: number;
|
||||
limit: number;
|
||||
};
|
||||
|
||||
export type TicketAvailability = {
|
||||
routing: { available: boolean; provider?: string | null };
|
||||
case_escalation: { available: boolean; provider?: string | null };
|
||||
modules: Record<string, boolean>;
|
||||
consequences: Record<string, string>;
|
||||
};
|
||||
|
||||
export function listTickets(
|
||||
settings: ApiSettings,
|
||||
options: { statuses?: string[]; priorities?: string[]; ticketTypes?: string[]; queueRef?: string; query?: string; limit?: number },
|
||||
signal?: AbortSignal
|
||||
): Promise<TicketListResponse> {
|
||||
return apiFetch(settings, apiPath("/api/v1/tickets", {
|
||||
status: options.statuses,
|
||||
priority: options.priorities,
|
||||
ticket_type: options.ticketTypes,
|
||||
queue_ref: options.queueRef,
|
||||
query: options.query,
|
||||
limit: options.limit
|
||||
}), { signal });
|
||||
}
|
||||
|
||||
export function getTicketAvailability(settings: ApiSettings, signal?: AbortSignal): Promise<TicketAvailability> {
|
||||
return apiFetch(settings, "/api/v1/tickets/availability", { signal });
|
||||
}
|
||||
|
||||
export function createTicket(settings: ApiSettings, record: TicketRecord): Promise<TicketRecord> {
|
||||
return apiFetch(settings, "/api/v1/tickets", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ record, idempotency_key: crypto.randomUUID() })
|
||||
});
|
||||
}
|
||||
|
||||
function mutation(record: TicketRecord, changeReason: string) {
|
||||
return {
|
||||
expected_revision: record.revision,
|
||||
recorded_at: new Date().toISOString(),
|
||||
change_reason: changeReason,
|
||||
idempotency_key: crypto.randomUUID()
|
||||
};
|
||||
}
|
||||
|
||||
export function triageTicket(settings: ApiSettings, record: TicketRecord, changes: Record<string, unknown>, changeReason: string): Promise<TicketRecord> {
|
||||
return apiFetch(settings, `/api/v1/tickets/${encodeURIComponent(record.ticket_id)}/triage`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ ...mutation(record, changeReason), changes })
|
||||
});
|
||||
}
|
||||
|
||||
export function assignTicket(settings: ApiSettings, record: TicketRecord, assignee: TicketSubject | null, changeReason: string): Promise<TicketRecord> {
|
||||
return apiFetch(settings, `/api/v1/tickets/${encodeURIComponent(record.ticket_id)}/assignment`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ ...mutation(record, changeReason), assignee })
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveTicket(
|
||||
settings: ApiSettings,
|
||||
record: TicketRecord,
|
||||
targetStatus: string,
|
||||
resolutionSummary: string | null,
|
||||
changeReason: string
|
||||
): Promise<TicketRecord> {
|
||||
return apiFetch(settings, `/api/v1/tickets/${encodeURIComponent(record.ticket_id)}/resolution`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ ...mutation(record, changeReason), target_status: targetStatus, resolution_summary: resolutionSummary })
|
||||
});
|
||||
}
|
||||
|
||||
export function addTicketComment(settings: ApiSettings, record: TicketRecord, body: string, visibility: "internal" | "external") {
|
||||
return apiFetch(settings, `/api/v1/tickets/${encodeURIComponent(record.ticket_id)}/comments`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
expected_revision: record.revision,
|
||||
comment_id: crypto.randomUUID(),
|
||||
body,
|
||||
visibility,
|
||||
recorded_at: new Date().toISOString(),
|
||||
idempotency_key: crypto.randomUUID()
|
||||
})
|
||||
}) as Promise<{ ticket: TicketRecord; comment: Record<string, unknown> }>;
|
||||
}
|
||||
|
||||
export function escalateTicket(settings: ApiSettings, record: TicketRecord, caseTypeKey: string, handoffNote: string) {
|
||||
return apiFetch(settings, `/api/v1/tickets/${encodeURIComponent(record.ticket_id)}/case-escalations`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
expected_revision: record.revision,
|
||||
case_type_key: caseTypeKey,
|
||||
handoff_note: handoffNote || null,
|
||||
occurred_at: new Date().toISOString(),
|
||||
idempotency_key: crypto.randomUUID()
|
||||
})
|
||||
}) as Promise<{ ticket: TicketRecord; escalation: Record<string, unknown> }>;
|
||||
}
|
||||
Reference in New Issue
Block a user