mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 12:04: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'
|
import { cn } from '../../lib/cn'
|
||||||
|
|
||||||
type SkeletonProps = {
|
type SkeletonProps = {
|
||||||
class?: string
|
class?: string
|
||||||
width?: string
|
width?: string
|
||||||
height?: string
|
height?: string
|
||||||
|
style?: JSX.CSSProperties
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Skeleton(props: SkeletonProps) {
|
export function Skeleton(props: SkeletonProps) {
|
||||||
@@ -12,7 +14,8 @@ export function Skeleton(props: SkeletonProps) {
|
|||||||
class={cn('ui-skeleton', props.class)}
|
class={cn('ui-skeleton', props.class)}
|
||||||
style={{
|
style={{
|
||||||
width: props.width,
|
width: props.width,
|
||||||
height: props.height
|
height: props.height,
|
||||||
|
...props.style
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ type DashboardContextValue = {
|
|||||||
setDashboard: (
|
setDashboard: (
|
||||||
value: MiniAppDashboard | null | ((prev: MiniAppDashboard | null) => MiniAppDashboard | null)
|
value: MiniAppDashboard | null | ((prev: MiniAppDashboard | null) => MiniAppDashboard | null)
|
||||||
) => void
|
) => void
|
||||||
|
loading: () => boolean
|
||||||
adminSettings: () => MiniAppAdminSettingsPayload | null
|
adminSettings: () => MiniAppAdminSettingsPayload | null
|
||||||
setAdminSettings: (
|
setAdminSettings: (
|
||||||
value:
|
value:
|
||||||
@@ -246,6 +247,7 @@ export function DashboardProvider(props: ParentProps) {
|
|||||||
const { copy } = useI18n()
|
const { copy } = useI18n()
|
||||||
|
|
||||||
const [dashboard, setDashboard] = createSignal<MiniAppDashboard | null>(null)
|
const [dashboard, setDashboard] = createSignal<MiniAppDashboard | null>(null)
|
||||||
|
const [loading, setLoading] = createSignal(true)
|
||||||
const [adminSettings, setAdminSettings] = createSignal<MiniAppAdminSettingsPayload | null>(null)
|
const [adminSettings, setAdminSettings] = createSignal<MiniAppAdminSettingsPayload | null>(null)
|
||||||
const [cycleState, setCycleState] = createSignal<MiniAppAdminCycleState | null>(null)
|
const [cycleState, setCycleState] = createSignal<MiniAppAdminCycleState | null>(null)
|
||||||
const [pendingMembers, setPendingMembers] = createSignal<readonly MiniAppPendingMember[]>([])
|
const [pendingMembers, setPendingMembers] = createSignal<readonly MiniAppPendingMember[]>([])
|
||||||
@@ -299,9 +301,11 @@ export function DashboardProvider(props: ParentProps) {
|
|||||||
onCleanup(unregisterDashboardRefreshListener)
|
onCleanup(unregisterDashboardRefreshListener)
|
||||||
|
|
||||||
async function loadDashboardData(initData: string, isAdmin: boolean) {
|
async function loadDashboardData(initData: string, isAdmin: boolean) {
|
||||||
// In demo mode, use demo data
|
setLoading(true)
|
||||||
|
|
||||||
if (!initData) {
|
if (!initData) {
|
||||||
applyDemoState()
|
applyDemoState()
|
||||||
|
setLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,6 +335,8 @@ export function DashboardProvider(props: ParentProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyDemoState() {
|
function applyDemoState() {
|
||||||
@@ -345,6 +351,7 @@ export function DashboardProvider(props: ParentProps) {
|
|||||||
value={{
|
value={{
|
||||||
dashboard,
|
dashboard,
|
||||||
setDashboard,
|
setDashboard,
|
||||||
|
loading,
|
||||||
adminSettings,
|
adminSettings,
|
||||||
setAdminSettings,
|
setAdminSettings,
|
||||||
cycleState,
|
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 { BarChart3 } from 'lucide-solid'
|
||||||
|
|
||||||
import { useI18n } from '../contexts/i18n-context'
|
import { useI18n } from '../contexts/i18n-context'
|
||||||
import { useDashboard } from '../contexts/dashboard-context'
|
import { useDashboard } from '../contexts/dashboard-context'
|
||||||
import { Card } from '../components/ui/card'
|
import { Card } from '../components/ui/card'
|
||||||
|
import { Skeleton } from '../components/ui/skeleton'
|
||||||
import { memberRemainingClass } from '../lib/ledger-helpers'
|
import { memberRemainingClass } from '../lib/ledger-helpers'
|
||||||
|
|
||||||
export default function BalancesRoute() {
|
export default function BalancesRoute() {
|
||||||
const { copy } = useI18n()
|
const { copy } = useI18n()
|
||||||
const { dashboard, memberBalanceVisuals, purchaseInvestmentChart } = useDashboard()
|
const { dashboard, loading, memberBalanceVisuals, purchaseInvestmentChart } = useDashboard()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="route route--balances">
|
<div class="route route--balances">
|
||||||
<Show
|
<Switch>
|
||||||
when={dashboard()}
|
<Match when={loading()}>
|
||||||
fallback={
|
<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>
|
<Card>
|
||||||
<p class="empty-state">{copy().balancesEmpty}</p>
|
<p class="empty-state">{copy().balancesEmpty}</p>
|
||||||
</Card>
|
</Card>
|
||||||
}
|
</Match>
|
||||||
>
|
|
||||||
{(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>
|
|
||||||
|
|
||||||
{/* ── Balance breakdown bars ──────────────── */}
|
<Match when={dashboard()}>
|
||||||
<Card>
|
{(data) => (
|
||||||
<div class="section-header">
|
<>
|
||||||
<BarChart3 size={16} />
|
{/* ── Household balances ─────────────────── */}
|
||||||
<strong>{copy().financeVisualsTitle}</strong>
|
<Card>
|
||||||
<p>{copy().financeVisualsBody}</p>
|
<div class="section-header">
|
||||||
</div>
|
<strong>{copy().householdBalancesTitle}</strong>
|
||||||
<div class="balance-visuals">
|
<p>{copy().householdBalancesBody}</p>
|
||||||
<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>
|
||||||
</div>
|
<div class="member-balance-list">
|
||||||
</Card>
|
<For each={data().members}>
|
||||||
|
{(member) => (
|
||||||
{/* ── Purchase investment donut ───────────── */}
|
<div class={`member-balance-row ${memberRemainingClass(member)}`}>
|
||||||
<Card>
|
<span class="member-balance-row__name">{member.displayName}</span>
|
||||||
<div class="section-header">
|
<div class="member-balance-row__amounts">
|
||||||
<strong>{copy().purchaseInvestmentsTitle}</strong>
|
<span class="member-balance-row__due">
|
||||||
<p>{copy().purchaseInvestmentsBody}</p>
|
{member.netDueMajor} {data().currency}
|
||||||
</div>
|
</span>
|
||||||
<Show
|
<span class="member-balance-row__remaining">
|
||||||
when={purchaseInvestmentChart().slices.length > 0}
|
{member.remainingMajor} {data().currency}
|
||||||
fallback={<p class="empty-state">{copy().purchaseInvestmentsEmpty}</p>}
|
</span>
|
||||||
>
|
|
||||||
<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>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</For>
|
)}
|
||||||
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Card>
|
||||||
</Card>
|
|
||||||
</>
|
{/* ── Purchase investment donut ───────────── */}
|
||||||
)}
|
<Card>
|
||||||
</Show>
|
<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>
|
</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 { produce } from 'solid-js/store'
|
||||||
import { Plus } from 'lucide-solid'
|
import { Plus } from 'lucide-solid'
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ import { Select } from '../components/ui/select'
|
|||||||
import { Field } from '../components/ui/field'
|
import { Field } from '../components/ui/field'
|
||||||
import { Collapsible } from '../components/ui/collapsible'
|
import { Collapsible } from '../components/ui/collapsible'
|
||||||
import { Toggle } from '../components/ui/toggle'
|
import { Toggle } from '../components/ui/toggle'
|
||||||
|
import { Skeleton } from '../components/ui/skeleton'
|
||||||
import {
|
import {
|
||||||
ledgerPrimaryAmount,
|
ledgerPrimaryAmount,
|
||||||
ledgerSecondaryAmount,
|
ledgerSecondaryAmount,
|
||||||
@@ -203,7 +204,7 @@ function ParticipantSplitInputs(props: ParticipantSplitInputsProps) {
|
|||||||
export default function LedgerRoute() {
|
export default function LedgerRoute() {
|
||||||
const { initData, refreshHouseholdData } = useSession()
|
const { initData, refreshHouseholdData } = useSession()
|
||||||
const { copy } = useI18n()
|
const { copy } = useI18n()
|
||||||
const { dashboard, effectiveIsAdmin, purchaseLedger, utilityLedger, paymentLedger } =
|
const { dashboard, loading, effectiveIsAdmin, purchaseLedger, utilityLedger, paymentLedger } =
|
||||||
useDashboard()
|
useDashboard()
|
||||||
|
|
||||||
// ── Purchase editor ──────────────────────────────
|
// ── Purchase editor ──────────────────────────────
|
||||||
@@ -542,161 +543,177 @@ export default function LedgerRoute() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="route route--ledger">
|
<div class="route route--ledger">
|
||||||
<Show
|
<Switch>
|
||||||
when={dashboard()}
|
<Match when={loading()}>
|
||||||
fallback={
|
<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>
|
<Card>
|
||||||
<p class="empty-state">{copy().ledgerEmpty}</p>
|
<p class="empty-state">{copy().ledgerEmpty}</p>
|
||||||
</Card>
|
</Card>
|
||||||
}
|
</Match>
|
||||||
>
|
|
||||||
{(_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>
|
|
||||||
|
|
||||||
{/* ── Utility bills ──────────────────────── */}
|
<Match when={dashboard()}>
|
||||||
<Collapsible title={copy().utilityLedgerTitle}>
|
{(_data) => (
|
||||||
<Show when={effectiveIsAdmin()}>
|
<>
|
||||||
<div class="ledger-actions">
|
{/* ── Purchases ──────────────────────────── */}
|
||||||
<Button variant="primary" size="sm" onClick={() => setAddUtilityOpen(true)}>
|
<Collapsible
|
||||||
<Plus size={14} />
|
title={copy().purchasesTitle}
|
||||||
{copy().addUtilityBillAction}
|
body={copy().purchaseReviewBody}
|
||||||
</Button>
|
defaultOpen
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<Show
|
|
||||||
when={utilityLedger().length > 0}
|
|
||||||
fallback={<p class="empty-state">{copy().utilityLedgerEmpty}</p>}
|
|
||||||
>
|
>
|
||||||
<div class="ledger-list">
|
<Show when={effectiveIsAdmin()}>
|
||||||
<For each={utilityLedger()}>
|
<div class="ledger-actions">
|
||||||
{(entry) => (
|
<Button
|
||||||
<button
|
variant="primary"
|
||||||
class="ledger-entry"
|
size="sm"
|
||||||
onClick={() => effectiveIsAdmin() && openUtilityEditor(entry)}
|
onClick={() => {
|
||||||
disabled={!effectiveIsAdmin()}
|
const members = dashboard()?.members ?? []
|
||||||
>
|
const currency = (dashboard()?.currency as 'USD' | 'GEL') ?? 'GEL'
|
||||||
<div class="ledger-entry__main">
|
setNewPurchase({
|
||||||
<span class="ledger-entry__title">{entry.title}</span>
|
description: '',
|
||||||
<span class="ledger-entry__actor">{entry.actorDisplayName}</span>
|
amountMajor: '',
|
||||||
</div>
|
currency,
|
||||||
<div class="ledger-entry__amounts">
|
splitMode: 'equal',
|
||||||
<strong>{ledgerPrimaryAmount(entry)}</strong>
|
splitInputMode: 'equal',
|
||||||
</div>
|
participants: members.map((m) => ({
|
||||||
</button>
|
memberId: m.memberId,
|
||||||
)}
|
included: true,
|
||||||
</For>
|
shareAmountMajor: '',
|
||||||
</div>
|
sharePercentage: ''
|
||||||
</Show>
|
}))
|
||||||
</Collapsible>
|
})
|
||||||
|
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 ───────────────────────────── */}
|
{/* ── Utility bills ──────────────────────── */}
|
||||||
<Collapsible
|
<Collapsible title={copy().utilityLedgerTitle}>
|
||||||
title={copy().paymentsTitle}
|
<Show when={effectiveIsAdmin()}>
|
||||||
{...(effectiveIsAdmin() && copy().paymentsAdminBody
|
<div class="ledger-actions">
|
||||||
? { body: copy().paymentsAdminBody }
|
<Button variant="primary" size="sm" onClick={() => setAddUtilityOpen(true)}>
|
||||||
: {})}
|
<Plus size={14} />
|
||||||
>
|
{copy().addUtilityBillAction}
|
||||||
<Show when={effectiveIsAdmin()}>
|
</Button>
|
||||||
<div class="ledger-actions">
|
</div>
|
||||||
<Button variant="primary" size="sm" onClick={() => setAddPaymentOpen(true)}>
|
</Show>
|
||||||
<Plus size={14} />
|
<Show
|
||||||
{copy().paymentsAddAction}
|
when={utilityLedger().length > 0}
|
||||||
</Button>
|
fallback={<p class="empty-state">{copy().utilityLedgerEmpty}</p>}
|
||||||
</div>
|
>
|
||||||
</Show>
|
<div class="ledger-list">
|
||||||
<Show
|
<For each={utilityLedger()}>
|
||||||
when={paymentLedger().length > 0}
|
{(entry) => (
|
||||||
fallback={<p class="empty-state">{copy().paymentsEmpty}</p>}
|
<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">
|
<Show when={effectiveIsAdmin()}>
|
||||||
<For each={paymentLedger()}>
|
<div class="ledger-actions">
|
||||||
{(entry) => (
|
<Button variant="primary" size="sm" onClick={() => setAddPaymentOpen(true)}>
|
||||||
<button
|
<Plus size={14} />
|
||||||
class="ledger-entry"
|
{copy().paymentsAddAction}
|
||||||
onClick={() => effectiveIsAdmin() && openPaymentEditor(entry)}
|
</Button>
|
||||||
disabled={!effectiveIsAdmin()}
|
</div>
|
||||||
>
|
</Show>
|
||||||
<div class="ledger-entry__main">
|
<Show
|
||||||
<span class="ledger-entry__title">
|
when={paymentLedger().length > 0}
|
||||||
{entry.paymentKind === 'rent'
|
fallback={<p class="empty-state">{copy().paymentsEmpty}</p>}
|
||||||
? copy().paymentLedgerRent
|
>
|
||||||
: copy().paymentLedgerUtilities}
|
<div class="ledger-list">
|
||||||
</span>
|
<For each={paymentLedger()}>
|
||||||
<span class="ledger-entry__actor">{entry.actorDisplayName}</span>
|
{(entry) => (
|
||||||
</div>
|
<button
|
||||||
<div class="ledger-entry__amounts">
|
class="ledger-entry"
|
||||||
<strong>{ledgerPrimaryAmount(entry)}</strong>
|
onClick={() => effectiveIsAdmin() && openPaymentEditor(entry)}
|
||||||
</div>
|
disabled={!effectiveIsAdmin()}
|
||||||
</button>
|
>
|
||||||
)}
|
<div class="ledger-entry__main">
|
||||||
</For>
|
<span class="ledger-entry__title">
|
||||||
</div>
|
{entry.paymentKind === 'rent'
|
||||||
</Show>
|
? copy().paymentLedgerRent
|
||||||
</Collapsible>
|
: copy().paymentLedgerUtilities}
|
||||||
</>
|
</span>
|
||||||
)}
|
<span class="ledger-entry__actor">{entry.actorDisplayName}</span>
|
||||||
</Show>
|
</div>
|
||||||
|
<div class="ledger-entry__amounts">
|
||||||
|
<strong>{ledgerPrimaryAmount(entry)}</strong>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</Collapsible>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
|
||||||
{/* ──────── Add Purchase Modal (Bug #4 fix) ──── */}
|
{/* ──────── Add Purchase Modal (Bug #4 fix) ──── */}
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
Reference in New Issue
Block a user