@@ -2,15 +2,21 @@ import { Controller } from "https://unpkg.com/@hotwired/stimulus@3.2.2/dist/stim
22
33const SUMMARY_REFRESH_MS = 60_000 ;
44
5- const ADH_CLASSES = Object . freeze ( {
6- early : [ "text-amber-700" , "bg-amber-50" ] ,
7- onTime : [ "text-emerald-700" , "bg-emerald-50" ] ,
8- late : [ "text-red-700" , "bg-red-50" ] ,
5+ // Source of truth: OneBusAway design package (separate repo) at
6+ // project/assets/colors_and_type.css — red = early, green = on time,
7+ // violet = late.
8+ const STATUS_STYLES = Object . freeze ( {
9+ early : { text : "text-status-early" , bg : "bg-status-early/10" , solid : "bg-status-early" } ,
10+ onTime : { text : "text-status-on-time-ink" , bg : "bg-status-on-time/10" , solid : "bg-status-on-time" } ,
11+ late : { text : "text-status-late" , bg : "bg-status-late/10" , solid : "bg-status-late" } ,
912} ) ;
10- const ALL_ADH_CLASSES = [ ...new Set ( Object . values ( ADH_CLASSES ) . flat ( ) ) ] ;
13+ const ALL_ADH_CLASSES = Object . values ( STATUS_STYLES ) . flatMap ( ( s ) => [ s . text , s . bg ] ) ;
14+ const ALL_STRIPE_CLASSES = [ ...Object . values ( STATUS_STYLES ) . map ( ( s ) => s . solid ) , "bg-gray-200" ] ;
15+
16+ const blocksLabel = ( n ) => `${ n } ${ n === 1 ? "block" : "blocks" } ` ;
1117
1218export default class extends Controller {
13- static targets = [ "summary" , "accordion" , "routeTemplate" , "blockTemplate" , "loadAll" ] ;
19+ static targets = [ "summary" , "asOf" , " accordion", "routeTemplate" , "blockTemplate" , "loadAll" ] ;
1420 static values = {
1521 earlyMsec : Number ,
1622 lateMsec : Number ,
@@ -81,20 +87,31 @@ export default class extends Controller {
8187
8288 #renderSummary( total ) {
8389 const blocks = total . blocks ?? 0 ;
84- const vehicleCount = ( total . late ?? 0 ) + ( total . ontime ?? 0 ) + ( total . early ?? 0 ) ;
90+ const late = total . late ?? 0 ;
91+ const onTime = total . ontime ?? 0 ;
92+ const early = total . early ?? 0 ;
93+ const assigned = late + onTime + early ;
8594 const pct = ( n ) => ( blocks ? `${ ( ( 100 * n ) / blocks ) . toFixed ( 0 ) } %` : "—" ) ;
8695
8796 this . #fill( "total-blocks" , blocks ) ;
88- this . #fill( "percent-late" , pct ( total . late ?? 0 ) ) ;
89- this . #fill( "percent-on-time" , pct ( total . ontime ?? 0 ) ) ;
90- this . #fill( "percent-early" , pct ( total . early ?? 0 ) ) ;
91- this . #fill( "as-of" , new Date ( ) . toLocaleTimeString ( ) ) ;
97+ this . #fill( "percent-late" , pct ( late ) ) ;
98+ this . #fill( "percent-on-time" , pct ( onTime ) ) ;
99+ this . #fill( "percent-early" , pct ( early ) ) ;
100+ this . #fill( "late-count" , late ? blocksLabel ( late ) : "" ) ;
101+ this . #fill( "on-time-count" , onTime ? blocksLabel ( onTime ) : "" ) ;
102+ this . #fill( "early-count" , early ? blocksLabel ( early ) : "" ) ;
103+ this . #fill( "assigned-detail" , blocks ? `${ assigned } /${ blocks } ` : "" ) ;
104+
105+ if ( this . hasAsOfTarget ) this . asOfTarget . textContent = new Date ( ) . toLocaleTimeString ( ) ;
92106
93107 const assignedEl = this . summaryTarget . querySelector ( "[data-field='percent-assigned']" ) ;
94108 if ( assignedEl ) {
95- assignedEl . textContent = pct ( vehicleCount ) ;
96- const assignedFrac = blocks ? vehicleCount / blocks : 1 ;
97- assignedEl . classList . toggle ( "text-red-600" , assignedFrac < 0.9 ) ;
109+ assignedEl . textContent = pct ( assigned ) ;
110+ const assignedFrac = blocks ? assigned / blocks : 1 ;
111+ // Drop the brand-accent color and switch to red when assignment is
112+ // unhealthy, so the at-a-glance signal still reads correctly.
113+ assignedEl . classList . toggle ( "text-brand-accent" , assignedFrac >= 0.9 ) ;
114+ assignedEl . classList . toggle ( "text-status-early" , assignedFrac < 0.9 ) ;
98115 }
99116 }
100117
@@ -121,8 +138,12 @@ export default class extends Controller {
121138 item . dataset . routeName = route . name ;
122139 accordion . appendChild ( item ) ;
123140 }
141+ const blockCount = route . block ?. length ?? 0 ;
142+ item . querySelector ( "[data-field='route-tag']" ) . textContent = route . id ;
124143 item . querySelector ( "[data-field='route-name']" ) . textContent = route . name ;
125- item . querySelector ( "[data-field='route-blocks']" ) . textContent = route . block ?. length ?? 0 ;
144+ item . querySelector ( "[data-field='route-blocks']" ) . textContent = blockCount ;
145+ item . querySelector ( "[data-field='route-blocks-noun']" ) . textContent = blockCount === 1 ? "block" : "blocks" ;
146+ item . querySelector ( "[data-field='route-blocks-denom']" ) . textContent = blockCount ;
126147 }
127148
128149 for ( const [ id , el ] of existing ) {
@@ -146,46 +167,61 @@ export default class extends Controller {
146167 const vehicleTotal = early + onTime + late ;
147168 const blockCount = ( route . block ?? [ ] ) . length ;
148169
149- const earlyEl = item . querySelector ( "[data-field='route-early']" ) ;
150- const onTimeEl = item . querySelector ( "[data-field='route-on-time']" ) ;
151- const lateEl = item . querySelector ( "[data-field='route-late']" ) ;
152- const vehEl = item . querySelector ( "[data-field='route-vehicles']" ) ;
153- const blocksEl = item . querySelector ( "[data-field='route-blocks']" ) ;
154-
155- earlyEl . textContent = early ;
156- onTimeEl . textContent = onTime ;
157- lateEl . textContent = late ;
158- vehEl . textContent = vehicleTotal ;
159- blocksEl . textContent = blockCount ;
160-
161- earlyEl . classList . toggle ( "text-amber-700" , early > 0 ) ;
162- earlyEl . classList . toggle ( "bg-amber-50" , early > 0 ) ;
163- lateEl . classList . toggle ( "text-red-700" , late > 0 ) ;
164- lateEl . classList . toggle ( "bg-red-50" , late > 0 ) ;
165- const understaffed = vehicleTotal < blockCount ;
166- vehEl . classList . toggle ( "text-red-700" , understaffed ) ;
167- vehEl . classList . toggle ( "bg-red-50" , understaffed ) ;
170+ item . querySelector ( "[data-field='route-early']" ) . textContent = early ;
171+ item . querySelector ( "[data-field='route-on-time']" ) . textContent = onTime ;
172+ item . querySelector ( "[data-field='route-late']" ) . textContent = late ;
173+ item . querySelector ( "[data-field='route-blocks']" ) . textContent = blockCount ;
174+ item . querySelector ( "[data-field='route-blocks-denom']" ) . textContent = blockCount ;
175+ item . querySelector ( "[data-field='route-blocks-noun']" ) . textContent = blockCount === 1 ? "block" : "blocks" ;
176+
177+ // Vehicle count below block count means the route is short-staffed;
178+ // surface that as red regardless of the per-vehicle adherence colors.
179+ const vehEl = item . querySelector ( "[data-field='route-vehicles']" ) ;
180+ vehEl . textContent = vehicleTotal ;
181+ vehEl . classList . toggle ( "text-status-early" , vehicleTotal < blockCount ) ;
182+
183+ this . #toggleVisible( item , "route-early-pair" , early > 0 ) ;
184+ this . #toggleVisible( item , "route-on-time-pair" , onTime > 0 ) ;
185+ this . #toggleVisible( item , "route-late-pair" , late > 0 ) ;
168186
169187 const summary = item . querySelector ( "[data-vehicle-summary]" ) ;
170188 summary ?. classList . remove ( "hidden" ) ;
171189 summary ?. classList . add ( "flex" ) ;
172190
191+ // Stripe priority is late > early > on-time, falling back to neutral
192+ // gray when no vehicles report yet — otherwise every unloaded card
193+ // would mis-signal "all on time".
194+ const stripe = item . querySelector ( "[data-field='route-stripe']" ) ;
195+ if ( stripe ) {
196+ stripe . classList . remove ( ...ALL_STRIPE_CLASSES ) ;
197+ const kind = late > 0 ? "late" : early > 0 ? "early" : onTime > 0 ? "onTime" : null ;
198+ stripe . classList . add ( kind ? STATUS_STYLES [ kind ] . solid : "bg-gray-200" ) ;
199+ }
200+
173201 const blockList = item . querySelector ( "[data-block-list]" ) ;
174202 blockList . replaceChildren ( ) ;
175203 for ( const block of route . block ?? [ ] ) {
176204 blockList . appendChild ( this . #renderBlock( block ) ) ;
177205 }
178206 }
179207
208+ #toggleVisible( item , field , visible ) {
209+ const el = item . querySelector ( `[data-field='${ field } ']` ) ;
210+ if ( ! el ) return ;
211+ el . classList . toggle ( "hidden" , ! visible ) ;
212+ el . classList . toggle ( "inline-flex" , visible ) ;
213+ }
214+
180215 #classifyAdh( schAdh ) {
181216 if ( schAdh < - this . lateMsecValue ) return "late" ;
182217 if ( schAdh > this . earlyMsecValue ) return "early" ;
183218 return "onTime" ;
184219 }
185220
186- #applyAdhClasses( el , schAdh ) {
221+ #applyAdhClasses( el , kind ) {
187222 el . classList . remove ( ...ALL_ADH_CLASSES ) ;
188- el . classList . add ( ...ADH_CLASSES [ this . #classifyAdh( schAdh ) ] ) ;
223+ const style = STATUS_STYLES [ kind ] ;
224+ el . classList . add ( style . text , style . bg ) ;
189225 }
190226
191227 #renderBlock( block ) {
@@ -213,8 +249,13 @@ export default class extends Controller {
213249
214250 vehiclesEl . textContent = vehicles . map ( ( v ) => v . id ) . join ( ", " ) ;
215251 const v0 = vehicles [ 0 ] ;
216- adhEl . textContent = v0 . schAdhStr ?? "—" ;
217- this . #applyAdhClasses( adhEl , v0 . schAdh ) ;
252+ const kind = this . #classifyAdh( parseInt ( v0 . schAdh , 10 ) ) ;
253+ adhEl . replaceChildren ( ) ;
254+ const dot = document . createElement ( "span" ) ;
255+ dot . className = `inline-block size-2 rounded-full ${ STATUS_STYLES [ kind ] . solid } ` ;
256+ adhEl . appendChild ( dot ) ;
257+ adhEl . appendChild ( document . createTextNode ( v0 . schAdhStr ?? "—" ) ) ;
258+ this . #applyAdhClasses( adhEl , kind ) ;
218259 return row ;
219260 }
220261}
0 commit comments