import type { AgileResponse } from "../api/types.ts"; // время Jira приходит и как «+0300» (поля задачи), и как «+03:00» (Agile REST); Date.parse понимает не всякое — приводим к «+03:00» (R-26) export function jiraDate(iso: string | null): Date | null { if (!iso) return null; const d = new Date(iso.replace(/([+-]\d{2})(\d{2})$/, "$1:$2")); return isNaN(d.getTime()) ? null : d; } const pad = (n: number) => String(n).padStart(2, "0"); const day = (d: Date) => `${pad(d.getDate())}.${pad(d.getMonth() + 1)}`; // «ДД.ММ» в часовом поясе браузера; нет даты или она негодная — «—» export const fmtDay = (iso: string | null): string => { const d = jiraDate(iso); return d ? day(d) : "—"; }; export const fmtRange = (start: string | null, end: string | null): string => `${fmtDay(start)}–${fmtDay(end)}`; // сколько календарных дней (в поясе браузера) от «сегодня» до конца спринта; отрицательное — срок прошёл export function daysLeft(endIso: string | null, now: Date): number | null { const end = jiraDate(endIso); if (!end) return null; const midnight = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime(); return Math.round((midnight(end) - midnight(now)) / 86_400_000); } // очки до двух знаков без хвостов: 0.1+0.2 → «0.3», 1/3 → «0.33» (M-6) export const fmtSp = (n: number | null): string => (n === null ? "—" : String(Number(n.toFixed(2)))); // вид типа задачи для значка и логики (имена Jira, без учёта регистра); прочее и пустое — other export type TypeKind = "epic" | "story" | "task" | "bug" | "subtask" | "other"; const TYPE_KINDS: [RegExp, TypeKind][] = [ [/^(Эпик|Epic|Фича)$/i, "epic"], [/^(История|Story)$/i, "story"], [/^(Задача|Task)$/i, "task"], [/^(Ошибка|Bug)$/i, "bug"], [/^(Подзадача|Sub-task|Subtask)$/i, "subtask"], ]; export function typeKind(type: string | null): TypeKind { const name = (type ?? "").trim(); return TYPE_KINDS.find(([re]) => re.test(name))?.[1] ?? "other"; } export const isEpic = (i: { issue_type: string | null }): boolean => typeKind(i.issue_type) === "epic"; // «1 задача», «2 задачи», «5 задач» (R-24) export function tasksWord(n: number): string { const m10 = n % 10, m100 = n % 100; if (m10 === 1 && m100 !== 11) return "задача"; if (m10 >= 2 && m10 <= 4 && (m100 < 12 || m100 > 14)) return "задачи"; return "задач"; } // три состояния данных доски (R-19, design #23) export function boardState(d: AgileResponse): "no-data" | "no-columns" | "ok" { if (!d.board || d.board.fetched_at === null) return "no-data"; return d.board.columns === null ? "no-columns" : "ok"; }