feat(documents): папки шаблонов, маппинги и поле document_template; fix(batch): атомарное создание задач, idempotency и массовое удаление в реестре
This commit is contained in:
@@ -425,7 +425,7 @@ export const formFields = pgTable("form_fields", {
|
||||
tabId: integer("tab_id").references(() => formTabs.id, { onDelete: "set null" }), // вкладка для поля
|
||||
name: varchar("name", { length: 200 }).notNull(),
|
||||
code: varchar("code", { length: 100 }).notNull(),
|
||||
type: varchar("type", { length: 50 }).notNull(), // text, number, date, select, checkbox, toggle, button, user, file, textarea, rich-text, datetime, history-number, table, task
|
||||
type: varchar("type", { length: 50 }).notNull(), // text, number, date, select, checkbox, toggle, button, user, file, textarea, rich-text, datetime, history-number, table, task, document_template
|
||||
isRequired: boolean("is_required").default(false),
|
||||
placeholder: text("placeholder"),
|
||||
defaultValue: text("default_value"),
|
||||
@@ -1135,7 +1135,7 @@ export const createFormSchema = z.object({
|
||||
fields: z.array(z.object({
|
||||
name: z.string().min(1, "Название поля обязательно").max(200),
|
||||
code: z.string().min(1, "Код поля обязателен").max(100).regex(/^[a-zA-Z_][a-zA-Z0-9_]*$/, "Код поля может содержать только буквы, цифры и подчеркивания"),
|
||||
type: z.enum(['text', 'number', 'date', 'select', 'checkbox', 'toggle', 'button', 'user', 'file', 'textarea', 'rich-text', 'datetime', 'history-number']),
|
||||
type: z.enum(['text', 'number', 'date', 'select', 'checkbox', 'toggle', 'button', 'user', 'file', 'textarea', 'rich-text', 'datetime', 'history-number', 'document_template']),
|
||||
isRequired: z.boolean().default(false),
|
||||
placeholder: z.string().optional(),
|
||||
defaultValue: z.string().optional(),
|
||||
@@ -3111,10 +3111,65 @@ export const documentTemplateCategories = pgTable("document_template_categories"
|
||||
export type DocumentTemplateCategory = typeof documentTemplateCategories.$inferSelect;
|
||||
export type InsertDocumentTemplateCategory = typeof documentTemplateCategories.$inferInsert;
|
||||
|
||||
export const documentTemplateFolders = pgTable("document_template_folders", {
|
||||
id: serial("id").primaryKey(),
|
||||
organizationId: integer("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
|
||||
parentId: integer("parent_id"),
|
||||
name: varchar("name", { length: 100 }).notNull(),
|
||||
position: integer("position").notNull().default(0),
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
updatedAt: timestamp("updated_at").defaultNow(),
|
||||
}, (table) => ({
|
||||
orgParentIdx: index("document_template_folders_org_parent_idx").on(table.organizationId, table.parentId),
|
||||
positionIdx: index("document_template_folders_position_idx").on(table.position),
|
||||
}));
|
||||
|
||||
export type DocumentTemplateFolder = typeof documentTemplateFolders.$inferSelect;
|
||||
export type InsertDocumentTemplateFolder = typeof documentTemplateFolders.$inferInsert;
|
||||
|
||||
export const documentFolderMappings = pgTable("document_folder_mappings", {
|
||||
id: serial("id").primaryKey(),
|
||||
folderId: integer("folder_id").notNull().references(() => documentTemplateFolders.id, { onDelete: "cascade" }),
|
||||
code: varchar("code", { length: 100 }).notNull(),
|
||||
label: varchar("label", { length: 200 }),
|
||||
description: text("description"),
|
||||
source: varchar("source", { length: 30 }).notNull(),
|
||||
mapping: jsonb("mapping").$type<{
|
||||
fieldCode?: string;
|
||||
formId?: number;
|
||||
systemKey?: string;
|
||||
staticValue?: string;
|
||||
userId?: number;
|
||||
relatedTaskId?: number;
|
||||
tabId?: number;
|
||||
assetType?: string;
|
||||
assetId?: number;
|
||||
selectionMode?: string;
|
||||
taskFieldCode?: string;
|
||||
// related_task_field support
|
||||
relationType?: 'task_field' | 'parent' | 'child' | 'contract_legal_entity';
|
||||
sourceFormId?: number;
|
||||
sourceFieldCode?: string;
|
||||
targetFormId?: number;
|
||||
targetFieldCode?: string;
|
||||
transform?: { type: string; params?: Record<string, any> } | string;
|
||||
locale?: string;
|
||||
}>().default({}),
|
||||
defaultValue: text("default_value"),
|
||||
sortOrder: integer("sort_order").default(0),
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
}, (table) => ({
|
||||
uniqueCodePerFolder: unique("document_folder_mappings_code_unique").on(table.folderId, table.code),
|
||||
}));
|
||||
|
||||
export type DocumentFolderMapping = typeof documentFolderMappings.$inferSelect;
|
||||
export type InsertDocumentFolderMapping = typeof documentFolderMappings.$inferInsert;
|
||||
|
||||
export const documentTemplates = pgTable("document_templates", {
|
||||
id: serial("id").primaryKey(),
|
||||
organizationId: integer("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
|
||||
categoryId: integer("category_id").references(() => documentTemplateCategories.id, { onDelete: "set null" }),
|
||||
folderId: integer("folder_id").references(() => documentTemplateFolders.id, { onDelete: "set null" }),
|
||||
formId: integer("form_id").references(() => forms.id, { onDelete: "set null" }),
|
||||
name: varchar("name", { length: 200 }).notNull(),
|
||||
description: text("description"),
|
||||
@@ -3133,6 +3188,11 @@ export const documentTemplates = pgTable("document_templates", {
|
||||
opacity?: number;
|
||||
}>>().default([]),
|
||||
sourceDocxFileKey: varchar("source_docx_file_key", { length: 500 }),
|
||||
// Target file field where generated document should be saved
|
||||
targetFormId: integer("target_form_id").references(() => forms.id, { onDelete: "set null" }),
|
||||
targetFieldCode: varchar("target_field_code", { length: 100 }),
|
||||
targetTaskSource: varchar("target_task_source", { length: 20 }).default("current"),
|
||||
targetSourceFieldCode: varchar("target_source_field_code", { length: 100 }),
|
||||
status: varchar("status", { length: 20 }).notNull().default("draft"),
|
||||
createdBy: integer("created_by").notNull().references(() => users.id),
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
@@ -3140,6 +3200,7 @@ export const documentTemplates = pgTable("document_templates", {
|
||||
}, (table) => ({
|
||||
orgStatusIdx: index("document_templates_org_status_idx").on(table.organizationId, table.status),
|
||||
orgCategoryIdx: index("document_templates_org_category_idx").on(table.organizationId, table.categoryId),
|
||||
orgFolderIdx: index("document_templates_org_folder_idx").on(table.organizationId, table.folderId),
|
||||
}));
|
||||
|
||||
export type DocumentTemplate = typeof documentTemplates.$inferSelect;
|
||||
@@ -3164,6 +3225,12 @@ export const templateVariables = pgTable("template_variables", {
|
||||
assetId?: number;
|
||||
selectionMode?: string;
|
||||
taskFieldCode?: string;
|
||||
// related_task_field support
|
||||
relationType?: 'task_field' | 'parent' | 'child' | 'contract_legal_entity';
|
||||
sourceFormId?: number;
|
||||
sourceFieldCode?: string;
|
||||
targetFormId?: number;
|
||||
targetFieldCode?: string;
|
||||
transform?: { type: string; params?: Record<string, any> } | string;
|
||||
locale?: string;
|
||||
}>().default({}),
|
||||
@@ -3462,6 +3529,28 @@ export interface AutoDetectConfig {
|
||||
trees: AutoDetectTree[];
|
||||
}
|
||||
|
||||
// =====================
|
||||
// Idempotency Keys (защита от дублей при тяжёлых batch-операциях)
|
||||
// =====================
|
||||
|
||||
export const idempotencyKeys = pgTable("idempotency_keys", {
|
||||
id: serial("id").primaryKey(),
|
||||
key: varchar("key", { length: 255 }).notNull(),
|
||||
organizationId: integer("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
|
||||
userId: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
endpoint: varchar("endpoint", { length: 255 }).notNull(),
|
||||
payloadHash: varchar("payload_hash", { length: 64 }).notNull(),
|
||||
responseJson: jsonb("response_json").notNull(),
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
expiresAt: timestamp("expires_at").notNull(),
|
||||
}, (table) => ({
|
||||
orgKeyIdx: uniqueIndex("idempotency_keys_org_key_idx").on(table.organizationId, table.key),
|
||||
expiresAtIdx: index("idempotency_keys_expires_at_idx").on(table.expiresAt),
|
||||
}));
|
||||
|
||||
export type IdempotencyKey = typeof idempotencyKeys.$inferSelect;
|
||||
export type InsertIdempotencyKey = typeof idempotencyKeys.$inferInsert;
|
||||
|
||||
export const FINANCE_COLORS = [
|
||||
"#2563eb", "#e11d48", "#16a34a", "#f59e0b",
|
||||
"#8b5cf6", "#06b6d4", "#ea580c", "#84cc16",
|
||||
|
||||
Reference in New Issue
Block a user