/** * Fix existing task titles where a {{task}} field was resolved to its numeric ID * instead of the linked task's title. * * Run inside Docker container: * docker exec -i crm-app-1 npx tsx scripts/fix-task-titles-with-task-refs.ts */ import { db } from "../server/db"; import { storage } from "../server/storage"; import { buildSystemFieldValues, formatFieldValueForTitle, resolveTaskFieldTitles } from "../server/routes/shared"; import { tasks, forms } from "../shared/schema"; import { sql, isNotNull } from "drizzle-orm"; async function main() { // Find all forms that have a titleTemplate const formsWithTemplate = await db .select() .from(forms) .where(isNotNull(forms.titleTemplate)); console.log(`Found ${formsWithTemplate.length} forms with titleTemplate`); let updated = 0; let skipped = 0; for (const form of formsWithTemplate) { // Load all tasks for this form const formTasks = await storage.getTasksByForm(form.id, form.organizationId); if (formTasks.length === 0) continue; // Preload form-level data once const [allFormFields, titleStatuses, titleUsers] = await Promise.all([ storage.getFormFields(form.id, form.organizationId), storage.getFormStatuses(form.id, form.organizationId), storage.getUsersByOrganization(form.organizationId), ]); const fieldCodeMap = new Map(allFormFields.map((f) => [f.code, f])); const usersMap = new Map(titleUsers.map((u) => [u.id, u])); for (const task of formTasks) { try { const savedValues = await storage.getTaskFieldValues(task.id, form.organizationId); const valueByFieldId = new Map(savedValues.map((v) => [v.fieldId, v.value])); const sysValues = await buildSystemFieldValues(task, form.organizationId, { statuses: titleStatuses, usersMap }); const taskTitleMap = await resolveTaskFieldTitles(valueByFieldId, allFormFields, form.organizationId); const computedTitle = (form.titleTemplate as string).replace(/\{\{([^}]+)\}\}/g, (_, code: string) => { if (sysValues.has(code)) return sysValues.get(code)!; const fld = fieldCodeMap.get(code); if (!fld) return ""; const val = valueByFieldId.get(fld.id); if (val === null || val === undefined) return ""; if (val === "__auto_prolongation__" && (fld.type === "date" || fld.type === "datetime")) { return fld.autoProlongationLabel || "Автопролонгация"; } return formatFieldValueForTitle(val, fld.type, taskTitleMap); }); if (computedTitle !== task.title) { await storage.updateTask(task.id, form.organizationId, { title: computedTitle }); updated++; console.log(` Update #${task.id}: "${task.title}" -> "${computedTitle}"`); } else { skipped++; } } catch (err: any) { skipped++; console.error(` Error #${task.id}:`, err.message); } } } console.log(`\nDone. Updated: ${updated}, Skipped: ${skipped}`); process.exit(0); } main().catch((err) => { console.error("Fatal error:", err); process.exit(1); });