Files
TREK/client/src/types.ts
T
Elmer 7bd9e08016 feat(vacay): add school holiday overlays (#1633)
* feat: add vacay school holiday overlays

* fix(vacay): remove SOH control character corruption in locale files

13 of 22 vacay locale files contained a 0x01 (SOH) control character
that corrupted the closing }; token. This broke the shared package
build (tsdown/rolldown PARSE_ERROR: Invalid Character) and cascaded
to all client tests failing to resolve @trek/shared.

Affected locales: ar, br, ca, cs, es, fr, gr, hu, id, it, ja, ko,
nl, pl, ru, sv, tr, uk, vi, zh, zh-TW

* fix(vacay): school-holiday migration order, i18n dupes, settings layout

- Move the school-holiday columns migration to the end of the migrations
  array. It sat before the #1435/#1281 migrations, which every existing DB
  had already run, so the runner skipped it and school_holidays_enabled /
  vacay_holiday_calendars.type were never created — PUT /addons/vacay/plan
  then 500'd with "no such column".
- Remove duplicate i18n keys in 13 locale vacay files (TS1117) that failed
  the shared + client typechecks.
- Move the school-holidays block into the right column, directly under
  public holidays, instead of full-width below everything.
- Add the school-holidays section to the mobile Vacay settings sheet.

* feat(vacay): modern school-holiday day styling + combined hover tooltip

- Replace the stacked 2px underlines with a single rounded accent band and a
  soft background wash on plain school-break days (desktop + mobile — mobile
  did not render school holidays at all before).
- Fold school holidays into the custom day tooltip under a divider instead of a
  separate native title, and show the tooltip on every set day (whole vacation
  days too), not only half days / shared calendars.

---------

Co-authored-by: Otto <uif86164@automotive-wan.com>
Co-authored-by: Maurice <mauriceboe@icloud.com>
2026-07-23 22:48:42 +02:00

390 lines
9.3 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,
AppearanceConfig,
} from '@trek/shared'
export type {
Trip,
TripMember,
Day,
DayNote,
Place,
AssignmentPlace,
PlaceCategory,
Assignment,
AssignmentParticipant,
PackingItem,
PackingBag,
PackingBagMember,
BudgetItem,
BudgetItemMember,
Reservation,
ReservationEndpoint,
Accommodation,
Tag,
Category,
AppearanceConfig,
}
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 type DistanceUnit = 'metric' | 'imperial'
export interface Settings {
map_tile_url: string
dark_mode: boolean | string
/** Display currency for Costs. Empty/null = follow each trip's own currency. */
default_currency: string | null
language: string
temperature_unit: string
distance_unit?: DistanceUnit
time_format: string
show_place_description: boolean
blur_booking_codes?: boolean
map_booking_labels?: boolean
map_poi_pill_enabled?: boolean
map_always_show_routes?: boolean
optimize_from_accommodation?: boolean
map_provider?: 'leaflet' | 'mapbox-gl' | 'maplibre-gl'
mapbox_access_token?: string
mapbox_style?: string
maplibre_style?: string
mapbox_3d_enabled?: boolean
mapbox_quality_mode?: boolean
// Dashboard widget prefs — persisted server-side so a (docker) upgrade keeps them (#1311).
dashboard_fx_from?: string
dashboard_fx_to?: string
dashboard_timezones?: string[]
// AI booking-import fallback (per-user config; used when the admin has not set
// instance-wide config on the llm_parsing addon). llm_api_key is masked on read.
llm_provider?: 'local' | 'openai' | 'anthropic'
llm_model?: string
llm_base_url?: string
llm_multimodal?: boolean
llm_api_key?: string
/** Per-user appearance/customization config (theming, transparency, typography, dashboard widgets). */
appearance?: AppearanceConfig
}
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
/** Extra text a plugin route attached to this leg (e.g. "25 min charge"). */
noteText?: string
/** The travel mode this leg was routed with (#1281) — drives the connector icon. */
mode?: string
}
/** An intermediate stop a plugin route places on the drawn line (charging stop, rest area). */
export interface RouteVia {
lat: number
lng: number
label?: string
tone: 'default' | 'success' | 'warn' | 'danger'
dwellSeconds?: number
}
export interface RouteWithLegs {
coordinates: [number, number][]
distance: number
duration: number
legs: RouteSegment[]
/** Present on plugin-provided routes only. */
vias?: RouteVia[]
}
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
type?: 'public_holiday' | 'school_holiday'
region: string
label: string | null
color: string
sort_order: number
}
export interface VacayPlan {
id: number
holidays_enabled: boolean
school_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
// Portion of a vacation day this entry counts as: 1 = full day, 0.5 = half
// day (#552). Absent on legacy entries, which are treated as full days.
fraction?: number
}
// 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
type?: 'public_holiday' | 'school_holiday'
}
export interface HolidaysMap {
[date: string]: HolidayInfo | HolidayInfo[]
}
// Read-only calendar shares (#444/#667)
export interface VacayShareOutgoing {
id: number
user_id: number
username: string
}
export interface VacayShareIncoming {
id: number
owner_id: number
username: string
color: string
hidden: boolean
}
export interface SharedVacayCalendar {
share_id: number
owner_id: number
owner_name: string
color: string
hidden: boolean
entries: { date: string; fraction?: number }[]
companyHolidays: { date: string; note?: string }[]
}
// 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
}