feat(fields): цветные опции select — глобальная подсветка задач (главная, реестр, канбан, шапка)

This commit is contained in:
2026-09-21 11:13:44 +03:00
parent aa316141be
commit c1463c71a1
12 changed files with 334 additions and 20 deletions

View File

@@ -2,9 +2,33 @@ import { Router } from 'express';
import { authenticateToken, type AuthenticatedRequest } from '../middleware/auth.middleware';
import { tenantIsolation } from '../middleware/tenant.middleware';
import { pool } from '../db';
import { storage } from '../storage';
import { computeTaskTagColor } from '@shared/tag-color';
const homeRouter = Router();
/**
* Добавляет каждой строке задачи tagColor/tagLabel — «главный» цветной тег
* (первая цветная опция select-поля по порядку полей формы). Одна загрузка
* полей на форму на запрос; вычисление в JS (shared/tag-color).
*/
async function decorateTagColor(
rows: Array<{ formId: number; fieldValues?: Record<string, unknown> }>,
orgId: number,
): Promise<void> {
if (rows.length === 0) return;
const formIds = [...new Set(rows.map(r => r.formId).filter(Boolean))] as number[];
const fieldsByForm = new Map<number, Awaited<ReturnType<typeof storage.getFormFields>>>();
await Promise.all(formIds.map(async (fid) => {
fieldsByForm.set(fid, await storage.getFormFields(fid, orgId));
}));
for (const row of rows) {
const tag = computeTaskTagColor(fieldsByForm.get(row.formId) ?? [], row.fieldValues);
(row as Record<string, unknown>).tagColor = tag?.color ?? null;
(row as Record<string, unknown>).tagLabel = tag?.label ?? null;
}
}
const INBOX_SORT_COLUMNS: Record<string, string> = {
title: 't.title',
status: 'fs.name',
@@ -253,6 +277,8 @@ homeRouter.get('/api/home/inbox',
)
`, [orgId, userId, search, formId, status, assignee, overdue]);
await decorateTagColor(result.rows, orgId);
res.json({
success: true,
tasks: result.rows,
@@ -333,7 +359,11 @@ homeRouter.get('/api/home/recent',
t.updated_at AS "updatedAt",
t.due_date AS "dueDate",
t.is_completed AS "isCompleted",
rm.last_at AS "lastInteractionAt"
rm.last_at AS "lastInteractionAt",
(
SELECT jsonb_object_agg('field_' || tfv.field_id::text, tfv.value)
FROM task_field_values tfv WHERE tfv.task_id = t.id
) AS "fieldValues"
FROM recent_max rm
JOIN tasks t ON t.id = rm.task_id AND t.organization_id = $1
JOIN accessible acc ON acc.task_id = t.id
@@ -376,6 +406,8 @@ homeRouter.get('/api/home/recent',
AND ($6 = '' OR CONCAT(u.first_name, ' ', u.last_name) ILIKE '%' || $6 || '%')
`, [orgId, userId, search, formId, status, assignee]);
await decorateTagColor(result.rows, orgId);
res.json({
success: true,
tasks: result.rows,
@@ -487,6 +519,10 @@ homeRouter.get('/api/home/snoozed',
t.updated_at AS "updatedAt",
t.due_date AS "dueDate",
t.is_completed AS "isCompleted",
(
SELECT jsonb_object_agg('field_' || tfv.field_id::text, tfv.value)
FROM task_field_values tfv WHERE tfv.task_id = t.id
) AS "fieldValues",
MIN(tr.remind_at) OVER (PARTITION BY t.id) AS "remindAt",
CONCAT(cu.first_name, ' ', cu.last_name) AS "createdByName"
FROM task_reminders tr
@@ -541,6 +577,8 @@ homeRouter.get('/api/home/snoozed',
AND ($3 = '' OR t.title ILIKE '%' || $3 || '%')
`, [orgId, userId, search]);
await decorateTagColor(result.rows, orgId);
res.json({
success: true,
tasks: result.rows,
@@ -583,7 +621,11 @@ homeRouter.get('/api/home/hidden',
t.updated_at AS "updatedAt",
t.due_date AS "dueDate",
t.is_completed AS "isCompleted",
tih.created_at AS "hiddenAt"
tih.created_at AS "hiddenAt",
(
SELECT jsonb_object_agg('field_' || tfv.field_id::text, tfv.value)
FROM task_field_values tfv WHERE tfv.task_id = t.id
) AS "fieldValues"
FROM task_inbox_hidden tih
JOIN tasks t ON t.id = tih.task_id AND t.organization_id = $1 AND t.is_completed = false
JOIN forms f ON f.id = t.form_id
@@ -603,6 +645,8 @@ homeRouter.get('/api/home/hidden',
AND ($3 = '' OR t.title ILIKE '%' || $3 || '%')
`, [orgId, userId, search]);
await decorateTagColor(result.rows, orgId);
res.json({
success: true,
tasks: result.rows,

View File

@@ -8,6 +8,7 @@ import { eq, inArray } from "drizzle-orm";
import { tasksMinimalCache } from "../utils/cache";
import { clampTasksLimit, parseTasksCursor, buildTasksNextCursor } from "../utils/task-pagination";
import { clampDirectoryRowsLimit } from "../utils/data-table-rows-query";
import { computeTaskTagColor } from "@shared/tag-color";
// Лимиты выдачи уникальных значений в /column-values (автокомплит фильтров)
const COLUMN_VALUES_DEFAULT_LIMIT = 20;
@@ -339,6 +340,14 @@ export function registerTaskCrudListRoutes(router: ReturnType<typeof import("exp
const formFields = await storage.getFormFields(formId, req.organizationId!);
const relatedTaskTitles = await buildRelatedTaskTitles(tasks, formFields);
// «Главный» цветной тег задачи (первая цветная опция select-поля) —
// подсветка в реестре и канбане
for (const t of tasks as Array<Task & { fieldValues?: Record<number, unknown>; tagColor?: string | null; tagLabel?: string | null }>) {
const tag = computeTaskTagColor(formFields, t.fieldValues);
t.tagColor = tag?.color ?? null;
t.tagLabel = tag?.label ?? null;
}
res.json({ success: true, tasks, total: tasks.length, limit, offset, relatedTaskTitles });
} catch (error) {
console.error('Get tasks with fields error:', error);