2026-10-14 22:02:31 +04:00

60 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { KIND, NO_EPIC, NOT_IN_FILE, NOT_IN_HUB, RELATED } from "../constants.ts";
import { whoKey } from "../model/util.ts";
import type { BoardModel, BoardNode, Filters, Link, VisLink, VisNode, Visible } from "../types.ts";
import { criticalPath } from "./critical.ts";
import { findProblems } from "./problems.ts";
// фильтры задач — пересечение спринта, исполнителя и вида работы; skip — какой из них не учитывать (для счётчиков в выпадашках).
// Заглушки «нет в файле» фильтры не проходят: они попадают на доску только как соседи по связям
export const passes = (x: BoardNode, f: Filters, skip?: "sprint" | "who" | "kind"): boolean => !x.stub
&& (skip === "sprint" || f.sprints.size === 0 || f.sprints.has(x.sprint))
&& (skip === "who" || f.who.size === 0 || f.who.has(whoKey(x.who)))
&& (skip === "kind" || !f.hideKind.has(KIND[x.kind] || "org"));
export const edgeClass = (l: Link): "rel" | "block" => (l.type === RELATED ? "rel" : "block");
// доска: ядро = задачи, прошедшие фильтры; контекст = их соседи по связям (если включён); стрелки = все связи между карточками на доске
// минус скрытые типы и, при «передачах», минус связи внутри одного человека; «скрыть без связей» убирает карточки без оставшихся стрелок.
// removed — сколько карточек/стрелок убрал каждый шаг, для строки статуса и объяснения пустой доски
export function computeVisible(model: BoardModel, f: Filters, laneOrder: string[], today: string): Visible {
const all = model.nodes, byKey = Object.fromEntries(all.map((x) => [x.key, x]));
const isHand = (l: Link) => l.type !== RELATED && whoKey(byKey[l.src].who) !== whoKey(byKey[l.dst].who);
const core = new Set(all.filter((x) => passes(x, f)).map((x) => x.key));
let keep = new Set(core);
if (f.neighbors) model.links.forEach((l) => { if (core.has(l.src) || core.has(l.dst)) { keep.add(l.src); keep.add(l.dst); } });
const removed = { core: core.size, ctx: keep.size - core.size, edge: 0, loose: 0 };
let rawLinks = model.links.filter((l) => keep.has(l.src) && keep.has(l.dst));
const allLinks = rawLinks.length;
if (f.hideEdge.size) rawLinks = rawLinks.filter((l) => !f.hideEdge.has(edgeClass(l)));
if (f.onlyHand) rawLinks = rawLinks.filter(isHand);
removed.edge = allLinks - rawLinks.length;
if (f.onlyLinked) {
const ends = new Set(rawLinks.flatMap((l) => [l.src, l.dst])), before = keep.size;
keep = new Set([...keep].filter((k) => ends.has(k)));
removed.loose = before - keep.size;
}
const links: VisLink[] = rawLinks.map((l) => ({ ...l, hand: isHand(l), crit: false }));
// ядро сплошной рамкой, соседи пунктиром
const nodes: VisNode[] = all.filter((x) => keep.has(x.key)).map((x) => ({ ...x, dim: !core.has(x.key), focus: false, crit: false }));
const crit = f.crit ? criticalPath(nodes, links) : { nodes: new Set<string>(), edges: new Set<string>() };
nodes.forEach((x) => { x.crit = crit.nodes.has(x.key); });
links.forEach((l) => { l.crit = crit.edges.has(l.src + ">" + l.dst); });
// дорожки по эпикам: по алфавиту, служебные внизу, затем сохранённый порядок пользователя
const byEpic: Record<string, string[]> = {};
nodes.forEach((x) => (byEpic[x.epic] = byEpic[x.epic] || []).push(x.key));
const tail = (name: string) => name === NO_EPIC || name === NOT_IN_FILE || name === NOT_IN_HUB;
const lanes = Object.entries(byEpic).sort((a, b) => Number(tail(a[0])) - Number(tail(b[0])) || a[0].localeCompare(b[0], "ru")).map(([name, keys]) => ({ name, keys }));
if (laneOrder.length) lanes.sort((a, b) => { const ia = laneOrder.indexOf(a.name), ib = laneOrder.indexOf(b.name); return (ia < 0 ? 1e9 : ia) - (ib < 0 ? 1e9 : ib); });
return { nodes, links, lanes, removed, problems: findProblems(nodes, links, today) };
}
export function emptyReason(model: BoardModel, vis: Visible, f: Filters): string {
const r = vis.removed;
if (!model.nodes.some((n) => !n.stub)) return model.emptyText || (model.stubLane === NOT_IN_HUB ? "В зеркале хаба нет задач." : "В файле нет задач.");
if (!r.core) {
const active = [f.sprints.size ? "спринт" : "", f.who.size ? "исполнитель" : "", f.hideKind.size ? "виды работ в легенде" : ""].filter(Boolean);
return `Фильтры не оставили ни одной задачи (${active.join(", ")}). Ослабьте один из них: «Снять выбор» в выпадашке или щелчок по зачёркнутому виду работы.`;
}
if (r.loose) return `Все ${r.loose} задач скрыты кнопкой «скрыть без связей»: ${f.onlyHand ? "передач между людьми на доске нет" : "видимых стрелок у них нет"}. Отключите её${f.onlyHand ? " или «передачи»" : f.hideEdge.size ? " или верните тип связи в легенде" : ""}.`;
return "Нет задач для показа.";
}