import { Show, For, createMemo, createSignal, Switch, Match } from 'solid-js' import { Clock, ChevronDown, ChevronUp, Copy, Check, CreditCard } from 'lucide-solid' import { useNavigate } from '@solidjs/router' import { useSession } from '../contexts/session-context' import { useI18n } from '../contexts/i18n-context' import { useDashboard } from '../contexts/dashboard-context' import { Card } from '../components/ui/card' import { Badge } from '../components/ui/badge' import { Button } from '../components/ui/button' import { Field } from '../components/ui/field' import { Input } from '../components/ui/input' import { Modal } from '../components/ui/dialog' import { Toast } from '../components/ui/toast' import { Skeleton } from '../components/ui/skeleton' import { ledgerPrimaryAmount } from '../lib/ledger-helpers' import { majorStringToMinor, minorToMajorString } from '../lib/money' import { compareTodayToPeriodDay, daysUntilPeriodDay, formatPeriodDay, nextCyclePeriod, parseCalendarDate } from '../lib/dates' import { submitMiniAppUtilityBill, addMiniAppPayment } from '../miniapp-api' import type { MiniAppDashboard } from '../miniapp-api' function sumMemberPaymentsByKind( data: MiniAppDashboard, memberId: string, kind: 'rent' | 'utilities' ): bigint { return data.ledger.reduce((sum, entry) => { if (entry.kind !== 'payment' || entry.memberId !== memberId || entry.paymentKind !== kind) { return sum } return sum + majorStringToMinor(entry.amountMajor) }, 0n) } function paymentProposalMinor( data: MiniAppDashboard, member: MiniAppDashboard['members'][number], kind: 'rent' | 'utilities' ): bigint { const purchaseOffsetMinor = majorStringToMinor(member.purchaseOffsetMajor) const baseMinor = kind === 'rent' ? majorStringToMinor(member.rentShareMajor) : majorStringToMinor(member.utilityShareMajor) if (data.paymentBalanceAdjustmentPolicy === kind) { return baseMinor + purchaseOffsetMinor } return baseMinor } function paymentRemainingMinor( data: MiniAppDashboard, member: MiniAppDashboard['members'][number], kind: 'rent' | 'utilities' ): bigint { const proposalMinor = paymentProposalMinor(data, member, kind) const paidMinor = sumMemberPaymentsByKind(data, member.memberId, kind) const remainingMinor = proposalMinor - paidMinor return remainingMinor > 0n ? remainingMinor : 0n } export default function HomeRoute() { const navigate = useNavigate() const { readySession, initData, refreshHouseholdData } = useSession() const { copy, locale } = useI18n() const { dashboard, loading, currentMemberLine, utilityLedger, utilityTotalMajor, testingPeriodOverride, testingTodayOverride } = useDashboard() const [showAllActivity, setShowAllActivity] = createSignal(false) const [utilityDraft, setUtilityDraft] = createSignal({ billName: '', amountMajor: '', currency: (dashboard()?.currency as 'USD' | 'GEL') ?? 'GEL' }) const [submittingUtilities, setSubmittingUtilities] = createSignal(false) const [copiedValue, setCopiedValue] = createSignal(null) const [quickPaymentOpen, setQuickPaymentOpen] = createSignal(false) const [quickPaymentType, setQuickPaymentType] = createSignal<'rent' | 'utilities'>('rent') const [quickPaymentContext, setQuickPaymentContext] = createSignal<'current' | 'overdue'>( 'current' ) const [quickPaymentAmount, setQuickPaymentAmount] = createSignal('') const [submittingPayment, setSubmittingPayment] = createSignal(false) const [toastState, setToastState] = createSignal<{ visible: boolean message: string type: 'success' | 'info' | 'error' }>({ visible: false, message: '', type: 'info' }) async function copyText(value: string): Promise { try { await navigator.clipboard.writeText(value) return true } catch { try { const element = document.createElement('textarea') element.value = value element.setAttribute('readonly', 'true') element.style.position = 'absolute' element.style.left = '-9999px' document.body.appendChild(element) element.select() document.execCommand('copy') document.body.removeChild(element) return true } catch {} } return false } async function handleCopy(value: string) { if (await copyText(value)) { setCopiedValue(value) setToastState({ visible: true, message: copy().copiedToast, type: 'success' }) setTimeout(() => { if (copiedValue() === value) { setCopiedValue(null) } }, 1400) } } function dueStatusBadge() { const data = dashboard() if (!data) return null const remaining = majorStringToMinor(data.totalRemainingMajor) if (remaining <= 0n) return { label: copy().homeSettledTitle, variant: 'accent' as const } return { label: copy().homeDueTitle, variant: 'danger' as const } } function paymentWindowStatus(input: { period: string timezone: string reminderDay: number dueDay: number todayOverride?: ReturnType }): { active: boolean; daysUntilDue: number | null } { if (!Number.isInteger(input.reminderDay) || !Number.isInteger(input.dueDay)) { return { active: false, daysUntilDue: null } } const start = compareTodayToPeriodDay( input.period, input.reminderDay, input.timezone, input.todayOverride ) const end = compareTodayToPeriodDay( input.period, input.dueDay, input.timezone, input.todayOverride ) if (start === null || end === null) { return { active: false, daysUntilDue: null } } const reminderPassed = start !== -1 const dueNotPassed = end !== 1 const daysUntilDue = daysUntilPeriodDay( input.period, input.dueDay, input.timezone, input.todayOverride ) return { active: reminderPassed && dueNotPassed, daysUntilDue } } const todayOverride = createMemo(() => { const raw = testingTodayOverride() if (!raw) return null return parseCalendarDate(raw) }) const effectivePeriod = createMemo(() => { const data = dashboard() if (!data) return null const override = testingPeriodOverride() if (!override) return data.period const match = /^(\d{4})-(\d{2})$/.exec(override) if (!match) return data.period const month = Number.parseInt(match[2] ?? '', 10) if (!Number.isInteger(month) || month < 1 || month > 12) return data.period return override }) const currentPaymentModes = createMemo(() => { const data = dashboard() const member = currentMemberLine() if (!data || !member) return [] as ('rent' | 'utilities')[] const period = effectivePeriod() ?? data.period const today = todayOverride() const utilities = paymentWindowStatus({ period, timezone: data.timezone, reminderDay: data.utilitiesReminderDay, dueDay: data.utilitiesDueDay, todayOverride: today }) const rent = paymentWindowStatus({ period, timezone: data.timezone, reminderDay: data.rentWarningDay, dueDay: data.rentDueDay, todayOverride: today }) const utilitiesDueMinor = paymentRemainingMinor(data, member, 'utilities') const rentDueMinor = paymentRemainingMinor(data, member, 'rent') const utilitiesActive = utilities.active && utilitiesDueMinor > 0n const rentActive = rent.active && rentDueMinor > 0n const modes: ('rent' | 'utilities')[] = [] if (utilitiesActive) { modes.push('utilities') } if (rentActive) { modes.push('rent') } return modes }) function overduePaymentFor(kind: 'rent' | 'utilities') { return currentMemberLine()?.overduePayments.find((payment) => payment.kind === kind) ?? null } async function handleSubmitUtilities() { const data = initData() const current = dashboard() const draft = utilityDraft() if (!data || !current || submittingUtilities()) return if (!draft.billName.trim() || !draft.amountMajor.trim()) return setSubmittingUtilities(true) try { await submitMiniAppUtilityBill(data, { billName: draft.billName, amountMajor: draft.amountMajor, currency: draft.currency }) setUtilityDraft({ billName: '', amountMajor: '', currency: current.currency }) await refreshHouseholdData(true, true) } finally { setSubmittingUtilities(false) } } function openQuickPayment( type: 'rent' | 'utilities', context: 'current' | 'overdue' = 'current' ) { const data = dashboard() if (!data || !currentMemberLine()) return const member = currentMemberLine()! const amount = context === 'overdue' ? (overduePaymentFor(type)?.amountMajor ?? '0.00') : minorToMajorString(paymentRemainingMinor(data, member, type)) setQuickPaymentType(type) setQuickPaymentContext(context) setQuickPaymentAmount(amount) setQuickPaymentOpen(true) } async function handleQuickPaymentSubmit() { const data = initData() const amount = quickPaymentAmount() const type = quickPaymentType() if (!data || !amount.trim() || !currentMemberLine()) return setSubmittingPayment(true) try { await addMiniAppPayment(data, { memberId: currentMemberLine()!.memberId, kind: type, amountMajor: amount, currency: (dashboard()?.currency as 'USD' | 'GEL') ?? 'GEL' }) setQuickPaymentOpen(false) setToastState({ visible: true, message: copy().quickPaymentSuccess, type: 'success' }) await refreshHouseholdData(true, true) } catch { setToastState({ visible: true, message: copy().quickPaymentFailed, type: 'error' }) } finally { setSubmittingPayment(false) } } return (
{/* ── Welcome hero ────────────────────────────── */}

{copy().welcome},

{readySession()?.member.displayName}

{/* ── Dashboard stats ─────────────────────────── */}

{copy().emptyDashboard}

{(data) => ( <> {(member) => { const policy = () => data().paymentBalanceAdjustmentPolicy const rentRemainingMinor = () => paymentRemainingMinor(data(), member(), 'rent') const utilitiesRemainingMinor = () => paymentRemainingMinor(data(), member(), 'utilities') const modes = () => currentPaymentModes() const currency = () => data().currency const timezone = () => data().timezone const period = () => effectivePeriod() ?? data().period const today = () => todayOverride() function upcomingDay(day: number): { dateLabel: string daysUntil: number | null } { const withinPeriodDays = daysUntilPeriodDay(period(), day, timezone(), today()) if (withinPeriodDays === null) { return { dateLabel: '—', daysUntil: null } } if (withinPeriodDays >= 0) { return { dateLabel: formatPeriodDay(period(), day, locale()), daysUntil: withinPeriodDays } } const next = nextCyclePeriod(period()) if (!next) { return { dateLabel: formatPeriodDay(period(), day, locale()), daysUntil: null } } return { dateLabel: formatPeriodDay(next, day, locale()), daysUntil: daysUntilPeriodDay(next, day, timezone(), today()) } } const rentDueDate = () => formatPeriodDay(period(), data().rentDueDay, locale()) const utilitiesDueDate = () => formatPeriodDay(period(), data().utilitiesDueDay, locale()) const rentDaysUntilDue = () => daysUntilPeriodDay(period(), data().rentDueDay, timezone(), today()) const utilitiesDaysUntilDue = () => daysUntilPeriodDay(period(), data().utilitiesDueDay, timezone(), today()) const rentUpcoming = () => upcomingDay(data().rentWarningDay) const utilitiesUpcoming = () => upcomingDay(data().utilitiesReminderDay) const focusBadge = () => { const badge = dueStatusBadge() return badge ? {badge.label} : null } const dueBadge = (days: number | null) => { if (days === null) return null if (days < 0) return {copy().overdueLabel} if (days === 0) return {copy().dueTodayLabel} return ( {copy().daysLeftLabel.replace('{count}', String(days))} ) } return ( <> {(overdue) => (
{copy().homeOverdueUtilitiesTitle}
{copy().overdueLabel}
{copy().finalDue} {overdue().amountMajor} {currency()}
{copy().homeOverduePeriodsLabel.replace( '{periods}', overdue().periods.join(', ') )}
)}
{(overdue) => (
{copy().homeOverdueRentTitle}
{copy().overdueLabel}
{copy().finalDue} {overdue().amountMajor} {currency()}
{copy().homeOverduePeriodsLabel.replace( '{periods}', overdue().periods.join(', ') )}
)}
{copy().homeUtilitiesTitle}
{focusBadge()}
{copy().finalDue} {minorToMajorString(utilitiesRemainingMinor())} {currency()}
{copy().dueOnLabel.replace('{date}', utilitiesDueDate())} {dueBadge(utilitiesDaysUntilDue())}
{copy().baseDue} {member().utilityShareMajor} {currency()}
{copy().balanceAdjustmentLabel} {member().purchaseOffsetMajor} {currency()}
0}>
{copy().homeUtilitiesBillsTitle} {utilityTotalMajor()} {currency()}
{(entry) => (
{entry.title} {ledgerPrimaryAmount(entry)}
)}
{copy().homeRentTitle}
{focusBadge()}
{copy().finalDue} {minorToMajorString(rentRemainingMinor())} {currency()}
{copy().dueOnLabel.replace('{date}', rentDueDate())} {dueBadge(rentDaysUntilDue())}
{copy().baseDue} {member().rentShareMajor} {currency()}
{copy().balanceAdjustmentLabel} {member().purchaseOffsetMajor} {currency()}
{copy().homeNoPaymentTitle}
{copy().homeUtilitiesUpcomingLabel.replace( '{date}', utilitiesUpcoming().dateLabel )} {utilitiesUpcoming().daysUntil !== null ? copy().daysLeftLabel.replace( '{count}', String(utilitiesUpcoming().daysUntil) ) : '—'}
{copy().homeRentUpcomingLabel.replace( '{date}', rentUpcoming().dateLabel )} {rentUpcoming().daysUntil !== null ? copy().daysLeftLabel.replace( '{count}', String(rentUpcoming().daysUntil) ) : '—'}
{copy().homeFillUtilitiesTitle}

{copy().homeFillUtilitiesBody}

setUtilityDraft((d) => ({ ...d, billName: e.currentTarget.value })) } /> setUtilityDraft((d) => ({ ...d, amountMajor: e.currentTarget.value })) } />
{(destination) => (
{destination.label}
{(value) => (
{copy().rentPaymentDestinationRecipient}
)}
{(value) => (
{copy().rentPaymentDestinationBank}
)}
{copy().rentPaymentDestinationAccount}
{(value) => (
{copy().rentPaymentDestinationLink}
)}
{(value) => (
{copy().rentPaymentDestinationNote}
)}
)}
) }}
{/* Rent FX card */}
{copy().rentFxTitle}
{copy().sourceAmountLabel} {data().rentSourceAmountMajor} {data().rentSourceCurrency}
{copy().settlementAmountLabel} {data().rentDisplayAmountMajor} {data().currency}
{copy().fxEffectiveDateLabel} {data().rentFxEffectiveDate}
{/* Latest activity */}
{copy().latestActivityTitle}
0} fallback={

{copy().latestActivityEmpty}

} >
{(entry) => (
{entry.title} {ledgerPrimaryAmount(entry)}
)}
5}>
)}
{/* Quick Payment Modal */} setQuickPaymentOpen(false)} footer={ <> } >
setQuickPaymentAmount(e.currentTarget.value)} placeholder="0.00" />
{/* Toast Notifications */} setToastState({ ...toastState(), visible: false })} />
) }