feat(fields): цветные опции select — глобальная подсветка задач (главная, реестр, канбан, шапка)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user