import { type Express } from "express"; import { authenticateToken, requireAdmin, type AuthenticatedRequest } from "../middleware/auth.middleware"; import { upload } from "../utils/upload"; import { DocumentTemplateService } from "./template.service"; import { AssetService } from "./asset.service"; import { DataResolutionService } from "./data-resolution.service"; import { DocumentGenerationService } from "./generation.service"; import { PackageService } from "./package.service"; import { SignatureService } from "./signature.service"; import { DocxImporterService } from "./docx-importer.service"; import * as storage from "../storage/documents.storage"; import { storage as appStorage } from "../storage/index"; export async function registerDocumentRoutes(app: Express) { const templateService = new DocumentTemplateService(); const assetService = new AssetService(); const dataResolution = new DataResolutionService(); const generationService = new DocumentGenerationService(templateService, dataResolution, assetService); const packageService = new PackageService(generationService); const signatureService = new SignatureService(); const docxImporter = new DocxImporterService(templateService); // ── Template Categories ──────────────────────────────────────────────────── app.get("/api/documents/templates/categories", authenticateToken, async (req: AuthenticatedRequest, res) => { const categories = await templateService.listCategories(req.organizationId!); res.json({ success: true, categories }); }); app.post("/api/documents/templates/categories", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => { const category = await templateService.createCategory({ ...req.body, organizationId: req.organizationId!, }); res.status(201).json({ success: true, category }); }); app.put("/api/documents/templates/categories/:id", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => { const category = await templateService.updateCategory( Number(req.params.id), req.organizationId!, req.body, ); if (!category) return res.status(404).json({ error: "Категория не найдена" }); res.json({ success: true, category }); }); app.delete("/api/documents/templates/categories/:id", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => { await templateService.deleteCategory(Number(req.params.id), req.organizationId!); res.json({ success: true }); }); // ── Template Folders ─────────────────────────────────────────────────────── app.get("/api/documents/folders", authenticateToken, async (req: AuthenticatedRequest, res) => { const folders = await templateService.listFolders(req.organizationId!); res.json({ success: true, folders }); }); app.post("/api/documents/folders", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => { const folder = await templateService.createFolder({ ...req.body, organizationId: req.organizationId!, }); res.status(201).json({ success: true, folder }); }); app.put("/api/documents/folders/:id", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => { const folder = await templateService.updateFolder( Number(req.params.id), req.organizationId!, req.body, ); if (!folder) return res.status(404).json({ error: "Папка не найдена" }); res.json({ success: true, folder }); }); app.delete("/api/documents/folders/:id", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => { try { await templateService.deleteFolder(Number(req.params.id), req.organizationId!); res.json({ success: true }); } catch (err: unknown) { const message = err instanceof Error ? err.message : "Ошибка удаления папки"; res.status(400).json({ error: message }); } }); app.patch("/api/documents/folders/reorder", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => { try { await templateService.reorderFolders(req.organizationId!, req.body.updates || []); res.json({ success: true }); } catch (err: unknown) { const message = err instanceof Error ? err.message : "Ошибка изменения порядка папок"; res.status(400).json({ error: message }); } }); app.get("/api/documents/folders/:id/mappings", authenticateToken, async (req: AuthenticatedRequest, res) => { const mappings = await templateService.listFolderMappings(Number(req.params.id), req.organizationId!); res.json({ success: true, mappings }); }); app.post("/api/documents/folders/:id/mappings", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => { const mapping = await templateService.createFolderMapping({ ...req.body, folderId: Number(req.params.id), }); res.status(201).json({ success: true, mapping }); }); app.put("/api/documents/folders/:id/mappings/:mappingId", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => { const mapping = await templateService.updateFolderMapping( Number(req.params.mappingId), Number(req.params.id), req.body, ); if (!mapping) return res.status(404).json({ error: "Маппинг не найден" }); res.json({ success: true, mapping }); }); app.delete("/api/documents/folders/:id/mappings/:mappingId", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => { await templateService.deleteFolderMapping(Number(req.params.mappingId), Number(req.params.id)); res.json({ success: true }); }); // ── Templates ────────────────────────────────────────────────────────────── app.get("/api/documents/templates", authenticateToken, async (req: AuthenticatedRequest, res) => { const templates = await templateService.list({ organizationId: req.organizationId!, status: req.query.status as string | undefined, categoryId: req.query.categoryId ? Number(req.query.categoryId) : undefined, folderId: req.query.folderId ? Number(req.query.folderId) : undefined, formId: req.query.formId ? Number(req.query.formId) : undefined, }); res.json({ success: true, templates }); }); app.get("/api/documents/templates/:id", authenticateToken, async (req: AuthenticatedRequest, res) => { const template = await templateService.getById(Number(req.params.id), req.organizationId!); if (!template) return res.status(404).json({ error: "Шаблон не найден" }); res.json({ success: true, template }); }); app.post("/api/documents/templates", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => { const template = await templateService.create({ ...req.body, organizationId: req.organizationId!, createdBy: req.user!.id, }); res.status(201).json({ success: true, template }); }); app.post("/api/documents/templates/import-docx", authenticateToken, requireAdmin, upload.single("file"), async (req: AuthenticatedRequest, res) => { try { if (!req.file) return res.status(400).json({ error: "Файл не передан" }); const ext = req.file.originalname.split(".").pop()?.toLowerCase(); if (ext !== "docx") { return res.status(400).json({ error: "Поддерживаются только DOCX-файлы" }); } const result = await docxImporter.importDocx(req.file, req.organizationId!, req.user!.id, { name: req.body.name, description: req.body.description, categoryId: req.body.categoryId ? Number(req.body.categoryId) : undefined, formId: req.body.formId ? Number(req.body.formId) : undefined, }); res.status(201).json({ success: true, template: result.template, variables: result.variables, warnings: result.warnings }); } catch (err: unknown) { const message = err instanceof Error ? err.message : "Ошибка импорта DOCX"; res.status(400).json({ error: message }); } }); app.post("/api/documents/templates/:id/replace-docx", authenticateToken, requireAdmin, upload.single("file"), async (req: AuthenticatedRequest, res) => { try { if (!req.file) return res.status(400).json({ error: "Файл не передан" }); const ext = req.file.originalname.split(".").pop()?.toLowerCase(); if (ext !== "docx") { return res.status(400).json({ error: "Поддерживаются только DOCX-файлы" }); } const result = await docxImporter.replaceDocx( req.file, Number(req.params.id), req.organizationId!, req.user!.id, ); res.json({ success: true, template: result.template, variables: result.variables, warnings: result.warnings }); } catch (err: unknown) { const message = err instanceof Error ? err.message : "Ошибка замены DOCX"; res.status(400).json({ error: message }); } }); app.put("/api/documents/templates/:id", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => { const template = await templateService.update( Number(req.params.id), req.organizationId!, req.body, ); if (!template) return res.status(404).json({ error: "Шаблон не найден" }); res.json({ success: true, template }); }); app.patch("/api/documents/templates/:id/folder", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => { const template = await templateService.moveTemplateToFolder( Number(req.params.id), req.organizationId!, req.body.folderId === null ? null : Number(req.body.folderId), ); if (!template) return res.status(404).json({ error: "Шаблон не найден" }); res.json({ success: true, template }); }); app.delete("/api/documents/templates/:id", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => { try { await templateService.delete(Number(req.params.id), req.organizationId!); res.json({ success: true }); } catch (err: unknown) { const message = err instanceof Error ? err.message : "Ошибка удаления шаблона"; res.status(400).json({ error: message }); } }); app.post("/api/documents/templates/:id/duplicate", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => { const template = await templateService.duplicate( Number(req.params.id), req.organizationId!, req.body.name, ); res.status(201).json({ success: true, template }); }); app.post("/api/documents/templates/:id/auto-variables", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => { const variables = await templateService.autoCreateVariablesFromContent( Number(req.params.id), req.organizationId!, ); res.json({ success: true, variables }); }); app.get("/api/documents/templates/:id/variables", authenticateToken, async (req: AuthenticatedRequest, res) => { const variables = await templateService.listVariables( Number(req.params.id), req.organizationId!, ); res.json({ success: true, variables }); }); // ── Assets ───────────────────────────────────────────────────────────────── app.get("/api/documents/assets", authenticateToken, async (req: AuthenticatedRequest, res) => { const assets = await assetService.list({ organizationId: req.organizationId!, type: req.query.type as string | undefined, isActive: req.query.isActive === 'true' ? true : req.query.isActive === 'false' ? false : undefined, }); res.json({ success: true, assets }); }); app.post("/api/documents/assets", authenticateToken, requireAdmin, upload.single("file"), async (req: AuthenticatedRequest, res) => { try { const asset = await assetService.create({ ...req.body, organizationId: req.organizationId!, file: req.file, }); res.status(201).json({ success: true, asset }); } catch (err: unknown) { const message = err instanceof Error ? err.message : 'Ошибка создания актива'; res.status(400).json({ error: message }); } }); app.get("/api/documents/assets/:id", authenticateToken, async (req: AuthenticatedRequest, res) => { const asset = await assetService.getById(Number(req.params.id), req.organizationId!); if (!asset) return res.status(404).json({ error: "Актив не найден" }); res.json({ success: true, asset }); }); app.put("/api/documents/assets/:id", authenticateToken, requireAdmin, upload.single("file"), async (req: AuthenticatedRequest, res) => { const asset = await assetService.update( Number(req.params.id), req.organizationId!, { ...req.body, file: req.file }, ); if (!asset) return res.status(404).json({ error: "Актив не найден" }); res.json({ success: true, asset }); }); app.delete("/api/documents/assets/:id", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => { try { await assetService.delete(Number(req.params.id), req.organizationId!); res.json({ success: true }); } catch (err: unknown) { const message = err instanceof Error ? err.message : "Ошибка удаления актива"; res.status(400).json({ error: message }); } }); // ── Generations ──────────────────────────────────────────────────────────── app.post("/api/documents/generations", authenticateToken, async (req: AuthenticatedRequest, res) => { try { const result = await generationService.generate({ templateId: Number(req.body.templateId), taskId: Number(req.body.taskId), organizationId: req.organizationId!, userId: req.user!.id, outputFormat: req.body.format || 'both', }); res.json({ success: true, ...result }); } catch (err: unknown) { const message = err instanceof Error ? err.message : 'Ошибка генерации документа'; res.status(400).json({ error: message }); } }); app.get("/api/documents/generate-link", authenticateToken, async (req: AuthenticatedRequest, res) => { try { const templateId = Number(req.query.templateId); const taskId = Number(req.query.taskId); const format = (req.query.format as string) || 'both'; const returnTo = (req.query.returnTo as string) || ''; if (!templateId || !taskId) { return res.status(400).json({ error: "templateId и taskId обязательны" }); } if (!['pdf', 'docx', 'both'].includes(format)) { return res.status(400).json({ error: "Недопустимый формат" }); } await generationService.generate({ templateId, taskId, organizationId: req.organizationId!, userId: req.user!.id, outputFormat: format as any, }); let redirectUrl = returnTo; if (!redirectUrl) { const task = await appStorage.getTask(taskId, req.organizationId!); redirectUrl = task ? `/forms/${task.formId}/tasks/${taskId}` : '/'; } res.redirect(redirectUrl); } catch (err: unknown) { const message = err instanceof Error ? err.message : "Ошибка генерации документа"; res.status(400).json({ error: message }); } }); app.post("/api/documents/generations/preview", authenticateToken, async (req: AuthenticatedRequest, res) => { try { const preview = await generationService.preview( Number(req.body.templateId), Number(req.body.taskId), req.organizationId!, req.user!.id, ); res.json({ success: true, ...preview }); } catch (err: unknown) { const message = err instanceof Error ? err.message : 'Ошибка предпросмотра'; res.status(400).json({ error: message }); } }); app.get("/api/documents/generations", authenticateToken, async (req: AuthenticatedRequest, res) => { const filters = { organizationId: req.organizationId!, templateId: req.query.templateId ? Number(req.query.templateId) : undefined, taskId: req.query.taskId ? Number(req.query.taskId) : undefined, status: req.query.status as string | undefined, limit: req.query.limit ? Number(req.query.limit) : 100, offset: req.query.offset ? Number(req.query.offset) : 0, }; const [generations, total] = await Promise.all([ storage.listDocumentGenerations(filters), storage.countDocumentGenerations(filters), ]); res.json({ success: true, generations, total }); }); app.get("/api/documents/generations/:id", authenticateToken, async (req: AuthenticatedRequest, res) => { const generation = await storage.getDocumentGeneration(Number(req.params.id), req.organizationId!); if (!generation) return res.status(404).json({ error: "Генерация не найдена" }); res.json({ success: true, generation }); }); app.get("/api/documents/generations/:id/download/:format", authenticateToken, async (req: AuthenticatedRequest, res) => { const generation = await storage.getDocumentGeneration(Number(req.params.id), req.organizationId!); if (!generation) return res.status(404).json({ error: "Генерация не найдена" }); const format = req.params.format as 'pdf' | 'docx'; const fileKey = format === 'pdf' ? generation.pdfFileKey : generation.docxFileKey; if (!fileKey) return res.status(404).json({ error: "Файл не найден" }); res.redirect(`/api/files/${fileKey}`); }); // ── Packages ─────────────────────────────────────────────────────────────── app.get("/api/documents/packages", authenticateToken, async (req: AuthenticatedRequest, res) => { const packages = await packageService.list(req.organizationId!); res.json({ success: true, packages }); }); app.get("/api/documents/packages/:id", authenticateToken, async (req: AuthenticatedRequest, res) => { const pkg = await packageService.getById(Number(req.params.id), req.organizationId!); if (!pkg) return res.status(404).json({ error: "Пакет не найден" }); res.json({ success: true, package: pkg }); }); app.post("/api/documents/packages", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => { const pkg = await packageService.create(req.body, req.organizationId!); res.status(201).json({ success: true, package: pkg }); }); app.put("/api/documents/packages/:id", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => { const pkg = await packageService.update(Number(req.params.id), req.organizationId!, req.body); if (!pkg) return res.status(404).json({ error: "Пакет не найден" }); res.json({ success: true, package: pkg }); }); app.delete("/api/documents/packages/:id", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => { try { await packageService.delete(Number(req.params.id), req.organizationId!); res.json({ success: true }); } catch (err: unknown) { const message = err instanceof Error ? err.message : "Ошибка удаления пакета"; res.status(400).json({ error: message }); } }); app.post("/api/documents/packages/:id/generate", authenticateToken, async (req: AuthenticatedRequest, res) => { try { const result = await packageService.generatePackage({ packageId: Number(req.params.id), taskId: Number(req.body.taskId), organizationId: req.organizationId!, userId: req.user!.id, }); res.json({ success: true, ...result }); } catch (err: unknown) { const message = err instanceof Error ? err.message : "Ошибка генерации пакета"; res.status(400).json({ error: message }); } }); // ── Signatures ───────────────────────────────────────────────────────────── app.get("/api/documents/signatures", authenticateToken, async (req: AuthenticatedRequest, res) => { const generationId = Number(req.query.generationId); if (!generationId) return res.status(400).json({ error: "generationId обязателен" }); const signatures = await signatureService.listByGeneration(generationId); res.json({ success: true, signatures }); }); app.post("/api/documents/signatures", authenticateToken, async (req: AuthenticatedRequest, res) => { try { const signature = await signatureService.createPendingSignature( Number(req.body.generationId), req.user!.id, req.body.comment, ); res.status(201).json({ success: true, signature }); } catch (err: unknown) { const message = err instanceof Error ? err.message : "Ошибка создания подписи"; res.status(400).json({ error: message }); } }); app.post("/api/documents/signatures/:id/sign", authenticateToken, async (req: AuthenticatedRequest, res) => { try { const signature = await signatureService.sign( Number(req.params.id), req.user!.id, req.body.comment, ); res.json({ success: true, signature }); } catch (err: unknown) { const message = err instanceof Error ? err.message : "Ошибка подписания"; res.status(400).json({ error: message }); } }); app.post("/api/documents/signatures/:id/reject", authenticateToken, async (req: AuthenticatedRequest, res) => { try { const signature = await signatureService.reject( Number(req.params.id), req.user!.id, req.body.comment, ); res.json({ success: true, signature }); } catch (err: unknown) { const message = err instanceof Error ? err.message : "Ошибка отклонения"; res.status(400).json({ error: message }); } }); }