mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 21:31:46 +00:00
1378c95078
* 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).
326 lines
7.1 KiB
TypeScript
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
|
|
}
|