sdlc-hub/web/src/modules/jira/agile/SprintPage.tsx

73 lines
4.0 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 { Link, useSearchParams } from "react-router";
import type { Project } from "../../../api/types.ts";
import { PageHeader, Panel } 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 { AgileToolbar } from "./AgileToolbar.tsx";
import { ColumnsMenu } from "./ColumnsMenu.tsx";
import { columnsOf } from "./columns.ts";
import { assigneeOptions, matches, parseSprintIds, selectableSprints, sprintIssues } from "./filters.ts";
import { SprintPicker } from "./SprintPicker.tsx";
import { useAgileView } from "./useAgileView.ts";
import { daysLeft, fmtRange, fmtSp, tasksWord } from "./model.ts";
import "./agile.css";
function SprintHead({ s }: { s: AgileSprint }) {
const left = s.state === "active" ? daysLeft(s.end_date, new Date()) : null;
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, setParams] = useSearchParams();
const { view, set, setQ, filter, reset } = useAgileView(project.label, "sprint");
return (
<div data-testid="page-sprint" className="agile agile--wide">
<PageHeader title="Спринт" actions={<RefreshButton />} />
<AgileGate project={project} kind="scrum">
{(d) => {
const selectable = selectableSprints(d.sprints);
const ids = parseSprintIds(params.get("sprint"), d.sprints);
const chosen = selectable.filter((s) => ids.includes(s.id));
const issues = sprintIssues(chosen);
const shown = issues.filter((i) => matches(i, filter));
const cols = columnsOf(d.board?.columns ?? null, shown);
const allNames = columnsOf(d.board?.columns ?? null, issues).map((c) => c.name); // меню — по задачам без фильтров (T7.2)
// выбор спринтов — в адресе, без новой записи истории
const pick = (next: number[]) => setParams((p) => { const n = new URLSearchParams(p); n.set("sprint", next.join(",")); return n; }, { replace: true });
return (
<>
<AgileToolbar filter={filter} setQ={setQ} set={set} onReset={reset} assignees={assigneeOptions(issues, view.assignee)} showHideDone>
{selectable.length > 0 && <SprintPicker sprints={selectable} selected={ids} onChange={pick} />}
{chosen.length > 0 && <ColumnsMenu names={allNames} hidden={view.hiddenColumns} onChange={(hiddenColumns) => set({ hiddenColumns })} />}
</AgileToolbar>
{chosen.length === 0 ? (
<Panel className="agile-section">
<p className="agile__empty">Нет активного спринта</p>
<Link className="agile__link" to={`/jira/backlog?project=${encodeURIComponent(project.label)}`}>Бэклог</Link>
</Panel>
) : (
<>
{chosen.map((s) => <SprintHead key={s.id} s={s} />)}
<BoardColumns columns={cols} hidden={view.hiddenColumns} />
</>
)}
</>
);
}}
</AgileGate>
</div>
);
}