mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 10:24:02 +00:00
feat: add skeleton loading states for initial data load
- Add loading signal to DashboardContext - Update Skeleton component to accept style prop - Add skeleton UI to HomeRoute, LedgerRoute, and BalancesRoute - Use Switch/Match pattern for three-state handling (loading/empty/content)
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import { JSX } from 'solid-js'
|
||||
import { cn } from '../../lib/cn'
|
||||
|
||||
type SkeletonProps = {
|
||||
class?: string
|
||||
width?: string
|
||||
height?: string
|
||||
style?: JSX.CSSProperties
|
||||
}
|
||||
|
||||
export function Skeleton(props: SkeletonProps) {
|
||||
@@ -12,7 +14,8 @@ export function Skeleton(props: SkeletonProps) {
|
||||
class={cn('ui-skeleton', props.class)}
|
||||
style={{
|
||||
width: props.width,
|
||||
height: props.height
|
||||
height: props.height,
|
||||
...props.style
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -72,6 +72,7 @@ type DashboardContextValue = {
|
||||
setDashboard: (
|
||||
value: MiniAppDashboard | null | ((prev: MiniAppDashboard | null) => MiniAppDashboard | null)
|
||||
) => void
|
||||
loading: () => boolean
|
||||
adminSettings: () => MiniAppAdminSettingsPayload | null
|
||||
setAdminSettings: (
|
||||
value:
|
||||
@@ -246,6 +247,7 @@ export function DashboardProvider(props: ParentProps) {
|
||||
const { copy } = useI18n()
|
||||
|
||||
const [dashboard, setDashboard] = createSignal<MiniAppDashboard | null>(null)
|
||||
const [loading, setLoading] = createSignal(true)
|
||||
const [adminSettings, setAdminSettings] = createSignal<MiniAppAdminSettingsPayload | null>(null)
|
||||
const [cycleState, setCycleState] = createSignal<MiniAppAdminCycleState | null>(null)
|
||||
const [pendingMembers, setPendingMembers] = createSignal<readonly MiniAppPendingMember[]>([])
|
||||
@@ -299,9 +301,11 @@ export function DashboardProvider(props: ParentProps) {
|
||||
onCleanup(unregisterDashboardRefreshListener)
|
||||
|
||||
async function loadDashboardData(initData: string, isAdmin: boolean) {
|
||||
// In demo mode, use demo data
|
||||
setLoading(true)
|
||||
|
||||
if (!initData) {
|
||||
applyDemoState()
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -331,6 +335,8 @@ export function DashboardProvider(props: ParentProps) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
function applyDemoState() {
|
||||
@@ -345,6 +351,7 @@ export function DashboardProvider(props: ParentProps) {
|
||||
value={{
|
||||
dashboard,
|
||||
setDashboard,
|
||||
loading,
|
||||
adminSettings,
|
||||
setAdminSettings,
|
||||
cycleState,
|
||||
|
||||
@@ -1,151 +1,169 @@
|
||||
import { Show, For } from 'solid-js'
|
||||
import { Show, For, Switch, Match } from 'solid-js'
|
||||
import { BarChart3 } from 'lucide-solid'
|
||||
|
||||
import { useI18n } from '../contexts/i18n-context'
|
||||
import { useDashboard } from '../contexts/dashboard-context'
|
||||
import { Card } from '../components/ui/card'
|
||||
import { Skeleton } from '../components/ui/skeleton'
|
||||
import { memberRemainingClass } from '../lib/ledger-helpers'
|
||||
|
||||
export default function BalancesRoute() {
|
||||
const { copy } = useI18n()
|
||||
const { dashboard, memberBalanceVisuals, purchaseInvestmentChart } = useDashboard()
|
||||
const { dashboard, loading, memberBalanceVisuals, purchaseInvestmentChart } = useDashboard()
|
||||
|
||||
return (
|
||||
<div class="route route--balances">
|
||||
<Show
|
||||
when={dashboard()}
|
||||
fallback={
|
||||
<Switch>
|
||||
<Match when={loading()}>
|
||||
<Card>
|
||||
<div class="section-header">
|
||||
<Skeleton style={{ width: '180px', height: '20px' }} />
|
||||
<Skeleton style={{ width: '100%', height: '16px', 'margin-top': '8px' }} />
|
||||
</div>
|
||||
<div style={{ 'margin-top': '16px' }}>
|
||||
<Skeleton style={{ width: '100%', height: '120px' }} />
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<Skeleton style={{ width: '200px', height: '20px' }} />
|
||||
<Skeleton style={{ width: '100%', height: '80px', 'margin-top': '16px' }} />
|
||||
</Card>
|
||||
</Match>
|
||||
|
||||
<Match when={!dashboard()}>
|
||||
<Card>
|
||||
<p class="empty-state">{copy().balancesEmpty}</p>
|
||||
</Card>
|
||||
}
|
||||
>
|
||||
{(data) => (
|
||||
<>
|
||||
{/* ── Household balances ─────────────────── */}
|
||||
<Card>
|
||||
<div class="section-header">
|
||||
<strong>{copy().householdBalancesTitle}</strong>
|
||||
<p>{copy().householdBalancesBody}</p>
|
||||
</div>
|
||||
<div class="member-balance-list">
|
||||
<For each={data().members}>
|
||||
{(member) => (
|
||||
<div class={`member-balance-row ${memberRemainingClass(member)}`}>
|
||||
<span class="member-balance-row__name">{member.displayName}</span>
|
||||
<div class="member-balance-row__amounts">
|
||||
<span class="member-balance-row__due">
|
||||
{member.netDueMajor} {data().currency}
|
||||
</span>
|
||||
<span class="member-balance-row__remaining">
|
||||
{member.remainingMajor} {data().currency}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Card>
|
||||
</Match>
|
||||
|
||||
{/* ── Balance breakdown bars ──────────────── */}
|
||||
<Card>
|
||||
<div class="section-header">
|
||||
<BarChart3 size={16} />
|
||||
<strong>{copy().financeVisualsTitle}</strong>
|
||||
<p>{copy().financeVisualsBody}</p>
|
||||
</div>
|
||||
<div class="balance-visuals">
|
||||
<For each={memberBalanceVisuals()}>
|
||||
{(item) => (
|
||||
<div class="balance-bar-row">
|
||||
<span class="balance-bar-row__name">{item.member.displayName}</span>
|
||||
<div
|
||||
class="balance-bar-row__track"
|
||||
style={{ width: `${Math.max(item.barWidthPercent, 8)}%` }}
|
||||
>
|
||||
<For each={item.segments}>
|
||||
{(segment) => (
|
||||
<div
|
||||
class={`balance-bar-row__segment balance-bar-row__segment--${segment.key}`}
|
||||
style={{ width: `${segment.widthPercent}%` }}
|
||||
title={`${segment.label}: ${segment.amountMajor} ${data().currency}`}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<span class={`balance-bar-row__label ${memberRemainingClass(item.member)}`}>
|
||||
{item.member.remainingMajor} {data().currency}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
<div class="balance-bar-legend">
|
||||
<span class="balance-bar-legend__item balance-bar-legend__item--rent">
|
||||
{copy().shareRent}
|
||||
</span>
|
||||
<span class="balance-bar-legend__item balance-bar-legend__item--utilities">
|
||||
{copy().shareUtilities}
|
||||
</span>
|
||||
<span class="balance-bar-legend__item balance-bar-legend__item--purchase">
|
||||
{copy().shareOffset}
|
||||
</span>
|
||||
<Match when={dashboard()}>
|
||||
{(data) => (
|
||||
<>
|
||||
{/* ── Household balances ─────────────────── */}
|
||||
<Card>
|
||||
<div class="section-header">
|
||||
<strong>{copy().householdBalancesTitle}</strong>
|
||||
<p>{copy().householdBalancesBody}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* ── Purchase investment donut ───────────── */}
|
||||
<Card>
|
||||
<div class="section-header">
|
||||
<strong>{copy().purchaseInvestmentsTitle}</strong>
|
||||
<p>{copy().purchaseInvestmentsBody}</p>
|
||||
</div>
|
||||
<Show
|
||||
when={purchaseInvestmentChart().slices.length > 0}
|
||||
fallback={<p class="empty-state">{copy().purchaseInvestmentsEmpty}</p>}
|
||||
>
|
||||
<div class="donut-chart">
|
||||
<svg viewBox="0 0 100 100" class="donut-chart__svg">
|
||||
<For each={purchaseInvestmentChart().slices}>
|
||||
{(slice) => (
|
||||
<circle
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="42"
|
||||
fill="none"
|
||||
stroke={slice.color}
|
||||
stroke-width="12"
|
||||
stroke-dasharray={slice.dasharray}
|
||||
stroke-dashoffset={slice.dashoffset}
|
||||
class="donut-chart__slice"
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
<text x="50" y="48" text-anchor="middle" class="donut-chart__total">
|
||||
{purchaseInvestmentChart().totalMajor}
|
||||
</text>
|
||||
<text x="50" y="58" text-anchor="middle" class="donut-chart__label">
|
||||
{data().currency}
|
||||
</text>
|
||||
</svg>
|
||||
<div class="donut-chart__legend">
|
||||
<For each={purchaseInvestmentChart().slices}>
|
||||
{(slice) => (
|
||||
<div class="donut-chart__legend-item">
|
||||
<span class="donut-chart__color" style={{ background: slice.color }} />
|
||||
<span>{slice.label}</span>
|
||||
<strong>
|
||||
{slice.amountMajor} ({slice.percentage}%)
|
||||
</strong>
|
||||
<div class="member-balance-list">
|
||||
<For each={data().members}>
|
||||
{(member) => (
|
||||
<div class={`member-balance-row ${memberRemainingClass(member)}`}>
|
||||
<span class="member-balance-row__name">{member.displayName}</span>
|
||||
<div class="member-balance-row__amounts">
|
||||
<span class="member-balance-row__due">
|
||||
{member.netDueMajor} {data().currency}
|
||||
</span>
|
||||
<span class="member-balance-row__remaining">
|
||||
{member.remainingMajor} {data().currency}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* ── Balance breakdown bars ──────────────── */}
|
||||
<Card>
|
||||
<div class="section-header">
|
||||
<BarChart3 size={16} />
|
||||
<strong>{copy().financeVisualsTitle}</strong>
|
||||
<p>{copy().financeVisualsBody}</p>
|
||||
</div>
|
||||
<div class="balance-visuals">
|
||||
<For each={memberBalanceVisuals()}>
|
||||
{(item) => (
|
||||
<div class="balance-bar-row">
|
||||
<span class="balance-bar-row__name">{item.member.displayName}</span>
|
||||
<div
|
||||
class="balance-bar-row__track"
|
||||
style={{ width: `${Math.max(item.barWidthPercent, 8)}%` }}
|
||||
>
|
||||
<For each={item.segments}>
|
||||
{(segment) => (
|
||||
<div
|
||||
class={`balance-bar-row__segment balance-bar-row__segment--${segment.key}`}
|
||||
style={{ width: `${segment.widthPercent}%` }}
|
||||
title={`${segment.label}: ${segment.amountMajor} ${data().currency}`}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<span class={`balance-bar-row__label ${memberRemainingClass(item.member)}`}>
|
||||
{item.member.remainingMajor} {data().currency}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
<div class="balance-bar-legend">
|
||||
<span class="balance-bar-legend__item balance-bar-legend__item--rent">
|
||||
{copy().shareRent}
|
||||
</span>
|
||||
<span class="balance-bar-legend__item balance-bar-legend__item--utilities">
|
||||
{copy().shareUtilities}
|
||||
</span>
|
||||
<span class="balance-bar-legend__item balance-bar-legend__item--purchase">
|
||||
{copy().shareOffset}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
</Card>
|
||||
|
||||
{/* ── Purchase investment donut ───────────── */}
|
||||
<Card>
|
||||
<div class="section-header">
|
||||
<strong>{copy().purchaseInvestmentsTitle}</strong>
|
||||
<p>{copy().purchaseInvestmentsBody}</p>
|
||||
</div>
|
||||
<Show
|
||||
when={purchaseInvestmentChart().slices.length > 0}
|
||||
fallback={<p class="empty-state">{copy().purchaseInvestmentsEmpty}</p>}
|
||||
>
|
||||
<div class="donut-chart">
|
||||
<svg viewBox="0 0 100 100" class="donut-chart__svg">
|
||||
<For each={purchaseInvestmentChart().slices}>
|
||||
{(slice) => (
|
||||
<circle
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="42"
|
||||
fill="none"
|
||||
stroke={slice.color}
|
||||
stroke-width="12"
|
||||
stroke-dasharray={slice.dasharray}
|
||||
stroke-dashoffset={slice.dashoffset}
|
||||
class="donut-chart__slice"
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
<text x="50" y="48" text-anchor="middle" class="donut-chart__total">
|
||||
{purchaseInvestmentChart().totalMajor}
|
||||
</text>
|
||||
<text x="50" y="58" text-anchor="middle" class="donut-chart__label">
|
||||
{data().currency}
|
||||
</text>
|
||||
</svg>
|
||||
<div class="donut-chart__legend">
|
||||
<For each={purchaseInvestmentChart().slices}>
|
||||
{(slice) => (
|
||||
<div class="donut-chart__legend-item">
|
||||
<span class="donut-chart__color" style={{ background: slice.color }} />
|
||||
<span>{slice.label}</span>
|
||||
<strong>
|
||||
{slice.amountMajor} ({slice.percentage}%)
|
||||
</strong>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
import { Show, For, Index, createSignal, createMemo } from 'solid-js'
|
||||
import { Show, For, Index, createSignal, createMemo, Switch, Match } from 'solid-js'
|
||||
import { produce } from 'solid-js/store'
|
||||
import { Plus } from 'lucide-solid'
|
||||
|
||||
@@ -13,6 +13,7 @@ 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,
|
||||
@@ -203,7 +204,7 @@ function ParticipantSplitInputs(props: ParticipantSplitInputsProps) {
|
||||
export default function LedgerRoute() {
|
||||
const { initData, refreshHouseholdData } = useSession()
|
||||
const { copy } = useI18n()
|
||||
const { dashboard, effectiveIsAdmin, purchaseLedger, utilityLedger, paymentLedger } =
|
||||
const { dashboard, loading, effectiveIsAdmin, purchaseLedger, utilityLedger, paymentLedger } =
|
||||
useDashboard()
|
||||
|
||||
// ── Purchase editor ──────────────────────────────
|
||||
@@ -542,161 +543,177 @@ export default function LedgerRoute() {
|
||||
|
||||
return (
|
||||
<div class="route route--ledger">
|
||||
<Show
|
||||
when={dashboard()}
|
||||
fallback={
|
||||
<Switch>
|
||||
<Match when={loading()}>
|
||||
<Card>
|
||||
<Skeleton style={{ width: '100%', height: '24px', 'margin-bottom': '12px' }} />
|
||||
<Skeleton style={{ width: '80%', height: '48px' }} />
|
||||
</Card>
|
||||
<Card>
|
||||
<Skeleton style={{ width: '100%', height: '24px', 'margin-bottom': '12px' }} />
|
||||
<Skeleton style={{ width: '80%', height: '48px' }} />
|
||||
</Card>
|
||||
</Match>
|
||||
|
||||
<Match when={!dashboard()}>
|
||||
<Card>
|
||||
<p class="empty-state">{copy().ledgerEmpty}</p>
|
||||
</Card>
|
||||
}
|
||||
>
|
||||
{(_data) => (
|
||||
<>
|
||||
{/* ── Purchases ──────────────────────────── */}
|
||||
<Collapsible title={copy().purchasesTitle} body={copy().purchaseReviewBody} defaultOpen>
|
||||
<Show when={effectiveIsAdmin()}>
|
||||
<div class="ledger-actions">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const members = dashboard()?.members ?? []
|
||||
const currency = (dashboard()?.currency as 'USD' | 'GEL') ?? 'GEL'
|
||||
setNewPurchase({
|
||||
description: '',
|
||||
amountMajor: '',
|
||||
currency,
|
||||
splitMode: 'equal',
|
||||
splitInputMode: 'equal',
|
||||
participants: members.map((m) => ({
|
||||
memberId: m.memberId,
|
||||
included: true,
|
||||
shareAmountMajor: '',
|
||||
sharePercentage: ''
|
||||
}))
|
||||
})
|
||||
setAddPurchaseOpen(true)
|
||||
}}
|
||||
>
|
||||
<Plus size={14} />
|
||||
{copy().purchaseSaveAction}
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
<Show
|
||||
when={purchaseLedger().length > 0}
|
||||
fallback={<p class="empty-state">{copy().purchasesEmpty}</p>}
|
||||
>
|
||||
<div class="ledger-list">
|
||||
<For each={purchaseLedger()}>
|
||||
{(entry) => (
|
||||
<button
|
||||
class="ledger-entry"
|
||||
onClick={() => effectiveIsAdmin() && openPurchaseEditor(entry)}
|
||||
disabled={!effectiveIsAdmin()}
|
||||
>
|
||||
<div class="ledger-entry__main">
|
||||
<span class="ledger-entry__title">{entry.title}</span>
|
||||
<span class="ledger-entry__actor">{entry.actorDisplayName}</span>
|
||||
</div>
|
||||
<div class="ledger-entry__amounts">
|
||||
<strong>{ledgerPrimaryAmount(entry)}</strong>
|
||||
<Show when={ledgerSecondaryAmount(entry)}>
|
||||
{(secondary) => (
|
||||
<span class="ledger-entry__secondary">{secondary()}</span>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</Collapsible>
|
||||
</Match>
|
||||
|
||||
{/* ── Utility bills ──────────────────────── */}
|
||||
<Collapsible title={copy().utilityLedgerTitle}>
|
||||
<Show when={effectiveIsAdmin()}>
|
||||
<div class="ledger-actions">
|
||||
<Button variant="primary" size="sm" onClick={() => setAddUtilityOpen(true)}>
|
||||
<Plus size={14} />
|
||||
{copy().addUtilityBillAction}
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
<Show
|
||||
when={utilityLedger().length > 0}
|
||||
fallback={<p class="empty-state">{copy().utilityLedgerEmpty}</p>}
|
||||
<Match when={dashboard()}>
|
||||
{(_data) => (
|
||||
<>
|
||||
{/* ── Purchases ──────────────────────────── */}
|
||||
<Collapsible
|
||||
title={copy().purchasesTitle}
|
||||
body={copy().purchaseReviewBody}
|
||||
defaultOpen
|
||||
>
|
||||
<div class="ledger-list">
|
||||
<For each={utilityLedger()}>
|
||||
{(entry) => (
|
||||
<button
|
||||
class="ledger-entry"
|
||||
onClick={() => effectiveIsAdmin() && openUtilityEditor(entry)}
|
||||
disabled={!effectiveIsAdmin()}
|
||||
>
|
||||
<div class="ledger-entry__main">
|
||||
<span class="ledger-entry__title">{entry.title}</span>
|
||||
<span class="ledger-entry__actor">{entry.actorDisplayName}</span>
|
||||
</div>
|
||||
<div class="ledger-entry__amounts">
|
||||
<strong>{ledgerPrimaryAmount(entry)}</strong>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</Collapsible>
|
||||
<Show when={effectiveIsAdmin()}>
|
||||
<div class="ledger-actions">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const members = dashboard()?.members ?? []
|
||||
const currency = (dashboard()?.currency as 'USD' | 'GEL') ?? 'GEL'
|
||||
setNewPurchase({
|
||||
description: '',
|
||||
amountMajor: '',
|
||||
currency,
|
||||
splitMode: 'equal',
|
||||
splitInputMode: 'equal',
|
||||
participants: members.map((m) => ({
|
||||
memberId: m.memberId,
|
||||
included: true,
|
||||
shareAmountMajor: '',
|
||||
sharePercentage: ''
|
||||
}))
|
||||
})
|
||||
setAddPurchaseOpen(true)
|
||||
}}
|
||||
>
|
||||
<Plus size={14} />
|
||||
{copy().purchaseSaveAction}
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
<Show
|
||||
when={purchaseLedger().length > 0}
|
||||
fallback={<p class="empty-state">{copy().purchasesEmpty}</p>}
|
||||
>
|
||||
<div class="ledger-list">
|
||||
<For each={purchaseLedger()}>
|
||||
{(entry) => (
|
||||
<button
|
||||
class="ledger-entry"
|
||||
onClick={() => effectiveIsAdmin() && openPurchaseEditor(entry)}
|
||||
disabled={!effectiveIsAdmin()}
|
||||
>
|
||||
<div class="ledger-entry__main">
|
||||
<span class="ledger-entry__title">{entry.title}</span>
|
||||
<span class="ledger-entry__actor">{entry.actorDisplayName}</span>
|
||||
</div>
|
||||
<div class="ledger-entry__amounts">
|
||||
<strong>{ledgerPrimaryAmount(entry)}</strong>
|
||||
<Show when={ledgerSecondaryAmount(entry)}>
|
||||
{(secondary) => (
|
||||
<span class="ledger-entry__secondary">{secondary()}</span>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</Collapsible>
|
||||
|
||||
{/* ── Payments ───────────────────────────── */}
|
||||
<Collapsible
|
||||
title={copy().paymentsTitle}
|
||||
{...(effectiveIsAdmin() && copy().paymentsAdminBody
|
||||
? { body: copy().paymentsAdminBody }
|
||||
: {})}
|
||||
>
|
||||
<Show when={effectiveIsAdmin()}>
|
||||
<div class="ledger-actions">
|
||||
<Button variant="primary" size="sm" onClick={() => setAddPaymentOpen(true)}>
|
||||
<Plus size={14} />
|
||||
{copy().paymentsAddAction}
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
<Show
|
||||
when={paymentLedger().length > 0}
|
||||
fallback={<p class="empty-state">{copy().paymentsEmpty}</p>}
|
||||
{/* ── Utility bills ──────────────────────── */}
|
||||
<Collapsible title={copy().utilityLedgerTitle}>
|
||||
<Show when={effectiveIsAdmin()}>
|
||||
<div class="ledger-actions">
|
||||
<Button variant="primary" size="sm" onClick={() => setAddUtilityOpen(true)}>
|
||||
<Plus size={14} />
|
||||
{copy().addUtilityBillAction}
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
<Show
|
||||
when={utilityLedger().length > 0}
|
||||
fallback={<p class="empty-state">{copy().utilityLedgerEmpty}</p>}
|
||||
>
|
||||
<div class="ledger-list">
|
||||
<For each={utilityLedger()}>
|
||||
{(entry) => (
|
||||
<button
|
||||
class="ledger-entry"
|
||||
onClick={() => effectiveIsAdmin() && openUtilityEditor(entry)}
|
||||
disabled={!effectiveIsAdmin()}
|
||||
>
|
||||
<div class="ledger-entry__main">
|
||||
<span class="ledger-entry__title">{entry.title}</span>
|
||||
<span class="ledger-entry__actor">{entry.actorDisplayName}</span>
|
||||
</div>
|
||||
<div class="ledger-entry__amounts">
|
||||
<strong>{ledgerPrimaryAmount(entry)}</strong>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</Collapsible>
|
||||
|
||||
{/* ── Payments ───────────────────────────── */}
|
||||
<Collapsible
|
||||
title={copy().paymentsTitle}
|
||||
{...(effectiveIsAdmin() && copy().paymentsAdminBody
|
||||
? { body: copy().paymentsAdminBody }
|
||||
: {})}
|
||||
>
|
||||
<div class="ledger-list">
|
||||
<For each={paymentLedger()}>
|
||||
{(entry) => (
|
||||
<button
|
||||
class="ledger-entry"
|
||||
onClick={() => effectiveIsAdmin() && openPaymentEditor(entry)}
|
||||
disabled={!effectiveIsAdmin()}
|
||||
>
|
||||
<div class="ledger-entry__main">
|
||||
<span class="ledger-entry__title">
|
||||
{entry.paymentKind === 'rent'
|
||||
? copy().paymentLedgerRent
|
||||
: copy().paymentLedgerUtilities}
|
||||
</span>
|
||||
<span class="ledger-entry__actor">{entry.actorDisplayName}</span>
|
||||
</div>
|
||||
<div class="ledger-entry__amounts">
|
||||
<strong>{ledgerPrimaryAmount(entry)}</strong>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</Collapsible>
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={effectiveIsAdmin()}>
|
||||
<div class="ledger-actions">
|
||||
<Button variant="primary" size="sm" onClick={() => setAddPaymentOpen(true)}>
|
||||
<Plus size={14} />
|
||||
{copy().paymentsAddAction}
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
<Show
|
||||
when={paymentLedger().length > 0}
|
||||
fallback={<p class="empty-state">{copy().paymentsEmpty}</p>}
|
||||
>
|
||||
<div class="ledger-list">
|
||||
<For each={paymentLedger()}>
|
||||
{(entry) => (
|
||||
<button
|
||||
class="ledger-entry"
|
||||
onClick={() => effectiveIsAdmin() && openPaymentEditor(entry)}
|
||||
disabled={!effectiveIsAdmin()}
|
||||
>
|
||||
<div class="ledger-entry__main">
|
||||
<span class="ledger-entry__title">
|
||||
{entry.paymentKind === 'rent'
|
||||
? copy().paymentLedgerRent
|
||||
: copy().paymentLedgerUtilities}
|
||||
</span>
|
||||
<span class="ledger-entry__actor">{entry.actorDisplayName}</span>
|
||||
</div>
|
||||
<div class="ledger-entry__amounts">
|
||||
<strong>{ledgerPrimaryAmount(entry)}</strong>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</Collapsible>
|
||||
</>
|
||||
)}
|
||||
</Match>
|
||||
</Switch>
|
||||
|
||||
{/* ──────── Add Purchase Modal (Bug #4 fix) ──── */}
|
||||
<Modal
|
||||
|
||||
Reference in New Issue
Block a user