import { Router } from "express"; import { storage } from "../storage"; import { authenticateToken, requirePermission, type AuthenticatedRequest } from "../middleware/auth.middleware"; import { tenantIsolation } from "../middleware/tenant.middleware"; import { isValidFieldType } from "@shared/field-types"; import { canManageUser } from "../utils/user-access"; export function registerUserProfileRoutes(router: Router): void { // === USER PROFILE TABS === router.get('/api/user-profile/tabs', authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const tabs = await storage.getUserProfileTabs(req.organizationId!); res.json({ success: true, tabs }); } catch (error) { console.error('Get user profile tabs error:', error); res.status(500).json({ success: false, error: 'Ошибка при получении вкладок профиля' }); } } ); router.post('/api/user-profile/tabs', authenticateToken, tenantIsolation, requirePermission('users.manage'), async (req: AuthenticatedRequest, res) => { try { const { tabs } = req.body; if (!Array.isArray(tabs)) { return res.status(400).json({ success: false, error: 'Вкладки должны быть массивом' }); } const savedTabs = await storage.bulkSaveUserProfileTabs(req.organizationId!, tabs); res.json({ success: true, tabs: savedTabs, message: 'Вкладки сохранены' }); } catch (error) { console.error('Save user profile tabs error:', error); res.status(500).json({ success: false, error: 'Ошибка при сохранении вкладок профиля' }); } } ); // === USER PROFILE FIELDS === router.get('/api/user-profile/fields', authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const fields = await storage.getUserProfileFields(req.organizationId!); res.json({ success: true, fields }); } catch (error) { console.error('Get user profile fields error:', error); res.status(500).json({ success: false, error: 'Ошибка при получении полей профиля' }); } } ); router.post('/api/user-profile/fields', authenticateToken, tenantIsolation, requirePermission('users.manage'), async (req: AuthenticatedRequest, res) => { try { const { fields } = req.body; if (!Array.isArray(fields)) { return res.status(400).json({ success: false, error: 'Поля должны быть массивом' }); } const existingFields = await storage.getUserProfileFields(req.organizationId!); const incomingIds = fields.filter((f: Record) => f.id).map((f: Record) => f.id as number); const fieldsToDelete = existingFields.filter(existing => !incomingIds.includes(existing.id)); // Check if fields to delete are in use if (fieldsToDelete.length > 0) { const allUsers = await storage.getUsersByOrganization(req.organizationId!); const usedFieldIds = new Set(); for (const user of allUsers) { const values = await storage.getUserProfileFieldValues(user.id); for (const value of values) { usedFieldIds.add(value.fieldId); } } for (const field of fieldsToDelete) { if (usedFieldIds.has(field.id)) { return res.status(400).json({ success: false, error: `Невозможно удалить поле "${field.name}" - оно используется в профилях пользователей. Сначала удалите значения этого поля во всех профилях.` }); } } } const fieldIdsToDelete = fieldsToDelete.map(f => f.id); const normalizedFields = fields.map((f: Record) => ({ ...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>) { 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, 'user')) { return res.status(400).json({ success: false, error: `Тип поля "${fieldType}" нельзя использовать в профиле пользователя.`, }); } } const resultFields = await storage.bulkSaveUserProfileFields( req.organizationId!, fieldIdsToDelete, normalizedFields as Parameters[2] ); res.status(201).json({ success: true, message: 'Поля сохранены', fields: resultFields }); } catch (error) { console.error('Save user profile fields error:', error); res.status(500).json({ success: false, error: 'Ошибка при сохранении полей профиля' }); } } ); // === USER PROFILE DETAIL === router.get('/api/users/:id/profile', authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const userId = parseInt(req.params.id); if (isNaN(userId)) { return res.status(400).json({ success: false, error: 'Неверный ID пользователя' }); } const targetUser = await storage.getUser(userId); if (!targetUser || targetUser.organizationId !== req.organizationId) { return res.status(404).json({ success: false, error: 'Пользователь не найден' }); } const detail = await storage.getUserProfileDetail(userId, req.organizationId!); const canEdit = await canManageUser(req.user, userId, req.organizationId!) || req.user?.id === userId; res.json({ success: true, ...detail, canEdit }); } catch (error) { console.error('Get user profile detail error:', error); res.status(500).json({ success: false, error: 'Ошибка при получении профиля пользователя' }); } } ); // === USER PROFILE AUDIT LOG === router.get('/api/users/:id/profile/audit-log', authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const userId = parseInt(req.params.id); if (isNaN(userId)) { return res.status(400).json({ success: false, error: 'Неверный ID пользователя' }); } const targetUser = await storage.getUser(userId); if (!targetUser || targetUser.organizationId !== req.organizationId) { return res.status(404).json({ success: false, error: 'Пользователь не найден' }); } const canView = await canManageUser(req.user, userId, req.organizationId!) || req.user?.id === userId; if (!canView) { return res.status(403).json({ success: false, error: 'Недостаточно прав' }); } const auditLog = await storage.getUserProfileAuditLog(userId, req.organizationId!); res.json({ success: true, auditLog }); } catch (error) { console.error('Get user profile audit log error:', error); res.status(500).json({ success: false, error: 'Ошибка при получении истории изменений' }); } } ); // === USER FIELD HISTORY (history-number) === const FIELD_HISTORY_PERIODS: Record = { week: { days: 7, bucket: 'day' }, month: { days: 30, bucket: 'day' }, quarter: { days: 90, bucket: 'week' }, year: { days: 365, bucket: 'month' }, }; router.get('/api/users/:id/field-history/:fieldId', authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const userId = parseInt(req.params.id); const fieldId = parseInt(req.params.fieldId); if (isNaN(userId) || isNaN(fieldId)) { return res.status(400).json({ success: false, error: 'Неверный ID' }); } // Права — как у просмотра профиля: любой сотрудник той же организации const targetUser = await storage.getUser(userId); if (!targetUser || targetUser.organizationId !== req.organizationId) { return res.status(404).json({ success: false, error: 'Пользователь не найден' }); } const field = await storage.getUserProfileFieldById(fieldId, req.organizationId!); if (!field || field.type !== 'history-number') { return res.status(404).json({ success: false, error: 'Поле не найдено' }); } const periodParam = String(req.query.period || 'month'); const period = FIELD_HISTORY_PERIODS[periodParam]; if (!period) { return res.status(400).json({ success: false, error: 'Неверный период. Допустимо: week, month, quarter, year' }); } const { points, overallAvg, totalCount } = await storage.getUserFieldHistoryPoints( userId, fieldId, req.organizationId!, period.days, period.bucket ); res.json({ success: true, field: { id: field.id, name: field.name }, period: periodParam, points, overallAvg, totalCount, }); } catch (error) { console.error('Get user field history error:', error); res.status(500).json({ success: false, error: 'Ошибка при получении истории поля' }); } } ); // === USER PROFILE FIELD VALUES === router.get('/api/users/:id/field-values', authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const userId = parseInt(req.params.id); if (isNaN(userId)) { return res.status(400).json({ success: false, error: 'Неверный ID пользователя' }); } const targetUser = await storage.getUser(userId); if (!targetUser || targetUser.organizationId !== req.organizationId) { return res.status(404).json({ success: false, error: 'Пользователь не найден' }); } const values = await storage.getUserProfileFieldValues(userId); res.json({ success: true, fieldValues: values }); } catch (error) { console.error('Get user profile field values error:', error); res.status(500).json({ success: false, error: 'Ошибка при получении значений полей' }); } } ); router.post('/api/users/:id/field-values', authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const userId = parseInt(req.params.id); if (isNaN(userId)) { return res.status(400).json({ success: false, error: 'Неверный ID пользователя' }); } const targetUser = await storage.getUser(userId); if (!targetUser || targetUser.organizationId !== req.organizationId) { return res.status(404).json({ success: false, error: 'Пользователь не найден' }); } const canEdit = await canManageUser(req.user, userId, req.organizationId!) || req.user?.id === userId; if (!canEdit) { return res.status(403).json({ success: false, error: 'Недостаточно прав' }); } const { fieldValues } = req.body; if (!Array.isArray(fieldValues)) { return res.status(400).json({ success: false, error: 'Значения полей должны быть массивом' }); } const changedBy = req.user!.id; const createdValues = []; for (const fv of fieldValues) { const existing = await storage.getUserProfileFieldValue(userId, fv.fieldId); if (fv.value === null || fv.value === undefined) { if (existing) { await storage.deleteUserProfileFieldValueWithAudit(userId, fv.fieldId, req.organizationId!, changedBy); } } else { if (existing) { const updated = await storage.updateUserProfileFieldValueWithAudit(userId, fv.fieldId, { value: fv.value }, req.organizationId!, changedBy); createdValues.push(updated); } else { const created = await storage.createUserProfileFieldValueWithAudit({ userId, fieldId: fv.fieldId, value: fv.value, }, req.organizationId!, changedBy); createdValues.push(created); } } } res.json({ success: true, fieldValues: createdValues }); } catch (error) { console.error('Save user profile field values error:', error); res.status(500).json({ success: false, error: 'Ошибка при сохранении значений полей' }); } } ); router.patch('/api/users/:id/field-values/:fieldId', authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const userId = parseInt(req.params.id); const fieldId = parseInt(req.params.fieldId); if (isNaN(userId) || isNaN(fieldId)) { return res.status(400).json({ success: false, error: 'Неверный ID' }); } const targetUser = await storage.getUser(userId); if (!targetUser || targetUser.organizationId !== req.organizationId) { return res.status(404).json({ success: false, error: 'Пользователь не найден' }); } const canEdit = await canManageUser(req.user, userId, req.organizationId!) || req.user?.id === userId; if (!canEdit) { return res.status(403).json({ success: false, error: 'Недостаточно прав' }); } const changedBy = req.user!.id; const { value } = req.body; const existing = await storage.getUserProfileFieldValue(userId, fieldId); if (value === null || value === undefined) { if (existing) { await storage.deleteUserProfileFieldValueWithAudit(userId, fieldId, req.organizationId!, changedBy); } res.json({ success: true, fieldValue: null }); } else { if (existing) { const updated = await storage.updateUserProfileFieldValueWithAudit(userId, fieldId, { value }, req.organizationId!, changedBy); res.json({ success: true, fieldValue: updated }); } else { const created = await storage.createUserProfileFieldValueWithAudit({ userId, fieldId, value }, req.organizationId!, changedBy); res.json({ success: true, fieldValue: created }); } } } catch (error) { console.error('Patch user profile field value error:', error); res.status(500).json({ success: false, error: 'Ошибка при обновлении значения поля' }); } } ); }