diff --git a/apps/admin/src/app/trips/[id]/_components/trip-insurance.tsx b/apps/admin/src/app/trips/[id]/_components/trip-insurance.tsx index bcb65b61d..6d4e7feb0 100644 --- a/apps/admin/src/app/trips/[id]/_components/trip-insurance.tsx +++ b/apps/admin/src/app/trips/[id]/_components/trip-insurance.tsx @@ -63,10 +63,30 @@ import type { TravelerInsuranceStatus, InsurancePolicyType, InsuranceProductDto, + AdvisorConsentMethod, + InsuranceResolutionSource, } from '@tailfire/shared-types/api' +import { ADVISOR_CONSENT_METHODS } from '@tailfire/shared-types/api' import { formatCurrency, dollarsToCents, centsToDollars } from '@/lib/pricing/currency-helpers' import { InsuranceProposalDialog } from '@/components/insurance/insurance-proposal-dialog' import { ConfirmDialog } from '@/components/ui/confirm-dialog' +import { fetchTripFormPdfUrl } from '@/hooks/use-trip-forms' + +// refs #1466 P3b — statuses that constitute a compliance "resolution". +const RESOLVED_TRAVELER_STATUSES: TravelerInsuranceStatus[] = [ + 'declined', + 'has_own_insurance', + 'selected_package', +] +const isResolvedTravelerStatus = (s: TravelerInsuranceStatus) => + RESOLVED_TRAVELER_STATUSES.includes(s) + +// refs #1466 P3b — human labels for how advisor consent was captured. +const CONSENT_METHOD_LABELS: Record = { + verbal_phone: 'Verbal (phone)', + email_confirmation: 'Email confirmation', + in_person_signature: 'In-person signature', +} interface TripInsuranceProps { trip: TripResponseDto @@ -193,6 +213,8 @@ export function TripInsurance({ trip }: TripInsuranceProps) { declinedReason: '', premiumPaidCents: null as number | null, notes: '', + // refs #1466 P3b — how the advisor captured consent for a resolved status. + consentMethod: null as AdvisorConsentMethod | null, }) const handleOpenPackageDialog = (pkg?: TripInsurancePackageDto) => { @@ -323,6 +345,8 @@ export function TripInsurance({ trip }: TripInsuranceProps) { declinedReason: existing.declinedReason || '', premiumPaidCents: existing.premiumPaidCents ?? null, notes: existing.notes || '', + // Pre-fill the prior consent method so re-saving doesn't force a re-pick. + consentMethod: (existing.consentMethod as AdvisorConsentMethod | null) ?? null, }) } else { setTravelerForm({ @@ -333,6 +357,7 @@ export function TripInsurance({ trip }: TripInsuranceProps) { declinedReason: '', premiumPaidCents: null, notes: '', + consentMethod: null, }) } setShowTravelerDialog(true) @@ -533,6 +558,13 @@ export function TripInsurance({ trip }: TripInsuranceProps) { {insurance.declinedReason} )} {status === 'pending' && Awaiting response} + {/* refs #1466 P3b — link the signed waiver when the resolution + came from a client e-sign (form_token_id present). */} + {insurance?.formTokenId && ( +
+ +
+ )} - + ) +} + // Separate component for traveler insurance dialog to use hooks properly interface TravelerInsuranceDialogProps { open: boolean @@ -999,6 +1107,10 @@ interface TravelerInsuranceDialogProps { declinedReason: string | null premiumPaidCents: number | null notes: string | null + // refs #1466 P3b — evidence context for the consent selector + waiver link. + resolutionSource?: InsuranceResolutionSource + consentMethod?: string | null + formTokenId?: string | null } form: { status: TravelerInsuranceStatus @@ -1008,6 +1120,7 @@ interface TravelerInsuranceDialogProps { declinedReason: string premiumPaidCents: number | null notes: string + consentMethod: AdvisorConsentMethod | null } setForm: (form: any) => void travelerName: string @@ -1051,6 +1164,28 @@ function TravelerInsuranceDialog({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [open, packagesLoaded, packages, form.selectedPackageId]) + // refs #1466 P3b — a resolved status recorded via the ADVISOR path must carry a + // consent method. A record already resolved by the client (client_esign) keeps + // its signed-waiver evidence for evidence-NEUTRAL edits (notes/premium). But if + // the advisor MATERIALLY changes the resolution (status or a status-defining + // field), it becomes an advisor override that must re-capture consent — mirror + // the server rule so the override isn't silently 400'd. + const isClientEsign = existingInsurance?.resolutionSource === 'client_esign' + const statusIsResolved = isResolvedTravelerStatus(form.status) + const esignMaterialChange = + !!existingInsurance && + isClientEsign && + (form.status !== existingInsurance.status || + (form.status === 'selected_package' && + (form.selectedPackageId ?? null) !== (existingInsurance.selectedPackageId ?? null)) || + (form.status === 'has_own_insurance' && + ((form.externalPolicyNumber || null) !== (existingInsurance.externalPolicyNumber ?? null) || + (form.externalProviderName || null) !== (existingInsurance.externalProviderName ?? null))) || + (form.status === 'declined' && + (form.declinedReason || null) !== (existingInsurance.declinedReason ?? null))) + const showConsent = statusIsResolved && (!isClientEsign || esignMaterialChange) + const needsConsent = showConsent && !form.consentMethod + const handleSave = async () => { if (!travelerId) return @@ -1064,6 +1199,9 @@ function TravelerInsuranceDialog({ // Premium payment only applies to an agency-sold package; clear it otherwise. premiumPaidCents: form.status === 'selected_package' ? form.premiumPaidCents : null, notes: form.notes || null, + // Only send consent when the advisor actually captured it (resolved status, + // non-e-sign record). The server ignores it for pending/e-sign records. + ...(showConsent ? { consentMethod: form.consentMethod } : {}), } if (existingInsurance) { @@ -1219,6 +1357,40 @@ function TravelerInsuranceDialog({ )} + {/* refs #1466 P3b — required consent capture for an advisor-recorded + resolution. Hidden for a client e-sign record (evidence on file). */} + {showConsent && ( +
+ + +

+ Records how the traveler's insurance decision was confirmed (compliance). +

+
+ )} + + {isClientEsign && existingInsurance?.formTokenId && ( + + )} +