88 lines
3.1 KiB
TypeScript
88 lines
3.1 KiB
TypeScript
// P4 Tasks (AK-P4b) §12 — audit + activity wiring for the Tasks module.
|
|
//
|
|
// Audit (DoD 13): every write action records a tamper-evident AuditEvent (M001)
|
|
// via createAuditEvent with the shared db client.
|
|
//
|
|
// Activity (DoD 14): user-facing timeline entries are logged against the most
|
|
// relevant entity. When the task is linked to a CRM entity that maps to an
|
|
// Activity subject (contact/company/lead/deal/project) the entry hangs off that
|
|
// entity's timeline with the task referenced; otherwise it hangs off the task's
|
|
// own timeline (`task` is itself a valid Activity subject).
|
|
|
|
import type { JsonValue } from "@toolctrl/foundation";
|
|
import { createAuditEvent } from "@toolctrl/db/audit-events.server";
|
|
import { logActivity, type ActivitySubjectType, type ActivityType } from "@hivegrid/activity";
|
|
import type { TasksRouteContext } from "./taskAccess.server";
|
|
import type { TaskEntityType } from "./taskService.server";
|
|
|
|
export type TaskAuditAction =
|
|
| "task.created"
|
|
| "task.updated"
|
|
| "task.reassigned"
|
|
| "task.completed"
|
|
| "task.archived"
|
|
| "task.reactivated"
|
|
| "task.deleted"
|
|
| "task.watcher_added"
|
|
| "task.watcher_removed";
|
|
|
|
export async function auditTaskEvent(input: {
|
|
ctx: Pick<TasksRouteContext, "tenantId" | "userId">;
|
|
action: TaskAuditAction;
|
|
taskId: string;
|
|
metadata?: Record<string, JsonValue>;
|
|
}): Promise<void> {
|
|
try {
|
|
await createAuditEvent({
|
|
tenantId: input.ctx.tenantId,
|
|
actorId: input.ctx.userId,
|
|
actorType: "user",
|
|
action: input.action,
|
|
targetType: "task",
|
|
targetId: input.taskId,
|
|
metadata: input.metadata ?? null,
|
|
});
|
|
} catch {
|
|
// Audit must never break the user action; the failure is logged upstream.
|
|
}
|
|
}
|
|
|
|
const ENTITY_TO_ACTIVITY_SUBJECT: Partial<Record<TaskEntityType, ActivitySubjectType>> = {
|
|
contact: "contact",
|
|
company: "company",
|
|
lead: "lead",
|
|
deal: "deal",
|
|
project: "project",
|
|
};
|
|
|
|
/**
|
|
* Log a task activity. When the task links to a CRM entity with an Activity
|
|
* subject, the entry hangs off that entity (with the task as a related ref);
|
|
* otherwise it hangs off the task's own timeline. Best-effort.
|
|
*/
|
|
export async function logTaskActivity(input: {
|
|
ctx: Pick<TasksRouteContext, "tenantId" | "userId">;
|
|
taskId: string;
|
|
activityType: Extract<ActivityType, "task_created" | "task_completed">;
|
|
summaryText: string;
|
|
linkedEntityType?: TaskEntityType | null;
|
|
linkedEntityId?: string | null;
|
|
}): Promise<void> {
|
|
const linkedSubject = input.linkedEntityType ? ENTITY_TO_ACTIVITY_SUBJECT[input.linkedEntityType] : undefined;
|
|
const useLinked = Boolean(linkedSubject && input.linkedEntityId);
|
|
try {
|
|
await logActivity({
|
|
tenantId: input.ctx.tenantId,
|
|
actorUserId: input.ctx.userId,
|
|
actorKind: "user",
|
|
activityType: input.activityType,
|
|
subjectType: useLinked ? (linkedSubject as ActivitySubjectType) : "task",
|
|
subjectId: useLinked ? (input.linkedEntityId as string) : input.taskId,
|
|
summaryText: input.summaryText,
|
|
relatedEntityRefs: useLinked ? [{ type: "task", id: input.taskId }] : undefined,
|
|
});
|
|
} catch {
|
|
// Activity logging is best-effort; never break the user action.
|
|
}
|
|
}
|