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

@@ -18,6 +18,7 @@ export interface ConditionConfig {
new_value?: unknown;
value?: unknown;
link_type?: unknown;
breach_type?: unknown;
}
export interface ConditionEvaluator {
@@ -97,6 +98,27 @@ export class OnLinkCreate implements ConditionEvaluator {
}
}
export class OnSlaBreach implements ConditionEvaluator {
evaluate(ticket: Ticket, _transactions: Transaction[], _context?: ConditionEvaluateContext, config?: ConditionConfig): boolean {
const breachType = config?.breach_type ?? 'any';
// Check if ticket has sla_breached set
const breached = (ticket as any).sla_breached as string | null;
if (breachType === 'any') {
return breached === 'response' || breached === 'resolution' || breached === 'both';
}
if (breachType === 'response') {
return breached === 'response' || breached === 'both';
}
if (breachType === 'resolution') {
return breached === 'resolution' || breached === 'both';
}
return false;
}
}
export class OnOverdue implements ConditionEvaluator {
evaluate(_ticket: Ticket, _transactions: Transaction[], context?: ConditionEvaluateContext, config?: ConditionConfig): boolean {
const fieldKey = config?.field_key ?? config?.field_id ?? config?.field;
@@ -130,6 +152,7 @@ const conditionRegistry: Record<string, ConditionEvaluator> = {
OnCustomFieldChange: new OnCustomFieldChange(),
OnLinkCreate: new OnLinkCreate(),
OnOverdue: new OnOverdue(),
OnSlaBreach: new OnSlaBreach(),
};
export function getConditionEvaluator(type: string): ConditionEvaluator | null {