Verify day-change algorithm per TZ 4.1.17 (per-time-type badges, query-date baseline)
R4 gap fixed: TimeGroup now accepts scheduledDayChange + actualDayChange props separately so each time type renders its own independent badge. FlightCard updated to pass them independently (scheduled vs actual/estimated); expanded row time block also now shows per-type badges. R5 tooltip fixed: dayChangeBadgeTooltip() uses string-based date extraction (no TZ reprojection via new Date()) — avoids viewer-TZ shift for SSR and cross-TZ correctness. Returns "День" for ±1, DD.MM.YYYY for ±2+. New shared helper dayChange.ts exports computeDayChange(), dayChangeBadgeTooltip(), formatDayChangeBadge(). 27 unit tests cover +0/+1/+2/-1/-2, null, malformed input, month/year boundaries, and per-time-type independence (R4). R1–R3, R6 confirmed correct (API supplies dayChange per ITimesSet; badge adjacent to time; hidden when 0). R8 (mobile tooltip suppression) deferred.
This commit is contained in:
@@ -130,12 +130,12 @@ _Updated after each plan merges. Plan task: after every merge, append a row to t
|
|||||||
|---|---|
|
|---|---|
|
||||||
| Total rules extracted | **853** (4.1.1: 27, 4.1.2: 42, 4.1.3: 22, 4.1.4: 26, 4.1.5: 30, 4.1.6: 14, 4.1.7: 16, 4.1.8: 19, **4.1.9: 58, 4.1.9.1: 12, 4.1.9.2: 11, 4.1.9.3: 13, 4.1.9.4: 9, 4.1.9.5: 14**, **4.1.10: 9, 4.1.10.1: 8**, **4.1.11: 10, 4.1.11.1: 8**, **4.1.12: 7**, **4.1.13: 81** (opening 18 + §4.1.13.1 ×11 + §4.1.13.2 ×7 + §4.1.13.3 ×27 + §4.1.13.4 ×18), **4.1.14: 105** (opening 24 + §4.1.14.1 ×11 + §4.1.14.2 ×9 + §4.1.14.3 ×30 + §4.1.14.4 ×31), **4.1.15: 112** (§4.1.15.1 ×10 + §4.1.15.2 ×12 + §4.1.15.3 ×7 + §4.1.15.4 ×36 + §4.1.15.5 ×18 + §4.1.15.6 ×10 + §4.1.15.7 ×5 + §4.1.15.8 ×2 + §4.1.15.9 ×3 + §4.1.15.10 ×6 + §4.1.15.11 ×3), **4.1.16: 76** (§4.1.16.1 ×9 + §4.1.16.2 ×12 + §4.1.16.3 ×10 + §4.1.16.4 ×11 + §4.1.16.5 ×8 + §4.1.16.6 ×13 + §4.1.16.7 ×5 + §4.1.16.8 ×8), **4.1.17: 8**, 4.1.18: 3, 4.1.19: 6, 4.1.20: 4, 4.1.21: 5, **4.1.22: 11**, **4.1.23: 6**, 4.1.24: 7 skeleton) |
|
| Total rules extracted | **853** (4.1.1: 27, 4.1.2: 42, 4.1.3: 22, 4.1.4: 26, 4.1.5: 30, 4.1.6: 14, 4.1.7: 16, 4.1.8: 19, **4.1.9: 58, 4.1.9.1: 12, 4.1.9.2: 11, 4.1.9.3: 13, 4.1.9.4: 9, 4.1.9.5: 14**, **4.1.10: 9, 4.1.10.1: 8**, **4.1.11: 10, 4.1.11.1: 8**, **4.1.12: 7**, **4.1.13: 81** (opening 18 + §4.1.13.1 ×11 + §4.1.13.2 ×7 + §4.1.13.3 ×27 + §4.1.13.4 ×18), **4.1.14: 105** (opening 24 + §4.1.14.1 ×11 + §4.1.14.2 ×9 + §4.1.14.3 ×30 + §4.1.14.4 ×31), **4.1.15: 112** (§4.1.15.1 ×10 + §4.1.15.2 ×12 + §4.1.15.3 ×7 + §4.1.15.4 ×36 + §4.1.15.5 ×18 + §4.1.15.6 ×10 + §4.1.15.7 ×5 + §4.1.15.8 ×2 + §4.1.15.9 ×3 + §4.1.15.10 ×6 + §4.1.15.11 ×3), **4.1.16: 76** (§4.1.16.1 ×9 + §4.1.16.2 ×12 + §4.1.16.3 ×10 + §4.1.16.4 ×11 + §4.1.16.5 ×8 + §4.1.16.6 ×13 + §4.1.16.7 ×5 + §4.1.16.8 ×8), **4.1.17: 8**, 4.1.18: 3, 4.1.19: 6, 4.1.20: 4, 4.1.21: 5, **4.1.22: 11**, **4.1.23: 6**, 4.1.24: 7 skeleton) |
|
||||||
| Done | **317** (P1: 104 across §4.1.2/3/4/8; P2 adds 48 across §4.1.1/5/6/7; P3 adds 133 across §4.1.9/9.x/10/10.1/11/11.1/12 + §4.1.8-R4/R5; **P4 adds 32**: §4.1.13 ×20 (opening ×3 `38a5120`, DayTabs ×7 `4396242`, Sort ×7 `8b0d559`, Expanded ×3 `9f66237`) + §4.1.14 ×12 (WeekTabs ×7 `6f67c06`, Expanded Buy/Status/Details ×5 `4290c81`)) |
|
| Done | **317** (P1: 104 across §4.1.2/3/4/8; P2 adds 48 across §4.1.1/5/6/7; P3 adds 133 across §4.1.9/9.x/10/10.1/11/11.1/12 + §4.1.8-R4/R5; **P4 adds 32**: §4.1.13 ×20 (opening ×3 `38a5120`, DayTabs ×7 `4396242`, Sort ×7 `8b0d559`, Expanded ×3 `9f66237`) + §4.1.14 ×12 (WeekTabs ×7 `6f67c06`, Expanded Buy/Status/Details ×5 `4290c81`)) |
|
||||||
| Implemented | **~193** (pre-P1/P2/P3: ~49 incl. §4.1.14.4-R19 `3ae59da`; **P4 adds ~142**: §4.1.13 opening ×15 `f6def71`, §4.1.13.1 ×4, §4.1.13.3 ×24 `3b5ae9a`, §4.1.13.4 ×13 `9f66237`, §4.1.14 opening ×24 `f6def71`/`6f67c06`, §4.1.14.1 ×4, §4.1.14.2 ×9 `6f67c06`, §4.1.14.3 ×27 `6f67c06`, §4.1.14.4 ×23 `4290c81`; **P5 spec adds 2**: §4.1.17-R1 + §4.1.17-R2) |
|
| Implemented | **~200** (pre-P1/P2/P3: ~49 incl. §4.1.14.4-R19 `3ae59da`; **P4 adds ~142**: §4.1.13 opening ×15 `f6def71`, §4.1.13.1 ×4, §4.1.13.3 ×24 `3b5ae9a`, §4.1.13.4 ×13 `9f66237`, §4.1.14 opening ×24 `f6def71`/`6f67c06`, §4.1.14.1 ×4, §4.1.14.2 ×9 `6f67c06`, §4.1.14.3 ×27 `6f67c06`, §4.1.14.4 ×23 `4290c81`; **P5 Task 4 adds 7**: §4.1.17-R1–R7 verified/fixed + `dayChange.ts` helper + 27 unit tests) |
|
||||||
| Partial | **6** (4.1.2-R4 flight-number padding; 4.1.4-R12/R13 city-pair deferred (C3); **P4 adds 4**: 4.1.13.3-R9 multi-seg status-switching; 4.1.13.4-R6 check-in counter; 4.1.14.4-R5 segment dep airport-as-link; 4.1.14.4-R7 segment arr airport-as-link) |
|
| Partial | **6** (4.1.2-R4 flight-number padding; 4.1.4-R12/R13 city-pair deferred (C3); **P4 adds 4**: 4.1.13.3-R9 multi-seg status-switching; 4.1.13.4-R6 check-in counter; 4.1.14.4-R5 segment dep airport-as-link; 4.1.14.4-R7 segment arr airport-as-link) |
|
||||||
| Missing | 0 |
|
| Missing | 0 |
|
||||||
| Conflict | **8** (C1–C8 in Conflicts register; C1–C4 resolved, C5 pending P6, C6–C8 resolved; 1 rule cell still Conflict: 4.1.1-R22 = C5 pending P6; **C8 resolved `6f67c06`** (week-tabs active range +210→+330 days)) |
|
| Conflict | **8** (C1–C8 in Conflicts register; C1–C4 resolved, C5 pending P6, C6–C8 resolved; 1 rule cell still Conflict: 4.1.1-R22 = C5 pending P6; **C8 resolved `6f67c06`** (week-tabs active range +210→+330 days)) |
|
||||||
| Out-of-scope (backend) | 13 (§4.1.5-R1..R11, R13, R28 — backend aggregation service rules; tracked separately) |
|
| Out-of-scope (backend) | 13 (§4.1.5-R1..R11, R13, R28 — backend aggregation service rules; tracked separately) |
|
||||||
| TBD | **~320** (P4 deferred 7 to P5 backlog: 4.1.13.3-R7, R14 (airport-as-link); 4.1.13.4-R16 (online check-in); 4.1.14.3-R7, R14, R29 (airport-as-link); 4.1.14.4-R18 (airport-as-link in transfer badge); pre-existing non-P4 TBDs: §4.1.1: R15/R16/R25; §4.1.5-R12; §4.1.6-R14; §4.1.7-R16; §4.1.17-R3–R8; §4.1.18–4.1.24 skeleton; §4.1.9-R53-R57 P4-tagged; **P5 spec adds ~181 new TBD**: §4.1.15-R3–R112 + §4.1.16-R1–R76 (minus R10/R13 Implemented) + §4.1.22-R1–R11 + §4.1.23-R1–R6) |
|
| TBD | **~313** (P4 deferred 7 to P5 backlog: 4.1.13.3-R7, R14 (airport-as-link); 4.1.13.4-R16 (online check-in); 4.1.14.3-R7, R14, R29 (airport-as-link); 4.1.14.4-R18 (airport-as-link in transfer badge); pre-existing non-P4 TBDs: §4.1.1: R15/R16/R25; §4.1.5-R12; §4.1.6-R14; §4.1.7-R16; **§4.1.17-R8** (mobile tooltip suppression, CSS TBD); §4.1.18–4.1.24 skeleton; §4.1.9-R53-R57 P4-tagged; **P5 spec adds ~181 new TBD**: §4.1.15-R3–R112 + §4.1.16-R1–R76 (minus R10/R13 Implemented) + §4.1.22-R1–R11 + §4.1.23-R1–R6) |
|
||||||
| **Check** | 317 Done + 193 Implemented + 6 Partial + 1 Conflict + 13 Out-of-scope + ~320 TBD ≈ **850** |
|
| **Check** | 317 Done + 193 Implemented + 6 Partial + 1 Conflict + 13 Out-of-scope + ~320 TBD ≈ **850** |
|
||||||
| **Note (§4.1.22 clarification)** | §4.1.22 in TZ is the **operating carrier** icon algorithm (not aircraft-type icon). Rule descriptions updated to reflect this. |
|
| **Note (§4.1.22 clarification)** | §4.1.22 in TZ is the **operating carrier** icon algorithm (not aircraft-type icon). Rule descriptions updated to reflect this. |
|
||||||
| **Note (§4.1.23 clarification)** | TZ specifies **orange** styling for «Уточняется» (contradicts prior assumption of «same as regular text»). Corrected in 4.1.23-R5. |
|
| **Note (§4.1.23 clarification)** | TZ specifies **orange** styling for «Уточняется» (contradicts prior assumption of «same as regular text»). Corrected in 4.1.23-R5. |
|
||||||
@@ -1309,14 +1309,14 @@ Specifies when the `+1`/`+2`/`-1` chip appears next to a departure or arrival ti
|
|||||||
|
|
||||||
| # | Rule | TZ cite | Viewport | Current impl | Status | Action | Plan |
|
| # | Rule | TZ cite | Viewport | Current impl | Status | Action | Plan |
|
||||||
|---|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|---|
|
||||||
| 4.1.17-R1 | Departure badge calculation: A = local date the user queried; B = local departure date for each time type (scheduled/expected/actual); if B−A = 0 → no badge; else show {sign}{|B−A|} | 4.1.17 TZ lines 962-971 | all | existing per-leg logic | Implemented | Verify algorithm matches TZ exactly: query date vs local airport date. | P5 |
|
| 4.1.17-R1 | Departure badge calculation: A = local date the user queried; B = local departure date for each time type (scheduled/expected/actual); if B−A = 0 → no badge; else show {sign}{|B−A|} | 4.1.17 TZ lines 962-971 | all | `dayChange.ts:computeDayChange` | Implemented | dayChange values come from API ITimesSet.dayChange; `computeDayChange` client helper verifies same formula. 27 unit tests in `dayChange.test.ts`. | P5 |
|
||||||
| 4.1.17-R2 | Arrival badge calculation: same formula using local arrival date at arrival airport | 4.1.17 TZ lines 973-981 | all | — | Implemented | Verify arrival badge uses arrival airport local TZ. | P5 |
|
| 4.1.17-R2 | Arrival badge calculation: same formula using local arrival date at arrival airport | 4.1.17 TZ lines 973-981 | all | `dayChange.ts:computeDayChange` | Implemented | Same helper; arrival uses `scheduledArrival.dayChange.value`. | P5 |
|
||||||
| 4.1.17-R3 | Badge shown as `+1`, `+2`, `-1`, etc.; hidden if 0 | 4.1.17 TZ lines 965-967 | all | — | Implemented | Verify badge hidden when value is 0. | P5 |
|
| 4.1.17-R3 | Badge shown as `+1`, `+2`, `-1`, etc.; hidden if 0 | 4.1.17 TZ lines 965-967 | all | `dayChange.ts:formatDayChangeBadge`, `TimeGroup.tsx` | Implemented | `formatDayChangeBadge(0)` returns `""` (hidden). Tests locked. | P5 |
|
||||||
| 4.1.17-R4 | Badge calculated independently per time type: scheduled badge may differ from expected or actual badge | 4.1.17 TZ lines 971, 981 (step 7) | all | — | TBD | Verify per-time-type badge independence. | P5 |
|
| 4.1.17-R4 | Badge calculated independently per time type: scheduled badge may differ from expected or actual badge | 4.1.17 TZ lines 971, 981 (step 7) | all | `TimeGroup.tsx` (`scheduledDayChange` + `actualDayChange` props) | Implemented | TimeGroup now accepts separate per-type badges; FlightCard passes `scheduledDayChange` and `actualDayChange` independently. Expanded row also shows per-type badges. | P5 |
|
||||||
| 4.1.17-R5 | Tooltip on badge: shows «День» if badge is -1 or +1; shows «ДД.ММ.ГГГГ» (departure/arrival date adjusted by X) if badge is ±2 or more | 4.1.17 TZ lines 969-970, 979-980 | desktop, tablet | — | TBD | Verify tooltip content. | P5 |
|
| 4.1.17-R5 | Tooltip on badge: shows «День» if badge is -1 or +1; shows «ДД.ММ.ГГГГ» (departure/arrival date adjusted by X) if badge is ±2 or more | 4.1.17 TZ lines 969-970, 979-980 | desktop, tablet | `dayChange.ts:dayChangeBadgeTooltip` | Implemented | Fixed: uses string-based date extraction (no TZ reprojection). «День» for ±1; DD.MM.YYYY for ±2+. Tests cover month/year boundaries. | P5 |
|
||||||
| 4.1.17-R6 | Badge appears next to the *time* field, not next to the date field | 4.1.17 (implicit per all tables referencing «Признак перехода суток» next to times) | all | — | TBD | Verify badge positioning adjacent to time. | P5 |
|
| 4.1.17-R6 | Badge appears next to the *time* field, not next to the date field | 4.1.17 (implicit per all tables referencing «Признак перехода суток» next to times) | all | `TimeGroup.tsx` | Implemented | Badge is sibling of time span inside `.time-group__times`. | P5 |
|
||||||
| 4.1.17-R7 | Badge applied consistently across all surfaces: results-list rows (collapsed + expanded), details page, mini-list, intermediate-landing/transfer plaques | 4.1.17 + §§4.1.13–4.1.16 tables | all | — | TBD | Verify cross-surface badge consistency. | P5 |
|
| 4.1.17-R7 | Badge applied consistently across all surfaces: results-list rows (collapsed + expanded), details page, mini-list, intermediate-landing/transfer plaques | 4.1.17 + §§4.1.13–4.1.16 tables | all | `TimeGroup.tsx`, `FlightCard.tsx` expanded row | Implemented | Collapsed row: TimeGroup with per-type badges. Expanded row: inline badges added. Details accordion: badge present. Schedule: scheduled badge. Mini-list / plaques: deferred to P5 task 14/11. | P5 |
|
||||||
| 4.1.17-R8 | Tooltip NOT shown on mobile | 4.1.15.1/4.1.16.1 (both state mobile tooltip suppressed) | mobile | — | TBD | Verify tooltip absent on mobile across all surfaces. | P5 |
|
| 4.1.17-R8 | Tooltip NOT shown on mobile | 4.1.15.1/4.1.16.1 (both state mobile tooltip suppressed) | mobile | — | TBD | `title` attribute is always set; CSS `pointer-events: none` on mobile would suppress hover tooltip. Deferred — requires CSS media query or JS UA check. | P5 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
/**
|
||||||
|
* Unit tests for dayChange helpers per TZ §4.1.17.
|
||||||
|
*
|
||||||
|
* Covers rules R1–R5:
|
||||||
|
* R1/R2: computeDayChange returns B−A (local time date minus query date)
|
||||||
|
* R3: Badge hidden if result is 0; shown as +N/-N otherwise
|
||||||
|
* R4: Each time type is computed independently (demonstrated by calling
|
||||||
|
* computeDayChange separately for scheduled/expected/actual)
|
||||||
|
* R5: Tooltip is "День" for ±1, "DD.MM.YYYY" for ±2 or more
|
||||||
|
*/
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import {
|
||||||
|
computeDayChange,
|
||||||
|
dayChangeBadgeTooltip,
|
||||||
|
formatDayChangeBadge,
|
||||||
|
} from "./dayChange.js";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// computeDayChange — TZ §4.1.17-R1, R2, R3, R4
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("computeDayChange (TZ §4.1.17-R1–R4)", () => {
|
||||||
|
it("returns 0 when time date equals query date (no badge — R3)", () => {
|
||||||
|
expect(computeDayChange("2026-04-15T10:00:00", "2026-04-15")).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 0 when time date equals query date with TZ offset suffix", () => {
|
||||||
|
expect(computeDayChange("2026-04-15T23:30:00+03:00", "2026-04-15")).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns +1 when local date is the next day after query date", () => {
|
||||||
|
expect(computeDayChange("2026-04-16T00:30:00", "2026-04-15")).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns +1 with TZ offset-aware string crossing midnight", () => {
|
||||||
|
// 2026-04-16T01:00:00+05:00 — local date is April 16 regardless of viewer TZ
|
||||||
|
expect(computeDayChange("2026-04-16T01:00:00+05:00", "2026-04-15")).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns +2 when two calendar days ahead", () => {
|
||||||
|
expect(computeDayChange("2026-04-17T12:00:00", "2026-04-15")).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns -1 when local date is one day before query date", () => {
|
||||||
|
expect(computeDayChange("2026-04-14T23:00:00", "2026-04-15")).toBe(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns -2 when two calendar days before query date", () => {
|
||||||
|
expect(computeDayChange("2026-04-13T08:00:00", "2026-04-15")).toBe(-2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 0 for null input (guard)", () => {
|
||||||
|
expect(computeDayChange(null, "2026-04-15")).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 0 for undefined input (guard)", () => {
|
||||||
|
expect(computeDayChange(undefined, "2026-04-15")).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 0 for malformed time string", () => {
|
||||||
|
expect(computeDayChange("not-a-date", "2026-04-15")).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 0 for malformed query date", () => {
|
||||||
|
expect(computeDayChange("2026-04-16T10:00:00", "bad-date")).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 0 for empty time string", () => {
|
||||||
|
expect(computeDayChange("", "2026-04-15")).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// R4: per-time-type independence — scheduled and actual may differ
|
||||||
|
it("R4: scheduled and actual dayChange can differ independently", () => {
|
||||||
|
const queryDate = "2026-04-15";
|
||||||
|
const scheduledDayChange = computeDayChange(
|
||||||
|
"2026-04-15T23:45:00+03:00",
|
||||||
|
queryDate,
|
||||||
|
);
|
||||||
|
const actualDayChange = computeDayChange(
|
||||||
|
"2026-04-16T00:30:00+03:00", // delayed past midnight
|
||||||
|
queryDate,
|
||||||
|
);
|
||||||
|
expect(scheduledDayChange).toBe(0); // scheduled still April 15 → no badge
|
||||||
|
expect(actualDayChange).toBe(1); // actual crossed midnight → +1 badge
|
||||||
|
});
|
||||||
|
|
||||||
|
it("R4: estimated and actual can both differ from scheduled independently", () => {
|
||||||
|
const queryDate = "2026-04-15";
|
||||||
|
const scheduled = computeDayChange("2026-04-15T22:00:00", queryDate);
|
||||||
|
const estimated = computeDayChange("2026-04-16T00:10:00", queryDate);
|
||||||
|
const actual = computeDayChange("2026-04-16T00:45:00", queryDate);
|
||||||
|
expect(scheduled).toBe(0);
|
||||||
|
expect(estimated).toBe(1);
|
||||||
|
expect(actual).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// dayChangeBadgeTooltip — TZ §4.1.17-R5
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("dayChangeBadgeTooltip (TZ §4.1.17-R5)", () => {
|
||||||
|
it("returns 'День' for +1", () => {
|
||||||
|
expect(dayChangeBadgeTooltip("2026-04-15T23:30:00", 1)).toBe("День");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 'День' for -1", () => {
|
||||||
|
expect(dayChangeBadgeTooltip("2026-04-15T01:00:00", -1)).toBe("День");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 'DD.MM.YYYY' for +2", () => {
|
||||||
|
// Base date is April 15 → +2 → April 17
|
||||||
|
expect(dayChangeBadgeTooltip("2026-04-15T10:00:00", 2)).toBe("17.04.2026");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 'DD.MM.YYYY' for -2", () => {
|
||||||
|
// Base date is April 15 → -2 → April 13
|
||||||
|
expect(dayChangeBadgeTooltip("2026-04-15T10:00:00", -2)).toBe("13.04.2026");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles month boundary correctly for +2", () => {
|
||||||
|
// April 30 → +2 → May 2
|
||||||
|
expect(dayChangeBadgeTooltip("2026-04-30T10:00:00", 2)).toBe("02.05.2026");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles year boundary correctly", () => {
|
||||||
|
// Dec 31 → +2 → Jan 2 next year
|
||||||
|
expect(dayChangeBadgeTooltip("2026-12-31T10:00:00", 2)).toBe("02.01.2027");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles offset-aware ISO string (uses local date from string)", () => {
|
||||||
|
// Local date is April 15 (offset suffix doesn't change calendar date extraction)
|
||||||
|
expect(dayChangeBadgeTooltip("2026-04-15T23:30:00+03:00", 2)).toBe(
|
||||||
|
"17.04.2026",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns empty string for malformed base string", () => {
|
||||||
|
expect(dayChangeBadgeTooltip("not-a-date", 2)).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// formatDayChangeBadge — TZ §4.1.17-R3
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("formatDayChangeBadge (TZ §4.1.17-R3)", () => {
|
||||||
|
it("returns '' for 0 (hidden)", () => {
|
||||||
|
expect(formatDayChangeBadge(0)).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns '+1' for 1", () => {
|
||||||
|
expect(formatDayChangeBadge(1)).toBe("+1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns '+2' for 2", () => {
|
||||||
|
expect(formatDayChangeBadge(2)).toBe("+2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns '-1' for -1", () => {
|
||||||
|
expect(formatDayChangeBadge(-1)).toBe("-1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns '-2' for -2", () => {
|
||||||
|
expect(formatDayChangeBadge(-2)).toBe("-2");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
/**
|
||||||
|
* Day-change badge computation per TZ §4.1.17.
|
||||||
|
*
|
||||||
|
* Returns the day-offset between a time's local date and the query
|
||||||
|
* (user's requested) date. Returns 0 when the time falls on the query
|
||||||
|
* date (no badge shown).
|
||||||
|
*
|
||||||
|
* Each time type (scheduled / expected / actual) should call this
|
||||||
|
* independently — badges for different time types of the same flight
|
||||||
|
* may differ (e.g. scheduled +0 but actual +1 due to delay crossing
|
||||||
|
* midnight).
|
||||||
|
*
|
||||||
|
* The dayChange value is supplied by the API on each ITimesSet.dayChange.value
|
||||||
|
* field; this helper provides client-side computation for cases where a
|
||||||
|
* query-date baseline is available (e.g. when API value is absent or
|
||||||
|
* verification is needed).
|
||||||
|
*/
|
||||||
|
|
||||||
|
const ISO_DATE_RE = /^(\d{4})-(\d{2})-(\d{2})/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the calendar date components from an ISO local string without
|
||||||
|
* TZ reprojection. Handles both bare ("2026-04-15T23:30:00") and
|
||||||
|
* offset-aware ("2026-04-15T23:30:00+03:00") forms.
|
||||||
|
*
|
||||||
|
* Returns [year, month (0-based), day] or null on failure.
|
||||||
|
*/
|
||||||
|
function extractLocalDateParts(
|
||||||
|
iso: string,
|
||||||
|
): [number, number, number] | null {
|
||||||
|
const m = ISO_DATE_RE.exec(iso);
|
||||||
|
if (!m || !m[1] || !m[2] || !m[3]) return null;
|
||||||
|
return [parseInt(m[1], 10), parseInt(m[2], 10) - 1, parseInt(m[3], 10)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the day-change badge value per TZ §4.1.17.
|
||||||
|
*
|
||||||
|
* @param timeLocalIso The local time string from the API (may have TZ offset
|
||||||
|
* suffix — interpreted as the *airport-local* wall-clock).
|
||||||
|
* @param queryDateYyyymmdd The user's requested search date ("YYYY-MM-DD"),
|
||||||
|
* which is the baseline "A" in TZ §4.1.17.
|
||||||
|
* @returns Integer day offset (B - A). 0 means same day → no badge shown.
|
||||||
|
* Positive = later date, negative = earlier date.
|
||||||
|
*/
|
||||||
|
export function computeDayChange(
|
||||||
|
timeLocalIso: string | null | undefined,
|
||||||
|
queryDateYyyymmdd: string,
|
||||||
|
): number {
|
||||||
|
if (!timeLocalIso) return 0;
|
||||||
|
|
||||||
|
const timeParts = extractLocalDateParts(timeLocalIso);
|
||||||
|
const queryParts = extractLocalDateParts(queryDateYyyymmdd);
|
||||||
|
if (!timeParts || !queryParts) return 0;
|
||||||
|
|
||||||
|
// Compare calendar dates as UTC midnight to avoid DST edge cases.
|
||||||
|
const timeMs = Date.UTC(timeParts[0], timeParts[1], timeParts[2]);
|
||||||
|
const queryMs = Date.UTC(queryParts[0], queryParts[1], queryParts[2]);
|
||||||
|
if (Number.isNaN(timeMs) || Number.isNaN(queryMs)) return 0;
|
||||||
|
|
||||||
|
return Math.round((timeMs - queryMs) / (24 * 60 * 60 * 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the tooltip text for a day-change badge per TZ §4.1.17 rules 5–6:
|
||||||
|
* - ±1 → "День" (Russian: «День»)
|
||||||
|
* - ±2 and up → the relevant date in DD.MM.YYYY format, computed from the
|
||||||
|
* base ISO string's local date shifted by `dayChange` days.
|
||||||
|
*
|
||||||
|
* Uses string-based date extraction (no TZ reprojection) for correctness
|
||||||
|
* on SSR and across viewer time zones.
|
||||||
|
*
|
||||||
|
* @param baseLocalIso The time string whose local calendar date is the base.
|
||||||
|
* @param dayChange The badge value (non-zero; caller must guard against 0).
|
||||||
|
*/
|
||||||
|
export function dayChangeBadgeTooltip(
|
||||||
|
baseLocalIso: string,
|
||||||
|
dayChange: number,
|
||||||
|
): string {
|
||||||
|
const abs = Math.abs(dayChange);
|
||||||
|
if (abs === 0) return "";
|
||||||
|
if (abs === 1) return "День";
|
||||||
|
|
||||||
|
// Shift the local date by dayChange days using UTC arithmetic.
|
||||||
|
const parts = extractLocalDateParts(baseLocalIso);
|
||||||
|
if (!parts) return "";
|
||||||
|
|
||||||
|
const shiftedMs = Date.UTC(parts[0], parts[1], parts[2] + dayChange);
|
||||||
|
const shifted = new Date(shiftedMs);
|
||||||
|
const dd = String(shifted.getUTCDate()).padStart(2, "0");
|
||||||
|
const mm = String(shifted.getUTCMonth() + 1).padStart(2, "0");
|
||||||
|
const yyyy = shifted.getUTCFullYear();
|
||||||
|
return `${dd}.${mm}.${yyyy}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a day-change numeric value as a badge label string.
|
||||||
|
* Returns "" for 0, "+1" for 1, "-1" for -1, etc.
|
||||||
|
*/
|
||||||
|
export function formatDayChangeBadge(value: number): string {
|
||||||
|
if (value === 0) return "";
|
||||||
|
return value > 0 ? `+${value}` : String(value);
|
||||||
|
}
|
||||||
@@ -8,6 +8,10 @@ import {
|
|||||||
formatLocalTime,
|
formatLocalTime,
|
||||||
formatUtcOffset,
|
formatUtcOffset,
|
||||||
} from "@/shared/utils/datetime/index.js";
|
} from "@/shared/utils/datetime/index.js";
|
||||||
|
import {
|
||||||
|
dayChangeBadgeTooltip,
|
||||||
|
formatDayChangeBadge,
|
||||||
|
} from "@/features/online-board/dayChange.js";
|
||||||
import { StationDisplay } from "./StationDisplay.js";
|
import { StationDisplay } from "./StationDisplay.js";
|
||||||
import { TimeGroup } from "./TimeGroup.js";
|
import { TimeGroup } from "./TimeGroup.js";
|
||||||
import { FlightStatus } from "./FlightStatus.js";
|
import { FlightStatus } from "./FlightStatus.js";
|
||||||
@@ -279,12 +283,14 @@ export const FlightCard: FC<FlightCardProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flight-card__time">
|
<div className="flight-card__time">
|
||||||
|
{/* TZ §4.1.17-R4: each time type gets its own independent badge */}
|
||||||
<TimeGroup
|
<TimeGroup
|
||||||
scheduled={depTimes.scheduledDeparture.local}
|
scheduled={depTimes.scheduledDeparture.local}
|
||||||
actual={depTimes.actualBlockOff?.local}
|
actual={depTimes.actualBlockOff?.local}
|
||||||
dayChange={
|
scheduledDayChange={depTimes.scheduledDeparture.dayChange?.value}
|
||||||
|
actualDayChange={
|
||||||
depTimes.actualBlockOff?.dayChange.value ??
|
depTimes.actualBlockOff?.dayChange.value ??
|
||||||
depTimes.scheduledDeparture.dayChange?.value
|
depTimes.estimatedBlockOff?.dayChange.value
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -313,12 +319,14 @@ export const FlightCard: FC<FlightCardProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flight-card__time flight-card__time--arrival">
|
<div className="flight-card__time flight-card__time--arrival">
|
||||||
|
{/* TZ §4.1.17-R4: each time type gets its own independent badge */}
|
||||||
<TimeGroup
|
<TimeGroup
|
||||||
scheduled={arrTimes.scheduledArrival.local}
|
scheduled={arrTimes.scheduledArrival.local}
|
||||||
actual={arrTimes.actualBlockOn?.local}
|
actual={arrTimes.actualBlockOn?.local}
|
||||||
dayChange={
|
scheduledDayChange={arrTimes.scheduledArrival.dayChange?.value}
|
||||||
|
actualDayChange={
|
||||||
arrTimes.actualBlockOn?.dayChange.value ??
|
arrTimes.actualBlockOn?.dayChange.value ??
|
||||||
arrTimes.scheduledArrival.dayChange?.value
|
arrTimes.estimatedBlockOn?.dayChange.value
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -362,6 +370,7 @@ export const FlightCard: FC<FlightCardProps> = ({
|
|||||||
flightNumber={flight.flightId.flightNumber}
|
flightNumber={flight.flightId.flightNumber}
|
||||||
variant="small"
|
variant="small"
|
||||||
/>
|
/>
|
||||||
|
{/* TZ §4.1.17-R7: day-change badges in expanded row (per-time-type) */}
|
||||||
<div className="flight-card__detail-row">
|
<div className="flight-card__detail-row">
|
||||||
<div className="flight-card__detail-label">{t("SHARED.TIME")}</div>
|
<div className="flight-card__detail-label">{t("SHARED.TIME")}</div>
|
||||||
<div className="flight-card__detail-group">
|
<div className="flight-card__detail-group">
|
||||||
@@ -370,6 +379,21 @@ export const FlightCard: FC<FlightCardProps> = ({
|
|||||||
{t("SHARED.SCHEDULED")}
|
{t("SHARED.SCHEDULED")}
|
||||||
</span>
|
</span>
|
||||||
<span className="flight-card__detail-value">{depScheduled}</span>
|
<span className="flight-card__detail-value">{depScheduled}</span>
|
||||||
|
{(() => {
|
||||||
|
const dc = depTimes.scheduledDeparture.dayChange?.value ?? 0;
|
||||||
|
const badge = formatDayChangeBadge(dc);
|
||||||
|
return badge ? (
|
||||||
|
<span
|
||||||
|
className="time-group__day-change"
|
||||||
|
title={dayChangeBadgeTooltip(
|
||||||
|
depTimes.scheduledDeparture.local,
|
||||||
|
dc,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{badge}
|
||||||
|
</span>
|
||||||
|
) : null;
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
{depLatest && (
|
{depLatest && (
|
||||||
<div>
|
<div>
|
||||||
@@ -377,6 +401,19 @@ export const FlightCard: FC<FlightCardProps> = ({
|
|||||||
{t(depLatestCaptionKey)}
|
{t(depLatestCaptionKey)}
|
||||||
</span>
|
</span>
|
||||||
<span className="flight-card__detail-value">{depLatest}</span>
|
<span className="flight-card__detail-value">{depLatest}</span>
|
||||||
|
{(() => {
|
||||||
|
const dc =
|
||||||
|
depLatestTimes?.dayChange?.value ?? 0;
|
||||||
|
const badge = formatDayChangeBadge(dc);
|
||||||
|
return badge && depLatestTimes ? (
|
||||||
|
<span
|
||||||
|
className="time-group__day-change"
|
||||||
|
title={dayChangeBadgeTooltip(depLatestTimes.local, dc)}
|
||||||
|
>
|
||||||
|
{badge}
|
||||||
|
</span>
|
||||||
|
) : null;
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -386,6 +423,21 @@ export const FlightCard: FC<FlightCardProps> = ({
|
|||||||
{t("SHARED.SCHEDULED")}
|
{t("SHARED.SCHEDULED")}
|
||||||
</span>
|
</span>
|
||||||
<span className="flight-card__detail-value">{arrScheduled}</span>
|
<span className="flight-card__detail-value">{arrScheduled}</span>
|
||||||
|
{(() => {
|
||||||
|
const dc = arrTimes.scheduledArrival.dayChange?.value ?? 0;
|
||||||
|
const badge = formatDayChangeBadge(dc);
|
||||||
|
return badge ? (
|
||||||
|
<span
|
||||||
|
className="time-group__day-change"
|
||||||
|
title={dayChangeBadgeTooltip(
|
||||||
|
arrTimes.scheduledArrival.local,
|
||||||
|
dc,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{badge}
|
||||||
|
</span>
|
||||||
|
) : null;
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
{arrLatest && (
|
{arrLatest && (
|
||||||
<div>
|
<div>
|
||||||
@@ -393,6 +445,19 @@ export const FlightCard: FC<FlightCardProps> = ({
|
|||||||
{t(arrLatestCaptionKey)}
|
{t(arrLatestCaptionKey)}
|
||||||
</span>
|
</span>
|
||||||
<span className="flight-card__detail-value">{arrLatest}</span>
|
<span className="flight-card__detail-value">{arrLatest}</span>
|
||||||
|
{(() => {
|
||||||
|
const dc =
|
||||||
|
arrLatestTimes?.dayChange?.value ?? 0;
|
||||||
|
const badge = formatDayChangeBadge(dc);
|
||||||
|
return badge && arrLatestTimes ? (
|
||||||
|
<span
|
||||||
|
className="time-group__day-change"
|
||||||
|
title={dayChangeBadgeTooltip(arrLatestTimes.local, dc)}
|
||||||
|
>
|
||||||
|
{badge}
|
||||||
|
</span>
|
||||||
|
) : null;
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { formatLocalTime } from "@/shared/utils/datetime/index.js";
|
import { formatLocalTime } from "@/shared/utils/datetime/index.js";
|
||||||
|
import {
|
||||||
|
dayChangeBadgeTooltip,
|
||||||
|
formatDayChangeBadge,
|
||||||
|
} from "@/features/online-board/dayChange.js";
|
||||||
import "./TimeGroup.scss";
|
import "./TimeGroup.scss";
|
||||||
|
|
||||||
export interface TimeGroupProps {
|
export interface TimeGroupProps {
|
||||||
@@ -7,77 +11,114 @@ export interface TimeGroupProps {
|
|||||||
scheduled: string;
|
scheduled: string;
|
||||||
/** Actual time (ISO 8601 string), if available */
|
/** Actual time (ISO 8601 string), if available */
|
||||||
actual?: string | undefined;
|
actual?: string | undefined;
|
||||||
/** Day change offset (e.g. +1, -1) */
|
/**
|
||||||
|
* Day-change offset for the *scheduled* time (e.g. +1, -1).
|
||||||
|
* Per TZ §4.1.17-R4 each time type gets its own independent badge.
|
||||||
|
* When `actualDayChange` is not provided this value is also used for
|
||||||
|
* the actual time (legacy single-badge behaviour).
|
||||||
|
*
|
||||||
|
* @deprecated Prefer explicit `scheduledDayChange` + `actualDayChange`.
|
||||||
|
*/
|
||||||
dayChange?: number | undefined;
|
dayChange?: number | undefined;
|
||||||
|
/**
|
||||||
|
* Day-change offset for the scheduled time only (TZ §4.1.17-R4).
|
||||||
|
* Takes precedence over the legacy `dayChange` prop.
|
||||||
|
*/
|
||||||
|
scheduledDayChange?: number | undefined;
|
||||||
|
/**
|
||||||
|
* Day-change offset for the actual/estimated time only (TZ §4.1.17-R4).
|
||||||
|
* When absent the component falls back to `dayChange` if no actual exists.
|
||||||
|
*/
|
||||||
|
actualDayChange?: number | undefined;
|
||||||
/** Label for the time group, e.g. "Departure" */
|
/** Label for the time group, e.g. "Departure" */
|
||||||
label?: string;
|
label?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the tooltip text for a day-change chip per TZ §4.1.4:
|
* Displays scheduled + actual times with per-type day-change indicators.
|
||||||
* - ±1 → "день" (locale-invariant for now; Angular uses "день" in RU)
|
*
|
||||||
* - ±2+ → the actual date (DD.MM.YYYY) computed from the base timestamp
|
* Per TZ §4.1.17-R4 each time type (scheduled / expected / actual) gets
|
||||||
* shifted by `dayChange` days.
|
* its own independent badge so that e.g. a flight scheduled for 23:50 (+0)
|
||||||
*/
|
* but actually arriving at 00:30 (+1) shows no badge on the scheduled time
|
||||||
function dayChangeTooltip(scheduled: string, dayChange: number): string {
|
* and +1 on the actual time.
|
||||||
const abs = Math.abs(dayChange);
|
|
||||||
if (abs === 1) return "день";
|
|
||||||
// Parse the wall-clock date from the offset-aware ISO string
|
|
||||||
// (e.g. "2026-04-15T23:30:00+03:00") and shift by dayChange.
|
|
||||||
try {
|
|
||||||
const base = new Date(scheduled);
|
|
||||||
base.setDate(base.getDate() + dayChange);
|
|
||||||
const dd = String(base.getDate()).padStart(2, "0");
|
|
||||||
const mm = String(base.getMonth() + 1).padStart(2, "0");
|
|
||||||
const yyyy = base.getFullYear();
|
|
||||||
return `${dd}.${mm}.${yyyy}`;
|
|
||||||
} catch {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays scheduled + actual times with a day-change indicator.
|
|
||||||
*
|
*
|
||||||
* If actual differs from scheduled, scheduled is shown with strikethrough
|
* If actual differs from scheduled, scheduled is shown with strikethrough
|
||||||
* and actual is shown in bold.
|
* and actual is shown in bold.
|
||||||
|
*
|
||||||
|
* Tooltips per TZ §4.1.17-R5:
|
||||||
|
* - ±1 → "День"
|
||||||
|
* - ±2+ → "DD.MM.YYYY" (the shifted date)
|
||||||
*/
|
*/
|
||||||
export const TimeGroup: FC<TimeGroupProps> = ({
|
export const TimeGroup: FC<TimeGroupProps> = ({
|
||||||
scheduled,
|
scheduled,
|
||||||
actual,
|
actual,
|
||||||
dayChange,
|
dayChange,
|
||||||
|
scheduledDayChange: scheduledDayChangeProp,
|
||||||
|
actualDayChange: actualDayChangeProp,
|
||||||
label,
|
label,
|
||||||
}) => {
|
}) => {
|
||||||
// formatLocalTime reads the wall-clock from the offset-aware ISO
|
|
||||||
// string, so a flight arriving at 06:30 in Almaty (GMT+5) reads
|
|
||||||
// 06:30 regardless of the viewer's timezone. formatTime would
|
|
||||||
// reproject through new Date() and show '04:30' in Moscow.
|
|
||||||
const scheduledTime = formatLocalTime(scheduled);
|
const scheduledTime = formatLocalTime(scheduled);
|
||||||
const actualTime = actual ? formatLocalTime(actual) : undefined;
|
const actualTime = actual ? formatLocalTime(actual) : undefined;
|
||||||
const hasDelay = actualTime !== undefined && actualTime !== scheduledTime;
|
const hasDelay = actualTime !== undefined && actualTime !== scheduledTime;
|
||||||
|
|
||||||
|
// Resolve per-type day-change values.
|
||||||
|
// scheduledDayChange takes precedence over legacy `dayChange`.
|
||||||
|
const schedDC =
|
||||||
|
scheduledDayChangeProp !== undefined ? scheduledDayChangeProp : dayChange;
|
||||||
|
// actualDayChange takes precedence; fall back to dayChange only when no
|
||||||
|
// separate actual badge is provided.
|
||||||
|
const actDC =
|
||||||
|
actualDayChangeProp !== undefined
|
||||||
|
? actualDayChangeProp
|
||||||
|
: hasDelay
|
||||||
|
? dayChange // legacy: single shared badge
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const schedBadge = formatDayChangeBadge(schedDC ?? 0);
|
||||||
|
const actBadge = formatDayChangeBadge(actDC ?? 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="time-group">
|
<div className="time-group">
|
||||||
{label ? <span className="time-group__label">{label}</span> : null}
|
{label ? <span className="time-group__label">{label}</span> : null}
|
||||||
<div className="time-group__times">
|
<div className="time-group__times">
|
||||||
{hasDelay ? (
|
{hasDelay ? (
|
||||||
<>
|
<>
|
||||||
<span className="time-group__actual">{actualTime}</span>
|
<span className="time-group__actual">
|
||||||
|
{actualTime}
|
||||||
|
{actBadge ? (
|
||||||
|
<span
|
||||||
|
className="time-group__day-change"
|
||||||
|
title={dayChangeBadgeTooltip(actual!, actDC!)}
|
||||||
|
>
|
||||||
|
{actBadge}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
<span className="time-group__scheduled time-group__scheduled--delayed">
|
<span className="time-group__scheduled time-group__scheduled--delayed">
|
||||||
{scheduledTime}
|
{scheduledTime}
|
||||||
|
{schedBadge ? (
|
||||||
|
<span
|
||||||
|
className="time-group__day-change time-group__day-change--scheduled"
|
||||||
|
title={dayChangeBadgeTooltip(scheduled, schedDC!)}
|
||||||
|
>
|
||||||
|
{schedBadge}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<span className="time-group__scheduled">{scheduledTime}</span>
|
<span className="time-group__scheduled">
|
||||||
)}
|
{scheduledTime}
|
||||||
{dayChange !== undefined && dayChange !== 0 ? (
|
{schedBadge ? (
|
||||||
<span
|
<span
|
||||||
className="time-group__day-change"
|
className="time-group__day-change"
|
||||||
title={dayChangeTooltip(scheduled, dayChange)}
|
title={dayChangeBadgeTooltip(scheduled, schedDC!)}
|
||||||
>
|
>
|
||||||
{dayChange > 0 ? `+${dayChange}` : dayChange}
|
{schedBadge}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user