From 7cc0327a1270fc417c2b2fa23966d6953142194a Mon Sep 17 00:00:00 2001 From: gnezim Date: Wed, 22 Apr 2026 13:55:53 +0300 Subject: [PATCH] Show all active transition blocks inline + gate on isActual (TIRREDESIGN-7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The inline expanded flight card used to pick one of boarding / deboarding based on the search direction and show just that block. Angular's board-flight-body renders registration, boarding and deboarding side-by-side, each gated on the API payload's isActual flag — TIRREDESIGN-7 expects the same so a flight mid-boarding can still show that its registration already finished. - FlightCard now iterates registration/boarding/deboarding and renders each row when its isActual flag is set. Gate + dispatch still come from depStation on boarding, gate + bag-belt from arrStation on deboarding. - shared.shouldShowTransition swaps 'status != Scheduled' for the isActual flag to match Angular's same-payload semantics on the details accordion. The Schedule/Cancelled short-circuits stand. - Test fixture makeFlightWithBoarding scopes its transition to the direction under test so the two blocks don't collide on testids. --- .../components/details-panels/shared.test.ts | 12 +- .../components/details-panels/shared.ts | 14 +- src/ui/flights/FlightCard.test.tsx | 28 ++- src/ui/flights/FlightCard.tsx | 173 ++++++++++-------- 4 files changed, 128 insertions(+), 99 deletions(-) diff --git a/src/features/online-board/components/details-panels/shared.test.ts b/src/features/online-board/components/details-panels/shared.test.ts index dba75926..3d9dd49e 100644 --- a/src/features/online-board/components/details-panels/shared.test.ts +++ b/src/features/online-board/components/details-panels/shared.test.ts @@ -27,12 +27,16 @@ describe("shouldShowTransition", () => { expect(shouldShowTransition(undefined, "Scheduled", "Onlineboard")).toBe(false); }); - it("returns false when item.status is Scheduled", () => { - const scheduled = { ...validItem, status: "Scheduled" as const }; - expect(shouldShowTransition(scheduled, "Scheduled", "Onlineboard")).toBe(false); + // TIRREDESIGN-7: the rule now reads the API's isActual flag rather + // than inferring from status, so a transition marked inactive hides + // even if its status has left Scheduled (e.g. a cancelled boarding + // phase that hasn't been cleared to Scheduled). + it("returns false when item.isActual is false", () => { + const inactive = { ...validItem, isActual: false }; + expect(shouldShowTransition(inactive, "Scheduled", "Onlineboard")).toBe(false); }); - it("returns true for active transition on Onlineboard", () => { + it("returns true when item.isActual is true on Onlineboard", () => { expect(shouldShowTransition(validItem, "Scheduled", "Onlineboard")).toBe(true); }); }); diff --git a/src/features/online-board/components/details-panels/shared.ts b/src/features/online-board/components/details-panels/shared.ts index da9b7a60..2701b931 100644 --- a/src/features/online-board/components/details-panels/shared.ts +++ b/src/features/online-board/components/details-panels/shared.ts @@ -8,9 +8,14 @@ import type { export type DetailsViewType = "Onlineboard" | "Schedule"; /** - * Matches Angular's `showBoardProperty` in flight-details-wrapper.component.ts. - * Transition panels are hidden for Schedule mode, Cancelled flights, - * missing data, or when the transition hasn't started. + * Matches Angular's `showBoardProperty` in flight-details-wrapper.component.ts, + * tightened for TIRREDESIGN-7 to use the payload's `isActual` flag. The + * API sets `isActual=true` precisely when a transition block is in its + * current operational phase — the backend computes that from status + + * clocked times, so consumers shouldn't rediscover it locally. + * + * Transition panels remain hidden for Schedule mode or Cancelled legs + * regardless of the flag (those contexts never show transition detail). */ export function shouldShowTransition( item: IFlightTransitionItem | undefined, @@ -20,8 +25,7 @@ export function shouldShowTransition( if (viewType === "Schedule") return false; if (legStatus === "Cancelled") return false; if (!item) return false; - if (item.status === "Scheduled") return false; - return true; + return item.isActual === true; } /** diff --git a/src/ui/flights/FlightCard.test.tsx b/src/ui/flights/FlightCard.test.tsx index adf5766e..a89e79f0 100644 --- a/src/ui/flights/FlightCard.test.tsx +++ b/src/ui/flights/FlightCard.test.tsx @@ -504,28 +504,36 @@ function makeFlightWithBoarding(opts: { gate?: string; dispatch?: string; bagBelt?: string; + /** + * Which transition phase to simulate as isActual. Both blocks now + * render side-by-side when both are active (TIRREDESIGN-7), so tests + * that assert on a single `transition-gate` testid need to scope to + * one direction at a time. + */ direction?: "departure" | "arrival"; }): ISimpleFlight { const base = makeFlight({}); const transition = makeTransitionItem(); - // `makeFlight` always returns a Direct flight so `leg` is safe to access const baseLeg = (base as ReturnType & { leg: ReturnType }).leg; + const transitionMap = + opts.direction === "arrival" + ? { deboarding: transition } + : { boarding: transition }; + const depGate = opts.direction === "arrival" ? undefined : opts.gate; + const arrGate = opts.direction === "arrival" ? opts.gate : undefined; return { ...base, leg: { ...baseLeg, - transition: { - boarding: transition, - deboarding: transition, - }, + transition: transitionMap, departure: { ...baseLeg.departure, - gate: opts.gate, + gate: depGate, dispatch: opts.dispatch, }, arrival: { ...baseLeg.arrival, - gate: opts.gate, + gate: arrGate, bagBelt: opts.bagBelt, }, }, @@ -572,8 +580,8 @@ describe("4.1.13.4 — Expanded row content (TZ §4.1.13.4.3)", () => { expect(expanded.textContent).toContain("BOARDING-STATUSES.InProgress"); }); - it("4.1.13.4-R: deboarding row shown on arrival direction", () => { - const flight = makeFlightWithBoarding({}); + it("4.1.13.4-R: deboarding row shown when deboarding transition is active (§4.1.13.4.3)", () => { + const flight = makeFlightWithBoarding({ direction: "arrival" }); render( {}} direction="arrival" />); const expanded = screen.getByTestId("flight-card-expanded"); expect(expanded.textContent).toContain("DETAILS.DEBOARDING"); @@ -620,7 +628,7 @@ describe("4.1.13.4 — Expanded row content (TZ §4.1.13.4.3)", () => { }); it("4.1.13.4-R: arrival gate rendered in deboarding row when arrival.gate is set", () => { - const flight = makeFlightWithBoarding({ gate: "C7" }); + const flight = makeFlightWithBoarding({ gate: "C7", direction: "arrival" }); render( {}} direction="arrival" />); expect(screen.getByTestId("transition-gate")).toBeTruthy(); expect(screen.getByTestId("transition-gate").textContent).toContain("C7"); diff --git a/src/ui/flights/FlightCard.tsx b/src/ui/flights/FlightCard.tsx index 16eb5428..038415be 100644 --- a/src/ui/flights/FlightCard.tsx +++ b/src/ui/flights/FlightCard.tsx @@ -214,19 +214,21 @@ export const FlightCard: FC = ({ ? "SHARED.ACTUAL" : "SHARED.EXPECTED"; - // Arrival pages show the deboarding (Высадка) transition; departure / - // route / flight-number views show boarding (Посадка). Matches Angular. - const isArrival = direction === "arrival"; - const transition = isArrival - ? arrivalLeg.transition?.deboarding - : departureLeg.transition?.boarding; - const transitionLabelKey = isArrival ? "DETAILS.DEBOARDING" : "DETAILS.BOARDING"; + // TIRREDESIGN-7 / TZ §4.1.13.4.1-3: the inline expanded panel renders + // registration (check-in), boarding and deboarding rows side-by-side, + // each visible independently when its `isActual` flag is true. Matches + // Angular's `board-flight-body` template which ngIfs on + // `transition?.X?.isActual` for all three blocks. `direction` no + // longer gates which single block wins — gate/dispatch/bag-belt + // still come from the appropriate end of the leg though. + const registration = departureLeg.transition?.registration; + const boarding = departureLeg.transition?.boarding; + const deboarding = arrivalLeg.transition?.deboarding; + const showRegistration = Boolean(registration?.isActual); + const showBoarding = Boolean(boarding?.isActual); + const showDeboarding = Boolean(deboarding?.isActual); + const anyTransition = showRegistration || showBoarding || showDeboarding; - // TZ §4.1.13.4.3: boarding block shows gate + dispatch (трап/автобус); - // deboarding block shows gate + baggage belt from arrival station. - const boardingGate = isArrival ? arrStation.gate : depStation.gate; - const boardingDispatch = isArrival ? arrStation.dispatch : depStation.dispatch; - const bagBelt = isArrival ? arrStation.bagBelt : undefined; const BOARDING_STATUS_KEY: Record = { Finished: "BOARDING-STATUSES.Finished", Expected: "BOARDING-STATUSES.Expected", @@ -235,6 +237,79 @@ export const FlightCard: FC = ({ Scheduled: "BOARDING-STATUSES.Scheduled", }; + const renderTransitionRow = ( + key: "registration" | "boarding" | "deboarding", + item: NonNullable, + ): JSX.Element => { + const labelKey = + key === "registration" + ? "DETAILS.REGISTRATION" + : key === "boarding" + ? "DETAILS.BOARDING" + : "DETAILS.DEBOARDING"; + // "Время начала" / "Время окончания" apply to all three rows — + // Angular reuses the same SHARED.BOARDING-START/END strings for + // registration rows too. + const startKey = "SHARED.BOARDING-START"; + const endKey = "SHARED.BOARDING-END"; + // Gate + dispatch surface on boarding (departure side); gate + bag + // belt surface on deboarding (arrival side). Registration has no + // gate-like affordance in the API payload. + const gate = key === "boarding" ? depStation.gate : key === "deboarding" ? arrStation.gate : undefined; + const dispatch = key === "boarding" ? depStation.dispatch : undefined; + const bagBelt = key === "deboarding" ? arrStation.bagBelt : undefined; + return ( +
+
{t(labelKey)}
+
+
+ {t("DETAILS.STATUS")} + + +
+ {item.start?.local && ( +
+ {t(startKey)} + + {formatLocalTime(item.start.local)} + +
+ )} + {item.end?.local && ( +
+ {t(endKey)} + + {formatLocalTime(item.end.local)} + +
+ )} + {gate && ( +
+ {t("DETAILS.GATE")} + {gate} +
+ )} + {dispatch && ( +
+ {t("DETAILS.DISPATCH")} + {t(`DISPATCH.${dispatch}`)} +
+ )} + {bagBelt && ( +
+ {t("DETAILS.BAG_BELT")} + {bagBelt} +
+ )} +
+
+ ); + }; + return (
= ({
- {transition && ( -
-
- {t(transitionLabelKey)} -
-
-
- - {t("DETAILS.STATUS")} - - - -
- {transition.start?.local && ( -
- - {t("SHARED.BOARDING-START")} - - - {formatLocalTime(transition.start.local)} - -
- )} - {transition.end?.local && ( -
- - {t("SHARED.BOARDING-END")} - - - {formatLocalTime(transition.end.local)} - -
- )} - {/* TZ §4.1.13.4.3: gate number (выход на посадку) when available */} - {boardingGate && ( -
- - {t("DETAILS.GATE")} - - {boardingGate} -
- )} - {/* TZ §4.1.13.4.3: dispatch type (трап/автобус) when available */} - {boardingDispatch && ( -
- - {t("DETAILS.DISPATCH")} - - - {t(`DISPATCH.${boardingDispatch}`)} - -
- )} - {/* TZ §4.1.13.4.3: baggage belt for deboarding (arrival) when available */} - {bagBelt && ( -
- - {t("DETAILS.BAG_BELT")} - - {bagBelt} -
- )} -
-
+ {anyTransition && ( + <> + {showRegistration && registration && renderTransitionRow("registration", registration)} + {showBoarding && boarding && renderTransitionRow("boarding", boarding)} + {showDeboarding && deboarding && renderTransitionRow("deboarding", deboarding)} + )} {onViewDetails && (