fix(number-fields): избегаем потери точности длинных чисел
- number-поля теперь рендерятся как text + inputMode=numeric, чтобы браузер не округлял значения через input type=number - пробелы при вставке в number-поля удаляются - бэкенд нормализует значения number-полей в строку перед сохранением - добавлен хелпер normalizeFieldValueForStorage Closes: искажение расчётного счёта и других длинных числовых полей
This commit is contained in:
386
server/documents/routes.ts
Normal file
386
server/documents/routes.ts
Normal file
@@ -0,0 +1,386 @@
|
||||
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";
|
||||
|
||||
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 });
|
||||
});
|
||||
|
||||
// ── 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,
|
||||
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.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.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 });
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user