Files
iistwin/scripts/fix-contract-titles.ts
Ильяс Султанов 1f5ecb6da4 fix(number-fields): избегаем потери точности длинных чисел
- number-поля теперь рендерятся как text + inputMode=numeric,
  чтобы браузер не округлял значения через input type=number
- пробелы при вставке в number-поля удаляются
- бэкенд нормализует значения number-полей в строку перед сохранением
- добавлен хелпер normalizeFieldValueForStorage

Closes: искажение расчётного счёта и других длинных числовых полей
2026-07-07 21:03:40 +03:00

81 lines
2.9 KiB
TypeScript

/**
* Fix existing task titles that contain [object Object]
* caused by String(jsonbObject) in title template generation.
*
* Run inside Docker container:
* docker exec -i crm-app-1 npx tsx scripts/fix-contract-titles.ts
*/
import { db } from "../server/db";
import { storage } from "../server/storage";
import { buildSystemFieldValues, formatFieldValueForTitle } from "../server/routes/shared";
import { tasks } from "../shared/schema";
import { sql } from "drizzle-orm";
async function main() {
const badTasks = await db
.select()
.from(tasks)
.where(sql`${tasks.title} LIKE '%[object Object]%'`);
console.log(`Found ${badTasks.length} tasks with "[object Object]" in title`);
let updated = 0;
let skipped = 0;
for (const task of badTasks) {
try {
const form = await storage.getForm(task.formId, task.organizationId);
if (!form || !form.titleTemplate) {
skipped++;
console.log(` Skip #${task.id}: no form or no titleTemplate`);
continue;
}
const [allFormFields, savedValues, titleStatuses, titleUsers] = await Promise.all([
storage.getFormFields(task.formId, task.organizationId),
storage.getTaskFieldValues(task.id, task.organizationId),
storage.getFormStatuses(task.formId, task.organizationId),
storage.getUsersByOrganization(task.organizationId),
]);
const fieldCodeMap = new Map(allFormFields.map((f) => [f.code, f]));
const valueByFieldId = new Map(savedValues.map((v) => [v.fieldId, v.value]));
const usersMap = new Map(titleUsers.map((u) => [u.id, u]));
const sysValues = await buildSystemFieldValues(task, task.organizationId, { statuses: titleStatuses, usersMap });
const computedTitle = form.titleTemplate.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);
});
if (computedTitle !== task.title) {
await storage.updateTask(task.id, task.organizationId, { title: computedTitle });
updated++;
console.log(` Update #${task.id}: "${task.title}" -> "${computedTitle}"`);
} else {
skipped++;
console.log(` Skip #${task.id}: title unchanged after recalc`);
}
} 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);
});