feat(automations): ctx.tasks.scheduleStatusChange — отложенная смена статуса (воркер 5с, отмена при ручном переходе)

This commit is contained in:
2026-09-17 16:38:56 +03:00
parent c1fe1c4e02
commit e4f9a1992a
9 changed files with 236 additions and 1 deletions

View File

@@ -2816,6 +2816,28 @@ export type TaskReminder = typeof taskReminders.$inferSelect;
export type InsertTaskReminder = z.infer<typeof insertTaskReminderSchema>;
export type ReminderRecipient = { type: 'user'; userId: number } | { type: 'role'; role: string } | { type: 'orgRole'; roleId: number };
// Отложенная смена статуса задачи (автоматизации: ctx.tasks.scheduleStatusChange).
// Воркер server/workers/delayed-status-change.worker.ts раз в 5 секунд
// подбирает просроченные строки и переводит задачу в target_status_id,
// только если она всё ещё в expected_status_id (иначе — отмена).
export const delayedStatusChanges = pgTable("delayed_status_changes", {
id: serial("id").primaryKey(),
organizationId: integer("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
taskId: integer("task_id").notNull().references(() => tasks.id, { onDelete: "cascade" }),
formId: integer("form_id").notNull(),
expectedStatusId: integer("expected_status_id").notNull(),
targetStatusId: integer("target_status_id").notNull(),
runAt: timestamp("run_at").notNull(),
executedAt: timestamp("executed_at"),
createdAt: timestamp("created_at").defaultNow(),
}, (table) => ({
// Частичный индекс для воркера: ищет неисполненные, отсортированные по времени
runAtIdx: index("delayed_status_changes_run_at_idx").on(table.runAt).where(sql`${table.executedAt} IS NULL`),
}));
export type DelayedStatusChange = typeof delayedStatusChanges.$inferSelect;
export type InsertDelayedStatusChange = typeof delayedStatusChanges.$inferInsert;
// Task audit log table (history of changes)
export const taskAuditLog = pgTable("task_audit_log", {
id: serial("id").primaryKey(),