- number-поля теперь рендерятся как text + inputMode=numeric, чтобы браузер не округлял значения через input type=number - пробелы при вставке в number-поля удаляются - бэкенд нормализует значения number-полей в строку перед сохранением - добавлен хелпер normalizeFieldValueForStorage Closes: искажение расчётного счёта и других длинных числовых полей
83 lines
3.1 KiB
TypeScript
83 lines
3.1 KiB
TypeScript
/**
|
|
* 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);
|
|
});
|