import { Router } from "express"; import { authenticateToken, type AuthenticatedRequest } from "../middleware/auth.middleware"; import { tenantIsolation } from "../middleware/tenant.middleware"; import { storage } from "../storage"; import { pool } from "../db"; const dashboardRouter = Router(); /** * GET /api/dashboard * * Query params: * formIds — comma-separated list of form IDs (default: "11") * * Returns aggregated analytics for the selected forms: * - forms: available forms for the user * - statusCounts: Record> * - overdueTasks: top 10 overdue tasks across selected forms * - myTasks: top 10 open tasks assigned to current user across selected forms * - assigneeDistribution: Record> */ dashboardRouter.get('/api/dashboard', authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const userId = req.user!.id; const orgId = req.user!.organizationId; // Parse formIds parameter const formIdsParam = (req.query.formIds as string) || '11'; const formIds = formIdsParam .split(',') .map((s) => parseInt(s.trim(), 10)) .filter((n) => !isNaN(n)); if (formIds.length === 0) { return res.status(400).json({ success: false, error: 'Необходимо указать хотя бы одну форму' }); } // Verify access to each requested form const accessibleFormIds: number[] = []; for (const fid of formIds) { const canAccess = await storage.canUserAccessForm(userId, fid, orgId, 'participate'); if (canAccess) accessibleFormIds.push(fid); } if (accessibleFormIds.length === 0) { return res.status(403).json({ success: false, error: 'Нет доступа к выбранным формам' }); } // Get form details for accessible forms const formsResult = await pool.query( `SELECT id, name FROM forms WHERE id = ANY($1) AND organization_id = $2`, [accessibleFormIds, orgId] ); const forms = formsResult.rows; // Status counts per form const statusCounts: Record> = {}; for (const fid of accessibleFormIds) { const counts = await storage.getTaskCountsByStatus(fid, orgId); statusCounts[fid] = counts; } // Overdue tasks (due_date < NOW(), not completed) across selected forms const overdueResult = await pool.query( ` SELECT t.id, t.title, t.form_id AS "formId", f.name AS "formName", fs.name AS "statusName", fs.color AS "statusColor", t.due_date AS "dueDate", COALESCE( CONCAT(u.first_name, ' ', u.middle_name, ' ', u.last_name), CONCAT(u.first_name, ' ', u.last_name) ) AS "assigneeName" FROM tasks t JOIN forms f ON f.id = t.form_id AND f.organization_id = $1 JOIN form_statuses fs ON fs.id = t.current_status_id LEFT JOIN users u ON u.id = t.assigned_to WHERE t.form_id = ANY($2) AND t.is_completed = false AND t.due_date IS NOT NULL AND t.due_date < NOW() ORDER BY t.due_date ASC LIMIT 10 `, [orgId, accessibleFormIds] ); // My tasks (assigned to current user, not completed) across selected forms const myTasksResult = await pool.query( ` SELECT t.id, t.title, t.form_id AS "formId", f.name AS "formName", fs.name AS "statusName", fs.color AS "statusColor", t.due_date AS "dueDate", COALESCE( CONCAT(u.first_name, ' ', u.middle_name, ' ', u.last_name), CONCAT(u.first_name, ' ', u.last_name) ) AS "assigneeName" FROM tasks t JOIN forms f ON f.id = t.form_id AND f.organization_id = $1 JOIN form_statuses fs ON fs.id = t.current_status_id LEFT JOIN users u ON u.id = t.assigned_to WHERE t.form_id = ANY($2) AND t.is_completed = false AND t.assigned_to = $3 ORDER BY t.due_date ASC NULLS LAST, t.updated_at DESC LIMIT 10 `, [orgId, accessibleFormIds, userId] ); // Assignee distribution per form const assigneeDistribution: Record> = {}; for (const fid of accessibleFormIds) { const distResult = await pool.query( ` SELECT t.assigned_to AS "userId", COALESCE( CONCAT(u.first_name, ' ', u.middle_name, ' ', u.last_name), CONCAT(u.first_name, ' ', u.last_name), 'Без назначения' ) AS "userName", COUNT(*)::int AS "count" FROM tasks t LEFT JOIN users u ON u.id = t.assigned_to WHERE t.form_id = $1 AND t.is_completed = false AND t.organization_id = $2 GROUP BY t.assigned_to, u.first_name, u.middle_name, u.last_name ORDER BY "count" DESC `, [fid, orgId] ); assigneeDistribution[fid] = distResult.rows; } res.json({ success: true, data: { forms, statusCounts, overdueTasks: overdueResult.rows, myTasks: myTasksResult.rows, assigneeDistribution, }, }); } catch (error) { console.error('[dashboard]', error); res.status(500).json({ success: false, error: 'Ошибка загрузки дашборда' }); } } ); export function registerDashboardRoutes(router: Router): void { router.use(dashboardRouter); }