506 lines
22 KiB
TypeScript
506 lines
22 KiB
TypeScript
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 });
|
|
}
|
|
});
|
|
}
|