pcp-tgu интерфейс

This commit is contained in:
Дмитрий Соловьев
2026-05-29 16:09:36 +03:00
parent 913b3d7c9b
commit 9fca4d4051
34 changed files with 3163 additions and 0 deletions
@@ -0,0 +1,102 @@
import type { TguPlanDecision } from "../../model/tguTypes";
import type { TguPlanUi } from "../../model/timelineTypes";
import { platformLabel } from "./tguTimelineMapper";
import { statusStyle } from "./tguStatus";
type TguPlanDetailsProps = {
plan?: TguPlanUi;
decisionInFlight: boolean;
notice?: string;
error?: string;
onDecision: (planId: string, decision: TguPlanDecision, reason?: string) => void;
onClose: () => void;
};
export function TguPlanDetails({ plan, decisionInFlight, notice, error, onDecision, onClose }: TguPlanDetailsProps) {
if (!plan) {
return (
<aside className="tgu-details tgu-details--empty">
<div className="tgu-details__placeholder">
<span className="tgu-details__placeholder-icon" aria-hidden="true" />
<span>Выберите plan bar на timeline.</span>
</div>
</aside>
);
}
const style = statusStyle(plan.status);
const duration = formatDuration(plan.endMs - plan.startMs);
const reject = () => {
const reason = window.prompt("Reason для REJECTED", "UI_TEST_REJECTED");
if (reason === null) return;
onDecision(plan.planId, "REJECTED", reason);
};
return (
<aside className="tgu-details">
<div className="tgu-details__header">
<div>
<div className="tgu-details__eyebrow">Выбранный план</div>
<h2>{plan.planId}</h2>
</div>
<button className="tgu-icon-button" type="button" onClick={onClose} aria-label="Закрыть детали">
x
</button>
</div>
<div className={`tgu-status-pill ${style.className}`}>{style.label}</div>
{notice && <div className="tgu-alert tgu-alert--success">{notice}</div>}
{error && <div className="tgu-alert tgu-alert--danger">{error}</div>}
<dl className="tgu-details-grid">
<Detail label="planId" value={plan.planId} mono />
<Detail label="spacecraftId" value={plan.spacecraftId} mono />
<Detail label="КА" value={platformLabel(plan.platform, plan.spacecraftId)} />
<Detail label="startTime" value={formatDateTime(plan.startTime)} mono />
<Detail label="endTime" value={formatDateTime(plan.endTime)} mono />
<Detail label="duration" value={duration} mono />
<Detail label="kppId" value={plan.kppId} mono />
<Detail label="status" value={plan.status} mono />
</dl>
{plan.status === "WAITING_DECISION" && (
<div className="tgu-details__actions">
<button
className="tgu-button tgu-button--accept"
type="button"
disabled={decisionInFlight}
onClick={() => onDecision(plan.planId, "ACCEPTED", "UI_TEST_ACCEPTED")}
>
Принять
</button>
<button className="tgu-button tgu-button--reject" type="button" disabled={decisionInFlight} onClick={reject}>
Отклонить
</button>
</div>
)}
</aside>
);
}
function Detail({ label, value, mono }: { label: string; value?: string; mono?: boolean }) {
return (
<div>
<dt>{label}</dt>
<dd className={mono ? "mono" : undefined}>{value || "-"}</dd>
</div>
);
}
function formatDateTime(value: string): string {
return new Date(value).toLocaleString("ru-RU");
}
function formatDuration(ms: number): string {
const minutes = Math.max(0, Math.round(ms / 60_000));
const hours = Math.floor(minutes / 60);
const restMinutes = minutes % 60;
if (hours === 0) return `${restMinutes} мин`;
return `${hours} ч ${restMinutes} мин`;
}