import { query } from "./db-client"; import { getTableConfig } from "./table-config"; import { storage } from "../storage"; import { sql } from "drizzle-orm"; import { db } from "../db"; import { formatFieldValueForTitle, buildTableRowMap } from "../routes/shared"; import { findCustomerByInn } from "./sync-utils"; const LEGAL_ENTITY_FORM_ID = 7; const CUSTOMER_FORM_ID = 8; const OMEGA_PROM_NAME = "ОМЕГАПРОМ"; export interface ParsedContract { raw: string; number: string; date: string; // YYYY-MM-DD inn: string; } export interface ContractSyncItem { raw: string; number: string; date: string; inn: string; customerTaskId?: number; } export interface SyncApplyContract { raw: string; number: string; date: string; inn: string; customerTaskId: number; } function parseDate(dateStr: string): string | null { const match = dateStr.match(/^(\d{2})\.(\d{2})\.(\d{2,4})$/); if (!match) return null; const [, day, month, yearStr] = match; let year = parseInt(yearStr, 10); if (yearStr.length === 2) { year = year >= 50 ? 1900 + year : 2000 + year; } return `${year}-${month}-${day}`; } export function parseContractString(raw: string): { number: string; date: string } | null { const trimmed = raw.trim(); if (!trimmed) return null; // Extract number: everything after № (with optional space) up to " от " const numberMatch = trimmed.match(/№\s*([^\s].*?)\s+от\s/); if (!numberMatch) return null; const number = numberMatch[1].trim(); // Extract date: DD.MM.YY or DD.MM.YYYY const dateMatch = trimmed.match(/(\d{2})\.(\d{2})\.(\d{2,4})/); if (!dateMatch) return null; const date = parseDate(`${dateMatch[1]}.${dateMatch[2]}.${dateMatch[3]}`); if (!date) return null; return { number, date }; } export async function getUniqueFinanceContracts(): Promise<{ contracts: ParsedContract[]; skipped: number; }> { const config = getTableConfig(); const result = await query( `SELECT DISTINCT "dogovorzakazchika" as raw, "innzakazchika" as inn FROM "${config.incomeTable}" WHERE "dogovorzakazchika" IS NOT NULL AND "dogovorzakazchika" <> ''` ); const contracts: ParsedContract[] = []; let skipped = 0; const seen = new Set(); for (const row of result.rows) { const raw = String(row.raw || "").trim(); const parsed = parseContractString(raw); if (!parsed) { skipped++; continue; } const inn = row.inn ? String(row.inn).trim() : ""; if (!inn || inn === "0") { skipped++; continue; } const key = `${parsed.number}:${parsed.date}:${inn}`; if (seen.has(key)) continue; seen.add(key); contracts.push({ raw, number: parsed.number, date: parsed.date, inn }); } contracts.sort((a, b) => a.raw.localeCompare(b.raw)); return { contracts, skipped }; } export async function findOmegaPromTaskId(organizationId: number): Promise { const result = await db.execute(sql` SELECT id FROM tasks WHERE form_id = ${LEGAL_ENTITY_FORM_ID} AND organization_id = ${organizationId} AND title ILIKE ${"%" + OMEGA_PROM_NAME + "%"} ORDER BY id LIMIT 1 `); const rows = result.rows as Array<{ id: number }>; return rows[0]?.id ?? null; } export async function findExistingContracts( formId: number, organizationId: number ): Promise> { const formFields = await storage.getFormFields(formId, organizationId); const contractField = formFields.find((f) => f.type === "contract-number"); const customerField = formFields.find((f) => f.type === "task" && f.linkedFormId === CUSTOMER_FORM_ID); if (!contractField || !customerField) { return new Set(); } const result = await db.execute(sql` SELECT t.id FROM tasks t JOIN task_field_values v1 ON t.id = v1.task_id AND t.form_id = v1.form_id JOIN task_field_values v2 ON t.id = v2.task_id AND t.form_id = v2.form_id WHERE t.form_id = ${formId} AND t.organization_id = ${organizationId} AND v1.field_id = ${contractField.id} AND v1.value IS NOT NULL AND v2.field_id = ${customerField.id} AND v2.value IS NOT NULL `); const existing = new Set(); for (const row of result.rows as Array<{ id: number }>) { // Load field values for this task const values = await storage.getTaskFieldValues(row.id, organizationId); const contractValue = values.find((v) => v.fieldId === contractField.id)?.value; const customerValue = values.find((v) => v.fieldId === customerField.id)?.value; if (contractValue && customerValue) { try { const parsed = typeof contractValue === "string" ? JSON.parse(contractValue) : contractValue; const number = parsed?.value || ""; const customerId = String(customerValue).trim(); if (number && customerId) { existing.add(`${number}:${customerId}`); } } catch { // ignore parse errors } } } return existing; } export async function previewContractSync( formId: number, organizationId: number ): Promise<{ auto: ContractSyncItem[]; ambiguous: Array; existing: ContractSyncItem[]; skipped: number; }> { const { contracts, skipped } = await getUniqueFinanceContracts(); const omegaPromTaskId = await findOmegaPromTaskId(organizationId); const existingContracts = await findExistingContracts(formId, organizationId); const auto: ContractSyncItem[] = []; const ambiguous: Array = []; const existing: ContractSyncItem[] = []; for (const contract of contracts) { const customerTaskId = await findCustomerByInn(contract.inn, organizationId); const existingKey = `${contract.number}:${customerTaskId || ""}`; if (existingContracts.has(existingKey)) { existing.push(contract); continue; } if (!omegaPromTaskId) { ambiguous.push({ ...contract, reason: "Не найдено юр.лицо ОМЕГАПРОМ" }); continue; } if (!customerTaskId) { ambiguous.push({ ...contract, reason: `Не найден заказчик по ИНН ${contract.inn}` }); continue; } auto.push({ ...contract, customerTaskId }); } return { auto, ambiguous, existing, skipped }; } export async function createContractTask( formId: number, organizationId: number, createdBy: number, contract: SyncApplyContract, omegaPromTaskId: number ): Promise<{ taskId: number; title: string }> { const formFields = await storage.getFormFields(formId, organizationId); const contractField = formFields.find((f) => f.type === "contract-number"); const dateField = formFields.find((f) => f.type === "date"); const customerField = formFields.find((f) => f.type === "task" && f.linkedFormId === CUSTOMER_FORM_ID); if (!contractField) { throw new Error("В форме не найдено поле типа contract-number"); } if (!dateField) { throw new Error("В форме не найдено поле типа date"); } if (!customerField) { throw new Error("В форме не найдено поле типа task, связанное с формой Заказчик"); } const form = await storage.getForm(formId, organizationId); if (!form) { throw new Error("Форма не найдена"); } let statuses = await storage.getFormStatuses(formId, organizationId); if (statuses.length === 0) { const defaultStatus = await storage.createFormStatus({ formId, organizationId, name: "Новый", color: "#e5e7eb", isInitial: true, isFinal: false, position: 0, } as any); statuses = [defaultStatus]; } const initialStatus = statuses[0]; // Build title from template or fallback const title = form.titleTemplate ? contract.number : `Договор №${contract.number}`; // Extract parentTaskId from contract-number field const contractValue = JSON.stringify({ mode: "theirs", value: contract.number, legalEntityTaskId: omegaPromTaskId, }); const task = await storage.createTask({ title, formId, organizationId, createdBy, currentStatusId: initialStatus.id, description: null, assignedTo: null, dueDate: null, completedAt: null, parentTaskId: omegaPromTaskId, } as any); // Insert field values const fieldValues = [ { fieldId: contractField.id, value: contractValue }, { fieldId: dateField.id, value: contract.date }, { fieldId: customerField.id, value: String(contract.customerTaskId) }, ]; for (const { fieldId, value } of fieldValues) { await storage.createTaskFieldValue({ taskId: task.id, fieldId, formId, value, }); } // Create task relation for customer field if needed if (customerField.taskRelationType) { if (customerField.taskRelationType === "parent") { await storage.upsertTaskRelation({ parentTaskId: contract.customerTaskId, childTaskId: task.id, fieldId: customerField.id, organizationId, }); } else if (customerField.taskRelationType === "child") { await storage.upsertTaskRelation({ parentTaskId: task.id, childTaskId: contract.customerTaskId, fieldId: customerField.id, organizationId, }); } } // Recalculate title if template exists if (form.titleTemplate) { const [allFormFields, savedValues, titleStatuses, titleUsers, titleRoles] = await Promise.all([ storage.getFormFields(formId, organizationId), storage.getTaskFieldValues(task.id, organizationId), storage.getFormStatuses(formId, organizationId), storage.getUsersByOrganization(organizationId), storage.getRoles(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 rolesMap = new Map(titleRoles.map((r) => [r.id, r])); const { buildSystemFieldValues } = await import("../routes/shared"); const sysValues = await buildSystemFieldValues(task, organizationId, { statuses: titleStatuses, usersMap }); const tableRowMap = await buildTableRowMap(allFormFields, organizationId); 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); return val === null || val === undefined ? "" : formatFieldValueForTitle(val, fld.type, { usersMap, rolesMap, tableRowMap }, fld.options); }); await storage.updateTask(task.id, organizationId, { title: computedTitle }); } return { taskId: task.id, title }; } export async function applyContractSync( formId: number, organizationId: number, createdBy: number, contracts: SyncApplyContract[] ): Promise<{ created: number; skipped: number; errors: Array<{ raw: string; error: string }> }> { const omegaPromTaskId = await findOmegaPromTaskId(organizationId); if (!omegaPromTaskId) { throw new Error("Не найдено юр.лицо ОМЕГАПРОМ"); } const existingContracts = await findExistingContracts(formId, organizationId); let created = 0; let skipped = 0; const errors: Array<{ raw: string; error: string }> = []; for (const contract of contracts) { try { const key = `${contract.number}:${contract.customerTaskId}`; if (existingContracts.has(key)) { skipped++; continue; } await createContractTask(formId, organizationId, createdBy, contract, omegaPromTaskId); created++; } catch (err: any) { errors.push({ raw: contract.raw, error: err.message || "Ошибка создания" }); } } return { created, skipped, errors }; }