import type { ChangeEvent, ChangeKind } from "../api/types.ts"; // период ленты: границы по местному времени браузера; «Сегодня» — по умолчанию и в адрес не пишется (spec web-app «Лента изменений») export type Period = "2h" | "today" | "yesterday" | "7d"; export const PERIODS: { value: Period; label: string }[] = [ { value: "2h", label: "2 часа" }, { value: "today", label: "Сегодня" }, { value: "yesterday", label: "Вчера" }, { value: "7d", label: "7 дней" }, ]; export const parsePeriod = (v: string | null): Period => (PERIODS.some((p) => p.value === v) ? (v as Period) : "today"); export function periodRange(p: Period, now: Date): { since: Date; until: Date } { const midnight = new Date(now.getFullYear(), now.getMonth(), now.getDate()); switch (p) { case "2h": return { since: new Date(now.getTime() - 2 * 3600_000), until: now }; case "yesterday": return { since: new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1), until: midnight }; case "7d": return { since: new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7, now.getHours(), now.getMinutes(), now.getSeconds(), now.getMilliseconds()), until: now }; default: return { since: midnight, until: now }; } } export const KINDS: { value: ChangeKind; label: string }[] = [ { value: "status", label: "Статусы" }, { value: "description", label: "Описание" }, { value: "dates", label: "Сроки" }, { value: "comment", label: "Комментарии" }, { value: "other", label: "Прочие поля" }, ]; export const DEFAULT_KINDS: ChangeKind[] = ["status", "description", "dates", "comment"]; // виды из адреса: неизвестные отбрасываются; пусто или ни одного известного — набор по умолчанию (M-5); «none» — пустой набор (R-2) export function parseKinds(v: string | null): ChangeKind[] { if (v === "none") return []; const asked = (v ?? "").split(",").map((s) => s.trim()); const known = KINDS.map((k) => k.value).filter((k) => asked.includes(k)); return known.length ? known : [...DEFAULT_KINDS]; } // в адрес — в порядке KINDS; набор по умолчанию не пишется export function kindsParam(kinds: ChangeKind[]): string | null { const ordered = KINDS.map((k) => k.value).filter((k) => kinds.includes(k)); return ordered.join(",") === DEFAULT_KINDS.join(",") ? null : ordered.join(","); } const pad = (n: number) => String(n).padStart(2, "0"); // «ЧЧ:ММ» для событий текущих суток, иначе «ДД.ММ ЧЧ:ММ»; без времени — «—» export function eventTime(at: string | null, now: Date, alwaysDate = false): string { const d = at ? new Date(at) : null; if (!d || isNaN(d.getTime())) return "—"; const hm = `${pad(d.getHours())}:${pad(d.getMinutes())}`; const sameDay = d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate(); return sameDay && !alwaysDate ? hm : `${pad(d.getDate())}.${pad(d.getMonth() + 1)} ${hm}`; } const FIELD_LABELS: Record = { assignee: "исполнитель", Sprint: "спринт", labels: "метки", priority: "приоритет", summary: "тема", epic_link: "эпик", Link: "связь", Attachment: "вложение", resolution: "резолюция", }; const DATE_LABELS: Record = { duedate: "срок", planned_start: "плановое начало", planned_end: "плановое окончание" }; const oneLine = (s: string | null) => (s ?? "").replace(/\s+/g, " ").trim(); const cut = (s: string, n: number) => (s.length <= n ? s : s.slice(0, n - 1) + "…"); const dash = (s: string | null) => (s ? s : "—"); // дата события сроков: по значению вида ГГГГ-ММ-ДД, иначе строка журнала как есть function day(value: string | null, text: string | null): string { const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(value ?? ""); return m ? `${m[3]}.${m[2]}` : dash(text); } export function eventText(e: ChangeEvent): string { switch (e.kind) { case "status": return `статус «${dash(e.from)}» → «${dash(e.to)}»`; case "description": return "описание изменено"; case "dates": return `${DATE_LABELS[e.field] ?? e.field} ${day(e.from_value, e.from)} → ${day(e.to_value, e.to)}`; case "comment": { // первые 300 символов текста и «…» (M-4); без текста — «—» (M-6) const s = oneLine(e.to); return "комментарий: " + (!s ? "—" : s.length <= 300 ? s : s.slice(0, 300) + "…"); } default: { const v = (s: string | null) => (s ? cut(oneLine(s), 80) : "—"); return `${FIELD_LABELS[e.field] ?? e.field}: «${v(e.from)}» → «${v(e.to)}»`; } } } export interface IssueGroup { key: string; summary: string | null; status: string | null; events: ChangeEvent[] } // лента уже новые сверху: группы — в порядке первого появления задачи export function groupByIssue(events: ChangeEvent[]): IssueGroup[] { const groups = new Map(); for (const e of events) { const key = e.key ?? ""; let g = groups.get(key); if (!g) groups.set(key, (g = { key, summary: e.summary ?? null, status: e.status ?? null, events: [] })); g.events.push(e); } return [...groups.values()]; }