Andrey Epifancev 815cd3885e feat(web): значки типов по образцу Jira и формат SP (#112, M-6)
- model.ts: fmtSp округляет до двух знаков (M-6); typeKind и isEpic по именам типов Jira без учёта регистра
- TypeIcon: квадрат 16 px, вид по typeKind, aria-label — исходное имя типа, без типа — «Тип не указан»
- icons.tsx: IconBookmark, IconCircle, IconSquares, IconDot; IconProps.size добавляет 12
- agile.css: фон значка — токены дизайн-системы, символ --color-text-on-primary

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-25 20:18:37 +04:00

56 lines
2.9 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 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";
}