import type { JiraSettings } from "../api/types.ts"; import type { BoardSnapshot } from "../api/types.ts"; export type Opt = { value: string; label: string }; const uniq = (xs: (string | null | undefined)[]) => [...new Set(xs.filter((x): x is string => !!x))].sort((a, b) => a.localeCompare(b, "ru")); const isEpic = (t: string | null) => /^(Фича|Epic|Эпик)$/i.test(t ?? ""); export const epicNames = (snap: BoardSnapshot | undefined): Record => Object.fromEntries((snap?.issues ?? []).filter((i) => isEpic(i.issue_type)).map((i) => [i.key, i.summary || i.key])); export const dueOf = (i: { planned_end: string | null; duedate: string | null }) => (i.planned_end || i.duedate || "").slice(0, 10); // T13.1: значение фильтра из адреса, которого нет среди вариантов (закрытый спринт, ушедший исполнитель), видно в списке; // пока снимок доски не загружен, пометку не ставим (R-4) export const withCurrent = (opts: Opt[], value: string | undefined, loaded: boolean): Opt[] => !value || opts.some((o) => o.value === value) ? opts : [{ value, label: loaded ? `${value} (нет на доске)` : value }, ...opts]; // варианты выпадающих списков фильтров: из снимка /board и словарей настроек, без новых маршрутов API export function filterOptions(snap: BoardSnapshot | undefined, s: JiraSettings | undefined) { const issues = snap?.issues ?? []; const names = epicNames(snap); const opt = (xs: string[]): Opt[] => xs.map((v) => ({ value: v, label: v })); return { sprint: opt(uniq(issues.map((i) => i.sprint))), assignee: opt(uniq(issues.map((i) => i.assignee))), work_kind: opt(uniq([...Object.keys(s?.kind_labels ?? {}), ...issues.map((i) => i.work_kind)])), status: opt(uniq(issues.map((i) => i.status))), label: opt(uniq(issues.flatMap((i) => i.labels))), epic: uniq(issues.map((i) => i.epic_key)).map((k) => ({ value: k, label: names[k] ?? k })), }; }