fix(number-fields): избегаем потери точности длинных чисел
- number-поля теперь рендерятся как text + inputMode=numeric, чтобы браузер не округлял значения через input type=number - пробелы при вставке в number-поля удаляются - бэкенд нормализует значения number-полей в строку перед сохранением - добавлен хелпер normalizeFieldValueForStorage Closes: искажение расчётного счёта и других длинных числовых полей
This commit is contained in:
168
server/routes/dashboard.routes.ts
Normal file
168
server/routes/dashboard.routes.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
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<formId, Array<{ statusId, name, color, count }>>
|
||||
* - overdueTasks: top 10 overdue tasks across selected forms
|
||||
* - myTasks: top 10 open tasks assigned to current user across selected forms
|
||||
* - assigneeDistribution: Record<formId, Array<{ userId, userName, count }>>
|
||||
*/
|
||||
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<number, Array<{ statusId: number; name: string; color: string; count: number }>> = {};
|
||||
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<number, Array<{ userId: number | null; userName: string; count: number }>> = {};
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user