Files
TREK/client/src/types.ts
T
Maurice 1378c95078 Explore places on the map, planner route fixes, and instance-wide Mapbox (#1147)
* feat(maps): add an OSM POI search endpoint (category within a viewport)

New /api/maps/pois queries OpenStreetMap via Overpass for places of a category
(restaurants, cafes, hotels, sights, …) inside a bounding box. OSM-only by design
— it never calls Google, even when a Google key is configured.

* feat(map): explore nearby places on the trip map (OSM category pill)

A floating, icon-only pill over the planner map lets you toggle a POI category and
see those OpenStreetMap places in the current view; clicking a marker opens the
add-place form pre-filled (name, address, website, phone). Single-select with a
'search this area' action after the map moves. Renders on both the Leaflet and
Mapbox maps, and can be turned off in settings (discussion #841).

* fix(planner): anchor timed places when optimising and route transports by location

- The day optimiser no longer reshuffles places that have a set time — they stay
  anchored to their time, like locked places.
- The route now uses a transport's departure/arrival location as a waypoint when it
  has one (e.g. a flight's airport), instead of breaking the route at every booking;
  transports without a location are ignored for routing but still show their leg's
  distance/duration under the booking.

* feat(admin): instance-wide Mapbox defaults in default user settings

Admins can set a shared Mapbox token (plus style, 3D and quality) as instance
defaults, so the whole instance can use Mapbox without each user pasting their own
key. Users without their own value inherit it via the existing admin-defaults
merge; the shared token is stored encrypted (discussion #920).
2026-06-11 23:42:16 +02:00

326 lines
7.1 KiB
TypeScript

// Shared types for the TREK travel planner.
//
// Domain entity/response types are now sourced from @trek/shared — the single
// source of truth shared with the server. The Zod schemas there are built to
// match the REAL server response shapes (see shared/src/<domain>/*.schema.ts,
// each documented against the producing service). Re-exported here so the rest
// of the client keeps importing from '../types' unchanged.
import type {
Trip,
TripMember,
Day,
DayNote,
Place,
AssignmentPlace,
PlaceCategory,
Assignment,
AssignmentParticipant,
PackingItem,
PackingBag,
PackingBagMember,
BudgetItem,
BudgetItemMember,
Reservation,
ReservationEndpoint,
Accommodation,
Tag,
Category,
} from '@trek/shared'
export type {
Trip,
TripMember,
Day,
DayNote,
Place,
AssignmentPlace,
PlaceCategory,
Assignment,
AssignmentParticipant,
PackingItem,
PackingBag,
PackingBagMember,
BudgetItem,
BudgetItemMember,
Reservation,
ReservationEndpoint,
Accommodation,
Tag,
Category,
}
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 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 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 | null)[]
linked_place_ids?: (number | null)[]
/** Served download path — always present on list/create/update responses (formatFile). */
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
blur_booking_codes?: boolean
map_booking_labels?: boolean
map_poi_pill_enabled?: boolean
optimize_from_accommodation?: 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]
distance: number
duration: number
walkingText: string
drivingText: string
distanceText: string
durationText?: string
}
export interface RouteWithLegs {
coordinates: [number, number][]
distance: number
duration: number
legs: RouteSegment[]
}
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
}
// Optional fixed start/end points for route optimization (e.g. the day's accommodation).
export interface RouteAnchors {
start?: Waypoint
end?: Waypoint
}
// User with optional OIDC fields
export interface UserWithOidc extends User {
oidc_issuer?: string | null
}
// 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
// Comma-separated weekday indices (e.g. '0,6'); stored as TEXT on vacay_plans.
weekend_days?: string
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
}
// Vacay per-user stats row as returned by getStats
// (server/src/services/vacayService.ts -> getStats).
export interface VacayStat {
user_id: number
person_name: string
person_color: string
year: number
vacation_days: number
carried_over: number
total_available: number
used: number
remaining: 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
}