import { Show, For, Index, createSignal, createMemo, Switch, Match } from 'solid-js' import { produce } from 'solid-js/store' import { Plus } from 'lucide-solid' 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 { Button } from '../components/ui/button' import { Modal } from '../components/ui/dialog' import { Input } from '../components/ui/input' import { Select } from '../components/ui/select' import { Field } from '../components/ui/field' import { Collapsible } from '../components/ui/collapsible' import { Toggle } from '../components/ui/toggle' import { Skeleton } from '../components/ui/skeleton' import { ledgerPrimaryAmount, ledgerSecondaryAmount, purchaseDraftForEntry, paymentDraftForEntry, computePaymentPrefill, rebalancePurchaseSplit, validatePurchaseDraft, type PurchaseDraft, type PaymentDraft } from '../lib/ledger-helpers' import { minorToMajorString, majorStringToMinor } from '../lib/money' import { addMiniAppPurchase, updateMiniAppPurchase, deleteMiniAppPurchase, addMiniAppPayment, updateMiniAppPayment, deleteMiniAppPayment, addMiniAppUtilityBill, updateMiniAppUtilityBill, deleteMiniAppUtilityBill, type MiniAppDashboard } from '../miniapp-api' interface ParticipantSplitInputsProps { draft: PurchaseDraft updateDraft: (fn: (d: PurchaseDraft) => PurchaseDraft) => void } function ParticipantSplitInputs(props: ParticipantSplitInputsProps) { const { dashboard } = useDashboard() const validation = () => validatePurchaseDraft(props.draft) return (
{(participant, idx) => { const member = () => dashboard()?.members.find((m) => m.memberId === participant().memberId) return (
{ props.updateDraft((prev) => { const participants = prev.participants.map((p, i) => i === idx ? { ...p, included: checked, lastUpdatedAt: Date.now(), isAutoCalculated: false } : p ) return rebalancePurchaseSplit({ ...prev, participants }, null, null) }) }} /> {member()?.displayName ?? 'Unknown'} { const value = e.currentTarget.value props.updateDraft( produce((d: PurchaseDraft) => { if (d.participants[idx]) { d.participants[idx].shareAmountMajor = value d.participants[idx].isAutoCalculated = false d.participants[idx].lastUpdatedAt = Date.now() } }) ) }} onBlur={(e) => { const value = e.currentTarget.value const minor = majorStringToMinor(value) props.updateDraft((prev) => { if (minor <= 0n) { const participants = prev.participants.map((p, i) => i === idx ? { ...p, included: false, shareAmountMajor: '0.00', sharePercentage: '' } : p ) return rebalancePurchaseSplit({ ...prev, participants }, null, null) } return rebalancePurchaseSplit(prev, participant().memberId, value) }) }} /> { const value = e.currentTarget.value props.updateDraft( produce((d: PurchaseDraft) => { if (d.participants[idx]) { d.participants[idx].sharePercentage = value d.participants[idx].isAutoCalculated = false d.participants[idx].lastUpdatedAt = Date.now() } }) ) }} onBlur={(e) => { const value = e.currentTarget.value const percentage = parseFloat(value) || 0 props.updateDraft((prev) => { if (percentage <= 0) { const participants = prev.participants.map((p, i) => i === idx ? { ...p, included: false, shareAmountMajor: '0.00', sharePercentage: '' } : p ) return rebalancePurchaseSplit({ ...prev, participants }, null, null) } const totalMinor = majorStringToMinor(prev.amountMajor) const shareMinor = (totalMinor * BigInt(Math.round(percentage * 100))) / 10000n const amountMajor = minorToMajorString(shareMinor) const updated = rebalancePurchaseSplit( prev, participant().memberId, amountMajor ) // Preserve the typed percentage string const participants = updated.participants.map((p, i) => i === idx ? { ...p, sharePercentage: value } : p ) return { ...updated, participants } }) }} />
) }}
p.included) && !validation().valid } >
{validation().error}
) } export default function LedgerRoute() { const { initData, refreshHouseholdData, session } = useSession() const { copy } = useI18n() const { dashboard, loading, effectiveIsAdmin, purchaseLedger, utilityLedger, paymentLedger } = useDashboard() // ── Purchase editor ────────────────────────────── const [editingPurchase, setEditingPurchase] = createSignal< MiniAppDashboard['ledger'][number] | null >(null) const [purchaseDraft, setPurchaseDraft] = createSignal(null) const [savingPurchase, setSavingPurchase] = createSignal(false) const [deletingPurchase, setDeletingPurchase] = createSignal(false) // ── New purchase form (Bug #4 fix) ─────────────── const [addPurchaseOpen, setAddPurchaseOpen] = createSignal(false) const [newPurchase, setNewPurchase] = createSignal({ description: '', amountMajor: '', currency: (dashboard()?.currency as 'USD' | 'GEL') ?? 'GEL', splitMode: 'equal', splitInputMode: 'equal', participants: [] }) const [addingPurchase, setAddingPurchase] = createSignal(false) // ── Utility bill editor ────────────────────────── const [editingUtility, setEditingUtility] = createSignal< MiniAppDashboard['ledger'][number] | null >(null) const [utilityDraft, setUtilityDraft] = createSignal<{ billName: string amountMajor: string currency: 'USD' | 'GEL' } | null>(null) const [savingUtility, setSavingUtility] = createSignal(false) const [deletingUtility, setDeletingUtility] = createSignal(false) // ── New utility bill form ──────────────────────── const [addUtilityOpen, setAddUtilityOpen] = createSignal(false) const [newUtility, setNewUtility] = createSignal({ billName: '', amountMajor: '', currency: (dashboard()?.currency as 'USD' | 'GEL') ?? 'GEL' }) const [addingUtility, setAddingUtility] = createSignal(false) // ── Payment editor ─────────────────────────────── const [editingPayment, setEditingPayment] = createSignal< MiniAppDashboard['ledger'][number] | null >(null) const [paymentDraftState, setPaymentDraft] = createSignal(null) const [savingPayment, setSavingPayment] = createSignal(false) const [deletingPayment, setDeletingPayment] = createSignal(false) // ── New payment form ───────────────────────────── const [addPaymentOpen, setAddPaymentOpen] = createSignal(false) const [newPayment, setNewPayment] = createSignal({ memberId: '', kind: 'rent', amountMajor: '', currency: (dashboard()?.currency as 'USD' | 'GEL') ?? 'GEL' }) const [addingPayment, setAddingPayment] = createSignal(false) const addPurchaseButtonText = createMemo(() => { if (addingPurchase()) return copy().savingPurchase if (newPurchase().splitInputMode !== 'equal' && !validatePurchaseDraft(newPurchase()).valid) { return copy().purchaseBalanceAction } return copy().purchaseSaveAction }) const editPurchaseButtonText = createMemo(() => { if (savingPurchase()) return copy().savingPurchase const draft = purchaseDraft() if (draft && draft.splitInputMode !== 'equal' && !validatePurchaseDraft(draft).valid) { return copy().purchaseBalanceAction } return copy().purchaseSaveAction }) function openPurchaseEditor(entry: MiniAppDashboard['ledger'][number]) { setEditingPurchase(entry) setPurchaseDraft(purchaseDraftForEntry(entry)) } function closePurchaseEditor() { setEditingPurchase(null) setPurchaseDraft(null) } function openPaymentEditor(entry: MiniAppDashboard['ledger'][number]) { setEditingPayment(entry) setPaymentDraft(paymentDraftForEntry(entry)) } function closePaymentEditor() { setEditingPayment(null) setPaymentDraft(null) } function openUtilityEditor(entry: MiniAppDashboard['ledger'][number]) { setEditingUtility(entry) setUtilityDraft({ billName: entry.title, amountMajor: entry.amountMajor, currency: entry.currency as 'USD' | 'GEL' }) } function closeUtilityEditor() { setEditingUtility(null) setUtilityDraft(null) } async function handleAddUtility() { const data = initData() const draft = newUtility() if (!data || !draft.billName.trim() || !draft.amountMajor.trim()) return setAddingUtility(true) try { await addMiniAppUtilityBill(data, draft) setAddUtilityOpen(false) setNewUtility({ billName: '', amountMajor: '', currency: (dashboard()?.currency as 'USD' | 'GEL') ?? 'GEL' }) await refreshHouseholdData(true, true) } finally { setAddingUtility(false) } } async function handleSaveUtility() { const data = initData() const entry = editingUtility() const draft = utilityDraft() if (!data || !entry || !draft) return setSavingUtility(true) try { await updateMiniAppUtilityBill(data, { billId: entry.id, ...draft }) closeUtilityEditor() await refreshHouseholdData(true, true) } finally { setSavingUtility(false) } } async function handleDeleteUtility() { const data = initData() const entry = editingUtility() if (!data || !entry) return setDeletingUtility(true) try { await deleteMiniAppUtilityBill(data, entry.id) closeUtilityEditor() await refreshHouseholdData(true, true) } finally { setDeletingUtility(false) } } async function handleSavePurchase() { const data = initData() const entry = editingPurchase() const draft = purchaseDraft() if (!data || !entry || !draft) return setSavingPurchase(true) try { await updateMiniAppPurchase(data, { purchaseId: entry.id, description: draft.description, amountMajor: draft.amountMajor, currency: draft.currency, ...(draft.payerMemberId ? { payerMemberId: draft.payerMemberId } : {}), split: { mode: draft.splitMode, participants: draft.participants.map((p) => ({ memberId: p.memberId, included: p.included, ...(draft.splitMode === 'custom_amounts' ? { shareAmountMajor: p.shareAmountMajor || '0.00' } : {}) })) } }) closePurchaseEditor() await refreshHouseholdData(true, true) } finally { setSavingPurchase(false) } } async function handleDeletePurchase() { const data = initData() const entry = editingPurchase() if (!data || !entry) return setDeletingPurchase(true) try { await deleteMiniAppPurchase(data, entry.id) closePurchaseEditor() await refreshHouseholdData(true, true) } finally { setDeletingPurchase(false) } } async function handleAddPurchase() { const data = initData() const draft = newPurchase() if (!data || !draft.description.trim() || !draft.amountMajor.trim()) return setAddingPurchase(true) try { await addMiniAppPurchase(data, { description: draft.description, amountMajor: draft.amountMajor, currency: draft.currency, ...(draft.payerMemberId ? { payerMemberId: draft.payerMemberId } : {}), ...(draft.participants.length > 0 ? { split: { mode: draft.splitMode, participants: draft.participants.map((p) => ({ memberId: p.memberId, included: p.included, ...(draft.splitMode === 'custom_amounts' ? { shareAmountMajor: p.shareAmountMajor || '0.00' } : {}) })) } } : {}) }) setAddPurchaseOpen(false) const currentSession = session() setNewPurchase({ description: '', amountMajor: '', currency: (dashboard()?.currency as 'USD' | 'GEL') ?? 'GEL', ...(currentSession.status === 'ready' ? { payerMemberId: currentSession.member.id } : {}), splitMode: 'equal', splitInputMode: 'equal', participants: [] }) await refreshHouseholdData(true, true) } finally { setAddingPurchase(false) } } async function handleSavePayment() { const data = initData() const entry = editingPayment() const draft = paymentDraftState() if (!data || !entry || !draft) return setSavingPayment(true) try { await updateMiniAppPayment(data, { paymentId: entry.id, memberId: draft.memberId, kind: draft.kind, amountMajor: draft.amountMajor, currency: draft.currency }) closePaymentEditor() await refreshHouseholdData(true, true) } finally { setSavingPayment(false) } } async function handleDeletePayment() { const data = initData() const entry = editingPayment() if (!data || !entry) return setDeletingPayment(true) try { await deleteMiniAppPayment(data, entry.id) closePaymentEditor() await refreshHouseholdData(true, true) } finally { setDeletingPayment(false) } } async function handleAddPayment() { const data = initData() const draft = newPayment() if (!data || !draft.memberId || !draft.amountMajor.trim()) return setAddingPayment(true) try { await addMiniAppPayment(data, { memberId: draft.memberId, kind: draft.kind, amountMajor: draft.amountMajor, currency: draft.currency }) setAddPaymentOpen(false) setNewPayment({ memberId: '', kind: 'rent', amountMajor: '', currency: (dashboard()?.currency as 'USD' | 'GEL') ?? 'GEL' }) await refreshHouseholdData(true, true) } finally { setAddingPayment(false) } } const currencyOptions = () => [ { value: 'GEL', label: 'GEL' }, { value: 'USD', label: 'USD' } ] const kindOptions = () => [ { value: 'rent', label: copy().shareRent }, { value: 'utilities', label: copy().shareUtilities } ] const memberOptions = createMemo(() => (dashboard()?.members ?? []).map((m) => ({ value: m.memberId, label: m.displayName })) ) const splitModeOptions = () => [ { value: 'equal', label: copy().purchaseSplitEqual }, { value: 'exact', label: copy().purchaseSplitExact }, { value: 'percentage', label: copy().purchaseSplitPercentage } ] return (

{copy().ledgerEmpty}

{(_data) => ( <> {/* ── Purchases ──────────────────────────── */}
0} fallback={

{copy().purchasesEmpty}

} >
{(entry) => ( )}
{/* ── Utility bills ──────────────────────── */}
0} fallback={

{copy().utilityLedgerEmpty}

} >
{(entry) => ( )}
{/* ── Payments ───────────────────────────── */}
0} fallback={

{copy().paymentsEmpty}

} >
{(entry) => ( )}
)}
{/* ──────── Add Purchase Modal (Bug #4 fix) ──── */} setAddPurchaseOpen(false)} footer={ } >
setNewPurchase((p) => ({ ...p, description: e.currentTarget.value }))} /> { const amountMajor = e.currentTarget.value setNewPurchase((p) => { const updated = { ...p, amountMajor } return rebalancePurchaseSplit(updated, null, null) }) }} /> setNewPurchase((p) => { const base = { ...p } if (value) { return { ...base, payerMemberId: value } } // eslint-disable-next-line @typescript-eslint/no-unused-vars const { payerMemberId, ...rest } = base return rest as PurchaseDraft }) } />
setPurchaseDraft((d) => (d ? { ...d, description: e.currentTarget.value } : d)) } /> { const amountMajor = e.currentTarget.value setPurchaseDraft((d) => { if (!d) return d const updated = { ...d, amountMajor } return rebalancePurchaseSplit(updated, null, null) }) }} /> setPurchaseDraft((d) => { if (!d) return d const base = { ...d } if (value) { return { ...base, payerMemberId: value } } // eslint-disable-next-line @typescript-eslint/no-unused-vars const { payerMemberId, ...rest } = base return rest as PurchaseDraft }) } />
{ const member = dashboard()?.members.find((m) => m.memberId === memberId) const prefill = computePaymentPrefill(member, newPayment().kind) setNewPayment((p) => ({ ...p, memberId, amountMajor: prefill })) }} /> setNewPayment((p) => ({ ...p, amountMajor: e.currentTarget.value }))} /> setNewUtility((p) => ({ ...p, billName: e.currentTarget.value }))} /> setNewUtility((p) => ({ ...p, amountMajor: e.currentTarget.value }))} /> setUtilityDraft((d) => (d ? { ...d, billName: e.currentTarget.value } : d)) } /> setUtilityDraft((d) => (d ? { ...d, amountMajor: e.currentTarget.value } : d)) } /> setPaymentDraft((d) => (d ? { ...d, memberId: value } : d))} /> setPaymentDraft((d) => (d ? { ...d, amountMajor: e.currentTarget.value } : d)) } />
)}
) }