ec11dd8d2b
Benutzerweiter Vessel-Pool (E2E, Sync, Migration von Legacy-Yachts) mit LogbookVesselSelection und LogbookVesselPicker. Profil mit Accordion (Flotte & Crew); Demo und Onboarding-Tour inkl. profile_vessel_pool. Co-authored-by: Cursor <cursoragent@cursor.com>
413 lines
12 KiB
TypeScript
413 lines
12 KiB
TypeScript
import { parseTrackFile } from './trackUpload.js'
|
|
import { computeTrackStats } from '../utils/trackStats.js'
|
|
import i18n from '../i18n/index.js'
|
|
import { isGermanLocale } from '../utils/i18nLanguages.js'
|
|
|
|
import kielLaboeGpx from '../assets/demo/kiel-laboe.gpx?raw'
|
|
import laboeDampGpx from '../assets/demo/laboe-damp.gpx?raw'
|
|
import dampSchleimuendeGpx from '../assets/demo/damp-schleimuende.gpx?raw'
|
|
|
|
/** Stable ID for the first demo travel day (public demo tour highlight). */
|
|
export const PUBLIC_DEMO_FIRST_ENTRY_ID = 'a0000001-0000-4000-8000-000000000001'
|
|
|
|
const PUBLIC_DEMO_ENTRY_IDS = [
|
|
PUBLIC_DEMO_FIRST_ENTRY_ID,
|
|
'a0000001-0000-4000-8000-000000000002',
|
|
'a0000001-0000-4000-8000-000000000003'
|
|
] as const
|
|
|
|
export const PUBLIC_DEMO_SKIPPER_ID = 'skipper'
|
|
export const PUBLIC_DEMO_VESSEL_ID = 'demo-vessel-1'
|
|
const PUBLIC_DEMO_CREW_MEMBER_ID = 'a0000001-0000-4000-8000-000000000010'
|
|
|
|
export interface DemoDaySpec {
|
|
date: string
|
|
dayOfTravel: string
|
|
departure: string
|
|
destination: string
|
|
gpx: string
|
|
filename: string
|
|
freshwater: { morning: number; refilled: number; evening: number; consumption: number }
|
|
fuel: { morning: number; refilled: number; evening: number; consumption: number }
|
|
greywaterLevel?: number
|
|
motorHours?: number
|
|
events: Array<Record<string, string>>
|
|
}
|
|
|
|
export interface DemoCrewRecord {
|
|
payloadId: string
|
|
data: {
|
|
name: string
|
|
address: string
|
|
birthDate: string
|
|
phone: string
|
|
nationality: string
|
|
passportNumber: string
|
|
bloodType: string
|
|
allergies: string
|
|
diseases: string
|
|
role: 'skipper' | 'crew'
|
|
photo: string | null
|
|
}
|
|
}
|
|
|
|
export interface DemoVesselRecord {
|
|
payloadId: string
|
|
data: Record<string, unknown> & { name: string }
|
|
}
|
|
|
|
export interface PublicDemoFixture {
|
|
title: string
|
|
yacht: Record<string, unknown>
|
|
vesselPool: DemoVesselRecord[]
|
|
logbookVesselSelection: {
|
|
activeVesselId: string | null
|
|
vesselSnapshot: Record<string, unknown> | null
|
|
}
|
|
/** @deprecated legacy share payload */
|
|
crews: DemoCrewRecord[]
|
|
personPool: DemoCrewRecord[]
|
|
logbookCrewSelection: {
|
|
activeSkipperId: string
|
|
activeCrewIds: string[]
|
|
snapshotsById: Record<string, DemoCrewRecord['data'] & { id: string }>
|
|
}
|
|
entries: Array<Record<string, unknown> & { payloadId: string }>
|
|
gpsTracks: Array<{ entryId: string; waypoints: unknown[]; filename: string; gpxContent?: string; fileType: string }>
|
|
photos: never[]
|
|
firstEntryId: string
|
|
}
|
|
|
|
export function buildDemoDays(): DemoDaySpec[] {
|
|
const isDe = isGermanLocale(i18n.language)
|
|
return [
|
|
{
|
|
date: '2026-05-29',
|
|
dayOfTravel: '1',
|
|
departure: 'Kiel',
|
|
destination: 'Laboe',
|
|
gpx: kielLaboeGpx,
|
|
filename: 'kiel-laboe.gpx',
|
|
freshwater: { morning: 120, refilled: 0, evening: 105, consumption: 15 },
|
|
fuel: { morning: 85, refilled: 0, evening: 78, consumption: 7 },
|
|
greywaterLevel: 25,
|
|
events: [
|
|
{
|
|
time: '10:15',
|
|
mgk: '042',
|
|
rwk: '038',
|
|
windDirection: 'NW',
|
|
windStrength: '4 Bft',
|
|
seaState: isDe ? 'leicht bewegt' : 'slight',
|
|
sailsOrMotor: isDe ? 'Großsegel + Genua' : 'Mainsail + Genoa',
|
|
remarks: isDe ? 'Abfahrt Kiellinie' : 'Departure Kiellinie'
|
|
},
|
|
{
|
|
time: '11:20',
|
|
mgk: '030',
|
|
rwk: '028',
|
|
windDirection: 'N',
|
|
windStrength: '3 Bft',
|
|
seaState: isDe ? 'ruhig' : 'calm',
|
|
sailsOrMotor: isDe ? 'Großsegel + Genua' : 'Mainsail + Genoa',
|
|
remarks: isDe ? 'Ankunft Laboe' : 'Arrival Laboe'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
date: '2026-05-30',
|
|
dayOfTravel: '2',
|
|
departure: 'Laboe',
|
|
destination: 'Damp',
|
|
gpx: laboeDampGpx,
|
|
filename: 'laboe-damp.gpx',
|
|
freshwater: { morning: 105, refilled: 25, evening: 110, consumption: 20 },
|
|
fuel: { morning: 78, refilled: 0, evening: 70, consumption: 8 },
|
|
greywaterLevel: 38,
|
|
motorHours: 1.5,
|
|
events: [
|
|
{
|
|
time: '09:00',
|
|
mgk: '055',
|
|
rwk: '050',
|
|
windDirection: 'NE',
|
|
windStrength: '3 Bft',
|
|
seaState: isDe ? 'leicht bewegt' : 'slight',
|
|
sailsOrMotor: isDe ? 'Großsegel + Genua' : 'Mainsail + Genoa',
|
|
remarks: isDe ? 'Auslaufen aus Laboe' : 'Departing Laboe'
|
|
},
|
|
{
|
|
time: '12:30',
|
|
mgk: '075',
|
|
rwk: '068',
|
|
windDirection: 'E',
|
|
windStrength: '4 Bft',
|
|
seaState: isDe ? 'mäßig bewegt' : 'moderate',
|
|
sailsOrMotor: isDe ? 'Großsegel + Genua' : 'Mainsail + Genoa',
|
|
remarks: isDe ? 'Kurs entlang der Küste' : 'Coastal passage'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
date: '2026-05-31',
|
|
dayOfTravel: '3',
|
|
departure: 'Damp',
|
|
destination: 'Schleimünde',
|
|
gpx: dampSchleimuendeGpx,
|
|
filename: 'damp-schleimuende.gpx',
|
|
freshwater: { morning: 110, refilled: 0, evening: 95, consumption: 15 },
|
|
fuel: { morning: 70, refilled: 15, evening: 80, consumption: 5 },
|
|
greywaterLevel: 52,
|
|
events: [
|
|
{
|
|
time: '08:30',
|
|
mgk: '290',
|
|
rwk: '285',
|
|
windDirection: 'W',
|
|
windStrength: '4 Bft',
|
|
seaState: isDe ? 'mäßig bewegt' : 'moderate',
|
|
sailsOrMotor: isDe ? 'Großsegel + Genua' : 'Mainsail + Genoa',
|
|
remarks: isDe ? 'Passage zur Schlei' : 'Passage toward Schlei'
|
|
},
|
|
{
|
|
time: '14:00',
|
|
mgk: '310',
|
|
rwk: '305',
|
|
windDirection: 'NW',
|
|
windStrength: '3 Bft',
|
|
seaState: isDe ? 'leicht bewegt' : 'slight',
|
|
sailsOrMotor: isDe ? 'Großsegel + Genua' : 'Mainsail + Genoa',
|
|
remarks: isDe ? 'Ziel Schleimünde' : 'Destination Schleimünde'
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
|
|
export function buildDemoYachtData(): Record<string, unknown> {
|
|
const isDe = isGermanLocale(i18n.language)
|
|
return {
|
|
name: 'Seeadler',
|
|
vesselType: isDe ? 'Segelyacht' : 'Sailing yacht',
|
|
lengthM: 12.5,
|
|
draftM: 1.9,
|
|
airDraftM: 18,
|
|
homePort: 'Kiel',
|
|
charterCompany: '',
|
|
owner: 'Demo Skipper',
|
|
registrationNumber: 'D-KI 1234',
|
|
callSign: 'DA1234',
|
|
atis: '',
|
|
mmsi: '',
|
|
sails: isDe ? ['Großsegel', 'Genua', 'Spinnaker'] : ['Mainsail', 'Genoa', 'Spinnaker'],
|
|
photo: null,
|
|
freshwaterCapacityL: 200,
|
|
fuelCapacityL: 100,
|
|
greywaterCapacityL: 80
|
|
}
|
|
}
|
|
|
|
export function buildDemoPersonPool(): DemoCrewRecord[] {
|
|
return buildDemoCrewRecords()
|
|
}
|
|
|
|
export function buildDemoCrewRecords(): DemoCrewRecord[] {
|
|
const isDe = isGermanLocale(i18n.language)
|
|
return [
|
|
{
|
|
payloadId: PUBLIC_DEMO_SKIPPER_ID,
|
|
data: {
|
|
name: 'Demo Skipper',
|
|
address: isDe ? 'Am Hafen 12, 24103 Kiel' : 'Harbour Quay 12, 24103 Kiel',
|
|
birthDate: '1980-06-15',
|
|
phone: '+49 431 987654',
|
|
nationality: isDe ? 'Deutsch' : 'German',
|
|
passportNumber: 'C12X34Y56',
|
|
bloodType: '0+',
|
|
allergies: '',
|
|
diseases: '',
|
|
role: 'skipper',
|
|
photo: null
|
|
}
|
|
},
|
|
{
|
|
payloadId: PUBLIC_DEMO_CREW_MEMBER_ID,
|
|
data: {
|
|
name: 'Anna Müller',
|
|
address: isDe ? 'Hafenstraße 1, 24103 Kiel' : 'Harbour St 1, 24103 Kiel',
|
|
birthDate: '1988-04-12',
|
|
phone: '+49 431 123456',
|
|
nationality: isDe ? 'Deutsch' : 'German',
|
|
passportNumber: 'C01X00T47',
|
|
bloodType: 'A+',
|
|
allergies: '',
|
|
diseases: '',
|
|
role: 'crew',
|
|
photo: null
|
|
}
|
|
}
|
|
]
|
|
}
|
|
|
|
function buildDemoVesselPool(yacht: Record<string, unknown>): DemoVesselRecord[] {
|
|
return [
|
|
{
|
|
payloadId: PUBLIC_DEMO_VESSEL_ID,
|
|
data: { name: String(yacht.name ?? 'Demo'), ...yacht }
|
|
}
|
|
]
|
|
}
|
|
|
|
function buildDemoLogbookVesselSelection(
|
|
yacht: Record<string, unknown>
|
|
): PublicDemoFixture['logbookVesselSelection'] {
|
|
return {
|
|
activeVesselId: PUBLIC_DEMO_VESSEL_ID,
|
|
vesselSnapshot: { id: PUBLIC_DEMO_VESSEL_ID, ...yacht }
|
|
}
|
|
}
|
|
|
|
function buildDemoLogbookCrewSelection(pool: DemoCrewRecord[]) {
|
|
const skipper = pool.find((p) => p.data.role === 'skipper')
|
|
const crew = pool.filter((p) => p.data.role === 'crew')
|
|
const snapshotsById: Record<string, DemoCrewRecord['data'] & { id: string }> = {}
|
|
for (const p of pool) {
|
|
snapshotsById[p.payloadId] = { id: p.payloadId, ...p.data }
|
|
}
|
|
return {
|
|
activeSkipperId: skipper?.payloadId ?? PUBLIC_DEMO_SKIPPER_ID,
|
|
activeCrewIds: crew.map((c) => c.payloadId),
|
|
snapshotsById
|
|
}
|
|
}
|
|
|
|
export function buildPublicDemoFixture(): PublicDemoFixture {
|
|
const title = i18n.t('demo.logbook_title')
|
|
const yacht = buildDemoYachtData()
|
|
const vesselPool = buildDemoVesselPool(yacht)
|
|
const logbookVesselSelection = buildDemoLogbookVesselSelection(yacht)
|
|
const personPool = buildDemoPersonPool()
|
|
const crews = personPool
|
|
const logbookCrewSelection = buildDemoLogbookCrewSelection(personPool)
|
|
const days = buildDemoDays()
|
|
const entries: PublicDemoFixture['entries'] = []
|
|
const gpsTracks: PublicDemoFixture['gpsTracks'] = []
|
|
|
|
days.forEach((day, index) => {
|
|
const entryId = PUBLIC_DEMO_ENTRY_IDS[index] ?? crypto.randomUUID()
|
|
const { waypoints } = parseTrackFile(day.gpx, day.filename)
|
|
const stats = computeTrackStats(waypoints)
|
|
|
|
const entryPayload: Record<string, unknown> = {
|
|
payloadId: entryId,
|
|
date: day.date,
|
|
dayOfTravel: day.dayOfTravel,
|
|
departure: day.departure,
|
|
destination: day.destination,
|
|
freshwater: { ...day.freshwater },
|
|
fuel: { ...day.fuel },
|
|
selectedSkipperId: logbookCrewSelection.activeSkipperId,
|
|
selectedCrewIds: [...logbookCrewSelection.activeCrewIds],
|
|
crewSnapshotsById: { ...logbookCrewSelection.snapshotsById },
|
|
signSkipper: '',
|
|
signCrew: '',
|
|
events: day.events
|
|
}
|
|
|
|
if (day.greywaterLevel != null && day.greywaterLevel > 0) {
|
|
entryPayload.greywater = { level: day.greywaterLevel }
|
|
}
|
|
|
|
if (stats) {
|
|
entryPayload.trackDistanceNm = stats.distanceNm
|
|
entryPayload.trackSpeedMaxKn = stats.speedMaxKn
|
|
entryPayload.trackSpeedAvgKn = stats.speedAvgKn
|
|
}
|
|
if (day.motorHours != null && day.motorHours > 0) {
|
|
entryPayload.motorHours = day.motorHours
|
|
}
|
|
|
|
entries.push(entryPayload as PublicDemoFixture['entries'][number])
|
|
|
|
gpsTracks.push({
|
|
entryId,
|
|
waypoints,
|
|
filename: day.filename,
|
|
gpxContent: day.gpx,
|
|
fileType: 'gpx'
|
|
})
|
|
})
|
|
|
|
return {
|
|
title,
|
|
yacht,
|
|
vesselPool,
|
|
logbookVesselSelection,
|
|
crews,
|
|
personPool,
|
|
logbookCrewSelection,
|
|
entries,
|
|
gpsTracks,
|
|
photos: [],
|
|
firstEntryId: PUBLIC_DEMO_FIRST_ENTRY_ID
|
|
}
|
|
}
|
|
|
|
export function getPublicDemoFirstEntryId(): string {
|
|
return PUBLIC_DEMO_FIRST_ENTRY_ID
|
|
}
|
|
|
|
/** Payloads for encrypted seeding (without payloadId on entries). */
|
|
export function buildDemoEntryPayloads(): Array<{
|
|
entryId: string
|
|
entryPayload: Record<string, unknown>
|
|
trackData: { waypoints: unknown[]; gpxContent: string; filename: string; fileType: string }
|
|
}> {
|
|
const logbookCrewSelection = buildDemoLogbookCrewSelection(buildDemoPersonPool())
|
|
const days = buildDemoDays()
|
|
return days.map((day) => {
|
|
const entryId = crypto.randomUUID()
|
|
const { waypoints } = parseTrackFile(day.gpx, day.filename)
|
|
const stats = computeTrackStats(waypoints)
|
|
|
|
const entryPayload: Record<string, unknown> = {
|
|
date: day.date,
|
|
dayOfTravel: day.dayOfTravel,
|
|
departure: day.departure,
|
|
destination: day.destination,
|
|
freshwater: { ...day.freshwater },
|
|
fuel: { ...day.fuel },
|
|
selectedSkipperId: logbookCrewSelection.activeSkipperId,
|
|
selectedCrewIds: [...logbookCrewSelection.activeCrewIds],
|
|
crewSnapshotsById: { ...logbookCrewSelection.snapshotsById },
|
|
signSkipper: '',
|
|
signCrew: '',
|
|
events: day.events
|
|
}
|
|
|
|
if (day.greywaterLevel != null && day.greywaterLevel > 0) {
|
|
entryPayload.greywater = { level: day.greywaterLevel }
|
|
}
|
|
|
|
if (stats) {
|
|
entryPayload.trackDistanceNm = stats.distanceNm
|
|
entryPayload.trackSpeedMaxKn = stats.speedMaxKn
|
|
entryPayload.trackSpeedAvgKn = stats.speedAvgKn
|
|
}
|
|
if (day.motorHours != null && day.motorHours > 0) {
|
|
entryPayload.motorHours = day.motorHours
|
|
}
|
|
|
|
return {
|
|
entryId,
|
|
entryPayload,
|
|
trackData: {
|
|
waypoints,
|
|
gpxContent: day.gpx,
|
|
filename: day.filename,
|
|
fileType: 'gpx'
|
|
}
|
|
}
|
|
})
|
|
}
|