151 lines
4.8 KiB
TypeScript
151 lines
4.8 KiB
TypeScript
import { CheckCircle2, AlertTriangle, Minus } from "lucide-react";
|
|
|
|
/**
|
|
* One 999 ACK segment status row. The 999 spec labels each transaction
|
|
* set with a 3-char code:
|
|
*
|
|
* "A" = Accepted
|
|
* "E" = Accepted, but errors are noted (one or more segments had
|
|
* errors; the transaction set as a whole was accepted)
|
|
* "R" = Rejected
|
|
* "P" = Partially accepted (mixed — some segments accepted, some
|
|
* not; a degraded success the operator must investigate)
|
|
*
|
|
* The wire shape's `rawJson` (set by the parser, see backend
|
|
* `cyclone.parsers.parsers_999`) carries an array of segment rows.
|
|
* Until that array is parsed into a stable UI shape, we render the
|
|
* rolled-up counts on the ack row (`acceptedCount` / `rejectedCount`)
|
|
* and a placeholder explaining how to read it.
|
|
*/
|
|
interface SegmentRow {
|
|
/** Loop / segment reference like "ST*999*0001" or "AK2*HC*0001". */
|
|
reference?: string;
|
|
/** "A" | "E" | "R" | "P". */
|
|
status: "A" | "E" | "R" | "P";
|
|
/** Free-form note from the parser (e.g. "SVC*HC:9450 missing"). */
|
|
note?: string;
|
|
}
|
|
|
|
interface Props {
|
|
segments: SegmentRow[];
|
|
}
|
|
|
|
/**
|
|
* Pill for one segment status. Color mirrors the badge palette used on
|
|
* the Acks page (`Acks.tsx`) so a status reads the same in both
|
|
* surfaces.
|
|
*/
|
|
function StatusPill({ status }: { status: SegmentRow["status"] }) {
|
|
const cfg = {
|
|
A: {
|
|
fg: "hsl(152 64% 30%)",
|
|
bg: "hsl(152 50% 88%)",
|
|
border: "hsl(152 64% 38% / 0.30)",
|
|
label: "Accepted",
|
|
Icon: CheckCircle2,
|
|
},
|
|
E: {
|
|
fg: "hsl(36 92% 30%)",
|
|
bg: "hsl(36 82% 88%)",
|
|
border: "hsl(36 92% 50% / 0.30)",
|
|
label: "Accepted w/ errors",
|
|
Icon: AlertTriangle,
|
|
},
|
|
R: {
|
|
fg: "hsl(358 70% 36%)",
|
|
bg: "hsl(358 70% 92%)",
|
|
border: "hsl(358 70% 50% / 0.30)",
|
|
label: "Rejected",
|
|
Icon: AlertTriangle,
|
|
},
|
|
P: {
|
|
fg: "hsl(36 92% 30%)",
|
|
bg: "hsl(36 82% 88%)",
|
|
border: "hsl(36 92% 50% / 0.30)",
|
|
label: "Partially accepted",
|
|
Icon: Minus,
|
|
},
|
|
}[status];
|
|
|
|
const Icon = cfg.Icon;
|
|
return (
|
|
<span
|
|
className="inline-flex items-center gap-1.5 rounded-sm border px-2 py-0.5 mono text-[10.5px] font-semibold uppercase tracking-[0.14em]"
|
|
style={{ color: cfg.fg, backgroundColor: cfg.bg, borderColor: cfg.border }}
|
|
>
|
|
<Icon className="h-3 w-3" strokeWidth={1.75} aria-hidden />
|
|
{cfg.label}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Per-segment status list inside the AckDrawer body (SP21 Phase 5
|
|
* Task 5.2). Renders one row per parsed 999 segment, each with the
|
|
* segment reference (when present), the parsed status code, and the
|
|
* parser's note (when present). An empty list renders a small
|
|
* explanatory note so the drawer doesn't look broken on old acks
|
|
* without the per-segment slice.
|
|
*/
|
|
export function SegmentStatusList({ segments }: Props) {
|
|
return (
|
|
<section
|
|
className="flex flex-col gap-3 px-6 py-4"
|
|
data-testid="ack-segment-list"
|
|
>
|
|
<div className="flex items-baseline justify-between gap-3">
|
|
<span className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
|
|
Segment status
|
|
</span>
|
|
<span
|
|
className="mono text-[10.5px] text-muted-foreground"
|
|
data-testid="ack-segment-count"
|
|
>
|
|
{segments.length} segment{segments.length === 1 ? "" : "s"}
|
|
</span>
|
|
</div>
|
|
{segments.length === 0 ? (
|
|
<p
|
|
className="text-[12.5px] text-muted-foreground"
|
|
data-testid="ack-segment-empty"
|
|
>
|
|
No per-segment breakdown for this ack — the rolled-up accepted
|
|
/ rejected counts above are the only signal.
|
|
</p>
|
|
) : (
|
|
<ul className="flex flex-col divide-y divide-border/40 border-y border-border/30">
|
|
{segments.map((seg, idx) => (
|
|
<li
|
|
key={`${seg.reference ?? idx}`}
|
|
className="flex items-start gap-3 py-2.5"
|
|
data-testid="ack-segment-row"
|
|
>
|
|
<span
|
|
className="mono text-[12px] tabular-nums text-muted-foreground shrink-0"
|
|
style={{ minWidth: 32 }}
|
|
>
|
|
{idx + 1}
|
|
</span>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<StatusPill status={seg.status} />
|
|
{seg.reference ? (
|
|
<span className="mono text-[12px] truncate">
|
|
{seg.reference}
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
{seg.note ? (
|
|
<p className="text-[12.5px] text-muted-foreground mt-1 leading-snug">
|
|
{seg.note}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|