import { buildTaskAction } from "@capakraken/shared"; import { VacationStatus, VacationType } from "@capakraken/db"; import { createNotification } from "../lib/create-notification.js"; import { sendEmail } from "../lib/email.js"; import { logger } from "../lib/logger.js"; import { dispatchWebhooks } from "../lib/webhook-dispatcher.js"; import { emitTaskAssigned } from "../sse/event-bus.js"; import type { TRPCContext } from "../trpc.js"; type VacationDb = TRPCContext["db"]; function runVacationBackgroundEffect( effectName: string, execute: () => unknown, metadata: Record = {}, ): void { void Promise.resolve() .then(execute) .catch((error) => { logger.error( { err: error, effectName, ...metadata }, "Vacation background side effect failed", ); }); } async function notifyVacationStatus( db: VacationDb, vacationId: string, resourceId: string, newStatus: VacationStatus, rejectionReason?: string | null, ) { const resource = await db.resource.findUnique({ where: { id: resourceId }, select: { displayName: true, user: { select: { id: true, email: true, name: true } }, }, }); if (!resource?.user) return; const statusLabel = newStatus === VacationStatus.APPROVED ? "approved" : "rejected"; const title = `Vacation request ${statusLabel}`; const body = rejectionReason ? `Your vacation request was ${statusLabel}. Reason: ${rejectionReason}` : `Your vacation request has been ${statusLabel}.`; await createNotification({ db, userId: resource.user.id, type: `VACATION_${newStatus}`, title, body, entityId: vacationId, entityType: "vacation", }); if (resource.user.email) { void sendEmail({ to: resource.user.email, subject: `CapaKraken — ${title}`, text: body, }); } } export function notifyVacationStatusInBackground( db: VacationDb, vacationId: string, resourceId: string, newStatus: VacationStatus, rejectionReason?: string | null, ): void { runVacationBackgroundEffect( "notifyVacationStatus", () => notifyVacationStatus(db, vacationId, resourceId, newStatus, rejectionReason), { vacationId, resourceId, newStatus }, ); } export function dispatchVacationWebhookInBackground( db: VacationDb, event: string, payload: Record, ): void { runVacationBackgroundEffect( "dispatchWebhooks", () => dispatchWebhooks(db, event, payload), { event }, ); } export async function createVacationApprovalTasks(args: { db: VacationDb; submittedByUserId: string; vacationId: string; resourceName: string; vacationType: VacationType; startDate: Date; endDate: Date; }): Promise { const { db, submittedByUserId, vacationId, resourceName, vacationType, startDate, endDate } = args; const startStr = startDate.toISOString().slice(0, 10); const endStr = endDate.toISOString().slice(0, 10); const managers = await db.user.findMany({ where: { systemRole: { in: ["ADMIN", "MANAGER"] } }, select: { id: true }, }); for (const manager of managers) { if (manager.id === submittedByUserId) continue; const taskId = await createNotification({ db, userId: manager.id, category: "APPROVAL", type: "VACATION_APPROVAL", priority: "NORMAL", title: `Vacation approval: ${resourceName}`, body: `${resourceName} requests ${vacationType} from ${startStr} to ${endStr}`, taskStatus: "OPEN", taskAction: buildTaskAction("approve_vacation", vacationId), entityId: vacationId, entityType: "vacation", link: "/vacations", senderId: submittedByUserId, channel: "in_app", }); emitTaskAssigned(manager.id, taskId); } } export async function completeVacationApprovalTasks( db: VacationDb, vacationId: string, completedById?: string, ): Promise { await db.notification.updateMany({ where: { taskAction: buildTaskAction("approve_vacation", vacationId), category: "APPROVAL", taskStatus: "OPEN", }, data: { taskStatus: "DONE", completedAt: new Date(), ...(completedById ? { completedBy: completedById } : {}), }, }); }