From dd4f3206be622cbe95a39dcc4c1efc6b518b3417 Mon Sep 17 00:00:00 2001 From: Fred Boniface Date: Thu, 30 Jul 2026 21:20:39 +0100 Subject: [PATCH] Add initial train service detail to departure boards --- .../components/ui/TrainServiceTable.svelte | 152 ++++++++++++++ .../station-board/ServiceDetailModal.svelte | 194 ++++++++++++++++++ .../ui/station-board/StaffServicesGrid.svelte | 80 ++++++-- src/lib/utils/delayCancelMsg.ts | 17 ++ src/routes/board/+page.svelte | 4 +- 5 files changed, 431 insertions(+), 16 deletions(-) create mode 100644 src/lib/components/ui/TrainServiceTable.svelte create mode 100644 src/lib/components/ui/station-board/ServiceDetailModal.svelte create mode 100644 src/lib/utils/delayCancelMsg.ts diff --git a/src/lib/components/ui/TrainServiceTable.svelte b/src/lib/components/ui/TrainServiceTable.svelte new file mode 100644 index 0000000..6d50473 --- /dev/null +++ b/src/lib/components/ui/TrainServiceTable.svelte @@ -0,0 +1,152 @@ + + +
+ {#if trainDetail.header.cr} + {formatTrainStatusMessage( + trainDetail.header.cr, + trainDetail.header.cl, + trainDetail.header.cn + )} + {/if} + {#if trainDetail.header.dr} + {formatTrainStatusMessage( + trainDetail.header.dr, + trainDetail.header.dl, + trainDetail.header.dn + )} + {/if} + {#if pisData} +
+ PIS: {pisData.code} + {#if pisData.skip?.skip} + + (skip {pisData.skip.position === 'head' ? 'first' : 'last'} + {pisData.skip.skip} + {pisData.skip.skip === 1 ? 'stop' : 'stops'} + ) + + {/if} +
+ {/if} + +
+ + + + + + + + + + + + + + + + + + {#each trainDetail.locations as loc} + + + + + {#if loc.r === 'PASS'} + + + + {:else} + + + + + {/if} + {#if loc} + {@const delay = calculateDelay(loc)} + + {/if} + + {#if loc.act && getRelevantActivities(loc.act).length > 0} + + + + {/if} + + {/each} +
ArrDep
LocationPlatSchActSchAct
+ {loc.t} + + {loc.p} + Pass{formatUkTime(loc.wtp)}{formatUkTime(loc.atp || loc.etp || '--')}{formatUkTime(loc.pta || loc.wta)}{formatUkTime(loc.ata || loc.eta || '--')}{formatUkTime(loc.ptd || loc.wtd)}{formatUkTime(loc.atd || loc.etd || '--')}{delay.val}
+
+ {#each getRelevantActivities(loc.act) as note} + {note} + {/each} +
+
+
+
diff --git a/src/lib/components/ui/station-board/ServiceDetailModal.svelte b/src/lib/components/ui/station-board/ServiceDetailModal.svelte new file mode 100644 index 0000000..abb0732 --- /dev/null +++ b/src/lib/components/ui/station-board/ServiceDetailModal.svelte @@ -0,0 +1,194 @@ + + + { + // Optional: close when clicking the native backdrop area outside the modal box + const rect = dialogElement?.getBoundingClientRect(); + if ( + rect && + (e.clientX < rect.left || + e.clientX > rect.right || + e.clientY < rect.top || + e.clientY > rect.bottom) + ) { + onClose(); + } + }} +> + + + + diff --git a/src/lib/components/ui/station-board/StaffServicesGrid.svelte b/src/lib/components/ui/station-board/StaffServicesGrid.svelte index 8ba346f..4dd765b 100644 --- a/src/lib/components/ui/station-board/StaffServicesGrid.svelte +++ b/src/lib/components/ui/station-board/StaffServicesGrid.svelte @@ -4,12 +4,26 @@ import { fade, slide, fly } from 'svelte/transition'; import { flip } from 'svelte/animate'; + import ServiceDetailModal from './ServiceDetailModal.svelte'; + let isModalOpen = $state(false); + let selectedRid = $state(''); + function handleOpenTrainDetails(rid: string) { + selectedRid = rid; + isModalOpen = true; + } + function handleCloseModal() { + isModalOpen = false; + } + let { services }: { services: ApiStationsBoard.BoardService[] } = $props(); const getRowKey = (s: ApiStationsBoard.BoardService) => `${s.r}${s.sta ?? ''}${s.std ?? ''}${s.wtp ?? ''}`; + + +
@@ -45,14 +59,38 @@ class:cancelled={service.c} >
{service.h}
-
+
handleOpenTrainDetails(service.r)} + onkeydown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleOpenTrainDetails(service.r); + } + }} + > {#key service.og.t}
{service.og.t}
{/key}
-
+
handleOpenTrainDetails(service.r)} + onkeydown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleOpenTrainDetails(service.r); + } + }} + > {#key service.dt.t}
{service.dt.t}
{/key}
@@ -64,32 +102,47 @@ {formatUkTime(service.wtp)}
- {#if isRightTime(service.wtp, service.atp || service.etp)} - RT - {:else if service.c} - CANC - {:else} - {realPass} + {#if isRightTime(service.wtp, service.atp || service.etp)} + RT + {:else if service.c} + CANC + {:else} + {realPass} {/if}
{:else}
{formatUkTime(service.sta)}
{#if isRightTime(service.sta, service.ata || service.eta)} RT - {:else if service.sta && service.c} CANC {:else} + {:else if service.sta && service.c} + CANC + {:else} {realArrival} {/if}
{formatUkTime(service.std)}
-
+
{#if isRightTime(service.std, service.atd || service.etd)} RT - {:else if service.std && service.c} CANC {:else} + {:else if service.std && service.c} + CANC + {:else} {realDeparture} {/if}
@@ -263,6 +316,7 @@ .cell.dest { font-stretch: 80%; font-weight: 400; + cursor: pointer; } .cell.orig { text-align: left; diff --git a/src/lib/utils/delayCancelMsg.ts b/src/lib/utils/delayCancelMsg.ts new file mode 100644 index 0000000..7dbfdf0 --- /dev/null +++ b/src/lib/utils/delayCancelMsg.ts @@ -0,0 +1,17 @@ +export function formatTrainStatusMessage( + reason: string | undefined, + location: string | undefined, + near: boolean | undefined +) { + let output = ''; + if (reason) { + output += reason.trim(); + } + + if (location) { + const preposition = near ? 'near' : 'at'; + output += ` ${preposition} ${location}`; + } + + return output.trim(); +} diff --git a/src/routes/board/+page.svelte b/src/routes/board/+page.svelte index d32b3c4..84bcdee 100644 --- a/src/routes/board/+page.svelte +++ b/src/routes/board/+page.svelte @@ -126,9 +126,7 @@
{:else} -
- No services at this location in the next three hours -
+
No services at this location in the next three hours
{/if}