feat: watcher/CC system, SLA engine, and rich text comments

- Watcher system: ticket_watchers table, watch/unwatch endpoints,
  notifications to watchers on comments and updates, watcher/cc
  recipient sources in SendEmail scrip action, watch toggle and
  watcher avatars in ticket detail UI
- SLA engine: sla_policies table, SLA deadline columns on tickets,
  CRUD routes, OnSlaBreach scrip condition, scheduler SLA calculation,
  deadlines set on create/reply, cleared on resolve, SLA indicators
  on ticket list and detail, SLA Policies tab in admin
- Rich text: marked-based markdown rendering with XSS safety,
  Write/Preview toggle in comment composer, styled prose output
This commit is contained in:
Gjermund Høsøien Wiggen
2026-06-15 21:40:18 +02:00
parent 9679734e3f
commit 653139ad0d
18 changed files with 1025 additions and 26 deletions

View File

@@ -21,6 +21,8 @@ import type {
AttachmentUploadResult,
TicketLink,
LoginResult,
Watcher,
SlaPolicy,
} from "./types";
const BASE_URL = "/api";
@@ -181,6 +183,52 @@ export async function revokeApiToken(id: string): Promise<{ data: { ok: boolean
return request<{ ok: boolean }>(`/auth/tokens/${id}`, { method: "DELETE" });
}
// Watchers
export async function getWatchers(ticketId: number): Promise<{ data: Watcher[] | null; error: string | null }> {
return request<Watcher[]>(`/tickets/${ticketId}/watchers`);
}
export async function addWatcher(ticketId: number, userId?: string): Promise<{ data: Watcher | null; error: string | null }> {
return request<Watcher>(`/tickets/${ticketId}/watchers`, { method: "POST", body: JSON.stringify(userId ? { user_id: userId } : {}) });
}
export async function removeWatcher(ticketId: number, userId: string): Promise<{ data: { ok: boolean } | null; error: string | null }> {
return request<{ ok: boolean }>(`/tickets/${ticketId}/watchers/${userId}`, { method: "DELETE" });
}
// SLA Policies
export async function getSlaPolicies(): Promise<{ data: SlaPolicy[] | null; error: string | null }> {
return request<SlaPolicy[]>("/sla-policies");
}
export async function createSlaPolicy(data: {
name: string;
queue_id?: string;
description?: string;
response_time_minutes?: number;
resolution_time_minutes?: number;
disabled?: boolean;
}): Promise<{ data: SlaPolicy | null; error: string | null }> {
return request<SlaPolicy>("/sla-policies", { method: "POST", body: JSON.stringify(data) });
}
export async function updateSlaPolicy(id: string, data: {
name?: string;
queue_id?: string | null;
description?: string;
response_time_minutes?: number | null;
resolution_time_minutes?: number | null;
disabled?: boolean;
}): Promise<{ data: SlaPolicy | null; error: string | null }> {
return request<SlaPolicy>(`/sla-policies/${id}`, { method: "PATCH", body: JSON.stringify(data) });
}
export async function deleteSlaPolicy(id: string): Promise<{ data: { ok: boolean } | null; error: string | null }> {
return request<{ ok: boolean }>(`/sla-policies/${id}`, { method: "DELETE" });
}
export async function getQueues(): Promise<{ data: Queue[] | null; error: string | null }> {
return request<Queue[]>("/queues");
}