feat(documents): папки шаблонов, маппинги и поле document_template; fix(batch): атомарное создание задач, idempotency и массовое удаление в реестре

This commit is contained in:
2026-07-08 13:33:14 +03:00
parent fc8e85832c
commit 6826d82850
34 changed files with 3770 additions and 467 deletions

View File

@@ -9,6 +9,7 @@ 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();
@@ -49,6 +50,79 @@ export async function registerDocumentRoutes(app: Express) {
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) => {
@@ -56,6 +130,7 @@ export async function registerDocumentRoutes(app: Express) {
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 });
@@ -132,6 +207,16 @@ export async function registerDocumentRoutes(app: Express) {
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!);
@@ -236,6 +321,40 @@ export async function registerDocumentRoutes(app: Express) {
}
});
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(