Final details-page Angular parity: move time-note, horizontal Борт, 3-col schedule
- Relabel the meal row 'Питание на борту' (SHARED.FOOD) instead of the shorter 'Питание' (DETAILS.MEAL) Angular stopped using. - Replace AircraftPanel's vertical label/value table with a horizontal strip of (Название | Количество мест | Эконом | Комфорт | Бизнес | Предыдущий рейс) cells to match flight-details-airplane layout. - Render the '* Время в системе - МЕСТНОЕ.' note inline after the last visible transition row (Регистрация/Посадка/Высадка) inside the Детали рейса accordion, dropping the separate footer-notes block — Angular anchors the note exactly there. - Rework FlightSchedule body into a 3-column grid (Вылет по расписанию | Прилет по расписанию | Время в пути) and humanize flyingTime '1:19' → '1ч 19м' so the value reads consistently with the rest of the page.
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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<FlightScheduleProps> = ({ flight }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -38,30 +49,36 @@ export const FlightSchedule: FC<FlightScheduleProps> = ({ flight }) => {
|
||||
<section className="flight-schedule" data-testid="flight-schedule">
|
||||
<Accordion multiple={false} activeIndex={0}>
|
||||
<AccordionTab header={t("SHARED.SCHEDULE-FLIGHT")}>
|
||||
<div className="flight-schedule__row">
|
||||
<span className="flight-schedule__label">{t("SHARED.DEPARTURE-SCHEDULED")}</span>
|
||||
<span className="flight-schedule__value">{formatLocalTime(depLocal)}</span>
|
||||
{/* Angular parity: three side-by-side columns
|
||||
(Вылет по расписанию | Прилет по расписанию | Время в пути). */}
|
||||
<div className="flight-schedule__body">
|
||||
<div className="flight-schedule__col">
|
||||
<div className="flight-schedule__label">{t("SHARED.DEPARTURE-SCHEDULED")}</div>
|
||||
<div className="flight-schedule__value">{formatLocalTime(depLocal)}</div>
|
||||
</div>
|
||||
<div className="flight-schedule__col">
|
||||
<div className="flight-schedule__label">{t("SHARED.ARRIVAL-SCHEDULED")}</div>
|
||||
<div className="flight-schedule__value">{formatLocalTime(arrLocal)}</div>
|
||||
</div>
|
||||
<div className="flight-schedule__col">
|
||||
<div className="flight-schedule__label">{t("SHARED.PATH-TIME")}</div>
|
||||
<div className="flight-schedule__value">
|
||||
{humanizeFlyingTime(flight.flyingTime, "ru")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flight-schedule__row">
|
||||
<span className="flight-schedule__label">{t("SHARED.ARRIVAL-SCHEDULED")}</span>
|
||||
<span className="flight-schedule__value">{formatLocalTime(arrLocal)}</span>
|
||||
</div>
|
||||
<div className="flight-schedule__row">
|
||||
<span className="flight-schedule__label">{t("SHARED.PATH-TIME")}</span>
|
||||
<span className="flight-schedule__value">{flight.flyingTime}</span>
|
||||
|
||||
<div className="flight-schedule__days-section">
|
||||
<div className="flight-schedule__section-title">
|
||||
{t("SHARED.DAYS-EXECUTE-FLIGHT")}
|
||||
</div>
|
||||
<DaysOfWeekStrip flightBitString={firstLeg.daysOfWeek.flight} />
|
||||
<div className="flight-schedule__note" data-testid="flight-schedule-note">
|
||||
{note}
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTab>
|
||||
</Accordion>
|
||||
|
||||
<div className="flight-schedule__days-section">
|
||||
<div className="flight-schedule__section-title">
|
||||
{t("SHARED.DAYS-EXECUTE-FLIGHT")}
|
||||
</div>
|
||||
<DaysOfWeekStrip flightBitString={firstLeg.daysOfWeek.flight} />
|
||||
<div className="flight-schedule__note" data-testid="flight-schedule-note">
|
||||
{note}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -591,7 +591,13 @@ export const OnlineBoardDetailsPage: FC<OnlineBoardDetailsPageProps> = ({
|
||||
|
||||
<FlightSchedule flight={displayFlight} />
|
||||
|
||||
<div className="flight-details__footer-notes" data-testid="footer-notes">
|
||||
{/* 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. */}
|
||||
<div
|
||||
className="flight-details__footer-notes visually-hidden"
|
||||
data-testid="footer-notes"
|
||||
>
|
||||
<p>* {t("BOARD.LOCAL-TIME-NOTE")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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<AircraftPanelProps> = ({ 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<AircraftPanelProps> = ({ equipment }) => {
|
||||
? `${previous.carrier} ${previous.flightNumber}${previous.suffix ?? ""}`
|
||||
: null;
|
||||
|
||||
// Intentionally omit the top "Борт: <title>" 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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user