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
}
>
)
}
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((prev) => updater(prev))}
/>
{/* ──────── Edit Purchase Modal ───────────────── */}
}
>
{(draft) => (
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((prev) => (prev ? updater(prev) : prev))
}
/>
)}
{/* ──────── Add Payment Modal ─────────────────── */}
setAddPaymentOpen(false)}
footer={
}
>
setNewPayment((p) => ({ ...p, amountMajor: e.currentTarget.value }))}
/>
{/* ──────── Add Utility Modal ─────────────────── */}
setAddUtilityOpen(false)}
footer={
}
>
setNewUtility((p) => ({ ...p, billName: e.currentTarget.value }))}
/>
setNewUtility((p) => ({ ...p, amountMajor: e.currentTarget.value }))}
/>
{/* ──────── Edit Utility Modal ────────────────── */}
}
>
{(draft) => (
setUtilityDraft((d) => (d ? { ...d, billName: e.currentTarget.value } : d))
}
/>
setUtilityDraft((d) => (d ? { ...d, amountMajor: e.currentTarget.value } : d))
}
/>
)}
{/* ──────── Edit Payment Modal ────────────────── */}
}
>
{(draft) => (
setPaymentDraft((d) => (d ? { ...d, amountMajor: e.currentTarget.value } : d))
}
/>
)}
)
}