pcp-tgu интерфейс
This commit is contained in:
@@ -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} мин`;
|
||||
}
|
||||
Reference in New Issue
Block a user