import React, { useEffect, useMemo, useState } from 'react' import { Settings2 } from 'lucide-react' import { adminApi } from '../../api/client' import { useTranslation } from '../../i18n' import { useToast } from '../shared/Toast' import Section from '../Settings/Section' import CustomSelect from '../shared/CustomSelect' import { MapView } from '../Map/MapView' import { SYMBOLS, currenciesWith } from '../Budget/BudgetPanel.constants' import type { DistanceUnit, Place } from '../../types' import { MAPBOX_DEFAULT_STYLE, defaultStyleForProvider, getStylePresets, isOpenFreeMapStyle, normalizeStyleForProvider, styleSettingKey, type GlMapProvider, } from '../Map/glProviders' const MAP_PRESETS = [ { name: 'OpenStreetMap', url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' }, { name: 'OpenStreetMap DE', url: 'https://tile.openstreetmap.de/{z}/{x}/{y}.png' }, { name: 'CartoDB Light', url: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png' }, { name: 'CartoDB Dark', url: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png' }, { name: 'Stadia Smooth', url: 'https://tiles.stadiamaps.com/tiles/alidade_smooth/{z}/{x}/{y}{r}.png' }, ] type Defaults = { temperature_unit?: string distance_unit?: DistanceUnit dark_mode?: string | boolean time_format?: string default_currency?: string blur_booking_codes?: boolean map_tile_url?: string map_provider?: string mapbox_access_token?: string mapbox_style?: string maplibre_style?: string mapbox_3d_enabled?: boolean mapbox_quality_mode?: boolean } type MapProvider = 'leaflet' | GlMapProvider function normalizeProvider(value: unknown): MapProvider { return value === 'mapbox-gl' || value === 'maplibre-gl' ? value : 'leaflet' } function styleForProvider(provider: MapProvider, style?: string | null): string { if (provider === 'leaflet') return style || MAPBOX_DEFAULT_STYLE if (provider === 'mapbox-gl' && isOpenFreeMapStyle(style)) return MAPBOX_DEFAULT_STYLE return normalizeStyleForProvider(provider, style) } function OptionRow({ label, hint, children, }: { label: React.ReactNode hint?: string children: React.ReactNode }) { return (
{hint &&

{hint}

}
{children}
) } function OptionButton({ active, onClick, children, }: { active: boolean onClick: () => void children: React.ReactNode }) { return ( ) } export default function DefaultUserSettingsTab(): React.ReactElement { const { t } = useTranslation() const toast = useToast() const [defaults, setDefaults] = useState({}) const [loaded, setLoaded] = useState(false) const [mapTileUrl, setMapTileUrl] = useState('') const [mapboxToken, setMapboxToken] = useState('') const [mapboxStyle, setMapboxStyle] = useState('') useEffect(() => { adminApi.getDefaultUserSettings().then((data: Defaults) => { const provider = normalizeProvider(data.map_provider) setDefaults(data) setMapTileUrl(data.map_tile_url || '') setMapboxToken(data.mapbox_access_token || '') setMapboxStyle(provider === 'leaflet' ? (data.mapbox_style || '') : styleForProvider(provider, provider === 'maplibre-gl' ? data.maplibre_style : data.mapbox_style)) setLoaded(true) }).catch(() => setLoaded(true)) }, []) const save = async (patch: Partial) => { try { const updated = await adminApi.updateDefaultUserSettings(patch as Record) setDefaults(updated) toast.success(t('admin.defaultSettings.saved')) } catch (err: unknown) { toast.error(err instanceof Error ? err.message : t('common.error')) } } const reset = async (key: keyof Defaults) => { try { const updated = await adminApi.updateDefaultUserSettings({ [key]: null }) setDefaults(updated) if (key === 'map_tile_url') setMapTileUrl('') if (key === 'mapbox_access_token') setMapboxToken('') if (key === 'mapbox_style' || key === 'maplibre_style') { const provider = normalizeProvider(defaults.map_provider) setMapboxStyle(provider === 'leaflet' ? '' : defaultStyleForProvider(provider)) } toast.success(t('admin.defaultSettings.reset')) } catch (err: unknown) { toast.error(err instanceof Error ? err.message : t('common.error')) } } const isSet = (key: keyof Defaults) => defaults[key] !== undefined const ResetButton = ({ field }: { field: keyof Defaults }) => isSet(field) ? ( ) : null const mapPreviewPlaces = useMemo((): Place[] => [{ id: 1, trip_id: 1, name: 'Preview center', description: null, notes: null, lat: 48.8566, lng: 2.3522, address: null, category_id: null, price: null, currency: null, image_url: null, google_place_id: null, osm_id: null, route_geometry: null, place_time: null, end_time: null, duration_minutes: null, transport_mode: null, website: null, phone: null, created_at: Date(), }], []) if (!loaded) { return

Loading…

} const darkMode = defaults.dark_mode const mapProvider = normalizeProvider(defaults.map_provider) const glStylePresets = mapProvider === 'leaflet' ? [] : getStylePresets(mapProvider) const styleKey: keyof Defaults = mapProvider === 'maplibre-gl' ? 'maplibre_style' : 'mapbox_style' const saveMapProvider = (nextProvider: MapProvider) => { const patch: Partial = { map_provider: nextProvider } if (nextProvider !== 'leaflet') { // Load + save the new provider's own style slot so the other provider's style is kept. const slot = nextProvider === 'maplibre-gl' ? defaults.maplibre_style : defaults.mapbox_style const nextStyle = styleForProvider(nextProvider, slot) setMapboxStyle(nextStyle) patch[styleSettingKey(nextProvider)] = nextStyle } save(patch) } return (

{t('admin.defaultSettings.description')}

{/* Color Mode */} {t('settings.colorMode')} }> {([ { value: 'light', label: t('settings.light') }, { value: 'dark', label: t('settings.dark') }, { value: 'auto', label: t('settings.auto') }, ] as const).map(opt => ( save({ dark_mode: opt.value })} > {opt.label} ))} {/* Temperature */} {t('settings.temperature')} }> {([ { value: 'celsius', label: '°C Celsius' }, { value: 'fahrenheit', label: '°F Fahrenheit' }, ] as const).map(opt => ( save({ temperature_unit: opt.value })} > {opt.label} ))} {/* Distance */} {t('settings.distance')} }> {([ { value: 'metric', label: 'km Metric' }, { value: 'imperial', label: 'mi Imperial' }, ] as const).map(opt => ( save({ distance_unit: opt.value })} > {opt.label} ))} {/* Time Format */} {t('settings.timeFormat')} }> {([ { value: '24h', label: '24h (14:30)' }, { value: '12h', label: '12h (2:30 PM)' }, ] as const).map(opt => ( save({ time_format: opt.value })} > {opt.label} ))} {/* Default Currency */}
{ if (value) save({ default_currency: value }) }} placeholder={t('settings.currency')} searchable options={currenciesWith(defaults.default_currency).map(c => ({ value: c, label: SYMBOLS[c] ? `${c} ${SYMBOLS[c]}` : c }))} size="sm" style={{ maxWidth: 240 }} />

{t('settings.currencyHint')}

{/* Blur Booking Codes */} {t('settings.blurBookingCodes')} }> {([ { value: true, label: t('settings.on') || 'On' }, { value: false, label: t('settings.off') || 'Off' }, ] as const).map(opt => ( save({ blur_booking_codes: opt.value })} > {opt.label} ))} {/* Map Tile URL */}
{ if (value) { setMapTileUrl(value); save({ map_tile_url: value }) } }} placeholder={t('settings.mapTemplatePlaceholder.select')} options={MAP_PRESETS.map(p => ({ value: p.url, label: p.name }))} size="sm" style={{ marginBottom: 8 }} /> ) => setMapTileUrl(e.target.value)} onBlur={() => save({ map_tile_url: mapTileUrl })} placeholder="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent" />

{t('settings.mapDefaultHint')}

{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} {React.createElement(MapView as any, { places: mapPreviewPlaces, dayPlaces: [], route: null, routeSegments: null, selectedPlaceId: null, onMarkerClick: null, onMapClick: null, onMapContextMenu: null, center: [48.8566, 2.3522], zoom: 10, tileUrl: mapTileUrl, fitKey: null, dayOrderMap: [], leftWidth: 0, rightWidth: 0, hasInspector: false, })}
{/* ── Map provider / instance-wide Mapbox ───────────────────────── */}
{t('admin.defaultSettings.mapProvider')} } hint={t('admin.defaultSettings.mapProviderHint')} > {([ { value: 'leaflet', label: t('admin.defaultSettings.providerLeaflet') }, { value: 'mapbox-gl', label: t('admin.defaultSettings.providerMapbox') }, { value: 'maplibre-gl', label: t('admin.defaultSettings.providerMapLibre') }, ] as const).map(opt => ( saveMapProvider(opt.value)} > {opt.label} ))} {mapProvider !== 'leaflet' && (
{mapProvider === 'mapbox-gl' && (
) => setMapboxToken(e.target.value)} onBlur={() => save({ mapbox_access_token: mapboxToken })} placeholder="pk.eyJ…" spellCheck={false} autoComplete="off" className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent" />

{t('admin.defaultSettings.mapboxTokenHint')}

)}
{ if (value) { setMapboxStyle(value); save({ [styleKey]: value }) } }} placeholder={t('admin.defaultSettings.mapboxStylePlaceholder')} options={glStylePresets.map(p => ({ value: p.url, label: p.name }))} size="sm" style={{ marginBottom: 8 }} /> ) => setMapboxStyle(e.target.value)} onBlur={() => { const nextStyle = normalizeStyleForProvider(mapProvider, mapboxStyle) setMapboxStyle(nextStyle) save({ [styleKey]: nextStyle }) }} placeholder={defaultStyleForProvider(mapProvider)} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent" />
{mapProvider === 'mapbox-gl' && ( <> {t('admin.defaultSettings.mapbox3d')} }> {([ { value: true, label: t('settings.on') || 'On' }, { value: false, label: t('settings.off') || 'Off' }, ] as const).map(opt => ( save({ mapbox_3d_enabled: opt.value })}> {opt.label} ))} {t('admin.defaultSettings.mapboxQuality')} }> {([ { value: true, label: t('settings.on') || 'On' }, { value: false, label: t('settings.off') || 'Off' }, ] as const).map(opt => ( save({ mapbox_quality_mode: opt.value })}> {opt.label} ))} )}
)}
) }