fix(number-fields): избегаем потери точности длинных чисел
- number-поля теперь рендерятся как text + inputMode=numeric, чтобы браузер не округлял значения через input type=number - пробелы при вставке в number-поля удаляются - бэкенд нормализует значения number-полей в строку перед сохранением - добавлен хелпер normalizeFieldValueForStorage Closes: искажение расчётного счёта и других длинных числовых полей
This commit is contained in:
199
server/documents/template.service.ts
Normal file
199
server/documents/template.service.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import {
|
||||
documentTemplates,
|
||||
documentTemplateCategories,
|
||||
templateVariables,
|
||||
type DocumentTemplate,
|
||||
type InsertDocumentTemplate,
|
||||
type DocumentTemplateCategory,
|
||||
type InsertDocumentTemplateCategory,
|
||||
type TemplateVariable,
|
||||
type InsertTemplateVariable,
|
||||
} from "@shared/schema";
|
||||
import * as storage from "../storage/documents.storage";
|
||||
|
||||
const VARIABLE_REGEX = /\{\{\s*([^}]+?)\s*\}\}/g;
|
||||
|
||||
export interface TemplateWithVariables extends DocumentTemplate {
|
||||
variables: TemplateVariable[];
|
||||
category?: DocumentTemplateCategory | null;
|
||||
}
|
||||
|
||||
export class DocumentTemplateService {
|
||||
// ── Categories ─────────────────────────────────────────────────────────────
|
||||
|
||||
async listCategories(organizationId: number): Promise<DocumentTemplateCategory[]> {
|
||||
return storage.listDocumentTemplateCategories(organizationId);
|
||||
}
|
||||
|
||||
async getCategory(id: number, organizationId: number): Promise<DocumentTemplateCategory | undefined> {
|
||||
return storage.getDocumentTemplateCategory(id, organizationId);
|
||||
}
|
||||
|
||||
async createCategory(data: InsertDocumentTemplateCategory): Promise<DocumentTemplateCategory> {
|
||||
return storage.createDocumentTemplateCategory(data);
|
||||
}
|
||||
|
||||
async updateCategory(
|
||||
id: number,
|
||||
organizationId: number,
|
||||
updates: Partial<DocumentTemplateCategory>,
|
||||
): Promise<DocumentTemplateCategory | undefined> {
|
||||
return storage.updateDocumentTemplateCategory(id, organizationId, updates);
|
||||
}
|
||||
|
||||
async deleteCategory(id: number, organizationId: number): Promise<void> {
|
||||
return storage.deleteDocumentTemplateCategory(id, organizationId);
|
||||
}
|
||||
|
||||
// ── Templates ──────────────────────────────────────────────────────────────
|
||||
|
||||
async list(filters: {
|
||||
organizationId: number;
|
||||
status?: string;
|
||||
categoryId?: number;
|
||||
formId?: number;
|
||||
}): Promise<DocumentTemplate[]> {
|
||||
return storage.listDocumentTemplates(filters);
|
||||
}
|
||||
|
||||
async getById(id: number, organizationId: number): Promise<TemplateWithVariables | undefined> {
|
||||
const template = await storage.getDocumentTemplate(id, organizationId);
|
||||
if (!template) return undefined;
|
||||
const variables = await storage.listTemplateVariables(id);
|
||||
const category = template.categoryId ? await this.getCategory(template.categoryId, organizationId) : null;
|
||||
return { ...template, variables, category: category || undefined };
|
||||
}
|
||||
|
||||
async getByIdRaw(id: number, organizationId: number): Promise<DocumentTemplate | undefined> {
|
||||
return storage.getDocumentTemplate(id, organizationId);
|
||||
}
|
||||
|
||||
async create(data: InsertDocumentTemplate & { variables?: InsertTemplateVariable[] }): Promise<TemplateWithVariables> {
|
||||
const template = await storage.createDocumentTemplate(data);
|
||||
const variables: TemplateVariable[] = [];
|
||||
|
||||
if (data.variables && data.variables.length > 0) {
|
||||
for (const v of data.variables) {
|
||||
variables.push(await storage.createTemplateVariable({ ...v, templateId: template.id }));
|
||||
}
|
||||
}
|
||||
|
||||
return { ...template, variables };
|
||||
}
|
||||
|
||||
async update(
|
||||
id: number,
|
||||
organizationId: number,
|
||||
updates: Partial<InsertDocumentTemplate> & { variables?: Array<Partial<TemplateVariable> & { id?: number }> },
|
||||
): Promise<TemplateWithVariables | undefined> {
|
||||
const template = await storage.updateDocumentTemplate(id, organizationId, updates);
|
||||
if (!template) return undefined;
|
||||
|
||||
if (updates.variables) {
|
||||
const existing = await storage.listTemplateVariables(id);
|
||||
const incomingIds = updates.variables.filter((v) => v.id && v.id > 0).map((v) => v.id!);
|
||||
const toDelete = existing.filter((e) => !incomingIds.includes(e.id));
|
||||
for (const v of toDelete) {
|
||||
await storage.deleteTemplateVariable(v.id, id);
|
||||
}
|
||||
|
||||
for (let i = 0; i < updates.variables.length; i++) {
|
||||
const v = updates.variables[i];
|
||||
const variableData: InsertTemplateVariable = {
|
||||
templateId: id,
|
||||
code: v.code || `var_${i}`,
|
||||
label: v.label || v.code || `var_${i}`,
|
||||
description: v.description || null,
|
||||
source: v.source || 'static',
|
||||
mapping: v.mapping || {},
|
||||
defaultValue: v.defaultValue || null,
|
||||
sortOrder: v.sortOrder ?? i,
|
||||
};
|
||||
if (v.id && v.id > 0) {
|
||||
await storage.updateTemplateVariable(v.id, id, variableData);
|
||||
} else {
|
||||
await storage.createTemplateVariable(variableData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.getById(id, organizationId);
|
||||
}
|
||||
|
||||
async delete(id: number, organizationId: number): Promise<void> {
|
||||
await storage.deleteTemplateVariablesByTemplate(id);
|
||||
await storage.deleteDocumentTemplate(id, organizationId);
|
||||
}
|
||||
|
||||
async duplicate(id: number, organizationId: number, newName?: string): Promise<TemplateWithVariables> {
|
||||
const source = await this.getById(id, organizationId);
|
||||
if (!source) throw new Error('Шаблон не найден');
|
||||
|
||||
const { id: _id, createdAt, updatedAt, status, ...rest } = source;
|
||||
const template = await storage.createDocumentTemplate({
|
||||
...rest,
|
||||
name: newName || `${source.name} (копия)`,
|
||||
status: 'draft',
|
||||
organizationId,
|
||||
createdBy: source.createdBy,
|
||||
});
|
||||
|
||||
const variables: TemplateVariable[] = [];
|
||||
for (const v of source.variables) {
|
||||
const { id: _vid, templateId, createdAt: _vCreatedAt, ...vRest } = v;
|
||||
variables.push(
|
||||
await storage.createTemplateVariable({
|
||||
...vRest,
|
||||
templateId: template.id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return { ...template, variables };
|
||||
}
|
||||
|
||||
// ── Variables ──────────────────────────────────────────────────────────────
|
||||
|
||||
async listVariables(templateId: number, organizationId: number): Promise<TemplateVariable[]> {
|
||||
// Убедимся, что шаблон принадлежит организации
|
||||
const template = await storage.getDocumentTemplate(templateId, organizationId);
|
||||
if (!template) throw new Error('Шаблон не найден');
|
||||
return storage.listTemplateVariables(templateId);
|
||||
}
|
||||
|
||||
async parseVariablesFromContent(content: string): Promise<string[]> {
|
||||
const codes = new Set<string>();
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = VARIABLE_REGEX.exec(content)) !== null) {
|
||||
codes.add(match[1].trim());
|
||||
}
|
||||
return Array.from(codes);
|
||||
}
|
||||
|
||||
async autoCreateVariablesFromContent(templateId: number, organizationId: number): Promise<TemplateVariable[]> {
|
||||
const template = await storage.getDocumentTemplate(templateId, organizationId);
|
||||
if (!template) throw new Error('Шаблон не найден');
|
||||
|
||||
const codes = await this.parseVariablesFromContent(template.content);
|
||||
const existing = await storage.listTemplateVariables(templateId);
|
||||
const existingCodes = new Set(existing.map((v) => v.code));
|
||||
|
||||
const created: TemplateVariable[] = [];
|
||||
for (let i = 0; i < codes.length; i++) {
|
||||
const code = codes[i];
|
||||
if (existingCodes.has(code)) continue;
|
||||
const variable = await storage.createTemplateVariable({
|
||||
templateId,
|
||||
code,
|
||||
label: code,
|
||||
source: 'static',
|
||||
mapping: {},
|
||||
defaultValue: null,
|
||||
sortOrder: existing.length + i,
|
||||
});
|
||||
created.push(variable);
|
||||
}
|
||||
|
||||
return created;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user