Files
TREK/client/src/pages/dashboard/dashboardModel.ts
T
Maurice f15a124016 @
fix(dashboard): fix the upcoming widget dropping bookings, and give it the stays back

Three things, all in the same widget.

reservation_time holds two shapes. The booking form writes the datetime-local
YYYY-MM-DDTHH:MM; day-anchored rows, the demo seed among them, hold a bare
HH:MM. The query compared the column against an ISO timestamp as a string,
where ":" sorts above "2": every booking from 20:00 on passed the filter and
everything before it silently vanished, and the order ran off the same column.
On the demo that is why the Broadway show shows and the ferry does not. The
query now separates the date from the time before it filters, so a value with
no date falls back to the day it hangs off instead of being read as one, and
the order is by day and clock. The demo seed writes full timestamps, which is
the shape every real write path already produces.

Hotels came out of this list because a stay covers a range and a week in one
would hold a slot for the whole week. Arriving and leaving do not: they are
moments, and they are the two worth reminding someone of. Check-in and
check-out now appear as their own entries, named after the stay place and
falling back to the linked booking. The stay itself stays out. The two carry
the accommodation id, so the list key had to take the type with it.

An unconfirmed booking looked exactly like a confirmed one. The status was
already in the payload and is now a badge, on both the desktop tool and the
phone widget.
@
2026-08-27 15:38:45 +02:00

85 lines
3.4 KiB
TypeScript

/**
* Dashboard data model + pure helpers — shared by the data hook (useDashboard)
* and the presentational components in DashboardPage. Kept free of React/IO so
* both sides can import it without a cycle. Part of the FE "page = wiring
* container + data hook" convention (see dashboard/README.md).
*/
import type { Trip } from '../../types'
import { localIsoDate } from '../../utils/localDate'
// The dashboard works with the canonical Trip shape returned by the list/get
// endpoints (it already carries the computed day_count/place_count/is_owner/
// owner_username/shared_count fields). Kept as a named alias so the existing
// imports stay stable.
export type DashboardTrip = Trip
export interface Member { id: number; username: string; avatar_url?: string | null }
export interface Place {
id: number; name: string; image_url: string | null; lat: number | null; lng: number | null
google_place_id: string | null; osm_id: string | null
category_color?: string | null; category_icon?: string | null
}
export interface HeroBundle { members: Member[]; places: Place[] }
export interface TravelStats { totalTrips?: number; totalDays?: number; totalPlaces?: number; totalDistanceKm?: number; countries?: string[] }
export interface UpcomingReservation {
/** Unique per `type`, not on its own: a stay's two moments carry the
* accommodation id, which can collide with a reservation id. */
id: number; trip_id: number; title: string
/** A reservation type, or 'checkin' / 'checkout' for a stay's two moments. */
type: string
status?: string | null
reservation_time?: string | null; day_date?: string | null
location?: string | null; place_name?: string | null; trip_title?: string | null
}
/** Stable list key — see the note on `id`. */
export function upcomingKey(r: UpcomingReservation): string {
return `${r.type}:${r.id}`
}
export const MS_PER_DAY = 86400000
/**
* Today as a local-calendar 'YYYY-MM-DD' — see utils/localDate for why this
* must never come from toISOString(). Trip dates are wall-clock dates
* (daysUntil below parses them as local midnight).
*/
export function localIsoToday(): string {
return localIsoDate()
}
export function daysUntil(dateStr: string | null | undefined): number | null {
if (!dateStr) return null
const today = new Date(); today.setHours(0, 0, 0, 0)
const d = new Date(dateStr + 'T00:00:00'); d.setHours(0, 0, 0, 0)
return Math.round((d.getTime() - today.getTime()) / MS_PER_DAY)
}
export function getTripStatus(trip: DashboardTrip): 'ongoing' | 'today' | 'tomorrow' | 'future' | 'past' | null {
const today = localIsoToday()
if (trip.start_date && trip.end_date && trip.start_date <= today && trip.end_date >= today) return 'ongoing'
const until = daysUntil(trip.start_date)
if (until === null) return null
if (until === 0) return 'today'
if (until === 1) return 'tomorrow'
if (until > 1) return 'future'
return 'past'
}
export function sortTrips(trips: DashboardTrip[]): DashboardTrip[] {
const today = localIsoToday()
const rank = (t: DashboardTrip) => {
if (t.start_date && t.end_date && t.start_date <= today && t.end_date >= today) return 0
if (t.start_date && t.start_date >= today) return 1
return 2
}
return [...trips].sort((a, b) => {
const ra = rank(a), rb = rank(b)
if (ra !== rb) return ra - rb
const ad = a.start_date || '', bd = b.start_date || ''
if (ra <= 1) return ad.localeCompare(bd)
return bd.localeCompare(ad)
})
}