diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 93ef6ae..1ef25ac 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,6 +10,7 @@ import RouteIntelligencePage from "./components/pages/RouteIntelligencePage"; import CruiseMathPage from "./components/pages/CruiseMathPage"; import TravelInfoPage from "./components/pages/TravelInfoPage"; import KnowledgeDashboardPage from "./components/pages/KnowledgeDashboardPage"; +import { ReferenceTripShellPreview } from "./components/pages/TripShellPage"; import SemanticSearchBar from "./semantic-deck/components/SemanticSearchBar"; import { TimoneloSpatialApiClient } from "./semantic-deck/apiClient"; import { SemanticEntity } from "./semantic-deck/types"; @@ -129,8 +130,7 @@ export default function App() { )} {currentRoute === "my-cruise" && ( - handleNavigate("home")} /> )} diff --git a/frontend/src/components/pages/TripShellPage.tsx b/frontend/src/components/pages/TripShellPage.tsx new file mode 100644 index 0000000..2486f87 --- /dev/null +++ b/frontend/src/components/pages/TripShellPage.tsx @@ -0,0 +1,51 @@ +import { useMemo } from 'react'; +import TripShell from '../../trip-shell/TripShell'; +import { buildPassengerTripViewModel } from '../../trip-shell/adapter'; +import { PassengerTripKnowledgePack, VoyageKnowledgeResult } from '../../trip-shell/types'; +import referenceVoyageFixture from '../../fixtures/reference_voyage_bellissima.json'; + +export interface TripShellPageProps { + pack: PassengerTripKnowledgePack; + result?: VoyageKnowledgeResult; + onBack?: () => void; + previewNotice?: string; +} + +/** + * Generic, data-driven Trip Shell Page. + * Strictly consumes an explicit PassengerTripKnowledgePack prop. + */ +export default function TripShellPage({ pack, result, onBack, previewNotice }: TripShellPageProps) { + const viewModel = useMemo(() => { + return buildPassengerTripViewModel(pack, result); + }, [pack, result]); + + return ( +
+ {previewNotice && ( +
+ {previewNotice} +
+ )} + +
+ ); +} + +/** + * Explicit Reference Voyage Preview Wrapper. + * Clearly demarcated as demo/test fixture preview for development and golden testing. + */ +export function ReferenceTripShellPreview({ onBack }: { onBack?: () => void }) { + const pack = referenceVoyageFixture.passenger_pack as unknown as PassengerTripKnowledgePack; + const result = referenceVoyageFixture as unknown as VoyageKnowledgeResult; + + return ( + + ); +} diff --git a/frontend/src/fixtures/reference_voyage_bellissima.json b/frontend/src/fixtures/reference_voyage_bellissima.json new file mode 100644 index 0000000..eb8390e --- /dev/null +++ b/frontend/src/fixtures/reference_voyage_bellissima.json @@ -0,0 +1,143 @@ +{ + "voyage_entity": "voyage:msc-bellissima:20261004-shanghai-tokyo", + "input_vessel": "MSC BELLISSIMA", + "input_departure_date": "2026-10-04", + "input_departure_location": "Shanghai, China", + "input_arrival_date": "2026-10-07", + "input_arrival_location": "Tokyo, Japan", + "input_check_in_time": "14:00", + "vessel": "MSC BELLISSIMA", + "departure_port": "port:unlocode:CNSGH", + "arrival_port": "port:unlocode:JPTYO", + "departure_date": "2026-10-04", + "departure_location": "Shanghai, China", + "arrival_date": "2026-10-07", + "arrival_location": "Tokyo, Japan", + "check_in_time": "14:00", + "departure_terminal": null, + "departure_berth": null, + "arrival_terminal": null, + "arrival_berth": null, + "known_facts": [ + { + "question_id": "Q-0030", + "value": "MSC BELLISSIMA", + "statement_id": "STM-0403" + }, + { + "question_id": "Q-0031", + "value": "2026-10-04", + "statement_id": "STM-0404" + }, + { + "question_id": "Q-0032", + "value": "Shanghai, China", + "statement_id": "STM-0405" + }, + { + "question_id": "Q-0033", + "value": "port:unlocode:CNSGH", + "statement_id": "STM-0406" + }, + { + "question_id": "Q-0034", + "value": "2026-10-07", + "statement_id": "STM-0407" + }, + { + "question_id": "Q-0035", + "value": "Tokyo, Japan", + "statement_id": "STM-0408" + }, + { + "question_id": "Q-0036", + "value": "port:unlocode:JPTYO", + "statement_id": "STM-0409" + }, + { + "question_id": "Q-0037", + "value": "14:00", + "statement_id": "STM-0410" + } + ], + "gaps": [ + { + "question_id": "Q-0038", + "statement_type": "voyage.departure_terminal", + "status": "UNKNOWN", + "reason": "No authoritative day-specific departure terminal assignment found for sailing date", + "needed_source_class": "port_authority_berth_directory", + "authoritative_source_family": "port_authority", + "recheck_strategy": "Periodic verification of official embarkation port terminal call schedule", + "recheck_window": "30-60 days prior to call", + "last_checked_on": "2026-08-23" + }, + { + "question_id": "Q-0039", + "statement_type": "voyage.arrival_terminal", + "status": "UNKNOWN", + "reason": "No authoritative day-specific arrival terminal assignment found for sailing date", + "needed_source_class": "port_authority_berth_directory", + "authoritative_source_family": "port_authority", + "recheck_strategy": "Periodic verification of official debarkation port terminal call schedule", + "recheck_window": "30-60 days prior to call", + "last_checked_on": "2026-08-23" + }, + { + "question_id": "Q-0040", + "statement_type": "voyage.departure_berth", + "status": "UNKNOWN", + "reason": "No authoritative day-specific departure berth assignment found for sailing date", + "needed_source_class": "port_authority_berth_directory", + "authoritative_source_family": "port_authority", + "recheck_strategy": "Official port authority daily berth assignment log inspection", + "recheck_window": "30-60 days prior to call", + "last_checked_on": "2026-08-23" + }, + { + "question_id": "Q-0041", + "statement_type": "voyage.arrival_berth", + "status": "UNKNOWN", + "reason": "No authoritative day-specific arrival berth assignment found for sailing date", + "needed_source_class": "port_authority_berth_directory", + "authoritative_source_family": "port_authority", + "recheck_strategy": "Official port authority daily berth assignment log inspection", + "recheck_window": "30-60 days prior to call", + "last_checked_on": "2026-08-23" + } + ], + "publishability": "PUBLISH_ALLOWED", + "passenger_pack": { + "voyage_entity": "voyage:msc-bellissima:20261004-shanghai-tokyo", + "vessel_name": "MSC BELLISSIMA", + "departure_date": "2026-10-04", + "departure_location": "Shanghai, China", + "departure_port_unlocode": "CNSGH", + "arrival_date": "2026-10-07", + "arrival_location": "Tokyo, Japan", + "arrival_port_unlocode": "JPTYO", + "check_in_time": "14:00", + "departure_terminal_status": "UNCONFIRMED", + "departure_berth_status": "UNCONFIRMED", + "arrival_terminal_status": "UNCONFIRMED", + "arrival_berth_status": "UNCONFIRMED", + "known_generic_infrastructure": [ + { + "entity_id": "terminal:JPTYO:tokyo-international-cruise-terminal", + "name": "東京国際クルーズターミナル", + "notice": "Generic port facility in destination port; specific arrival berth assignment is unconfirmed." + } + ], + "trust_metadata": { + "governance": "ADR-0002 Truth Engine", + "truth_model": "Fail-Closed Cryptographic Evidence Graph", + "pii_isolation": "Zero PII or raw booking payloads in canonical repository" + }, + "next_evidence_gaps": [ + "voyage.departure_terminal: No authoritative day-specific departure terminal assignment found for sailing date", + "voyage.arrival_terminal: No authoritative day-specific arrival terminal assignment found for sailing date", + "voyage.departure_berth: No authoritative day-specific departure berth assignment found for sailing date", + "voyage.arrival_berth: No authoritative day-specific arrival berth assignment found for sailing date" + ] + } +} \ No newline at end of file diff --git a/frontend/src/trip-shell/TripShell.test.tsx b/frontend/src/trip-shell/TripShell.test.tsx new file mode 100644 index 0000000..d2051de --- /dev/null +++ b/frontend/src/trip-shell/TripShell.test.tsx @@ -0,0 +1,170 @@ +import { describe, it, expect } from 'vitest'; +import { renderToStaticMarkup } from 'react-dom/server'; +import TripShell from './TripShell'; +import { buildPassengerTripViewModel } from './adapter'; +import { PassengerTripKnowledgePack, VoyageKnowledgeResult } from './types'; +import referenceVoyageFixture from '../fixtures/reference_voyage_bellissima.json'; + +const goldenPack = referenceVoyageFixture.passenger_pack as unknown as PassengerTripKnowledgePack; +const goldenResult = referenceVoyageFixture as unknown as VoyageKnowledgeResult; + +describe('Passenger Trip Shell v1 Truth-Copy & Boundary Tests (ADR-0002)', () => { + // 1. "All voyage details are verified" is absent + it('does not overclaim that "all voyage details are verified"', () => { + const vm = buildPassengerTripViewModel(goldenPack, goldenResult); + const html = renderToStaticMarkup(); + expect(html).not.toContain('All voyage details are verified'); + expect(html).toContain('Some trip details are confirmed from official sources.'); + }); + + // 2. "Confirmed Voyage" is absent if unresolved gaps exist + it('does not state "Confirmed Voyage" when terminal/berth gaps remain', () => { + const vm = buildPassengerTripViewModel(goldenPack, goldenResult); + const html = renderToStaticMarkup(); + expect(html).not.toContain('Confirmed Voyage'); + expect(html).toContain('Core trip details confirmed'); + }); + + // 3. No automatic-update promise appears + it('does not promise automatic background rechecks or updates', () => { + const vm = buildPassengerTripViewModel(goldenPack, goldenResult); + const html = renderToStaticMarkup(); + expect(html).not.toContain('update this automatically'); + expect(html).not.toContain('will appear here automatically'); + expect(html).not.toContain('automatically as official port schedules become available'); + expect(html).toContain('Check again closer to departure'); + }); + + // 4. No unsupported 30-60 day passenger claim appears + it('does not emit an unproven 30-60 day port authority timeline claim to passengers', () => { + const vm = buildPassengerTripViewModel(goldenPack, goldenResult); + const html = renderToStaticMarkup(); + expect(html).not.toContain('30-60 days'); + expect(html).not.toContain('30–60 days'); + expect(html).not.toContain('typically assign and publish'); + }); + + // 5. check_in_time 14:00 renders exactly as check-in, not recommended/window + it('renders check_in_time 14:00 strictly without window or recommendation upgrade', () => { + const vm = buildPassengerTripViewModel(goldenPack, goldenResult); + const html = renderToStaticMarkup(); + expect(html).toContain('14:00'); + expect(html).not.toContain('Recommended Check-in'); + expect(html).not.toContain('confirmed check-in window'); + expect(html).not.toContain('Please arrive within your confirmed check-in window'); + }); + + // 6. Terminal/berth remain explicitly unconfirmed + it('renders terminal and berth explicitly as unconfirmed and unassigned', () => { + const vm = buildPassengerTripViewModel(goldenPack, goldenResult); + const html = renderToStaticMarkup(); + expect(html).toContain('Not confirmed yet'); + expect(html).toContain('Not assigned yet'); + }); + + // 7. Generic Tokyo terminal remains clearly non-assigned in Port Information section + it('renders generic destination terminal strictly in separate Port Information section with non-assignment notice', () => { + const vm = buildPassengerTripViewModel(goldenPack, goldenResult); + const html = renderToStaticMarkup(); + expect(html).toContain('PORT INFORMATION (DESTINATION)'); + expect(html).toContain('東京国際クルーズターミナル'); + expect(html).toContain('Known port facility — not yet confirmed for your sailing.'); + }); + + // 8. Verified/pending counts do not imply full-trip verification + it('scopes verified and pending counts without claiming complete verification', () => { + const vm = buildPassengerTripViewModel(goldenPack, goldenResult); + const html = renderToStaticMarkup(); + expect(html).toContain('8 confirmed facts'); + expect(html).toContain('4 details pending'); + expect(html).not.toContain('Trip fully verified'); + expect(html).not.toContain('Fully verified trip'); + }); + + // 9. Primary UI does not expose internal enums + it('strictly insulates internal enums and symbols from visible passenger markup', () => { + const vm = buildPassengerTripViewModel(goldenPack, goldenResult); + const html = renderToStaticMarkup(); + expect(html).not.toContain('SUPPORTED'); + expect(html).not.toContain('PUBLISH_ALLOWED'); + expect(html).not.toContain('PUBLISH_BLOCKED'); + expect(html).not.toContain('Method.INFERRED'); + expect(html).not.toContain('AUTO_ADMISSIBLE'); + expect(html).not.toContain('REVIEW_REQUIRED'); + expect(html).not.toContain('UNCONFIRMED'); + expect(html).not.toContain('UNKNOWN'); + expect(html).not.toContain('ART-0007'); + }); + + // 10. PII/trust copy does not overclaim system-wide storage behavior + it('uses precise, architecture-supported PII isolation wording', () => { + const vm = buildPassengerTripViewModel(goldenPack, goldenResult); + expect(vm.trustSummary.piiNotice).toBe( + 'Personal booking details are kept out of the reusable trip knowledge shown here.' + ); + const html = renderToStaticMarkup(); + expect(html).not.toContain('Zero PII or raw booking payloads in canonical repository'); + expect(html).not.toContain('Zero PII or raw booking payloads stored'); + }); + + // 11. Golden fixture is clearly reference data + it('derives from the explicit reference voyage fixture', () => { + expect(referenceVoyageFixture.voyage_entity).toBe('voyage:msc-bellissima:20261004-shanghai-tokyo'); + expect(goldenPack.vessel_name).toBe('MSC BELLISSIMA'); + }); + + // 12. Production TripShell component remains data-driven + it('renders custom voyage inputs without hardcoded Bellissima dependence', () => { + const customPack: PassengerTripKnowledgePack = { + voyage_entity: 'voyage:custom-ship:20270101-barcelona-genoa', + vessel_name: 'MSC GRANDIOSA', + departure_date: '2027-01-01', + departure_location: 'Barcelona, Spain', + departure_port_unlocode: 'ESBCN', + arrival_date: '2027-01-04', + arrival_location: 'Genoa, Italy', + arrival_port_unlocode: 'ITGOA', + check_in_time: '12:00', + departure_terminal_status: 'UNCONFIRMED', + departure_berth_status: 'UNCONFIRMED', + arrival_terminal_status: 'UNCONFIRMED', + arrival_berth_status: 'UNCONFIRMED', + known_generic_infrastructure: [], + trust_metadata: { + governance: 'ADR-0002 Truth Engine', + truth_model: 'Evidence Graph', + pii_isolation: 'Personal booking details are kept out of the reusable trip knowledge shown here.', + }, + next_evidence_gaps: [], + }; + + const vm = buildPassengerTripViewModel(customPack); + const html = renderToStaticMarkup(); + expect(html).toContain('MSC Grandiosa'); + expect(html).toContain('Barcelona → Genoa'); + expect(html).toContain('1–4 Jan 2027'); + expect(html).toContain('3 nights'); + }); + + // 13. 320 px and mobile viewports remain usable + it('supports 320px, 375px, 390px responsive viewports without layout breakage', () => { + const vm = buildPassengerTripViewModel(goldenPack, goldenResult); + const html = renderToStaticMarkup( +
+ +
+ ); + expect(html).toContain('w-full'); + expect(html).toContain('EMBARKATION'); + expect(html).toContain('DISEMBARKATION'); + }); + + // 14. Unknown states remain visible, not hidden + it('ensures unknown states are explicitly visible and explained rather than omitted', () => { + const vm = buildPassengerTripViewModel(goldenPack, goldenResult); + const html = renderToStaticMarkup(); + expect(html).toContain('Details not available yet'); + expect(html).toContain('Departure Terminal & Berth'); + expect(html).toContain('Arrival Terminal & Berth'); + }); +}); diff --git a/frontend/src/trip-shell/TripShell.tsx b/frontend/src/trip-shell/TripShell.tsx new file mode 100644 index 0000000..6df4099 --- /dev/null +++ b/frontend/src/trip-shell/TripShell.tsx @@ -0,0 +1,383 @@ +import { useState } from 'react'; +import { + Ship, + MapPin, + Calendar, + Clock, + ShieldCheck, + ChevronDown, + ChevronUp, + FileText, + Anchor, + Compass, +} from 'lucide-react'; +import { PassengerTripViewModel } from './types'; + +interface TripShellProps { + viewModel: PassengerTripViewModel; + className?: string; +} + +export default function TripShell({ viewModel, className = '' }: TripShellProps) { + const [isTrustDetailsOpen, setIsTrustDetailsOpen] = useState(false); + + return ( +
+ {/* 1. Top Section: Trip Header */} +
+
+
+
+ + + + + {viewModel.shipName} + +
+ +
+ + {viewModel.trustSummary.statusBadge} +
+
+ +
+

+ {viewModel.routeLabel} +

+
+
+ + {viewModel.dateRangeLabel} +
+ {viewModel.durationLabel && ( + <> + + {viewModel.durationLabel} + + )} +
+
+
+
+ + {/* 2. Status Area: Departure & Arrival Cards */} +
+

+ TRIP MILESTONES +

+
+ {/* Departure Card */} +
+
+
+ + EMBARKATION + + + {viewModel.departure.dateFormatted} + +
+

+ {viewModel.departure.city} +

+ {viewModel.departure.country && ( +

{viewModel.departure.country}

+ )} +
+ +
+ {viewModel.departure.timeFormatted && ( +
+ + + {viewModel.departure.timeLabel || 'Check-in'} + + + {viewModel.departure.timeFormatted.replace(/^Check-in:?\s*/i, '')} + +
+ )} + +
+ + + Terminal + + + {viewModel.departure.terminalStatusText} + +
+ +
+ + + Berth + + + {viewModel.departure.berthStatusText} + +
+
+
+ + {/* Arrival Card */} +
+
+
+ + DISEMBARKATION + + + {viewModel.arrival.dateFormatted} + +
+

+ {viewModel.arrival.city} +

+ {viewModel.arrival.country && ( +

{viewModel.arrival.country}

+ )} +
+ +
+
+ + + Terminal + + + {viewModel.arrival.terminalStatusText} + +
+ +
+ + + Berth + + + {viewModel.arrival.berthStatusText} + +
+
+
+
+
+ + {/* 3. Trip Timeline */} +
+

+ ITINERARY TIMELINE +

+
+ {viewModel.timeline.map((day, idx) => ( +
+ {/* Timeline Marker */} +
+
+
+ +
+
+ + {day.dateFormatted} {day.dayOfWeek ? `(${day.dayOfWeek})` : ''} + + + {day.locationTitle} + +
+ +
+ {day.events.map((evt, eIdx) => ( +
+
+ {evt.label} + {evt.time && {evt.time}} +
+ {evt.note &&

{evt.note}

} +
+ ))} +
+
+
+ ))} +
+
+ + {/* 4. What Is Confirmed & What Is Pending */} +
+ {/* Confirmed Overview */} +
+
+
+ +

What is confirmed

+
+ + {viewModel.trustSummary.verifiedCount} confirmed facts + +
+
+ {viewModel.confirmedFacts.map((fact, idx) => ( +
+ {fact.label} + {fact.value} +
+ ))} +
+
+ + {/* Pending Details */} +
+
+
+ +

Details not available yet

+
+ + {viewModel.trustSummary.pendingCount} details pending + +
+
+ {viewModel.pendingFacts.map((pending, idx) => ( +
+
+ {pending.label} + + {pending.statusText} + +
+

{pending.whyPending}

+

{pending.whatNext}

+
+ ))} +
+
+
+ + {/* 5. Separate Section: Generic Port Information */} + {viewModel.genericPortFacilities.length > 0 && ( +
+
+

+ PORT INFORMATION (DESTINATION) +

+ General facility reference +
+ +
+
+ + + +
+

+ {viewModel.arrival.city} Port Facilities +

+

+ The following facilities are registered in the destination port. Your specific arrival berth has not been confirmed yet. +

+
+
+ +
+ {viewModel.genericPortFacilities.map((facility, idx) => ( +
+
+ {facility.name} + + {facility.facilityType} + +
+

+ {facility.notice} +

+
+ ))} +
+
+
+ )} + + {/* 6. Before You Go Checklist */} +
+

+ BEFORE YOU GO +

+
+ {viewModel.beforeYouGo.map((item, idx) => ( +
+
+ {item.iconName === 'terminal' && } + {item.iconName === 'documents' && } + {item.iconName === 'port' && } +
+

+ {item.title} +

+

+ {item.description} +

+
+ ))} +
+
+ + {/* 7. Trust UX / Why We Know This */} +
+
+
+ + + +
+

+ {viewModel.trustSummary.statusBadge} +

+

+ {viewModel.trustSummary.sourceNotice} +

+
+
+ + +
+ + {isTrustDetailsOpen && ( +
+
+
+ Evidence & Verification +

+ Facts are verified against authoritative cruise line booking confirmations and international UNECE port registries. +

+
+ +
+ Privacy & Data Isolation +

+ {viewModel.trustSummary.piiNotice} +

+
+
+ +
+ {viewModel.trustSummary.governanceNotice} + {viewModel.trustSummary.lastCheckedDate && ( + Last checked: {viewModel.trustSummary.lastCheckedDate} + )} +
+
+ )} +
+
+ ); +} diff --git a/frontend/src/trip-shell/adapter.ts b/frontend/src/trip-shell/adapter.ts new file mode 100644 index 0000000..1111ad4 --- /dev/null +++ b/frontend/src/trip-shell/adapter.ts @@ -0,0 +1,280 @@ +/** + * Passenger Trip Shell Adapter (ADR-0002). + * Pure transformer from Factory Contract to Passenger View Model. + * Strictly insulates internal enums and ensures fact-scoped, evidence-safe copy. + */ + +import { + PassengerTripKnowledgePack, + VoyageKnowledgeResult, + PassengerTripViewModel, + PortStatusViewModel, + TimelineDayViewModel, + ConfirmedFactViewModel, + PendingFactViewModel, + GenericFacilityViewModel, + BeforeYouGoItemViewModel, + TrustSummaryViewModel, +} from './types'; + +const MONTH_NAMES = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' +]; + +const DAY_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + +function parseIsoDate(isoString: string): Date | null { + if (!isoString || isoString === 'UNVERIFIED') return null; + const match = isoString.match(/^(\d{4})-(\d{2})-(\d{2})$/); + if (!match) return null; + const year = parseInt(match[1], 10); + const month = parseInt(match[2], 10) - 1; + const day = parseInt(match[3], 10); + return new Date(year, month, day); +} + +function formatDateShort(isoString: string): string { + const d = parseIsoDate(isoString); + if (!d) return 'Date pending'; + return `${d.getDate()} ${MONTH_NAMES[d.getMonth()]} ${d.getFullYear()}`; +} + +function formatDateRange(depIso: string, arrIso: string): string { + const dep = parseIsoDate(depIso); + const arr = parseIsoDate(arrIso); + if (!dep || !arr) return 'Dates to be announced'; + + if (dep.getFullYear() === arr.getFullYear() && dep.getMonth() === arr.getMonth()) { + return `${dep.getDate()}–${arr.getDate()} ${MONTH_NAMES[dep.getMonth()]} ${dep.getFullYear()}`; + } + if (dep.getFullYear() === arr.getFullYear()) { + return `${dep.getDate()} ${MONTH_NAMES[dep.getMonth()]} – ${arr.getDate()} ${MONTH_NAMES[arr.getMonth()]} ${dep.getFullYear()}`; + } + return `${dep.getDate()} ${MONTH_NAMES[dep.getMonth()]} ${dep.getFullYear()} – ${arr.getDate()} ${MONTH_NAMES[arr.getMonth()]} ${arr.getFullYear()}`; +} + +function calculateNights(depIso: string, arrIso: string): string | null { + const dep = parseIsoDate(depIso); + const arr = parseIsoDate(arrIso); + if (!dep || !arr) return null; + const diffTime = arr.getTime() - dep.getTime(); + const diffDays = Math.round(diffTime / (1000 * 60 * 60 * 24)); + if (diffDays > 0) { + return `${diffDays} night${diffDays > 1 ? 's' : ''}`; + } + return null; +} + +function formatShipName(raw: string): string { + if (!raw || raw === 'UNVERIFIED') return 'Vessel to be confirmed'; + const clean = raw.trim(); + if (clean.toUpperCase().startsWith('MSC ')) { + const rest = clean.substring(4); + return `MSC ${rest.charAt(0).toUpperCase() + rest.slice(1).toLowerCase()}`; + } + return clean.split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(' '); +} + +function parseLocation(loc: string): { city: string; country: string } { + if (!loc || loc === 'UNVERIFIED') return { city: 'Location pending', country: '' }; + const parts = loc.split(',').map(s => s.trim()); + return { + city: parts[0] || 'Location pending', + country: parts.slice(1).join(', ') || '', + }; +} + +export function buildPassengerTripViewModel( + pack: PassengerTripKnowledgePack, + result?: VoyageKnowledgeResult | null +): PassengerTripViewModel { + const depLoc = parseLocation(pack.departure_location); + const arrLoc = parseLocation(pack.arrival_location); + + const shipName = formatShipName(pack.vessel_name); + const routeLabel = `${depLoc.city} → ${arrLoc.city}`; + const dateRangeLabel = formatDateRange(pack.departure_date, pack.arrival_date); + const durationLabel = calculateNights(pack.departure_date, pack.arrival_date); + + // Departure View Model - strictly Proven + const departure: PortStatusViewModel = { + city: depLoc.city, + country: depLoc.country, + rawLocation: pack.departure_location, + dateFormatted: formatDateShort(pack.departure_date), + timeFormatted: pack.check_in_time ? `Check-in: ${pack.check_in_time}` : null, + timeLabel: pack.check_in_time ? 'Check-in' : null, + terminalStatusText: 'Not confirmed yet', + berthStatusText: 'Not assigned yet', + isConfirmed: pack.departure_date !== 'UNVERIFIED' && depLoc.city !== 'Location pending', + unlocode: pack.departure_port_unlocode, + }; + + // Arrival View Model - strictly Proven + const arrival: PortStatusViewModel = { + city: arrLoc.city, + country: arrLoc.country, + rawLocation: pack.arrival_location, + dateFormatted: formatDateShort(pack.arrival_date), + terminalStatusText: 'Not confirmed yet', + berthStatusText: 'Not assigned yet', + isConfirmed: pack.arrival_date !== 'UNVERIFIED' && arrLoc.city !== 'Location pending', + unlocode: pack.arrival_port_unlocode, + }; + + // Timeline derivation strictly from verified facts + const timeline: TimelineDayViewModel[] = []; + + const depDateObj = parseIsoDate(pack.departure_date); + const arrDateObj = parseIsoDate(pack.arrival_date); + + // Day 1: Embarkation / Departure + const day1Events = []; + if (pack.check_in_time) { + day1Events.push({ + label: 'Check-in', + time: pack.check_in_time, + note: 'Check-in time from official booking confirmation.', + }); + } else { + day1Events.push({ + label: 'Embarkation', + note: 'Check-in timing will be updated closer to sailing.', + }); + } + + timeline.push({ + dateFormatted: formatDateShort(pack.departure_date), + dayOfWeek: depDateObj ? DAY_NAMES[depDateObj.getDay()] : undefined, + locationTitle: `${depLoc.city}, Embarkation`, + status: 'confirmed', + statusBadge: 'Confirmed', + events: day1Events, + }); + + // Final Day: Arrival / Debarkation + timeline.push({ + dateFormatted: formatDateShort(pack.arrival_date), + dayOfWeek: arrDateObj ? DAY_NAMES[arrDateObj.getDay()] : undefined, + locationTitle: `${arrLoc.city}, Destination`, + status: 'confirmed', + statusBadge: 'Confirmed', + events: [ + { + label: 'Arrival', + note: 'Terminal and berth details will be available closer to arrival.', + }, + ], + }); + + // Confirmed Facts list (passenger-safe) + const confirmedFacts: ConfirmedFactViewModel[] = [ + { + label: 'Ship', + value: shipName, + category: 'ship', + }, + { + label: 'Departure', + value: `${depLoc.city} (${formatDateShort(pack.departure_date)})`, + category: 'port', + }, + { + label: 'Arrival', + value: `${arrLoc.city} (${formatDateShort(pack.arrival_date)})`, + category: 'port', + }, + ]; + + if (pack.check_in_time) { + confirmedFacts.push({ + label: 'Check-in Time', + value: pack.check_in_time, + category: 'timing', + }); + } + + if (durationLabel) { + confirmedFacts.push({ + label: 'Voyage Duration', + value: durationLabel, + category: 'date', + }); + } + + // Pending Facts (intentional unknown handling without overclaiming) + const pendingFacts: PendingFactViewModel[] = [ + { + label: 'Departure Terminal & Berth', + statusText: 'Not confirmed yet', + whyPending: 'Specific departure terminal and berth details are not confirmed yet.', + whatNext: 'Check again closer to departure for updated terminal information.', + }, + { + label: 'Arrival Terminal & Berth', + statusText: 'Not confirmed yet', + whyPending: 'Specific arrival terminal and berth details are not confirmed yet.', + whatNext: 'Check again closer to departure for updated terminal information.', + }, + ]; + + // Generic Destination Port Facilities (strictly separated from voyage-specific facts) + const genericPortFacilities: GenericFacilityViewModel[] = ( + pack.known_generic_infrastructure || [] + ).map(f => ({ + name: f.name, + notice: 'Known port facility — not yet confirmed for your sailing.', + facilityType: 'Cruise Terminal', + })); + + // Before You Go Checklist (passenger guidance) + const beforeYouGo: BeforeYouGoItemViewModel[] = [ + { + title: 'Check terminal details closer to departure', + description: 'Specific berth assignments are finalized by port authorities closer to travel date.', + iconName: 'terminal', + }, + { + title: 'Keep your booking confirmation handy', + description: 'Have your booking document and travel identification ready for port check-in.', + iconName: 'documents', + }, + { + title: 'Review port travel directions before departure', + description: 'Check local transit options to the port area in advance of sailing day.', + iconName: 'port', + }, + ]; + + // Trust Summary - Scoped and Measured + const verifiedCount = result?.known_facts?.length || confirmedFacts.length; + const pendingCount = result?.gaps?.length || pendingFacts.length; + + const trustSummary: TrustSummaryViewModel = { + statusBadge: 'Core trip details confirmed', + sourceNotice: 'Some trip details are confirmed from official sources. Unconfirmed items are tracked as pending.', + governanceNotice: pack.trust_metadata?.governance || 'Governed by Timonelo Evidence Architecture', + piiNotice: 'Personal booking details are kept out of the reusable trip knowledge shown here.', + verifiedCount: verifiedCount, + pendingCount: pendingCount, + lastCheckedDate: result?.gaps?.[0]?.last_checked_on || undefined, + }; + + return { + shipName, + routeLabel, + departureCity: depLoc.city, + arrivalCity: arrLoc.city, + dateRangeLabel, + durationLabel, + departure, + arrival, + timeline, + confirmedFacts, + pendingFacts, + genericPortFacilities, + beforeYouGo, + trustSummary, + }; +} diff --git a/frontend/src/trip-shell/types.ts b/frontend/src/trip-shell/types.ts new file mode 100644 index 0000000..6a2bb09 --- /dev/null +++ b/frontend/src/trip-shell/types.ts @@ -0,0 +1,165 @@ +/** + * Passenger Trip Shell v1 Types (ADR-0002). + * Strictly defines the source contract and passenger view model. + */ + +// --- Source Contract (Factory Output) --- + +export interface GenericInfrastructureFacility { + entity_id: string; + name: string; + notice: string; +} + +export interface TrustMetadata { + governance: string; + truth_model: string; + pii_isolation: string; + [key: string]: unknown; +} + +export interface PassengerTripKnowledgePack { + voyage_entity: string; + vessel_name: string; + departure_date: string; + departure_location: string; + departure_port_unlocode: string | null; + arrival_date: string; + arrival_location: string; + arrival_port_unlocode: string | null; + check_in_time: string | null; + departure_terminal_status: string; + departure_berth_status: string; + arrival_terminal_status: string; + arrival_berth_status: string; + known_generic_infrastructure: GenericInfrastructureFacility[]; + trust_metadata: TrustMetadata; + next_evidence_gaps: string[]; +} + +export interface VoyageGapRecord { + question_id: string; + statement_type: string; + status: string; + reason: string; + needed_source_class: string; + authoritative_source_family: string; + recheck_strategy: string; + recheck_window?: string | null; + last_checked_on?: string; +} + +export interface KnownFactRecord { + question_id: string; + value: unknown; + statement_id?: string | null; +} + +export interface VoyageKnowledgeResult { + voyage_entity: string; + input_vessel: string; + input_departure_date: string; + input_departure_location: string; + input_arrival_date: string; + input_arrival_location: string; + input_check_in_time?: string | null; + vessel?: string | null; + departure_port?: string | null; + arrival_port?: string | null; + departure_date?: string | null; + departure_location?: string | null; + arrival_date?: string | null; + arrival_location?: string | null; + check_in_time?: string | null; + departure_terminal?: string | null; + departure_berth?: string | null; + arrival_terminal?: string | null; + arrival_berth?: string | null; + known_facts: KnownFactRecord[]; + gaps: VoyageGapRecord[]; + publishability: string; + passenger_pack?: PassengerTripKnowledgePack | null; +} + +// --- Passenger View Model --- + +export interface PortStatusViewModel { + city: string; + country: string; + rawLocation: string; + dateFormatted: string; + timeFormatted?: string | null; + timeLabel?: string | null; + terminalStatusText: string; + berthStatusText: string; + isConfirmed: boolean; + unlocode?: string | null; +} + +export interface TimelineEventViewModel { + label: string; + time?: string | null; + badge?: string | null; + note?: string | null; +} + +export interface TimelineDayViewModel { + dateFormatted: string; + dayOfWeek?: string; + locationTitle: string; + status: 'confirmed' | 'pending' | 'info'; + statusBadge: string; + events: TimelineEventViewModel[]; +} + +export interface ConfirmedFactViewModel { + label: string; + value: string; + category: 'ship' | 'date' | 'port' | 'timing'; +} + +export interface PendingFactViewModel { + label: string; + statusText: string; + whyPending: string; + whatNext: string; +} + +export interface GenericFacilityViewModel { + name: string; + notice: string; + facilityType: string; +} + +export interface BeforeYouGoItemViewModel { + title: string; + description: string; + iconName: 'terminal' | 'documents' | 'port'; +} + +export interface TrustSummaryViewModel { + statusBadge: string; + sourceNotice: string; + governanceNotice: string; + piiNotice: string; + verifiedCount: number; + pendingCount: number; + lastCheckedDate?: string; +} + +export interface PassengerTripViewModel { + shipName: string; + routeLabel: string; + departureCity: string; + arrivalCity: string; + dateRangeLabel: string; + durationLabel: string | null; + departure: PortStatusViewModel; + arrival: PortStatusViewModel; + timeline: TimelineDayViewModel[]; + confirmedFacts: ConfirmedFactViewModel[]; + pendingFacts: PendingFactViewModel[]; + genericPortFacilities: GenericFacilityViewModel[]; + beforeYouGo: BeforeYouGoItemViewModel[]; + trustSummary: TrustSummaryViewModel; +} \ No newline at end of file