103 lines
3.5 KiB
TypeScript
103 lines
3.5 KiB
TypeScript
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} мин`;
|
||
}
|