- Add session-based authentication (login page, middleware, auth context) - Add cron-like scrip scheduler for time-based conditions - Add layout builder, scrip wizard, searchable select components - Add trend chart widget for dashboards - Add notifications, attachments, queue-permissions API routes - Add seed-users script - Update schema with 10 new migrations (0008-0017) - Apply redesign: Linear-inspired dark theme, conversation-centric UI - Gitignore runtime data directory Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
140 lines
5.0 KiB
TypeScript
140 lines
5.0 KiB
TypeScript
import type { Ticket } from '../models/ticket.ts';
|
|
import type { Transaction } from '../models/transaction.ts';
|
|
import { LifecycleValidator } from '../lifecycle/validator.ts';
|
|
import type { LifecycleDefinition } from '../lifecycle/validator.ts';
|
|
|
|
export interface ConditionEvaluateContext {
|
|
lifecycleDef?: LifecycleDefinition;
|
|
customFields?: Record<string, string>; // key → value map of CF values
|
|
}
|
|
|
|
export interface ConditionConfig {
|
|
from_status?: unknown;
|
|
to_status?: unknown;
|
|
field_key?: unknown;
|
|
field_id?: unknown;
|
|
field?: unknown;
|
|
old_value?: unknown;
|
|
new_value?: unknown;
|
|
value?: unknown;
|
|
link_type?: unknown;
|
|
}
|
|
|
|
export interface ConditionEvaluator {
|
|
evaluate(ticket: Ticket, transactions: Transaction[], context?: ConditionEvaluateContext, config?: ConditionConfig): boolean;
|
|
}
|
|
|
|
function matchesStatusFilter(value: string | null, filter: unknown): boolean {
|
|
if (filter === undefined || filter === null || filter === '') return true;
|
|
if (value === null) return false;
|
|
const normalizedValue = value.toLowerCase();
|
|
if (Array.isArray(filter)) {
|
|
return filter.map((item) => String(item).toLowerCase()).includes(normalizedValue);
|
|
}
|
|
return normalizedValue === String(filter).toLowerCase();
|
|
}
|
|
|
|
export class OnCreate implements ConditionEvaluator {
|
|
evaluate(_ticket: Ticket, transactions: Transaction[]): boolean {
|
|
return transactions.some((tx) => tx.transaction_type === 'Create');
|
|
}
|
|
}
|
|
|
|
export class OnStatusChange implements ConditionEvaluator {
|
|
evaluate(_ticket: Ticket, transactions: Transaction[], _context?: ConditionEvaluateContext, config?: ConditionConfig): boolean {
|
|
return transactions.some((tx) =>
|
|
tx.transaction_type === 'StatusChange' &&
|
|
matchesStatusFilter(tx.old_value, config?.from_status) &&
|
|
matchesStatusFilter(tx.new_value, config?.to_status)
|
|
);
|
|
}
|
|
}
|
|
|
|
export class OnResolve implements ConditionEvaluator {
|
|
private lifecycleValidator = new LifecycleValidator();
|
|
|
|
evaluate(_ticket: Ticket, transactions: Transaction[], context?: ConditionEvaluateContext, config?: ConditionConfig): boolean {
|
|
const lifecycleDef = context?.lifecycleDef;
|
|
|
|
return transactions.some((tx) => {
|
|
if (tx.transaction_type !== 'StatusChange' || tx.new_value === null) return false;
|
|
if (!matchesStatusFilter(tx.old_value, config?.from_status)) return false;
|
|
if (!matchesStatusFilter(tx.new_value, config?.to_status)) return false;
|
|
|
|
if (lifecycleDef) {
|
|
return this.lifecycleValidator.isResolvedStatus(lifecycleDef, tx.new_value);
|
|
}
|
|
|
|
return ['resolved', 'closed', 'completed', 'rejected', 'cancelled'].includes(tx.new_value.toLowerCase());
|
|
});
|
|
}
|
|
}
|
|
|
|
export class OnCustomFieldChange implements ConditionEvaluator {
|
|
evaluate(_ticket: Ticket, transactions: Transaction[], _context?: ConditionEvaluateContext, config?: ConditionConfig): boolean {
|
|
const fieldFilter = config?.field_key ?? config?.field_id ?? config?.field;
|
|
const newValueFilter = config?.new_value ?? config?.value;
|
|
|
|
return transactions.some((tx) =>
|
|
tx.transaction_type === 'CustomFieldChange' &&
|
|
matchesStatusFilter(tx.field, fieldFilter) &&
|
|
matchesStatusFilter(tx.old_value, config?.old_value) &&
|
|
matchesStatusFilter(tx.new_value, newValueFilter)
|
|
);
|
|
}
|
|
}
|
|
|
|
export class OnLinkCreate implements ConditionEvaluator {
|
|
evaluate(_ticket: Ticket, transactions: Transaction[], _context?: ConditionEvaluateContext, config?: ConditionConfig): boolean {
|
|
return transactions.some((tx) => {
|
|
if (tx.transaction_type !== 'LinkCreate') return false;
|
|
if (config?.link_type) {
|
|
const linkType = tx.field;
|
|
if (!matchesStatusFilter(linkType, config.link_type)) return false;
|
|
}
|
|
return true;
|
|
});
|
|
}
|
|
}
|
|
|
|
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;
|
|
if (!fieldKey) return false;
|
|
|
|
const cfValue = context?.customFields?.[String(fieldKey)];
|
|
if (!cfValue) return false;
|
|
|
|
// Parse the date value
|
|
const dueDate = new Date(cfValue);
|
|
if (isNaN(dueDate.getTime())) return false;
|
|
|
|
// Check if overdue (past due date)
|
|
if (new Date() <= dueDate) return false;
|
|
|
|
// Check that ticket is still active (not in inactive state)
|
|
const lifecycleDef = context?.lifecycleDef;
|
|
if (lifecycleDef) {
|
|
const inactiveStates = lifecycleDef.statuses.inactive;
|
|
if (inactiveStates.includes(_ticket.status)) return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
const conditionRegistry: Record<string, ConditionEvaluator> = {
|
|
OnCreate: new OnCreate(),
|
|
OnStatusChange: new OnStatusChange(),
|
|
OnResolve: new OnResolve(),
|
|
OnCustomFieldChange: new OnCustomFieldChange(),
|
|
OnLinkCreate: new OnLinkCreate(),
|
|
OnOverdue: new OnOverdue(),
|
|
};
|
|
|
|
export function getConditionEvaluator(type: string): ConditionEvaluator | null {
|
|
return conditionRegistry[type] ?? null;
|
|
}
|
|
|
|
export { conditionRegistry };
|