196 lines
9.2 KiB
TypeScript
196 lines
9.2 KiB
TypeScript
import { Router } from "express";
|
|
import { storage } from "../storage";
|
|
import { requireFormAdminOrPermission, type AuthenticatedRequest } from "../middleware/auth.middleware";
|
|
import { formsCache } from "../utils/cache";
|
|
import { indexFormWithSummaryAsync } from "./task-helpers";
|
|
import { isValidFieldType } from "@shared/field-types";
|
|
|
|
export function registerFormStructureRoutes(router: ReturnType<typeof import("express").Router>): void {
|
|
// === FORM TABS ENDPOINTS ===
|
|
|
|
router.get('/api/forms/:id/tabs', async (req: AuthenticatedRequest, res) => {
|
|
try {
|
|
const formId = parseInt(req.params.id);
|
|
if (isNaN(formId)) {
|
|
return res.status(400).json({ success: false, error: 'Неверный ID формы' });
|
|
}
|
|
const existingForm = await storage.getForm(formId, req.organizationId!);
|
|
if (!existingForm) {
|
|
return res.status(404).json({ success: false, error: 'Форма не найдена' });
|
|
}
|
|
|
|
const hasManageForms = await storage.hasAppRolePermission(req.user!.appRole, ['forms.manage']);
|
|
if (!hasManageForms) {
|
|
const canAccess = await storage.canUserAccessForm(req.user!.id, formId, req.organizationId!, 'participate');
|
|
if (!canAccess) {
|
|
return res.status(403).json({ success: false, error: 'Нет доступа к этой форме' });
|
|
}
|
|
}
|
|
const tabs = await storage.getFormTabsByCreationOrder(formId, req.organizationId!);
|
|
res.json({ success: true, tabs });
|
|
} catch (error) {
|
|
console.error('Get form tabs error:', error);
|
|
res.status(500).json({ success: false, error: 'Ошибка при получении вкладок формы' });
|
|
}
|
|
});
|
|
|
|
router.post('/api/forms/:id/tabs',
|
|
requireFormAdminOrPermission('forms.manage'),
|
|
async (req: AuthenticatedRequest, res) => {
|
|
try {
|
|
const formId = parseInt(req.params.id);
|
|
if (isNaN(formId)) {
|
|
return res.status(400).json({ success: false, error: 'Неверный ID формы' });
|
|
}
|
|
const existingForm = await storage.getForm(formId, req.organizationId!);
|
|
if (!existingForm) {
|
|
return res.status(404).json({ success: false, error: 'Форма не найдена' });
|
|
}
|
|
|
|
const { tabs } = req.body;
|
|
if (!Array.isArray(tabs)) {
|
|
return res.status(400).json({ success: false, error: 'Вкладки должны быть массивом' });
|
|
}
|
|
const savedTabs = await storage.bulkSaveFormTabs(formId, req.organizationId!, tabs);
|
|
formsCache.invalidatePrefix(`forms:${req.organizationId}`);
|
|
res.json({ success: true, tabs: savedTabs, message: 'Вкладки сохранены' });
|
|
} catch (error) {
|
|
console.error('Save form tabs error:', error);
|
|
res.status(500).json({ success: false, error: 'Ошибка при сохранении вкладок' });
|
|
}
|
|
}
|
|
);
|
|
|
|
// === FORM FIELDS ENDPOINTS ===
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/forms/{id}/fields:
|
|
* get:
|
|
* tags: [Forms]
|
|
* summary: Поля формы
|
|
* post:
|
|
* tags: [Forms]
|
|
* summary: Добавить поле в форму
|
|
*/
|
|
router.get('/api/forms/:id/fields', async (req: AuthenticatedRequest, res) => {
|
|
try {
|
|
const formId = parseInt(req.params.id);
|
|
if (isNaN(formId)) {
|
|
return res.status(400).json({ success: false, error: 'Неверный ID формы' });
|
|
}
|
|
const existingForm = await storage.getForm(formId, req.organizationId!);
|
|
if (!existingForm) {
|
|
return res.status(404).json({ success: false, error: 'Форма не найдена' });
|
|
}
|
|
|
|
const hasManageForms = await storage.hasAppRolePermission(req.user!.appRole, ['forms.manage']);
|
|
if (!hasManageForms) {
|
|
const canAccess = await storage.canUserAccessForm(req.user!.id, formId, req.organizationId!, 'participate');
|
|
if (!canAccess) {
|
|
return res.status(403).json({ success: false, error: 'Нет доступа к этой форме' });
|
|
}
|
|
}
|
|
const fields = await storage.getFormFieldsByCreationOrder(formId, req.organizationId!);
|
|
res.json({ success: true, fields });
|
|
} catch (error) {
|
|
console.error('Get form fields error:', error);
|
|
res.status(500).json({ success: false, error: 'Ошибка при получении полей формы' });
|
|
}
|
|
});
|
|
|
|
router.post('/api/forms/:id/fields',
|
|
requireFormAdminOrPermission('forms.manage'),
|
|
async (req: AuthenticatedRequest, res) => {
|
|
try {
|
|
const formId = parseInt(req.params.id);
|
|
if (isNaN(formId)) {
|
|
return res.status(400).json({ success: false, error: 'Неверный ID формы' });
|
|
}
|
|
const existingForm = await storage.getForm(formId, req.organizationId!);
|
|
if (!existingForm) {
|
|
return res.status(404).json({ success: false, error: 'Форма не найдена' });
|
|
}
|
|
|
|
const { fields } = req.body;
|
|
if (!Array.isArray(fields)) {
|
|
return res.status(400).json({ success: false, error: 'Поля должны быть массивом' });
|
|
}
|
|
|
|
const existingFields = await storage.getFormFields(formId, req.organizationId!);
|
|
const incomingIds = fields.filter((f: Record<string, unknown>) => f.id).map((f: Record<string, unknown>) => f.id as number);
|
|
const fieldsToDelete = existingFields.filter(existing => !incomingIds.includes(existing.id));
|
|
|
|
// Мягкое удаление: значения в задачах сохраняются (поле уходит в архив),
|
|
// поэтому раньше существовавшая блокировка «поле используется в задачах» снята.
|
|
const fieldIdsToDelete = fieldsToDelete.map(f => f.id);
|
|
const normalizedFields = fields.map((f: Record<string, unknown>) => ({
|
|
...f,
|
|
maxFileCount: f.maxFileCount != null && parseInt(String(f.maxFileCount)) > 0 ? parseInt(String(f.maxFileCount)) : null,
|
|
maxFileSizeMB: f.maxFileSizeMB != null && parseInt(String(f.maxFileSizeMB)) > 0 ? parseInt(String(f.maxFileSizeMB)) : null,
|
|
}));
|
|
|
|
// Validate field types for new fields
|
|
for (const field of fields as Array<Record<string, unknown>>) {
|
|
const fieldId = field.id as number | undefined;
|
|
const isNew = !fieldId || (typeof fieldId === 'number' && fieldId < 0);
|
|
const fieldType = field.type as string;
|
|
if (isNew && fieldType && !isValidFieldType(fieldType, 'form')) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: `Тип поля "${fieldType}" нельзя создать напрямую в форме. Используйте глобальные поля.`,
|
|
});
|
|
}
|
|
}
|
|
|
|
const resultFields = await storage.bulkSaveFormFields(formId, req.organizationId!, fieldIdsToDelete, normalizedFields as Parameters<typeof storage.bulkSaveFormFields>[3]);
|
|
formsCache.invalidatePrefix(`forms:${req.organizationId}`);
|
|
indexFormWithSummaryAsync(formId, req.organizationId!).catch(() => {});
|
|
|
|
res.status(201).json({ success: true, message: 'Поля сохранены', fields: resultFields });
|
|
} catch (error) {
|
|
console.error('Save form fields error:', error);
|
|
res.status(500).json({ success: false, error: 'Ошибка при сохранении полей' });
|
|
}
|
|
}
|
|
);
|
|
|
|
// Архив удалённых (мягко) полей формы
|
|
router.get('/api/forms/:id/fields/deleted',
|
|
requireFormAdminOrPermission('forms.manage'),
|
|
async (req: AuthenticatedRequest, res) => {
|
|
try {
|
|
const formId = parseInt(req.params.id);
|
|
if (isNaN(formId)) {
|
|
return res.status(400).json({ success: false, error: 'Неверный ID формы' });
|
|
}
|
|
const fields = await storage.getDeletedFormFields(formId, req.organizationId!);
|
|
res.json({ success: true, fields });
|
|
} catch (error) {
|
|
console.error('Get deleted fields error:', error);
|
|
res.status(500).json({ success: false, error: 'Ошибка при загрузке удалённых полей' });
|
|
}
|
|
}
|
|
);
|
|
|
|
// Восстановление удалённого поля (значения в задачах сохранились и вернутся сами)
|
|
router.post('/api/forms/:id/fields/:fieldId/restore',
|
|
requireFormAdminOrPermission('forms.manage'),
|
|
async (req: AuthenticatedRequest, res) => {
|
|
try {
|
|
const formId = parseInt(req.params.id);
|
|
const fieldId = parseInt(req.params.fieldId);
|
|
if (isNaN(formId) || isNaN(fieldId)) {
|
|
return res.status(400).json({ success: false, error: 'Неверный ID формы или поля' });
|
|
}
|
|
const restored = await storage.restoreFormField(fieldId, formId, req.organizationId!);
|
|
formsCache.invalidatePrefix(`forms:${req.organizationId}`);
|
|
res.json({ success: true, field: restored });
|
|
} catch (error) {
|
|
console.error('Restore field error:', error);
|
|
res.status(400).json({ success: false, error: error instanceof Error ? error.message : 'Ошибка при восстановлении поля' });
|
|
}
|
|
}
|
|
);
|
|
}
|