import { m } from '@/i18n/messages' import { flagReason as flagReasonLabels } from '@/i18n/i18n-enum.gen' import { useLocale, getLocale } from '@/i18n/config' import { asI18n } from '@pikku/react' import { Alert, Group, Progress, Stack, Stepper, Text, } from '@pikku/mantine/core' import { AlertTriangle, Clock } from 'lucide-react' import { fmtDate } from '../lib/dates' import type { BookingStatus } from '../lib/status' // ─── Types ──────────────────────────────────────────────────────────────── type DepositInvoice = { invoiceId: string kind: string dueOn: Date | null paidOn: Date | null status: string } type BookingMilestones = { bookingId: string status: BookingStatus startDate: Date | null enquiryAt: Date reservedAt: Date | null confirmedAt: Date | null endedAt: Date | null cancelledAt: Date | null contractSentAt: Date | null contractResponse: string | null contractResponseAt: Date | null depositReminderSentAt: Date | null flaggedAt: Date | null flagReason: string | null } // ─── Helpers ────────────────────────────────────────────────────────────── function berlinToday(): string { return new Date().toLocaleDateString('sv-SE', { timeZone: 'Europe/Berlin' }) } function berlinDateStr(d: Date): string { return d.toLocaleDateString('sv-SE', { timeZone: 'Europe/Berlin' }) } function daysBetween(from: Date, toYmd: string): number { return Math.round((Date.parse(toYmd) - from.getTime()) / 86_400_000) } function addDaysToDate(d: Date, n: number): Date { const result = new Date(d) result.setUTCDate(result.getUTCDate() + n) return result } type NextAction = | { kind: 'flagged'; reason: string } | { kind: 'awaiting_1yr' } | { kind: 'awaiting_signature' } | { kind: 'awaiting_deposit'; dueOn: Date | null } | { kind: 'contract_declined' } | null function nextAction(booking: BookingMilestones, deposit: DepositInvoice | null): NextAction { const { status, flaggedAt, flagReason, contractSentAt, contractResponse } = booking if (status === 'cancelled' || status === 'ended' || status === 'confirmed' || status === 'enquiry') return null if (flaggedAt && flagReason === 'contract_declined') return { kind: 'contract_declined' } if (flaggedAt) return { kind: 'flagged', reason: flagReason ?? '' } if (status === 'reserved') { if (!contractSentAt) return { kind: 'awaiting_1yr' } if (!contractResponse) return { kind: 'awaiting_signature' } if (contractResponse === 'approved' && !deposit?.paidOn) return { kind: 'awaiting_deposit', dueOn: deposit?.dueOn ?? null } } return null } // ─── Sub-components ─────────────────────────────────────────────────────── function DepositProgress({ contractSentAt, deposit, lng }: { contractSentAt: Date deposit: DepositInvoice | null lng: string }) { useLocale() const today = berlinToday() const daysSent = daysBetween(contractSentAt, today) const pct = Math.min(100, Math.round((daysSent / 14) * 100)) const color = daysSent >= 14 ? 'red' : daysSent >= 7 ? 'yellow' : 'plum' const dueDate = deposit?.dueOn ?? addDaysToDate(contractSentAt, 14) const dueDateStr = berlinDateStr(dueDate) const overdue = !deposit?.paidOn && dueDateStr < today return ( {deposit?.paidOn ? m.common__pages__admin_booking__status__milestones__deposit_paid() : overdue ? m.common__pages__admin_booking__status__deposit_overdue() : m.common__pages__admin_booking__status__deposit_window({ day: daysSent })} {deposit?.paidOn ? null : ( {m.common__pages__admin_booking__status__due_on({ date: fmtDate(dueDate, lng) })} )} {!deposit?.paidOn && ( )} ) } // ─── Main component ─────────────────────────────────────────────────────── export function BookingStatusPanel({ booking, invoices, }: { booking: BookingMilestones invoices: DepositInvoice[] }) { useLocale() const lng = getLocale() const deposit = invoices.find(inv => inv.kind === 'deposit') ?? null const action = nextAction(booking, deposit) // ── Stepper steps ── const steps = [ { key: 'enquiry', label: m.common__pages__admin_booking__status__milestones__enquiry(), description: fmtDate(booking.enquiryAt, lng), done: true, }, { key: 'reserved', label: m.common__pages__admin_booking__status__milestones__reserved(), description: booking.reservedAt ? fmtDate(booking.reservedAt, lng) : null, done: ['reserved', 'confirmed', 'ended'].includes(booking.status), }, { key: 'contractSent', label: m.common__pages__admin_booking__status__milestones__contract_sent(), description: booking.contractSentAt ? fmtDate(booking.contractSentAt, lng) : null, done: !!booking.contractSentAt, }, { key: 'contractSigned', label: m.common__pages__admin_booking__status__milestones__contract_signed(), description: booking.contractResponseAt && booking.contractResponse === 'approved' ? fmtDate(booking.contractResponseAt, lng) : null, done: booking.contractResponse === 'approved', }, { key: 'depositPaid', label: m.common__pages__admin_booking__status__milestones__deposit_paid(), description: deposit?.paidOn ? fmtDate(deposit.paidOn, lng) : null, done: !!deposit?.paidOn, }, { key: 'confirmed', label: m.common__pages__admin_booking__status__milestones__confirmed(), description: booking.confirmedAt ? fmtDate(booking.confirmedAt, lng) : null, done: ['confirmed', 'ended'].includes(booking.status), }, { key: 'ended', label: m.common__pages__admin_booking__status__milestones__ended(), description: booking.endedAt ? fmtDate(booking.endedAt, lng) : null, done: booking.status === 'ended', }, ] const activeStep = (() => { switch (booking.status) { case 'ended': return steps.length // all steps get checkmarks case 'confirmed': return steps.length - 1 // "ended" step is current case 'cancelled': return steps.reduce((acc, s, i) => (s.done ? i : acc), -1) default: return steps.findIndex(s => !s.done) // enquiry/reserved: use milestone data } })() return ( {/* Flagged alert */} {booking.flaggedAt && booking.flagReason && ( } title={m.common__pages__admin_booking__status__flagged()}> {booking.flagReason in flagReasonLabels ? flagReasonLabels[booking.flagReason as keyof typeof flagReasonLabels]() : asI18n(booking.flagReason)} )} {/* ── Horizontal stepper ── */} {steps.map((step) => ( ))} {booking.status === 'cancelled' && ( )} {/* Deposit progress bar (when contract sent but deposit not yet paid) */} {booking.contractSentAt && !deposit?.paidOn && ( )} {/* ── Next action alert (informational; controls live in BookingActions) ── */} {action && ( } > {action.kind === 'flagged' && (action.reason in flagReasonLabels ? flagReasonLabels[action.reason as keyof typeof flagReasonLabels]() : asI18n(action.reason))} {action.kind === 'awaiting_1yr' && m.common__pages__admin_booking__status__contract_not_sent_yet({ date: booking.startDate ? fmtDate(addDaysToDate(booking.startDate, -365), lng) : '—', })} {action.kind === 'awaiting_signature' && m.common__pages__admin_booking__status__awaiting_signature()} {action.kind === 'awaiting_deposit' && ( <> {m.common__pages__admin_booking__status__awaiting_deposit()} {action.dueOn && ( {m.common__pages__admin_booking__status__due_on({ date: fmtDate(action.dueOn, lng) })} )} )} {action.kind === 'contract_declined' && flagReasonLabels.contract_declined()} )} ) }