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(), edges: new Set() }; 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 = {}; 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 "Нет задач для показа."; }