From 621bd75148e20a37b20fae2b9ee1ddea720e2111 Mon Sep 17 00:00:00 2001 From: whekin Date: Mon, 23 Mar 2026 22:17:51 +0400 Subject: [PATCH 1/3] feat(miniapp): redesign admin payment management --- apps/bot/src/miniapp-dashboard.ts | 21 + apps/miniapp/src/components/layout/shell.tsx | 5 +- apps/miniapp/src/demo/miniapp-demo.ts | 54 ++ apps/miniapp/src/i18n.ts | 24 +- apps/miniapp/src/index.css | 30 +- apps/miniapp/src/lib/ledger-helpers.ts | 52 +- apps/miniapp/src/miniapp-api.ts | 21 + apps/miniapp/src/routes/home.tsx | 89 ++-- apps/miniapp/src/routes/ledger.tsx | 474 ++++++++++++++---- apps/miniapp/src/routes/settings.tsx | 3 +- .../src/finance-command-service.test.ts | 72 +++ .../src/finance-command-service.ts | 278 +++++++++- packages/application/src/payment-guidance.ts | 21 +- 13 files changed, 983 insertions(+), 161 deletions(-) diff --git a/apps/bot/src/miniapp-dashboard.ts b/apps/bot/src/miniapp-dashboard.ts index 84890d5..6839438 100644 --- a/apps/bot/src/miniapp-dashboard.ts +++ b/apps/bot/src/miniapp-dashboard.ts @@ -124,6 +124,27 @@ export function createMiniAppDashboardHandler(options: { })), explanations: line.explanations })), + paymentPeriods: (dashboard.paymentPeriods ?? []).map((period) => ({ + period: period.period, + utilityTotalMajor: period.utilityTotal.toMajorString(), + hasOverdueBalance: period.hasOverdueBalance, + isCurrentPeriod: period.isCurrentPeriod, + kinds: period.kinds.map((kind) => ({ + kind: kind.kind, + totalDueMajor: kind.totalDue.toMajorString(), + totalPaidMajor: kind.totalPaid.toMajorString(), + totalRemainingMajor: kind.totalRemaining.toMajorString(), + unresolvedMembers: kind.unresolvedMembers.map((member) => ({ + memberId: member.memberId, + displayName: member.displayName, + suggestedAmountMajor: member.suggestedAmount.toMajorString(), + baseDueMajor: member.baseDue.toMajorString(), + paidMajor: member.paid.toMajorString(), + remainingMajor: member.remaining.toMajorString(), + effectivelySettled: member.effectivelySettled + })) + })) + })), ledger: dashboard.ledger.map((entry) => ({ id: entry.id, kind: entry.kind, diff --git a/apps/miniapp/src/components/layout/shell.tsx b/apps/miniapp/src/components/layout/shell.tsx index c2f5a54..5675a9f 100644 --- a/apps/miniapp/src/components/layout/shell.tsx +++ b/apps/miniapp/src/components/layout/shell.tsx @@ -5,6 +5,7 @@ import { Settings } from 'lucide-solid' import { useSession } from '../../contexts/session-context' import { useI18n } from '../../contexts/i18n-context' import { useDashboard } from '../../contexts/dashboard-context' +import { formatCyclePeriod } from '../../lib/dates' import { NavigationTabs } from './navigation-tabs' import { Badge } from '../ui/badge' import { Button, IconButton } from '../ui/button' @@ -234,7 +235,9 @@ export function AppShell(props: ParentProps) {
{copy().testingPeriodCurrentLabel ?? ''} - {dashboard()?.period ?? '—'} + + {dashboard()?.period ? formatCyclePeriod(dashboard()!.period, locale()) : '—'} +
diff --git a/apps/miniapp/src/demo/miniapp-demo.ts b/apps/miniapp/src/demo/miniapp-demo.ts index 26acafb..2571336 100644 --- a/apps/miniapp/src/demo/miniapp-demo.ts +++ b/apps/miniapp/src/demo/miniapp-demo.ts @@ -377,6 +377,59 @@ function createDashboard(state: { members: MiniAppDashboard['members'] ledger?: MiniAppDashboard['ledger'] }): MiniAppDashboard { + const paymentPeriods: MiniAppDashboard['paymentPeriods'] = [ + { + period: '2026-03', + utilityTotalMajor: '286.00', + hasOverdueBalance: state.members.some((member) => member.overduePayments.length > 0), + isCurrentPeriod: true, + kinds: [ + { + kind: 'rent', + totalDueMajor: state.members + .reduce((sum, member) => sum + Number(member.rentShareMajor), 0) + .toFixed(2), + totalPaidMajor: '0.00', + totalRemainingMajor: state.members + .reduce((sum, member) => sum + Number(member.rentShareMajor), 0) + .toFixed(2), + unresolvedMembers: state.members + .filter((member) => Number(member.rentShareMajor) > 0) + .map((member) => ({ + memberId: member.memberId, + displayName: member.displayName, + suggestedAmountMajor: member.rentShareMajor, + baseDueMajor: member.rentShareMajor, + paidMajor: '0.00', + remainingMajor: member.rentShareMajor, + effectivelySettled: false + })) + }, + { + kind: 'utilities', + totalDueMajor: state.members + .reduce((sum, member) => sum + Number(member.utilityShareMajor), 0) + .toFixed(2), + totalPaidMajor: '0.00', + totalRemainingMajor: state.members + .reduce((sum, member) => sum + Number(member.utilityShareMajor), 0) + .toFixed(2), + unresolvedMembers: state.members + .filter((member) => Number(member.utilityShareMajor) > 0) + .map((member) => ({ + memberId: member.memberId, + displayName: member.displayName, + suggestedAmountMajor: member.utilityShareMajor, + baseDueMajor: member.utilityShareMajor, + paidMajor: '0.00', + remainingMajor: member.utilityShareMajor, + effectivelySettled: false + })) + } + ] + } + ] + return { period: '2026-03', currency: 'GEL', @@ -396,6 +449,7 @@ function createDashboard(state: { rentFxRateMicros: '2760000', rentFxEffectiveDate: '2026-03-17', members: state.members, + paymentPeriods, ledger: state.ledger ?? baseLedger() } } diff --git a/apps/miniapp/src/i18n.ts b/apps/miniapp/src/i18n.ts index 46d60c5..64764ad 100644 --- a/apps/miniapp/src/i18n.ts +++ b/apps/miniapp/src/i18n.ts @@ -133,6 +133,7 @@ export const dictionary = { purchasesTitle: 'Shared purchases', purchasesEmpty: 'No shared purchases recorded for this cycle yet.', utilityLedgerTitle: 'Utility bills', + utilityHistoryTitle: 'Utilities by period', utilityLedgerEmpty: 'No utility bills recorded for this cycle yet.', paymentsTitle: 'Payments', paymentsEmpty: 'No payment confirmations recorded for this cycle yet.', @@ -193,8 +194,17 @@ export const dictionary = { purchaseEditorBody: 'Review the purchase details and adjust the split only when needed.', purchasePayerLabel: 'Paid by', paymentsAdminTitle: 'Payments', - paymentsAdminBody: 'Add, fix, or remove payment records for the current cycle.', + paymentsAdminBody: + 'Resolve open rent and utility obligations period by period, or add a custom payment when needed.', paymentsAddAction: 'Add payment', + paymentsResolveAction: 'Resolve', + paymentsCustomAmountAction: 'Custom amount', + paymentsHistoryTitle: 'Payment history', + paymentsPeriodTitle: 'Period {period}', + paymentsPeriodCurrentBody: 'Current payment obligations for this billing period.', + paymentsPeriodOverdueBody: 'This period still has overdue base rent or utility payments.', + paymentsPeriodHistoryBody: 'Review and resolve older payment periods from here.', + paymentsBaseDueLabel: 'Base due {amount} · Remaining {remaining}', copiedToast: 'Copied!', quickPaymentTitle: 'Record payment', quickPaymentBody: 'Quickly record a {type} payment for the current cycle.', @@ -513,6 +523,7 @@ export const dictionary = { purchasesTitle: 'Общие покупки', purchasesEmpty: 'Пока нет общих покупок в этом цикле.', utilityLedgerTitle: 'Коммунальные платежи', + utilityHistoryTitle: 'Коммуналка по периодам', utilityLedgerEmpty: 'Пока нет коммунальных платежей в этом цикле.', paymentsTitle: 'Оплаты', paymentsEmpty: 'В этом цикле пока нет подтверждённых оплат.', @@ -575,8 +586,17 @@ export const dictionary = { 'Проверь покупку и меняй детали разделения только если это действительно нужно.', purchasePayerLabel: 'Оплатил', paymentsAdminTitle: 'Оплаты', - paymentsAdminBody: 'Добавляй, исправляй или удаляй оплаты за текущий цикл.', + paymentsAdminBody: + 'Закрывай открытые платежи по аренде и коммуналке по периодам или добавляй оплату с произвольной суммой.', paymentsAddAction: 'Добавить оплату', + paymentsResolveAction: 'Закрыть', + paymentsCustomAmountAction: 'Своя сумма', + paymentsHistoryTitle: 'История оплат', + paymentsPeriodTitle: 'Период {period}', + paymentsPeriodCurrentBody: 'Текущие обязательства по оплатам за этот биллинговый период.', + paymentsPeriodOverdueBody: 'В этом периоде остались просроченные базовые оплаты.', + paymentsPeriodHistoryBody: 'Здесь можно быстро проверить и закрыть старые периоды.', + paymentsBaseDueLabel: 'База {amount} · Осталось {remaining}', copiedToast: 'Скопировано!', quickPaymentTitle: 'Записать оплату', quickPaymentBody: 'Быстро запиши оплату {type} за текущий цикл.', diff --git a/apps/miniapp/src/index.css b/apps/miniapp/src/index.css index 4b83b10..c6488d4 100644 --- a/apps/miniapp/src/index.css +++ b/apps/miniapp/src/index.css @@ -589,11 +589,13 @@ a { .ui-badge { display: inline-flex; align-items: center; + justify-content: center; padding: 2px 8px; border-radius: var(--radius-full); font-size: var(--text-xs); font-weight: 600; letter-spacing: 0.02em; + white-space: nowrap; background: var(--accent-soft); color: var(--accent); border: none; @@ -739,7 +741,8 @@ a { transition: transform var(--transition-base); } -.ui-collapsible[data-expanded] .ui-collapsible__chevron { +.ui-collapsible__trigger[data-expanded] .ui-collapsible__chevron, +.ui-collapsible__trigger[aria-expanded='true'] .ui-collapsible__chevron { transform: rotate(180deg); } @@ -1532,6 +1535,15 @@ a { flex-direction: column; } +.editable-list-section-title { + font-size: var(--text-xs); + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--text-secondary); + margin-bottom: var(--spacing-sm); +} + .editable-list-row { display: flex; justify-content: space-between; @@ -1554,6 +1566,15 @@ a { background: var(--bg-input); } +.editable-list-row--static { + cursor: default; +} + +.editable-list-row--stacked { + align-items: flex-start; + gap: var(--spacing-sm); +} + .editable-list-row:disabled { cursor: default; } @@ -1591,6 +1612,13 @@ a { font-variant-numeric: tabular-nums; } +.editable-list-inline-actions { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: var(--spacing-xs); +} + .editable-list-row__secondary { font-size: var(--text-xs); color: var(--text-muted); diff --git a/apps/miniapp/src/lib/ledger-helpers.ts b/apps/miniapp/src/lib/ledger-helpers.ts index e8d81c8..89cef5a 100644 --- a/apps/miniapp/src/lib/ledger-helpers.ts +++ b/apps/miniapp/src/lib/ledger-helpers.ts @@ -99,6 +99,25 @@ export function ledgerSecondaryAmount(entry: MiniAppDashboard['ledger'][number]) return `${entry.amountMajor} ${entry.currency}` } +export function localizedCurrencyLabel( + locale: 'en' | 'ru', + currency: MiniAppDashboard['currency'] +): string { + if (locale === 'ru' && currency === 'GEL') { + return 'Лари' + } + + return currency +} + +export function formatMoneyLabel( + amountMajor: string, + currency: MiniAppDashboard['currency'], + locale: 'en' | 'ru' +): string { + return `${amountMajor} ${localizedCurrencyLabel(locale, currency)}` +} + export function cycleUtilityBillDrafts( bills: MiniAppAdminCycleState['utilityBills'] ): Record { @@ -407,29 +426,30 @@ export function resolvedMemberAbsencePolicy( * Bug #5 fix: Prefill with the remaining amount for the selected payment kind. */ export function computePaymentPrefill( - member: MiniAppDashboard['members'][number] | null | undefined, - kind: 'rent' | 'utilities' + dashboard: MiniAppDashboard | null | undefined, + memberId: string, + kind: 'rent' | 'utilities', + period: string ): string { - if (!member) { + if (!dashboard) { return '' } - const rentMinor = majorStringToMinor(member.rentShareMajor) - const utilityMinor = majorStringToMinor(member.utilityShareMajor) - const remainingMinor = majorStringToMinor(member.remainingMajor) - - if (remainingMinor <= 0n) { + const periodSummary = (dashboard.paymentPeriods ?? []).find((entry) => entry.period === period) + const kindSummary = periodSummary?.kinds.find((entry) => entry.kind === kind) + const memberSummary = kindSummary?.unresolvedMembers.find((entry) => entry.memberId === memberId) + if (!memberSummary) { return '0.00' } - // Estimate unpaid per kind (simplified: if total due matches, - // use share for that kind as an approximation) - const dueMinor = kind === 'rent' ? rentMinor : utilityMinor - if (dueMinor <= 0n) { - return '0.00' + let prefillMinor = majorStringToMinor(memberSummary.remainingMajor) + if (periodSummary?.isCurrentPeriod && dashboard.paymentBalanceAdjustmentPolicy === kind) { + const member = dashboard.members.find((entry) => entry.memberId === memberId) + const purchaseOffsetMinor = majorStringToMinor(member?.purchaseOffsetMajor ?? '0.00') + if (purchaseOffsetMinor > 0n) { + prefillMinor += purchaseOffsetMinor + } } - // If remaining is less than due for this kind, use remaining - const prefillMinor = remainingMinor < dueMinor ? remainingMinor : dueMinor - return minorToMajorString(prefillMinor) + return minorToMajorString(prefillMinor > 0n ? prefillMinor : 0n) } diff --git a/apps/miniapp/src/miniapp-api.ts b/apps/miniapp/src/miniapp-api.ts index 368f8ba..03650c0 100644 --- a/apps/miniapp/src/miniapp-api.ts +++ b/apps/miniapp/src/miniapp-api.ts @@ -137,6 +137,27 @@ export interface MiniAppDashboard { }[] explanations: readonly string[] }[] + paymentPeriods?: { + period: string + utilityTotalMajor: string + hasOverdueBalance: boolean + isCurrentPeriod: boolean + kinds: { + kind: 'rent' | 'utilities' + totalDueMajor: string + totalPaidMajor: string + totalRemainingMajor: string + unresolvedMembers: { + memberId: string + displayName: string + suggestedAmountMajor: string + baseDueMajor: string + paidMajor: string + remainingMajor: string + effectivelySettled: boolean + }[] + }[] + }[] ledger: { id: string kind: 'purchase' | 'utility' | 'payment' diff --git a/apps/miniapp/src/routes/home.tsx b/apps/miniapp/src/routes/home.tsx index f99acbb..ad879c2 100644 --- a/apps/miniapp/src/routes/home.tsx +++ b/apps/miniapp/src/routes/home.tsx @@ -13,11 +13,12 @@ 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 { formatMoneyLabel, localizedCurrencyLabel } from '../lib/ledger-helpers' import { majorStringToMinor, minorToMajorString } from '../lib/money' import { compareTodayToPeriodDay, daysUntilPeriodDay, + formatCyclePeriod, formatPeriodDay, nextCyclePeriod, parseCalendarDate @@ -50,11 +51,17 @@ function paymentProposalMinor( ? majorStringToMinor(member.rentShareMajor) : majorStringToMinor(member.utilityShareMajor) - if (data.paymentBalanceAdjustmentPolicy === kind) { - return baseMinor + purchaseOffsetMinor + const proposalMinor = + data.paymentBalanceAdjustmentPolicy === kind ? baseMinor + purchaseOffsetMinor : baseMinor + + if (kind !== 'rent' || proposalMinor <= 0n) { + return proposalMinor } - return baseMinor + const wholeMinor = proposalMinor / 100n + const remainderMinor = proposalMinor % 100n + + return (remainderMinor >= 50n ? wholeMinor + 1n : wholeMinor) * 100n } function paymentRemainingMinor( @@ -380,7 +387,10 @@ export default function HomeRoute() { paymentRemainingMinor(data(), member(), 'utilities') const modes = () => currentPaymentModes() - const currency = () => data().currency + const formatMajorAmount = ( + amountMajor: string, + currencyCode: 'USD' | 'GEL' = data().currency + ) => formatMoneyLabel(amountMajor, currencyCode, locale()) const timezone = () => data().timezone const period = () => effectivePeriod() ?? data().period const today = () => todayOverride() @@ -470,15 +480,17 @@ export default function HomeRoute() {
{copy().finalDue} - - {overdue().amountMajor} {currency()} - + {formatMajorAmount(overdue().amountMajor)}
{copy().homeOverduePeriodsLabel.replace( '{periods}', - overdue().periods.join(', ') + overdue() + .periods.map((period) => + formatCyclePeriod(period, locale()) + ) + .join(', ') )}
@@ -513,15 +525,17 @@ export default function HomeRoute() {
{copy().finalDue} - - {overdue().amountMajor} {currency()} - + {formatMajorAmount(overdue().amountMajor)}
{copy().homeOverduePeriodsLabel.replace( '{periods}', - overdue().periods.join(', ') + overdue() + .periods.map((period) => + formatCyclePeriod(period, locale()) + ) + .join(', ') )}
@@ -552,7 +566,7 @@ export default function HomeRoute() {
{copy().finalDue} - {minorToMajorString(utilitiesRemainingMinor())} {currency()} + {formatMajorAmount(minorToMajorString(utilitiesRemainingMinor()))}
@@ -563,30 +577,30 @@ export default function HomeRoute() {
{copy().baseDue} - - {member().utilityShareMajor} {currency()} - + {formatMajorAmount(member().utilityShareMajor)}
{copy().balanceAdjustmentLabel} - - {member().purchaseOffsetMajor} {currency()} - + {formatMajorAmount(member().purchaseOffsetMajor)}
0}>
{copy().homeUtilitiesBillsTitle} - - {utilityTotalMajor()} {currency()} - + {formatMajorAmount(utilityTotalMajor())}
{(entry) => (
{entry.title} - {ledgerPrimaryAmount(entry)} + + {formatMoneyLabel( + entry.displayAmountMajor, + entry.displayCurrency, + locale() + )} +
)}
@@ -617,7 +631,7 @@ export default function HomeRoute() {
{copy().finalDue} - {minorToMajorString(rentRemainingMinor())} {currency()} + {formatMajorAmount(minorToMajorString(rentRemainingMinor()))}
@@ -626,16 +640,12 @@ export default function HomeRoute() {
{copy().baseDue} - - {member().rentShareMajor} {currency()} - + {formatMajorAmount(member().rentShareMajor)}
{copy().balanceAdjustmentLabel} - - {member().purchaseOffsetMajor} {currency()} - + {formatMajorAmount(member().purchaseOffsetMajor)}
@@ -924,7 +934,13 @@ export default function HomeRoute() { {(entry) => (
{entry.title} - {ledgerPrimaryAmount(entry)} + + {formatMoneyLabel( + entry.displayAmountMajor, + entry.displayCurrency, + locale() + )} +
)} @@ -997,7 +1013,14 @@ export default function HomeRoute() { /> - +
diff --git a/apps/miniapp/src/routes/ledger.tsx b/apps/miniapp/src/routes/ledger.tsx index e4eff55..dfd7004 100644 --- a/apps/miniapp/src/routes/ledger.tsx +++ b/apps/miniapp/src/routes/ledger.tsx @@ -15,17 +15,19 @@ import { Collapsible } from '../components/ui/collapsible' import { Toggle } from '../components/ui/toggle' import { Skeleton } from '../components/ui/skeleton' import { - ledgerPrimaryAmount, + formatMoneyLabel, ledgerSecondaryAmount, purchaseDraftForEntry, paymentDraftForEntry, computePaymentPrefill, + localizedCurrencyLabel, rebalancePurchaseSplit, validatePurchaseDraft, type PurchaseDraft, type PaymentDraft } from '../lib/ledger-helpers' import { minorToMajorString, majorStringToMinor } from '../lib/money' +import { formatCyclePeriod, formatFriendlyDate } from '../lib/dates' import { addMiniAppPurchase, updateMiniAppPurchase, @@ -39,6 +41,10 @@ import { type MiniAppDashboard } from '../miniapp-api' +function joinSubtitleParts(parts: readonly (string | null | undefined)[]): string { + return parts.filter(Boolean).join(' · ') +} + interface ParticipantSplitInputsProps { draft: PurchaseDraft updateDraft: (fn: (d: PurchaseDraft) => PurchaseDraft) => void @@ -203,7 +209,7 @@ function ParticipantSplitInputs(props: ParticipantSplitInputsProps) { export default function LedgerRoute() { const { initData, refreshHouseholdData, session } = useSession() - const { copy } = useI18n() + const { copy, locale } = useI18n() const { dashboard, loading, effectiveIsAdmin, purchaseLedger, utilityLedger, paymentLedger } = useDashboard() const unresolvedPurchaseLedger = createMemo(() => @@ -214,26 +220,15 @@ export default function LedgerRoute() { ) const paymentPeriodOptions = createMemo(() => { const periods = new Set() - if (dashboard()?.period) { - periods.add(dashboard()!.period) + for (const summary of dashboard()?.paymentPeriods ?? []) { + periods.add(summary.period) } - for (const entry of purchaseLedger()) { - if (entry.originPeriod) { - periods.add(entry.originPeriod) - } - } - - for (const member of dashboard()?.members ?? []) { - for (const overdue of member.overduePayments) { - for (const period of overdue.periods) { - periods.add(period) - } - } - } - - return [...periods].sort().map((period) => ({ value: period, label: period })) + return [...periods] + .sort() + .map((period) => ({ value: period, label: formatCyclePeriod(period, locale()) })) }) + const paymentPeriodSummaries = createMemo(() => dashboard()?.paymentPeriods ?? []) // ── Purchase editor ────────────────────────────── const [editingPurchase, setEditingPurchase] = createSignal< @@ -294,6 +289,7 @@ export default function LedgerRoute() { period: dashboard()?.period ?? '' }) const [addingPayment, setAddingPayment] = createSignal(false) + const [paymentActionError, setPaymentActionError] = createSignal(null) const addPurchaseButtonText = createMemo(() => { if (addingPurchase()) return copy().savingPurchase @@ -543,6 +539,7 @@ export default function LedgerRoute() { setAddingPayment(true) try { + setPaymentActionError(null) await addMiniAppPayment(data, { memberId: draft.memberId, kind: draft.kind, @@ -559,13 +556,58 @@ export default function LedgerRoute() { period: dashboard()?.period ?? '' }) await refreshHouseholdData(true, true) + } catch (error) { + setPaymentActionError(error instanceof Error ? error.message : copy().quickPaymentFailed) } finally { setAddingPayment(false) } } + async function handleResolveSuggestedPayment(input: { + memberId: string + kind: 'rent' | 'utilities' + period: string + amountMajor: string + }) { + const data = initData() + if (!data) return + + setAddingPayment(true) + try { + setPaymentActionError(null) + await addMiniAppPayment(data, { + memberId: input.memberId, + kind: input.kind, + period: input.period, + amountMajor: input.amountMajor, + currency: (dashboard()?.currency as 'USD' | 'GEL') ?? 'GEL' + }) + await refreshHouseholdData(true, true) + } catch (error) { + setPaymentActionError(error instanceof Error ? error.message : copy().quickPaymentFailed) + } finally { + setAddingPayment(false) + } + } + + function openCustomPayment(input: { + memberId: string + kind: 'rent' | 'utilities' + period: string + }) { + setPaymentActionError(null) + setNewPayment({ + memberId: input.memberId, + kind: input.kind, + amountMajor: computePaymentPrefill(dashboard(), input.memberId, input.kind, input.period), + currency: (dashboard()?.currency as 'USD' | 'GEL') ?? 'GEL', + period: input.period + }) + setAddPaymentOpen(true) + } + const currencyOptions = () => [ - { value: 'GEL', label: 'GEL' }, + { value: 'GEL', label: localizedCurrencyLabel(locale(), 'GEL') }, { value: 'USD', label: 'USD' } ] @@ -648,7 +690,9 @@ export default function LedgerRoute() { >
- {copy().unresolvedPurchasesTitle} +
+ {copy().unresolvedPurchasesTitle} +
0} fallback={

{copy().unresolvedPurchasesEmpty}

} @@ -664,13 +708,23 @@ export default function LedgerRoute() {
{entry.title} - {[entry.actorDisplayName, entry.originPeriod, 'Unresolved'] - .filter(Boolean) - .join(' · ')} + {joinSubtitleParts([ + entry.actorDisplayName, + entry.originPeriod + ? formatCyclePeriod(entry.originPeriod, locale()) + : null, + 'Unresolved' + ])}
- {ledgerPrimaryAmount(entry)} + + {formatMoneyLabel( + entry.displayAmountMajor, + entry.displayCurrency, + locale() + )} + {(secondary) => ( @@ -686,8 +740,7 @@ export default function LedgerRoute() {
-
- {copy().resolvedPurchasesTitle} + 0} fallback={

{copy().resolvedPurchasesEmpty}

} @@ -703,13 +756,25 @@ export default function LedgerRoute() {
{entry.title} - {[entry.actorDisplayName, entry.originPeriod, entry.resolvedAt] - .filter(Boolean) - .join(' · ')} + {joinSubtitleParts([ + entry.actorDisplayName, + entry.originPeriod + ? formatCyclePeriod(entry.originPeriod, locale()) + : null, + entry.resolvedAt + ? formatFriendlyDate(entry.resolvedAt, locale()) + : null + ])}
- {ledgerPrimaryAmount(entry)} + + {formatMoneyLabel( + entry.displayAmountMajor, + entry.displayCurrency, + locale() + )} + {(secondary) => ( @@ -723,47 +788,91 @@ export default function LedgerRoute() {
-
+
{/* ── Utility bills ──────────────────────── */} - -
- -
-
- 0} - fallback={

{copy().utilityLedgerEmpty}

} - > -
- - {(entry) => ( - - )} - -
-
+
+ +
+ +
+
+ 0} + fallback={

{copy().utilityLedgerEmpty}

} + > +
+ + {(entry) => ( + + )} + +
+
+ + 0} + fallback={

{copy().utilityLedgerEmpty}

} + > +
+ + {(summary) => ( +
+
+ + {formatCyclePeriod(summary.period, locale())} + + + {summary.isCurrentPeriod + ? copy().currentCycleLabel + : summary.hasOverdueBalance + ? copy().overdueLabel + : copy().homeSettledTitle} + +
+
+ + {formatMoneyLabel( + summary.utilityTotalMajor, + (dashboard()?.currency as 'USD' | 'GEL') ?? 'GEL', + locale() + )} + +
+
+ )} +
+
+
+
+
{/* ── Payments ───────────────────────────── */} @@ -776,7 +885,7 @@ export default function LedgerRoute() {
+ + {(error) =>

{error()}

} +
0} + when={paymentPeriodSummaries().length > 0} fallback={

{copy().paymentsEmpty}

} > -
- - {(entry) => ( - + +
+
+
+ )} + + + + + )} + -
- {ledgerPrimaryAmount(entry)} -
- + )} + + + 0} + fallback={

{copy().paymentsEmpty}

} + > +
+ + {(entry) => ( + + )} + +
+
+
@@ -1093,6 +1354,7 @@ export default function LedgerRoute() { } > + {(error) =>

{error()}

}