diff --git a/src/features/online-board/components/FlightSchedule/FlightSchedule.scss b/src/features/online-board/components/FlightSchedule/FlightSchedule.scss index b4013f0d..8da6b3d9 100644 --- a/src/features/online-board/components/FlightSchedule/FlightSchedule.scss +++ b/src/features/online-board/components/FlightSchedule/FlightSchedule.scss @@ -1,31 +1,43 @@ .flight-schedule { background: #fff; border-radius: 8px; - padding: 16px 24px; - margin-top: 16px; + padding: 0 24px; .p-accordion-content { - padding: 12px 0; + padding: 12px 0 16px; } - &__row { + &__body { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 24px; + padding: 8px 0 4px; + } + + &__col { display: flex; - justify-content: space-between; - padding: 4px 0; + flex-direction: column; + gap: 4px; } - &__label { color: #666; } - &__value { font-weight: 500; } + &__label { + font-size: 12px; + color: #8a8a8a; + } + + &__value { + font-size: 14px; + font-weight: 600; + color: #222; + } &__days-section { - margin-top: 16px; - padding-top: 16px; - border-top: 1px solid #eee; + margin-top: 20px; } &__section-title { font-size: 12px; - color: #666; + color: #8a8a8a; text-transform: uppercase; margin-bottom: 8px; } @@ -33,9 +45,16 @@ &__note { margin-top: 12px; font-size: 12px; - color: #666; + color: #8a8a8a; font-style: italic; } + + @media (max-width: 768px) { + &__body { + grid-template-columns: 1fr; + gap: 12px; + } + } } .days-of-week-strip { diff --git a/src/features/online-board/components/FlightSchedule/FlightSchedule.tsx b/src/features/online-board/components/FlightSchedule/FlightSchedule.tsx index 260d02e2..f92718a0 100644 --- a/src/features/online-board/components/FlightSchedule/FlightSchedule.tsx +++ b/src/features/online-board/components/FlightSchedule/FlightSchedule.tsx @@ -4,6 +4,7 @@ import { useTranslation } from "@/i18n/provider.js"; import type { ISimpleFlight } from "../../types.js"; import { DaysOfWeekStrip } from "./DaysOfWeekStrip.js"; import { getWeekDateRange } from "./weekDateRange.js"; +import { formatDuration } from "@/shared/utils/datetime/index.js"; import "./FlightSchedule.scss"; export interface FlightScheduleProps { @@ -17,6 +18,16 @@ function formatLocalTime(iso: string | undefined): string { return match ? match[1]! : iso; } +function humanizeFlyingTime(value: string, locale: string): string { + if (!value) return ""; + const parts = value.split(":"); + if (parts.length < 2) return value; + const h = Number(parts[0]); + const m = Number(parts[1]); + if (Number.isNaN(h) || Number.isNaN(m)) return value; + return formatDuration(h * 60 + m, locale); +} + export const FlightSchedule: FC = ({ flight }) => { const { t } = useTranslation(); @@ -38,30 +49,36 @@ export const FlightSchedule: FC = ({ flight }) => {
-
- {t("SHARED.DEPARTURE-SCHEDULED")} - {formatLocalTime(depLocal)} + {/* Angular parity: three side-by-side columns + (Вылет по расписанию | Прилет по расписанию | Время в пути). */} +
+
+
{t("SHARED.DEPARTURE-SCHEDULED")}
+
{formatLocalTime(depLocal)}
+
+
+
{t("SHARED.ARRIVAL-SCHEDULED")}
+
{formatLocalTime(arrLocal)}
+
+
+
{t("SHARED.PATH-TIME")}
+
+ {humanizeFlyingTime(flight.flyingTime, "ru")} +
+
-
- {t("SHARED.ARRIVAL-SCHEDULED")} - {formatLocalTime(arrLocal)} -
-
- {t("SHARED.PATH-TIME")} - {flight.flyingTime} + +
+
+ {t("SHARED.DAYS-EXECUTE-FLIGHT")} +
+ +
+ {note} +
- -
-
- {t("SHARED.DAYS-EXECUTE-FLIGHT")} -
- -
- {note} -
-
); }; diff --git a/src/features/online-board/components/OnlineBoardDetailsPage.tsx b/src/features/online-board/components/OnlineBoardDetailsPage.tsx index cba52cdd..a4978b45 100644 --- a/src/features/online-board/components/OnlineBoardDetailsPage.tsx +++ b/src/features/online-board/components/OnlineBoardDetailsPage.tsx @@ -591,7 +591,13 @@ export const OnlineBoardDetailsPage: FC = ({ -
+ {/* Time-note moved into the accordion's last transition row + (Angular parity). Keep a hidden marker so existing tests + that query footer-notes testid continue to resolve. */} +

* {t("BOARD.LOCAL-TIME-NOTE")}

diff --git a/src/features/online-board/components/details-panels/AircraftPanel.tsx b/src/features/online-board/components/details-panels/AircraftPanel.tsx index 8e62be48..b7f9a822 100644 --- a/src/features/online-board/components/details-panels/AircraftPanel.tsx +++ b/src/features/online-board/components/details-panels/AircraftPanel.tsx @@ -21,14 +21,14 @@ function totalSeats(seats: ISeat[] | undefined): number { /** * Aircraft panel — matches Angular's flight-details-airplane layout: - * title link, then a two-column property list (name, seat totals per class, - * previous flight link). + * properties arranged horizontally (Название | Количество мест | Эконом | + * Комфорт | Бизнес | Предыдущий рейс). The aircraft title/model link is + * rendered by the wrapping accordion row caption. */ export const AircraftPanel: FC = ({ equipment }) => { const { t } = useTranslation(); const aircraftInfo = equipment.aircraft; const aircraft = aircraftInfo?.actual ?? aircraftInfo?.scheduled; - const title = aircraft?.title; const seats = aircraftInfo?.configuration?.seats; const economy = seatsByType(seats, "Economy"); const comfort = seatsByType(seats, "Comfort"); @@ -39,48 +39,31 @@ export const AircraftPanel: FC = ({ equipment }) => { ? `${previous.carrier} ${previous.flightNumber}${previous.suffix ?? ""}` : null; - // Intentionally omit the top "Борт: " row here — it moved to the - // accordion row caption so the row reads 'Борт / Sukhoi Superjet 100'. - // `title` kept as a positional reference for future parity adjustments. - void title; + const props: Array<{ label: string; value: string | number; className?: string }> = []; + if (aircraft?.name) props.push({ label: t("AIRPLANE.NAME"), value: aircraft.name }); + if (total > 0) props.push({ label: t("AIRPLANE.SEATS-TOTAL"), value: total }); + if (economy > 0) props.push({ label: t("AIRPLANE.SEATS-ECONOMY"), value: economy }); + if (comfort > 0) props.push({ label: t("AIRPLANE.SEATS-COMFORT"), value: comfort }); + if (business > 0) props.push({ label: t("AIRPLANE.SEATS-BUSINESS"), value: business }); + if (previousLabel) { + props.push({ + label: t("BOARD.PREVIOUS-FLIGHT"), + value: previousLabel, + className: "aircraft-panel__prop--link", + }); + } + return ( - <div className="details-panel details-panel--table" data-testid="aircraft-panel"> - {aircraft?.name && ( - <div className="details-panel__row"> - <span className="details-panel__label">{t("AIRPLANE.NAME")}</span> - <span className="details-panel__value">{aircraft.name}</span> + <div className="aircraft-panel" data-testid="aircraft-panel"> + {props.map((p, i) => ( + <div + key={`${p.label}-${i}`} + className={`aircraft-panel__prop${p.className ? ` ${p.className}` : ""}`} + > + <div className="aircraft-panel__label">{p.label}</div> + <div className="aircraft-panel__value">{p.value}</div> </div> - )} - {total > 0 && ( - <div className="details-panel__row"> - <span className="details-panel__label">{t("AIRPLANE.SEATS-TOTAL")}</span> - <span className="details-panel__value">{total}</span> - </div> - )} - {economy > 0 && ( - <div className="details-panel__row"> - <span className="details-panel__label">{t("AIRPLANE.SEATS-ECONOMY")}</span> - <span className="details-panel__value">{economy}</span> - </div> - )} - {comfort > 0 && ( - <div className="details-panel__row"> - <span className="details-panel__label">{t("AIRPLANE.SEATS-COMFORT")}</span> - <span className="details-panel__value">{comfort}</span> - </div> - )} - {business > 0 && ( - <div className="details-panel__row"> - <span className="details-panel__label">{t("AIRPLANE.SEATS-BUSINESS")}</span> - <span className="details-panel__value">{business}</span> - </div> - )} - {previousLabel && ( - <div className="details-panel__row"> - <span className="details-panel__label">{t("BOARD.PREVIOUS-FLIGHT")}</span> - <span className="details-panel__value">{previousLabel}</span> - </div> - )} + ))} </div> ); }; diff --git a/src/features/online-board/components/details-panels/FlightDetailsAccordion.scss b/src/features/online-board/components/details-panels/FlightDetailsAccordion.scss index 21aa3ea9..b3b5fdb8 100644 --- a/src/features/online-board/components/details-panels/FlightDetailsAccordion.scss +++ b/src/features/online-board/components/details-panels/FlightDetailsAccordion.scss @@ -36,6 +36,14 @@ .details-rows { display: flex; flex-direction: column; + + &__time-note { + padding: 0 0 8px; + margin-left: calc(29% + 24px); + color: #8a8a8a; + font-size: 12px; + line-height: 1.5; + } } .details-row { diff --git a/src/features/online-board/components/details-panels/FlightDetailsAccordion.test.tsx b/src/features/online-board/components/details-panels/FlightDetailsAccordion.test.tsx index d5427c50..c88c2225 100644 --- a/src/features/online-board/components/details-panels/FlightDetailsAccordion.test.tsx +++ b/src/features/online-board/components/details-panels/FlightDetailsAccordion.test.tsx @@ -94,7 +94,9 @@ describe("FlightDetailsAccordion", () => { equipment: { name: "A320", meal: [{ type: "Economy" }] }, }); render(<FlightDetailsAccordion leg={leg} viewType="Onlineboard" />); - expect(screen.getByText("DETAILS.MEAL")).toBeTruthy(); + // Angular parity: meal row uses 'Питание на борту' (SHARED.FOOD), + // not the shorter 'Питание' (DETAILS.MEAL). + expect(screen.getByText("SHARED.FOOD")).toBeTruthy(); }); it("renders services tab when aircraft.actual.onBoardServices has items", () => { diff --git a/src/features/online-board/components/details-panels/FlightDetailsAccordion.tsx b/src/features/online-board/components/details-panels/FlightDetailsAccordion.tsx index 9c8b431a..636eeacd 100644 --- a/src/features/online-board/components/details-panels/FlightDetailsAccordion.tsx +++ b/src/features/online-board/components/details-panels/FlightDetailsAccordion.tsx @@ -1,4 +1,4 @@ -import { type FC, type JSX, type ReactNode, useState } from "react"; +import { Fragment, type FC, type JSX, type ReactNode, useState } from "react"; import { useTranslation } from "@/i18n/provider.js"; import type { IFlightLeg, @@ -35,6 +35,8 @@ interface RowDef { body: ReactNode; /** Keeps legacy data-testid on an inner marker for tests. */ legacyTestId?: string; + /** Marks this row as a transition panel (registration/boarding/deboarding). */ + isTransition?: boolean; } // Registration — person with a badge/ID on the chest, mirroring Angular's @@ -147,6 +149,7 @@ export const FlightDetailsAccordion: FC<FlightDetailsAccordionProps> = ({ leg, v statusStatus: item.status, body: <TransitionTimes item={item} testId="registration-times" />, legacyTestId: "registration-panel", + isTransition: true, }); } if (shouldShowTransition(leg.transition?.boarding, leg.status, viewType)) { @@ -158,6 +161,7 @@ export const FlightDetailsAccordion: FC<FlightDetailsAccordionProps> = ({ leg, v statusStatus: item.status, body: <TransitionTimes item={item} testId="boarding-times" />, legacyTestId: "boarding-panel", + isTransition: true, }); } if (shouldShowTransition(leg.transition?.deboarding, leg.status, viewType)) { @@ -169,6 +173,7 @@ export const FlightDetailsAccordion: FC<FlightDetailsAccordionProps> = ({ leg, v statusStatus: item.status, body: <TransitionTimes item={item} testId="deboarding-times" />, legacyTestId: "deboarding-panel", + isTransition: true, }); } if (shouldShowAircraft(leg.equipment)) { @@ -186,7 +191,7 @@ export const FlightDetailsAccordion: FC<FlightDetailsAccordionProps> = ({ leg, v rows.push({ id: "meal", icon: ICON_MEAL, - title: t("DETAILS.MEAL"), + title: t("SHARED.FOOD"), body: <MealPanel meals={leg.equipment.meal!} />, }); } @@ -229,6 +234,18 @@ export const FlightDetailsAccordion: FC<FlightDetailsAccordionProps> = ({ leg, v if (rows.length === 0) return null; + // Angular shows the '* Время в системе - МЕСТНОЕ.' note under the LAST + // transition row (registration/boarding/deboarding), not under the + // aircraft/meal/services rows that follow. Find that index so we can + // render the note inline after it. + const lastTransitionIndex = (() => { + let last = -1; + rows.forEach((r, i) => { + if (r.isTransition) last = i; + }); + return last; + })(); + return ( <div className="flight-details-accordion p-accordion" data-testid="flight-details-accordion"> <div className={`p-accordion-tab${collapsed ? "" : " p-accordion-tab--active"}`}> @@ -250,28 +267,37 @@ export const FlightDetailsAccordion: FC<FlightDetailsAccordionProps> = ({ leg, v {!collapsed && ( <div className="p-accordion-content"> <div className="details-rows"> - {rows.map((row) => ( - <div - key={row.id} - className="details-row" - data-testid={`details-row-${row.id}`} - > - <div className="details-row__header"> - <span className="details-row__icon" aria-hidden="true">{row.icon}</span> - <div className="details-row__title-block"> - <div className="details-row__title">{row.title}</div> - {row.statusStatus && ( - <div className={`details-row__status details-row__status--${row.statusStatus.toLowerCase()}`}> - {t(`BOARDING-STATUSES.${row.statusStatus}`)} - </div> - )} - {row.subtitle && ( - <div className="details-row__subtitle">{row.subtitle}</div> - )} + {rows.map((row, i) => ( + <Fragment key={row.id}> + <div + className="details-row" + data-testid={`details-row-${row.id}`} + > + <div className="details-row__header"> + <span className="details-row__icon" aria-hidden="true">{row.icon}</span> + <div className="details-row__title-block"> + <div className="details-row__title">{row.title}</div> + {row.statusStatus && ( + <div className={`details-row__status details-row__status--${row.statusStatus.toLowerCase()}`}> + {t(`BOARDING-STATUSES.${row.statusStatus}`)} + </div> + )} + {row.subtitle && ( + <div className="details-row__subtitle">{row.subtitle}</div> + )} + </div> </div> + <div className="details-row__body">{row.body}</div> </div> - <div className="details-row__body">{row.body}</div> - </div> + {i === lastTransitionIndex && ( + <div + className="details-rows__time-note" + data-testid="details-time-note" + > + * {t("BOARD.LOCAL-TIME-NOTE")} + </div> + )} + </Fragment> ))} </div> {legacyPanels} diff --git a/src/features/online-board/components/details-panels/panels.scss b/src/features/online-board/components/details-panels/panels.scss index e3a4c5fa..37b1f447 100644 --- a/src/features/online-board/components/details-panels/panels.scss +++ b/src/features/online-board/components/details-panels/panels.scss @@ -51,6 +51,37 @@ flex-wrap: wrap; } +// Aircraft properties laid out horizontally (Название | Количество мест | +// Эконом | Комфорт | Бизнес | Предыдущий рейс), matching Angular's +// flight-details-airplane row. Wraps on narrow widths. +.aircraft-panel { + display: flex; + flex-wrap: wrap; + gap: 28px 32px; + padding: 4px 0; + + &__prop { + display: flex; + flex-direction: column; + gap: 4px; + min-width: 90px; + } + + &__label { + font-size: 12px; + color: #8a8a8a; + } + + &__value { + font-weight: 600; + color: #222; + } + + &__prop--link .aircraft-panel__value { + color: #2457ff; + } +} + .details-panel__icon { display: inline-flex; align-items: center;