mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
78d6f2ba77
* fix: replace raw day-ID range checks with position-based helper (issue #889 follow-up)
Commit 8e05ba7 fixed the accommodation date-range pickers, but the
post-save state filters in DayDetailPanel and several other consumers
still compared `day.id >= start_day_id && day.id <= end_day_id`. With
non-monotonic ID layouts (day_number 1-9 → IDs 17-25, day_number 10-16
→ IDs 1-7) this made the just-saved accommodation immediately invisible
— matching the regression reported in the last comment of #889.
Introduces `isDayInAccommodationRange` in `client/src/utils/dayOrder.ts`
which compares positional order (`day_number` with `indexOf` fallback)
rather than raw IDs. Falls back to the old numeric comparison when
endpoint days are absent from the loaded array (sparse test data or
partial loads) so existing tests are unaffected.
Fixed call sites:
- DayDetailPanel.tsx (initial load, post-create, post-delete, post-edit-save)
- DayPlanSidebar.tsx (daily badge renderer)
- SharedTripPage.tsx (public share view)
- TripPDF.tsx (PDF export filter + sort)
Also declares `day_number?: number` on the client `Day` type (already
returned by the server but previously untyped).
Adds regression tests FE-PLANNER-DAYDETAIL-060/061/062 covering the
edit-save, create-save, and initial-load paths with the reporter's exact
non-monotonic ID layout.
* fix: non-transport reservations no longer appear as transports in day planner (issue #914)
getTransportForDay now uses TRANSPORT_TYPES allowlist instead of only excluding hotels,
and the click handler dispatches to onEditReservation for non-transport types instead of
always opening TransportModal, preventing silent type coercion to 'flight'.
* feat: add file attachment support to TransportModal (issue #918)
Transports (flight/train/car/cruise) now support file attachments identical to the reservation modal — upload on create/edit, link existing files, and unlink. The Files tab and Assign File modal now differentiate between bookings and transports with separate sections and type-specific icons. Translations added for all 15 locales.
439 lines
9.0 KiB
TypeScript
439 lines
9.0 KiB
TypeScript
// Shared types for the TREK travel planner
|
|
|
|
export interface User {
|
|
id: number
|
|
username: string
|
|
email: string
|
|
role: 'admin' | 'user'
|
|
avatar_url: string | null
|
|
maps_api_key: string | null
|
|
created_at: string
|
|
/** Present after load; true when TOTP MFA is enabled for password login */
|
|
mfa_enabled?: boolean
|
|
/** True when a password change is required before the user can continue */
|
|
must_change_password?: boolean
|
|
}
|
|
|
|
export interface Trip {
|
|
id: number
|
|
name: string
|
|
description: string | null
|
|
start_date: string
|
|
end_date: string
|
|
cover_url: string | null
|
|
is_archived: boolean
|
|
reminder_days: number
|
|
owner_id: number
|
|
created_at: string
|
|
updated_at: string
|
|
}
|
|
|
|
export interface Day {
|
|
id: number
|
|
trip_id: number
|
|
day_number?: number
|
|
date: string
|
|
title: string | null
|
|
notes: string | null
|
|
assignments: Assignment[]
|
|
notes_items: DayNote[]
|
|
}
|
|
|
|
export interface Place {
|
|
id: number
|
|
trip_id: number
|
|
name: string
|
|
description: string | null
|
|
notes: string | null
|
|
lat: number | null
|
|
lng: number | null
|
|
address: string | null
|
|
category_id: number | null
|
|
icon: string | null
|
|
price: string | null
|
|
currency: string | null
|
|
image_url: string | null
|
|
google_place_id: string | null
|
|
osm_id: string | null
|
|
route_geometry: string | null
|
|
place_time: string | null
|
|
end_time: string | null
|
|
duration_minutes: number | null
|
|
transport_mode: string | null
|
|
website: string | null
|
|
phone: string | null
|
|
created_at: string
|
|
}
|
|
|
|
export interface Assignment {
|
|
id: number
|
|
day_id: number
|
|
place_id?: number
|
|
order_index: number
|
|
notes: string | null
|
|
place: Place
|
|
}
|
|
|
|
export interface DayNote {
|
|
id: number
|
|
day_id: number
|
|
text: string
|
|
time: string | null
|
|
icon: string | null
|
|
sort_order?: number
|
|
created_at: string
|
|
}
|
|
|
|
export interface PackingItem {
|
|
id: number
|
|
trip_id: number
|
|
name: string
|
|
category: string | null
|
|
checked: number
|
|
quantity: number
|
|
}
|
|
|
|
export interface TodoItem {
|
|
id: number
|
|
trip_id: number
|
|
name: string
|
|
category: string | null
|
|
checked: number
|
|
sort_order: number
|
|
due_date: string | null
|
|
description: string | null
|
|
assigned_user_id: number | null
|
|
priority: number
|
|
}
|
|
|
|
export interface Tag {
|
|
id: number
|
|
name: string
|
|
color: string | null
|
|
user_id: number
|
|
}
|
|
|
|
export interface Category {
|
|
id: number
|
|
name: string
|
|
icon: string | null
|
|
user_id: number
|
|
}
|
|
|
|
export interface BudgetItem {
|
|
id: number
|
|
trip_id: number
|
|
name: string
|
|
amount: number
|
|
currency: string
|
|
category: string | null
|
|
paid_by: number | null
|
|
persons: number
|
|
members: BudgetMember[]
|
|
expense_date: string | null
|
|
}
|
|
|
|
export interface BudgetMember {
|
|
user_id: number
|
|
paid: boolean
|
|
}
|
|
|
|
export interface ReservationEndpoint {
|
|
id?: number
|
|
reservation_id?: number
|
|
role: 'from' | 'to' | 'stop'
|
|
sequence: number
|
|
name: string
|
|
code: string | null
|
|
lat: number
|
|
lng: number
|
|
timezone: string | null
|
|
local_time: string | null
|
|
local_date: string | null
|
|
}
|
|
|
|
export interface Reservation {
|
|
id: number
|
|
trip_id: number
|
|
name: string
|
|
title?: string
|
|
type: string
|
|
status: 'pending' | 'confirmed'
|
|
date: string | null
|
|
time: string | null
|
|
reservation_time?: string | null
|
|
reservation_end_time?: string | null
|
|
location?: string | null
|
|
confirmation_number: string | null
|
|
notes: string | null
|
|
url: string | null
|
|
day_id?: number | null
|
|
end_day_id?: number | null
|
|
place_id?: number | null
|
|
assignment_id?: number | null
|
|
accommodation_id?: number | null
|
|
accommodation_start_day_id?: number | null
|
|
accommodation_end_day_id?: number | null
|
|
day_plan_position?: number | null
|
|
metadata?: Record<string, string> | string | null
|
|
needs_review?: number
|
|
endpoints?: ReservationEndpoint[]
|
|
created_at: string
|
|
}
|
|
|
|
export interface TripFile {
|
|
id: number
|
|
trip_id: number
|
|
place_id?: number | null
|
|
reservation_id?: number | null
|
|
note_id?: number | null
|
|
uploaded_by?: number | null
|
|
uploaded_by_name?: string | null
|
|
uploaded_by_avatar?: string | null
|
|
filename: string
|
|
original_name: string
|
|
file_size?: number | null
|
|
mime_type: string
|
|
description?: string | null
|
|
starred?: number
|
|
deleted_at?: string | null
|
|
created_at: string
|
|
reservation_title?: string
|
|
linked_reservation_ids?: number[]
|
|
url?: string
|
|
}
|
|
|
|
export interface Settings {
|
|
map_tile_url: string
|
|
default_lat: number
|
|
default_lng: number
|
|
default_zoom: number
|
|
dark_mode: boolean | string
|
|
default_currency: string
|
|
language: string
|
|
temperature_unit: string
|
|
time_format: string
|
|
show_place_description: boolean
|
|
route_calculation?: boolean
|
|
blur_booking_codes?: boolean
|
|
map_booking_labels?: boolean
|
|
map_provider?: 'leaflet' | 'mapbox-gl'
|
|
mapbox_access_token?: string
|
|
mapbox_style?: string
|
|
mapbox_3d_enabled?: boolean
|
|
mapbox_quality_mode?: boolean
|
|
}
|
|
|
|
export interface AssignmentsMap {
|
|
[dayId: string]: Assignment[]
|
|
}
|
|
|
|
export interface DayNotesMap {
|
|
[dayId: string]: DayNote[]
|
|
}
|
|
|
|
export interface RouteSegment {
|
|
mid: [number, number]
|
|
from: [number, number]
|
|
to: [number, number]
|
|
walkingText: string
|
|
drivingText: string
|
|
}
|
|
|
|
export interface RouteResult {
|
|
coordinates: [number, number][]
|
|
distance: number
|
|
duration: number
|
|
distanceText: string
|
|
durationText: string
|
|
walkingText: string
|
|
drivingText: string
|
|
}
|
|
|
|
export interface Waypoint {
|
|
lat: number
|
|
lng: number
|
|
}
|
|
|
|
// User with optional OIDC fields
|
|
export interface UserWithOidc extends User {
|
|
oidc_issuer?: string | null
|
|
}
|
|
|
|
// Accommodation type
|
|
export interface Accommodation {
|
|
id: number
|
|
trip_id: number
|
|
name: string
|
|
address: string | null
|
|
check_in: string | null
|
|
check_in_end: string | null
|
|
check_out: string | null
|
|
confirmation_number: string | null
|
|
notes: string | null
|
|
url: string | null
|
|
created_at: string
|
|
}
|
|
|
|
// Trip member (owner or collaborator)
|
|
export interface TripMember {
|
|
id: number
|
|
username: string
|
|
email?: string
|
|
avatar_url?: string | null
|
|
role?: string
|
|
}
|
|
|
|
// Photo type
|
|
export interface Photo {
|
|
id: number
|
|
trip_id: number
|
|
filename: string
|
|
original_name: string
|
|
mime_type: string
|
|
size: number
|
|
caption: string | null
|
|
place_id: number | null
|
|
day_id: number | null
|
|
created_at: string
|
|
}
|
|
|
|
// Atlas place detail
|
|
export interface AtlasPlace {
|
|
id: number
|
|
name: string
|
|
lat: number | null
|
|
lng: number | null
|
|
}
|
|
|
|
// GeoJSON types (simplified for atlas map)
|
|
export interface GeoJsonFeature {
|
|
type: 'Feature'
|
|
properties: Record<string, string | number | null | undefined>
|
|
geometry: {
|
|
type: string
|
|
coordinates: unknown
|
|
}
|
|
id?: string
|
|
}
|
|
|
|
export interface GeoJsonFeatureCollection {
|
|
type: 'FeatureCollection'
|
|
features: GeoJsonFeature[]
|
|
}
|
|
|
|
// App config from /auth/app-config
|
|
export interface AppConfig {
|
|
has_users: boolean
|
|
allow_registration: boolean
|
|
demo_mode: boolean
|
|
oidc_configured: boolean
|
|
oidc_display_name?: string
|
|
oidc_only_mode?: boolean
|
|
has_maps_key?: boolean
|
|
allowed_file_types?: string
|
|
timezone?: string
|
|
/** When true, users without MFA cannot use the app until they enable it */
|
|
require_mfa?: boolean
|
|
// Granular auth toggles
|
|
password_login?: boolean
|
|
password_registration?: boolean
|
|
oidc_login?: boolean
|
|
oidc_registration?: boolean
|
|
env_override_oidc_only?: boolean
|
|
}
|
|
|
|
// Translation function type
|
|
export type TranslationFn = (key: string, params?: Record<string, string | number | null>) => string
|
|
|
|
// WebSocket event type
|
|
export interface WebSocketEvent {
|
|
type: string
|
|
[key: string]: unknown
|
|
}
|
|
|
|
// Vacay types
|
|
export interface VacayHolidayCalendar {
|
|
id: number
|
|
plan_id: number
|
|
region: string
|
|
label: string | null
|
|
color: string
|
|
sort_order: number
|
|
}
|
|
|
|
export interface VacayPlan {
|
|
id: number
|
|
holidays_enabled: boolean
|
|
holidays_region: string | null
|
|
holiday_calendars: VacayHolidayCalendar[]
|
|
block_weekends: boolean
|
|
carry_over_enabled: boolean
|
|
company_holidays_enabled: boolean
|
|
week_start?: number
|
|
name?: string
|
|
year?: number
|
|
owner_id?: number
|
|
created_at?: string
|
|
updated_at?: string
|
|
}
|
|
|
|
export interface VacayUser {
|
|
id: number
|
|
username: string
|
|
color: string | null
|
|
}
|
|
|
|
export interface VacayEntry {
|
|
date: string
|
|
user_id: number
|
|
plan_id?: number
|
|
person_color?: string
|
|
person_name?: string
|
|
}
|
|
|
|
export interface VacayStat {
|
|
user_id: number
|
|
vacation_days: number
|
|
used: number
|
|
}
|
|
|
|
export interface HolidayInfo {
|
|
name: string
|
|
localName: string
|
|
color: string
|
|
label: string | null
|
|
}
|
|
|
|
export interface HolidaysMap {
|
|
[date: string]: HolidayInfo
|
|
}
|
|
|
|
// API error shape from axios
|
|
export interface ApiError {
|
|
response?: {
|
|
data?: {
|
|
error?: string
|
|
}
|
|
status?: number
|
|
}
|
|
message: string
|
|
}
|
|
|
|
/** Safely extract an error message from an unknown catch value */
|
|
export function getApiErrorMessage(err: unknown, fallback: string): string {
|
|
if (typeof err === 'object' && err !== null && 'response' in err) {
|
|
const apiErr = err as ApiError
|
|
if (apiErr.response?.data?.error) return apiErr.response.data.error
|
|
}
|
|
if (err instanceof Error) return err.message
|
|
return fallback
|
|
}
|
|
|
|
// MergedItem used in day notes hook
|
|
export interface MergedItem {
|
|
type: 'assignment' | 'note' | 'place' | 'transport'
|
|
sortKey: number
|
|
data: Assignment | DayNote | Reservation
|
|
}
|