Abstract related object sections #93

Closed
opened 2026-04-11 23:53:34 +00:00 by maxtkc · 0 comments
Owner

Summary

This refactor introduces a centralized src/utils/entity-references.ts utility with two pure render functions — renderRouteReference and renderServiceReference — that become the canonical way to display a related object row anywhere in the Browse tab. All four view contexts (home, agency, route, service) will consume these functions, replacing the scattered, inconsistent HTML today. The key behavioral changes: on the service page the related-routes list becomes a "Timetables" list where row click → timetable and a "View Route" button navigates to the route; on the route page the same inversion applies with "View Service". Service reference rows gain rich calendar metadata (days of week formatted as "Mon–Fri" or "Mon, Wed, Fri", date range, counts). Route reference rows get a color dot, name, agency, and trip count.

Relevant Context

Files to create:

  • src/utils/entity-references.ts — new shared render functions + CSS class constants

Files to modify:

  • src/modules/page-content-renderer.ts
    • renderRoute() (ln 477–621): replace "Services" section with renderServiceReference rows
    • renderHome() (ln 273–384): replace bare service cards with renderServiceReference rows
    • addEventListeners() (ln 667–765): swap class selectors for new CSS constants
  • src/modules/service-view-controller.ts
    • renderRelatedRoutes() (ln 172–297): replace DaisyUI table with renderRouteReference rows
    • addEventListeners() (ln 317–353): swap class selectors
  • src/modules/agency-view-controller.ts
    • renderRouteItem() (ln 134–147): replace with renderRouteReference
    • addEventListeners() (ln 206–217): swap .route-item for ROUTE_REF_ROW constant

Key existing utilities:

  • src/utils/entity-display.tsgetRouteDisplay(), getServiceDisplay(), renderCardLabel() — continue to use these
  • src/types/gtfs.tsRoutes, Calendar, CalendarDates types
  • src/utils/agency-helpers.tsnormalizeAgencyId()

Event wiring pattern: all click handlers live in addEventListeners() methods; HTML is rendered as template strings with data-* attributes and CSS class selectors. The new CSS class constants must be imported from entity-references.ts rather than hard-coded as strings.


Phase 1: Create src/utils/entity-references.ts

All other phases depend on this. The functions are pure (no async, no DB access) — callers pre-fetch everything and pass it in.

  • Define and export CSS class constants:

    export const ROUTE_REF_ROW = 'route-ref-row';
    export const SERVICE_REF_ROW = 'service-ref-row';
    export const TIMETABLE_REF_BTN = 'timetable-ref-btn';
    export const ENTITY_REF_BTN = 'entity-ref-btn';
    
  • Define RouteReferenceOpts:

    export interface RouteReferenceOpts {
      agencyName?: string;   // shown below route name; omit on agency page
      tripCount?: number;    // shown as a badge
      service_id?: string;   // when present: row click → timetable, "View Route" button appears
                             // when absent:  row click → route page, no secondary button
    }
    
  • Define ServiceReferenceOpts:

    export interface ServiceReferenceOpts {
      tripCount?: number;
      routeCount?: number;
      route_id?: string;     // when present: row click → timetable, "View Service" button appears
                             // when absent:  row click → service page (home context)
      calendarDates?: Array<{ date: string; exception_type: string | number }>;
                             // needed to compute date range for calendar_dates-only services
    }
    
  • Implement formatDaysOfWeek(service: Record<string, unknown>): string

    • Fields: monday, tuesday, wednesday, thursday, friday, saturday, sunday
    • Accept '1'/'0' strings or 1/0 numbers
    • Collect active day abbreviations in order: Mon, Tue, Wed, Thu, Fri, Sat, Sun
    • If active indices are consecutive and ≥ 3, show a range: 'Mon–Fri'
    • Otherwise comma-list: 'Mon, Wed, Fri'
    • If no day fields exist on the record (calendar_dates-only), return 'Specific Days'
    • If all days are 0, return 'No regular days'
  • Implement formatDateRange(service: Record<string, unknown>, calendarDates?: Array<{date: string; exception_type: string | number}>): string

    • If service.start_date exists: return '${start_date} – ${end_date}'
    • Else if calendarDates provided with positive exceptions (exception_type === '1' or === 1): derive min/max date and return '${min} – ${max}'
    • Else return ''
  • Implement renderRouteReference(route: Record<string, unknown>, opts: RouteReferenceOpts): string

    • Layout: flex items-center gap-3 p-3 rounded-lg hover:bg-base-200 cursor-pointer transition-colors ${ROUTE_REF_ROW}
    • data-route-id="${route.route_id}" on the container
    • data-service-id="${opts.service_id}" on the container when service_id present
    • Colored dot: <div class="w-3 h-3 rounded-full flex-shrink-0" style="background-color: #${route.route_color || '6366f1'}"></div>
    • Main label: renderCardLabel(getRouteDisplay(route as Record<string, string>)) — primary = short/long name, secondary = ID
    • Agency subline: if agencyName provided, show it as a small muted line below the route label
    • Trip count badge: if tripCount !== undefined, show <div class="badge badge-outline badge-sm">${tripCount} trip${tripCount !== 1 ? 's' : ''}</div>
    • When service_id present: add <button class="btn btn-xs btn-ghost ${ENTITY_REF_BTN}" data-route-id="${route.route_id}">View Route</button>. The timetable link lives on the row itself (row click).
    • When service_id absent: no secondary button; clicking the row navigates to the route page.
  • Implement renderServiceReference(service: Record<string, unknown>, opts: ServiceReferenceOpts): string

    • Layout: flex items-center gap-3 p-3 rounded-lg hover:bg-base-200 cursor-pointer transition-colors ${SERVICE_REF_ROW}
    • data-service-id="${service.service_id}" on the container
    • data-route-id="${opts.route_id}" when present
    • Primary: service_id in bold (use renderCardLabel(getServiceDisplay(service as Record<string, string>)))
    • Days string from formatDaysOfWeek(service) — shown as a small muted text if non-empty
    • Date range from formatDateRange(service, opts.calendarDates) — shown as small muted text if non-empty
    • Trip count badge if provided
    • Route count badge if provided
    • When route_id present: add <button class="btn btn-xs btn-ghost ${ENTITY_REF_BTN}" data-service-id="${service.service_id}">View Service</button>. Timetable link lives on the row.
    • When route_id absent: no secondary button; clicking the row navigates to the service page.

Gotchas:

  • Both functions must be pure and synchronous — no fallback DB queries
  • route_color in GTFS has no # prefix; always prepend it
  • If route.route_color is empty/undefined, fall back to a neutral color (e.g., #6366f1)
  • formatDaysOfWeek must handle records where day fields are completely absent (calendar_dates-only services have only { service_id })

Phase 2: Refactor Route Page timetables (page-content-renderer.ts::renderRoute)

Replace the "Services" section with rich service reference rows. Row click → timetable; "View Service" button → service page.

  • Import renderServiceReference, SERVICE_REF_ROW, TIMETABLE_REF_BTN, ENTITY_REF_BTN from entity-references.ts
  • After the existing allServices fetch (calendar rows), build a lookup map: const calendarByServiceId = new Map(allServices.map(s => [s.service_id as string, s]))
  • For each entry in serviceGroups, call:
    renderServiceReference(
      calendarByServiceId.get(service_id) ?? { service_id },
      { tripCount: serviceTrips.length, route_id }
    )
    
  • Replace the Object.entries(serviceGroups).map(...) block with the above calls
  • Rename section <h2> from "Services" to "Timetables"
  • Keep the #new-service-select dropdown (for adding new timetables) exactly as-is — it sits above the reference rows
  • In addEventListeners():
    • Remove the .service-row click handler (old: navigates to service page)
    • Remove the .route-timetable-btn click handler
    • Add SERVICE_REF_ROW click handler: onTimetableClick(route_id, service_id) — get both IDs from data-* attributes
    • Add ENTITY_REF_BTN click handler (scoped to route page): navigate to service via onServiceClick(service_id) — get service_id from data-service-id on the button

Gotchas:

  • Services that exist in trips but not in calendar (calendar_dates-only): the lookup map won't have them. Fall back to { service_id }renderServiceReference handles this gracefully via formatDaysOfWeek's absent-fields path.
  • onServiceClick is currently optional in ContentRendererDependencies. Keep defensive guard in the event handler.
  • The ENTITY_REF_BTN class will also appear on the service page (Phase 3) after DOM insertion. Since addEventListeners scopes queries to the provided container, there's no cross-page interference.
  • The existing .service-link handler in addEventListeners() (ln 727–737) should also be removed as it's superseded.

Phase 3: Refactor Service Page timetables (service-view-controller.ts)

Replace the DaisyUI table table-pin-rows (grouped by agency) with route reference rows. Section renamed to "Timetables". Row click → timetable; "View Route" button → route page.

  • Import renderRouteReference, ROUTE_REF_ROW, ENTITY_REF_BTN from entity-references.ts
  • In renderServiceView(), fetch trips for this service and build a trip count map:
    const trips = await (this.dependencies.gtfsRelationships?.getTripsForService?.(service_id) 
      ?? this.dependencies.gtfsDatabase?.queryRows('trips', { service_id }) 
      ?? []);
    const tripCountByRoute = new Map<string, number>();
    for (const trip of trips) {
      const rid = (trip as Record<string, unknown>).route_id as string;
      tripCountByRoute.set(rid, (tripCountByRoute.get(rid) ?? 0) + 1);
    }
    
  • Build agency name lookup: keep getAgenciesForRoutes(routes) call, then build Map<normalizedAgencyId, agencyName> from it
  • Rename renderRelatedRoutes to renderTimetablesSection; change signature to (routes: Routes[], agencyNameByNormalizedId: Map<string, string>, tripCountByRoute: Map<string, number>, service_id: string): string
  • Inside the renamed method, replace the entire tableHTML / table.table-pin-rows with route reference rows wrapped in <div class="space-y-2">
  • Update section title from "Routes Using This Service" to "Timetables"
  • In addEventListeners():
    • Remove .route-row click handler
    • Remove .agency-link click handler
    • Remove .timetable-btn click handler
    • Add ROUTE_REF_ROW click handler: onTimetableClick(route_id, service_id) — read both data-route-id and data-service-id from the row
    • Add ENTITY_REF_BTN click handler: onRouteClick(route_id) — read data-route-id from the button

Gotchas:

  • Routes with no agency_id (single-agency GTFS feed): agencyName will be undefined, which is fine — renderRouteReference omits it silently
  • The DaisyUI table table-pin-rows with agency group headers is removed entirely; agency name appears inline in each row instead — this is a deliberate simplification per the issue
  • ServiceViewDependencies.onAgencyClick is currently optional and only used for agency header links. After this phase it is no longer wired up to any rendered element. Leave the dependency in place (don't remove it from the interface) to avoid breaking callers, but it is effectively unused in rendering.

Phase 4: Enrich Home Page service cards (page-content-renderer.ts::renderHome)

Replace bare service name cards with renderServiceReference rows that include days, date range, and counts.

  • Import renderServiceReference, SERVICE_REF_ROW at the top of page-content-renderer.ts (already importing from Phase 2)
  • In renderHome(), after fetching services, fetch all trips once:
    const allTrips = await this.dependencies.gtfsDatabase.getAllRows('trips') as Record<string, unknown>[];
    
  • Build per-service maps:
    const tripCountByService = new Map<string, number>();
    const routesByService = new Map<string, Set<string>>();
    for (const trip of allTrips) {
      const sid = trip.service_id as string;
      const rid = trip.route_id as string;
      tripCountByService.set(sid, (tripCountByService.get(sid) ?? 0) + 1);
      if (!routesByService.has(sid)) routesByService.set(sid, new Set());
      routesByService.get(sid)!.add(rid);
    }
    
  • Replace the serviceItems map with calls to renderServiceReference(serviceData, { tripCount, routeCount }) — no route_id (home context)
  • In addEventListeners(): replace .service-card click handler with SERVICE_REF_ROW click handler → onServiceClick(service_id) (updated the existing SERVICE_REF_ROW handler to handle both route-page context with route_id → timetable, and home-page context without route_id → service page)

Gotchas:

  • getAllRows('trips') adds one extra async DB call on the home page render. This is acceptable given dataset sizes typical of GTFS.
  • extraServices (calendar_dates-only, shaped as { service_id }) will have tripCount from the trips map but no day/date fields — renderServiceReference handles this gracefully
  • The existing data-inline-create="service" input for creating new services stays in place above the list

Phase 5: Enrich Agency Page route items (agency-view-controller.ts)

Replace renderRouteItem with renderRouteReference. No service context (row click → route page). Add trip count per route.

  • Import renderRouteReference, ROUTE_REF_ROW from entity-references.ts
  • In renderAgencyView(), after fetching routes, fetch trip counts via queryRows('trips') (no filter = all rows; QueryOnlyDatabase only exposes queryRows, not getAllRows)
  • In renderRoutesList(), replace .map(route => this.renderRouteItem(route)) with .map(route => renderRouteReference(route as Record<string, unknown>, { tripCount: tripCountByRoute.get(route.route_id) }))
  • Pass tripCountByRoute into renderRoutesList() — updated method signature accordingly
  • Delete the renderRouteItem() method
  • In addEventListeners(): replace .route-item click handler with ROUTE_REF_ROW click handler → onRouteClick(route_id)

Gotchas:

  • No agencyName passed to renderRouteReference (we're on the agency page — redundant)
  • No service_id passed — row click → route page, no timetable button
  • getAllRows('trips') filtered client-side is efficient enough for typical GTFS datasets; avoids one query per route
  • AgencyViewController only has gtfsDatabase and onRouteClick in its deps — both are already available

Original Issue

These can be more consistent and a bit more descriptive. We should make proper abstractions so that a browse page is basically just what is described here, then we describe how to display each corresponding item.

Pages

Feed Information

  • Agencies
  • Services

Agency Properties

  • Routes

Service Schedule

  • Timetables (each row is a route and should show all route props, then lets make a "View Service" button and the row should default to linking to the timetable, the reverse of what is done now)

Route Page

  • Timetables (each row is a service and should show all service props, then lets make a "View Service" button and the row should default to linking to the timetable, the reverse of what is done now)

References

To a Service

  • ID
  • Days of the week (Mon-Fri or Mon,Wed,Fri)
  • Date range
  • Number of Trips
  • Number of Routes
  • (Lets put lots of information in here for now, if it's simple and performant and obvious, then we can trim it down)

To a Route

  • ID
  • Color
  • Name
  • Agency
  • Number Trips
## Summary This refactor introduces a centralized `src/utils/entity-references.ts` utility with two pure render functions — `renderRouteReference` and `renderServiceReference` — that become the canonical way to display a related object row anywhere in the Browse tab. All four view contexts (home, agency, route, service) will consume these functions, replacing the scattered, inconsistent HTML today. The key behavioral changes: on the service page the related-routes list becomes a "Timetables" list where **row click → timetable** and a "View Route" button navigates to the route; on the route page the same inversion applies with "View Service". Service reference rows gain rich calendar metadata (days of week formatted as "Mon–Fri" or "Mon, Wed, Fri", date range, counts). Route reference rows get a color dot, name, agency, and trip count. ## Relevant Context **Files to create:** - `src/utils/entity-references.ts` — new shared render functions + CSS class constants **Files to modify:** - `src/modules/page-content-renderer.ts` - `renderRoute()` (ln 477–621): replace "Services" section with `renderServiceReference` rows - `renderHome()` (ln 273–384): replace bare service cards with `renderServiceReference` rows - `addEventListeners()` (ln 667–765): swap class selectors for new CSS constants - `src/modules/service-view-controller.ts` - `renderRelatedRoutes()` (ln 172–297): replace DaisyUI table with `renderRouteReference` rows - `addEventListeners()` (ln 317–353): swap class selectors - `src/modules/agency-view-controller.ts` - `renderRouteItem()` (ln 134–147): replace with `renderRouteReference` - `addEventListeners()` (ln 206–217): swap `.route-item` for `ROUTE_REF_ROW` constant **Key existing utilities:** - `src/utils/entity-display.ts` — `getRouteDisplay()`, `getServiceDisplay()`, `renderCardLabel()` — continue to use these - `src/types/gtfs.ts` — `Routes`, `Calendar`, `CalendarDates` types - `src/utils/agency-helpers.ts` — `normalizeAgencyId()` **Event wiring pattern:** all click handlers live in `addEventListeners()` methods; HTML is rendered as template strings with `data-*` attributes and CSS class selectors. The new CSS class constants must be imported from `entity-references.ts` rather than hard-coded as strings. --- ## Phase 1: Create `src/utils/entity-references.ts` All other phases depend on this. The functions are pure (no async, no DB access) — callers pre-fetch everything and pass it in. - [x] Define and export CSS class constants: ```ts export const ROUTE_REF_ROW = 'route-ref-row'; export const SERVICE_REF_ROW = 'service-ref-row'; export const TIMETABLE_REF_BTN = 'timetable-ref-btn'; export const ENTITY_REF_BTN = 'entity-ref-btn'; ``` - [x] Define `RouteReferenceOpts`: ```ts export interface RouteReferenceOpts { agencyName?: string; // shown below route name; omit on agency page tripCount?: number; // shown as a badge service_id?: string; // when present: row click → timetable, "View Route" button appears // when absent: row click → route page, no secondary button } ``` - [x] Define `ServiceReferenceOpts`: ```ts export interface ServiceReferenceOpts { tripCount?: number; routeCount?: number; route_id?: string; // when present: row click → timetable, "View Service" button appears // when absent: row click → service page (home context) calendarDates?: Array<{ date: string; exception_type: string | number }>; // needed to compute date range for calendar_dates-only services } ``` - [x] Implement `formatDaysOfWeek(service: Record<string, unknown>): string` - Fields: `monday`, `tuesday`, `wednesday`, `thursday`, `friday`, `saturday`, `sunday` - Accept '1'/'0' strings or 1/0 numbers - Collect active day abbreviations in order: Mon, Tue, Wed, Thu, Fri, Sat, Sun - If active indices are consecutive and ≥ 3, show a range: `'Mon–Fri'` - Otherwise comma-list: `'Mon, Wed, Fri'` - If no day fields exist on the record (calendar_dates-only), return `'Specific Days'` - If all days are 0, return `'No regular days'` - [x] Implement `formatDateRange(service: Record<string, unknown>, calendarDates?: Array<{date: string; exception_type: string | number}>): string` - If `service.start_date` exists: return `'${start_date} – ${end_date}'` - Else if `calendarDates` provided with positive exceptions (`exception_type === '1'` or `=== 1`): derive min/max date and return `'${min} – ${max}'` - Else return `''` - [x] Implement `renderRouteReference(route: Record<string, unknown>, opts: RouteReferenceOpts): string` - Layout: `flex items-center gap-3 p-3 rounded-lg hover:bg-base-200 cursor-pointer transition-colors ${ROUTE_REF_ROW}` - `data-route-id="${route.route_id}"` on the container - `data-service-id="${opts.service_id}"` on the container when `service_id` present - Colored dot: `<div class="w-3 h-3 rounded-full flex-shrink-0" style="background-color: #${route.route_color || '6366f1'}"></div>` - Main label: `renderCardLabel(getRouteDisplay(route as Record<string, string>))` — primary = short/long name, secondary = ID - Agency subline: if `agencyName` provided, show it as a small muted line below the route label - Trip count badge: if `tripCount !== undefined`, show `<div class="badge badge-outline badge-sm">${tripCount} trip${tripCount !== 1 ? 's' : ''}</div>` - When `service_id` present: add `<button class="btn btn-xs btn-ghost ${ENTITY_REF_BTN}" data-route-id="${route.route_id}">View Route</button>`. The timetable link lives on the row itself (row click). - When `service_id` absent: no secondary button; clicking the row navigates to the route page. - [x] Implement `renderServiceReference(service: Record<string, unknown>, opts: ServiceReferenceOpts): string` - Layout: `flex items-center gap-3 p-3 rounded-lg hover:bg-base-200 cursor-pointer transition-colors ${SERVICE_REF_ROW}` - `data-service-id="${service.service_id}"` on the container - `data-route-id="${opts.route_id}"` when present - Primary: service_id in bold (use `renderCardLabel(getServiceDisplay(service as Record<string, string>))`) - Days string from `formatDaysOfWeek(service)` — shown as a small muted text if non-empty - Date range from `formatDateRange(service, opts.calendarDates)` — shown as small muted text if non-empty - Trip count badge if provided - Route count badge if provided - When `route_id` present: add `<button class="btn btn-xs btn-ghost ${ENTITY_REF_BTN}" data-service-id="${service.service_id}">View Service</button>`. Timetable link lives on the row. - When `route_id` absent: no secondary button; clicking the row navigates to the service page. **Gotchas:** - Both functions must be pure and synchronous — no fallback DB queries - `route_color` in GTFS has no `#` prefix; always prepend it - If `route.route_color` is empty/undefined, fall back to a neutral color (e.g., `#6366f1`) - `formatDaysOfWeek` must handle records where day fields are completely absent (calendar_dates-only services have only `{ service_id }`) --- ## Phase 2: Refactor Route Page timetables (`page-content-renderer.ts::renderRoute`) Replace the "Services" section with rich service reference rows. Row click → timetable; "View Service" button → service page. - [x] Import `renderServiceReference`, `SERVICE_REF_ROW`, `TIMETABLE_REF_BTN`, `ENTITY_REF_BTN` from `entity-references.ts` - [x] After the existing `allServices` fetch (calendar rows), build a lookup map: `const calendarByServiceId = new Map(allServices.map(s => [s.service_id as string, s]))` - [x] For each entry in `serviceGroups`, call: ```ts renderServiceReference( calendarByServiceId.get(service_id) ?? { service_id }, { tripCount: serviceTrips.length, route_id } ) ``` - [x] Replace the `Object.entries(serviceGroups).map(...)` block with the above calls - [x] Rename section `<h2>` from "Services" to "Timetables" - [x] Keep the `#new-service-select` dropdown (for adding new timetables) exactly as-is — it sits above the reference rows - [x] In `addEventListeners()`: - Remove the `.service-row` click handler (old: navigates to service page) - Remove the `.route-timetable-btn` click handler - Add `SERVICE_REF_ROW` click handler: `onTimetableClick(route_id, service_id)` — get both IDs from `data-*` attributes - Add `ENTITY_REF_BTN` click handler (scoped to route page): navigate to service via `onServiceClick(service_id)` — get `service_id` from `data-service-id` on the button **Gotchas:** - Services that exist in trips but not in calendar (calendar_dates-only): the lookup map won't have them. Fall back to `{ service_id }` — `renderServiceReference` handles this gracefully via `formatDaysOfWeek`'s absent-fields path. - `onServiceClick` is currently `optional` in `ContentRendererDependencies`. Keep defensive guard in the event handler. - The `ENTITY_REF_BTN` class will also appear on the service page (Phase 3) after DOM insertion. Since `addEventListeners` scopes queries to the provided `container`, there's no cross-page interference. - The existing `.service-link` handler in `addEventListeners()` (ln 727–737) should also be removed as it's superseded. --- ## Phase 3: Refactor Service Page timetables (`service-view-controller.ts`) Replace the DaisyUI `table table-pin-rows` (grouped by agency) with route reference rows. Section renamed to "Timetables". Row click → timetable; "View Route" button → route page. - [x] Import `renderRouteReference`, `ROUTE_REF_ROW`, `ENTITY_REF_BTN` from `entity-references.ts` - [x] In `renderServiceView()`, fetch trips for this service and build a trip count map: ```ts const trips = await (this.dependencies.gtfsRelationships?.getTripsForService?.(service_id) ?? this.dependencies.gtfsDatabase?.queryRows('trips', { service_id }) ?? []); const tripCountByRoute = new Map<string, number>(); for (const trip of trips) { const rid = (trip as Record<string, unknown>).route_id as string; tripCountByRoute.set(rid, (tripCountByRoute.get(rid) ?? 0) + 1); } ``` - [x] Build agency name lookup: keep `getAgenciesForRoutes(routes)` call, then build `Map<normalizedAgencyId, agencyName>` from it - [x] Rename `renderRelatedRoutes` to `renderTimetablesSection`; change signature to `(routes: Routes[], agencyNameByNormalizedId: Map<string, string>, tripCountByRoute: Map<string, number>, service_id: string): string` - [x] Inside the renamed method, replace the entire `tableHTML` / `table.table-pin-rows` with route reference rows wrapped in `<div class="space-y-2">` - [x] Update section title from "Routes Using This Service" to "Timetables" - [x] In `addEventListeners()`: - Remove `.route-row` click handler - Remove `.agency-link` click handler - Remove `.timetable-btn` click handler - Add `ROUTE_REF_ROW` click handler: `onTimetableClick(route_id, service_id)` — read both `data-route-id` and `data-service-id` from the row - Add `ENTITY_REF_BTN` click handler: `onRouteClick(route_id)` — read `data-route-id` from the button **Gotchas:** - Routes with no `agency_id` (single-agency GTFS feed): `agencyName` will be `undefined`, which is fine — `renderRouteReference` omits it silently - The DaisyUI `table table-pin-rows` with agency group headers is removed entirely; agency name appears inline in each row instead — this is a deliberate simplification per the issue - `ServiceViewDependencies.onAgencyClick` is currently optional and only used for agency header links. After this phase it is no longer wired up to any rendered element. Leave the dependency in place (don't remove it from the interface) to avoid breaking callers, but it is effectively unused in rendering. --- ## Phase 4: Enrich Home Page service cards (`page-content-renderer.ts::renderHome`) Replace bare service name cards with `renderServiceReference` rows that include days, date range, and counts. - [x] Import `renderServiceReference`, `SERVICE_REF_ROW` at the top of `page-content-renderer.ts` (already importing from Phase 2) - [x] In `renderHome()`, after fetching services, fetch all trips once: ```ts const allTrips = await this.dependencies.gtfsDatabase.getAllRows('trips') as Record<string, unknown>[]; ``` - [x] Build per-service maps: ```ts const tripCountByService = new Map<string, number>(); const routesByService = new Map<string, Set<string>>(); for (const trip of allTrips) { const sid = trip.service_id as string; const rid = trip.route_id as string; tripCountByService.set(sid, (tripCountByService.get(sid) ?? 0) + 1); if (!routesByService.has(sid)) routesByService.set(sid, new Set()); routesByService.get(sid)!.add(rid); } ``` - [x] Replace the `serviceItems` map with calls to `renderServiceReference(serviceData, { tripCount, routeCount })` — no `route_id` (home context) - [x] In `addEventListeners()`: replace `.service-card` click handler with `SERVICE_REF_ROW` click handler → `onServiceClick(service_id)` (updated the existing `SERVICE_REF_ROW` handler to handle both route-page context with `route_id` → timetable, and home-page context without `route_id` → service page) **Gotchas:** - `getAllRows('trips')` adds one extra async DB call on the home page render. This is acceptable given dataset sizes typical of GTFS. - `extraServices` (calendar_dates-only, shaped as `{ service_id }`) will have `tripCount` from the trips map but no day/date fields — `renderServiceReference` handles this gracefully - The existing `data-inline-create="service"` input for creating new services stays in place above the list --- ## Phase 5: Enrich Agency Page route items (`agency-view-controller.ts`) Replace `renderRouteItem` with `renderRouteReference`. No service context (row click → route page). Add trip count per route. - [x] Import `renderRouteReference`, `ROUTE_REF_ROW` from `entity-references.ts` - [x] In `renderAgencyView()`, after fetching routes, fetch trip counts via `queryRows('trips')` (no filter = all rows; `QueryOnlyDatabase` only exposes `queryRows`, not `getAllRows`) - [x] In `renderRoutesList()`, replace `.map(route => this.renderRouteItem(route))` with `.map(route => renderRouteReference(route as Record<string, unknown>, { tripCount: tripCountByRoute.get(route.route_id) }))` - [x] Pass `tripCountByRoute` into `renderRoutesList()` — updated method signature accordingly - [x] Delete the `renderRouteItem()` method - [x] In `addEventListeners()`: replace `.route-item` click handler with `ROUTE_REF_ROW` click handler → `onRouteClick(route_id)` **Gotchas:** - No `agencyName` passed to `renderRouteReference` (we're on the agency page — redundant) - No `service_id` passed — row click → route page, no timetable button - `getAllRows('trips')` filtered client-side is efficient enough for typical GTFS datasets; avoids one query per route - `AgencyViewController` only has `gtfsDatabase` and `onRouteClick` in its deps — both are already available --- ## Original Issue These can be more consistent and a bit more descriptive. We should make proper abstractions so that a browse page is basically just what is described here, then we describe how to display each corresponding item. ### Pages Feed Information - Agencies - Services Agency Properties - Routes Service Schedule - Timetables (each row is a route and should show all route props, then lets make a "View Service" button and the row should default to linking to the timetable, the reverse of what is done now) Route Page - Timetables (each row is a service and should show all service props, then lets make a "View Service" button and the row should default to linking to the timetable, the reverse of what is done now) ### References To a Service - ID - Days of the week (Mon-Fri or Mon,Wed,Fri) - Date range - Number of Trips - Number of Routes - (Lets put lots of information in here for now, if it's simple and performant and obvious, then we can trim it down) To a Route - ID - Color - Name - Agency - Number Trips
maxtkc self-assigned this 2026-04-11 23:53:34 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
gtfs.zone/coloring-book#93
No description provided.