62 lines
2.9 KiB
TypeScript
62 lines
2.9 KiB
TypeScript
import { Link, useSearchParams } from "react-router";
|
||
import type { Project } from "../../../api/types.ts";
|
||
import { PageHeader, Panel, Segmented } from "../../../ui/index.ts";
|
||
import type { AgileSprint } from "../api/types.ts";
|
||
import { RefreshButton } from "../RefreshButton.tsx";
|
||
import { AgileGate } from "./AgileGate.tsx";
|
||
import { BoardColumns } from "./BoardColumns.tsx";
|
||
import { columnsOf } from "./columns.ts";
|
||
import { daysLeft, fmtRange, fmtSp, tasksWord } from "./model.ts";
|
||
import "./agile.css";
|
||
|
||
function SprintHead({ s }: { s: AgileSprint }) {
|
||
const left = daysLeft(s.end_date, new Date());
|
||
return (
|
||
<header className="agile-section__head">
|
||
<h2 className="agile-section__title">{s.name}</h2>
|
||
<span className="agile-section__meta">{fmtRange(s.start_date, s.end_date)}</span>
|
||
{left !== null && <span className="agile-section__meta">{left >= 0 ? `осталось ${left} дн.` : "срок прошёл"}</span>}
|
||
{s.goal && <span className="agile-section__goal">{s.goal}</span>}
|
||
<span className="agile-section__meta">{`${s.count_done} из ${s.count} ${tasksWord(s.count)}`}</span>
|
||
<span className="agile-section__meta">{`${fmtSp(s.points_done)} из ${fmtSp(s.points)} SP`}</span>
|
||
</header>
|
||
);
|
||
}
|
||
|
||
// «Спринт» (spec web-app): колонки доски активного спринта; только просмотр
|
||
export function SprintPage({ project }: { project: Project }) {
|
||
const [params] = useSearchParams();
|
||
const wanted = params.get("sprint");
|
||
return (
|
||
<div data-testid="page-sprint" className="agile agile--wide">
|
||
<PageHeader title="Спринт" actions={<RefreshButton />} />
|
||
<AgileGate project={project} kind="scrum">
|
||
{(d) => {
|
||
const active = d.sprints.filter((s) => s.state === "active");
|
||
// устаревший или мусорный ?sprint= — первый активный (Review Focus 2)
|
||
const sprint = active.find((s) => String(s.id) === wanted) ?? active[0];
|
||
if (!sprint) {
|
||
return (
|
||
<Panel className="agile-section">
|
||
<p className="agile__empty">Нет активного спринта</p>
|
||
<Link className="agile__link" to={`/jira/backlog?project=${encodeURIComponent(project.label)}`}>Бэклог</Link>
|
||
</Panel>
|
||
);
|
||
}
|
||
const proj = encodeURIComponent(project.label);
|
||
return (
|
||
<>
|
||
{active.length > 1 && (
|
||
<Segmented label="Спринты" value={String(sprint.id)}
|
||
options={active.map((s) => ({ value: String(s.id), label: s.name, href: `/jira/sprint?project=${proj}&sprint=${s.id}` }))} />
|
||
)}
|
||
<SprintHead s={sprint} />
|
||
<BoardColumns columns={columnsOf(d.board?.columns ?? null, sprint.issues)} />
|
||
</>
|
||
);
|
||
}}
|
||
</AgileGate>
|
||
</div>
|
||
);
|
||
}
|