diff --git a/apps/bot/src/dm-assistant.test.ts b/apps/bot/src/dm-assistant.test.ts
index 06d5c3e..0b88ee7 100644
--- a/apps/bot/src/dm-assistant.test.ts
+++ b/apps/bot/src/dm-assistant.test.ts
@@ -311,6 +311,9 @@ function createFinanceService(): FinanceCommandService {
generateDashboard: async () => ({
period: '2026-03',
currency: 'GEL',
+ timezone: 'Asia/Tbilisi',
+ rentDueDay: 20,
+ utilitiesDueDay: 4,
paymentBalanceAdjustmentPolicy: 'utilities',
totalDue: Money.fromMajor('1000.00', 'GEL'),
totalPaid: Money.fromMajor('500.00', 'GEL'),
diff --git a/apps/bot/src/finance-commands.test.ts b/apps/bot/src/finance-commands.test.ts
index 5191cd5..2123112 100644
--- a/apps/bot/src/finance-commands.test.ts
+++ b/apps/bot/src/finance-commands.test.ts
@@ -124,6 +124,9 @@ function createDashboard(): NonNullable<
return {
period: '2026-03',
currency: 'GEL',
+ timezone: 'Asia/Tbilisi',
+ rentDueDay: 20,
+ utilitiesDueDay: 4,
paymentBalanceAdjustmentPolicy: 'utilities',
totalDue: Money.fromMajor('400', 'GEL'),
totalPaid: Money.fromMajor('100', 'GEL'),
diff --git a/apps/bot/src/miniapp-dashboard.ts b/apps/bot/src/miniapp-dashboard.ts
index 1c2de71..c2ea8f7 100644
--- a/apps/bot/src/miniapp-dashboard.ts
+++ b/apps/bot/src/miniapp-dashboard.ts
@@ -88,6 +88,9 @@ export function createMiniAppDashboardHandler(options: {
dashboard: {
period: dashboard.period,
currency: dashboard.currency,
+ timezone: dashboard.timezone,
+ rentDueDay: dashboard.rentDueDay,
+ utilitiesDueDay: dashboard.utilitiesDueDay,
paymentBalanceAdjustmentPolicy: dashboard.paymentBalanceAdjustmentPolicy,
totalDueMajor: dashboard.totalDue.toMajorString(),
totalPaidMajor: dashboard.totalPaid.toMajorString(),
diff --git a/apps/bot/src/payment-topic-ingestion.test.ts b/apps/bot/src/payment-topic-ingestion.test.ts
index 54455be..ef01efc 100644
--- a/apps/bot/src/payment-topic-ingestion.test.ts
+++ b/apps/bot/src/payment-topic-ingestion.test.ts
@@ -168,6 +168,9 @@ function createFinanceService(): FinanceCommandService {
generateDashboard: async () => ({
period: '2026-03',
currency: 'GEL',
+ timezone: 'Asia/Tbilisi',
+ rentDueDay: 20,
+ utilitiesDueDay: 4,
paymentBalanceAdjustmentPolicy: 'utilities',
totalDue: Money.fromMajor('1000', 'GEL'),
totalPaid: Money.zero('GEL'),
diff --git a/apps/miniapp/src/components/finance/member-balance-card.tsx b/apps/miniapp/src/components/finance/member-balance-card.tsx
index 9e73fee..8753f41 100644
--- a/apps/miniapp/src/components/finance/member-balance-card.tsx
+++ b/apps/miniapp/src/components/finance/member-balance-card.tsx
@@ -1,7 +1,7 @@
import { Show } from 'solid-js'
import { cn } from '../../lib/cn'
-import { formatFriendlyDate } from '../../lib/dates'
+import { formatCyclePeriod, formatFriendlyDate } from '../../lib/dates'
import { majorStringToMinor, sumMajorStrings } from '../../lib/money'
import type { MiniAppDashboard } from '../../miniapp-api'
import { MiniChip, StatCard } from '../ui'
@@ -44,25 +44,25 @@ export function MemberBalanceCard(props: Props) {
- {props.copy.totalDue ?? ''}
-
- {props.member.netDueMajor} {props.dashboard.currency}
-
+ {props.copy.currentCycleLabel ?? ''}
+ {formatCyclePeriod(props.dashboard.period, props.locale)}
{props.copy.paidLabel ?? ''}
diff --git a/apps/miniapp/src/demo/miniapp-demo.ts b/apps/miniapp/src/demo/miniapp-demo.ts
index 89bfa85..4290a0e 100644
--- a/apps/miniapp/src/demo/miniapp-demo.ts
+++ b/apps/miniapp/src/demo/miniapp-demo.ts
@@ -26,6 +26,9 @@ export const demoTelegramUser: NonNullable = {
export const demoDashboard: MiniAppDashboard = {
period: '2026-03',
currency: 'GEL',
+ timezone: 'Asia/Tbilisi',
+ rentDueDay: 20,
+ utilitiesDueDay: 4,
paymentBalanceAdjustmentPolicy: 'utilities',
totalDueMajor: '2410.00',
totalPaidMajor: '650.00',
diff --git a/apps/miniapp/src/i18n.ts b/apps/miniapp/src/i18n.ts
index 9f98e20..442822a 100644
--- a/apps/miniapp/src/i18n.ts
+++ b/apps/miniapp/src/i18n.ts
@@ -58,17 +58,25 @@ export const dictionary = {
ledgerEntries: 'Ledger entries',
pendingRequests: 'Pending requests',
yourBalanceTitle: 'Your balance',
- yourBalanceBody:
- 'See rent, pure utilities, purchase balance adjustment, and what is still left to pay.',
- payNowTitle: 'Pay now',
- payNowBody:
- 'Your current-cycle summary stays here so you can see the number that matters first.',
+ yourBalanceBody: 'Current cycle breakdown.',
+ payNowTitle: 'This month',
+ payNowBody: '',
+ homeDueTitle: 'Due',
+ homeSettledTitle: 'Settled',
currentCycleLabel: 'Current cycle',
+ cycleTotalLabel: 'Cycle total',
cycleBillLabel: 'Cycle bill',
balanceAdjustmentLabel: 'Balance adjustment',
pureUtilitiesLabel: 'Pure utilities',
+ utilitiesBalanceLabel: 'Utilities + balance',
rentAdjustedTotalLabel: 'Rent after adjustment',
utilitiesAdjustedTotalLabel: 'Utilities after adjustment',
+ paidThisCycleLabel: 'Paid this cycle',
+ rentPaidLabel: 'Rent paid',
+ utilitiesPaidLabel: 'Utilities paid',
+ dueOnLabel: 'Due {date}',
+ upcomingLabel: 'Upcoming',
+ notBilledYetLabel: 'Not billed yet',
baseDue: 'Base due',
finalDue: 'Final due',
houseSnapshotTitle: 'House totals',
@@ -338,17 +346,25 @@ export const dictionary = {
ledgerEntries: 'Записи леджера',
pendingRequests: 'Ожидают подтверждения',
yourBalanceTitle: 'Твой баланс',
- yourBalanceBody:
- 'Здесь отдельно видно аренду, чистую коммуналку, поправку по покупкам и то, что осталось оплатить.',
- payNowTitle: 'К оплате сейчас',
- payNowBody:
- 'Здесь остаётся только короткая сводка по текущему циклу, чтобы сразу видеть нужную сумму.',
+ yourBalanceBody: 'Разбор по текущему циклу.',
+ payNowTitle: 'Этот месяц',
+ payNowBody: '',
+ homeDueTitle: 'К оплате',
+ homeSettledTitle: 'Закрыто',
currentCycleLabel: 'Текущий цикл',
+ cycleTotalLabel: 'Всего за цикл',
cycleBillLabel: 'Счёт за цикл',
balanceAdjustmentLabel: 'Поправка по балансу',
pureUtilitiesLabel: 'Чистая коммуналка',
+ utilitiesBalanceLabel: 'Коммуналка + баланс',
rentAdjustedTotalLabel: 'Аренда после зачёта',
utilitiesAdjustedTotalLabel: 'Коммуналка после зачёта',
+ paidThisCycleLabel: 'Оплачено за цикл',
+ rentPaidLabel: 'По аренде оплачено',
+ utilitiesPaidLabel: 'По коммуналке оплачено',
+ dueOnLabel: 'Срок {date}',
+ upcomingLabel: 'Ещё не срок',
+ notBilledYetLabel: 'Ещё не начислено',
baseDue: 'База к оплате',
finalDue: 'Итог к оплате',
houseSnapshotTitle: 'Сводка по дому',
diff --git a/apps/miniapp/src/index.css b/apps/miniapp/src/index.css
index cd4cc56..e04969e 100644
--- a/apps/miniapp/src/index.css
+++ b/apps/miniapp/src/index.css
@@ -490,6 +490,12 @@ button:disabled {
gap: 8px;
}
+.balance-spotlight__copy small,
+.balance-detail-row__main small {
+ color: #c6c2bb;
+ font-size: 0.86rem;
+}
+
.balance-spotlight__hero {
display: grid;
gap: 6px;
@@ -1176,6 +1182,32 @@ button:disabled {
margin-top: 4px;
}
+.balance-section {
+ gap: 16px;
+}
+
+.balance-section__header {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: start;
+ justify-content: space-between;
+ gap: 12px;
+}
+
+.balance-section__copy {
+ display: grid;
+ gap: 8px;
+}
+
+.household-balance-list {
+ display: grid;
+ gap: 12px;
+}
+
+.household-balance-list__card .ledger-compact-card__meta {
+ margin-top: 12px;
+}
+
.app-context-row {
display: flex;
flex-wrap: wrap;
diff --git a/apps/miniapp/src/lib/dates.ts b/apps/miniapp/src/lib/dates.ts
index 823862a..12712b1 100644
--- a/apps/miniapp/src/lib/dates.ts
+++ b/apps/miniapp/src/lib/dates.ts
@@ -32,6 +32,48 @@ function formatCalendarDate(
}).format(new Date(Date.UTC(year, month - 1, day)))
}
+function parsePeriod(period: string): { year: number; month: number } | null {
+ const [yearValue, monthValue] = period.split('-')
+ const year = Number.parseInt(yearValue ?? '', 10)
+ const month = Number.parseInt(monthValue ?? '', 10)
+
+ if (!Number.isInteger(year) || !Number.isInteger(month) || month < 1 || month > 12) {
+ return null
+ }
+
+ return {
+ year,
+ month
+ }
+}
+
+function daysInMonth(year: number, month: number): number {
+ return new Date(Date.UTC(year, month, 0)).getUTCDate()
+}
+
+function formatTodayParts(timezone: string): { year: number; month: number; day: number } | null {
+ try {
+ const parts = new Intl.DateTimeFormat('en-CA', {
+ timeZone: timezone,
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit'
+ }).formatToParts(new Date())
+
+ const year = Number.parseInt(parts.find((part) => part.type === 'year')?.value ?? '', 10)
+ const month = Number.parseInt(parts.find((part) => part.type === 'month')?.value ?? '', 10)
+ const day = Number.parseInt(parts.find((part) => part.type === 'day')?.value ?? '', 10)
+
+ if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) {
+ return null
+ }
+
+ return { year, month, day }
+ } catch {
+ return null
+ }
+}
+
export function formatFriendlyDate(value: string, locale: Locale): string {
const calendarDateMatch = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value)
if (calendarDateMatch) {
@@ -61,19 +103,56 @@ export function formatFriendlyDate(value: string, locale: Locale): string {
}
export function formatCyclePeriod(period: string, locale: Locale): string {
- const [yearValue, monthValue] = period.split('-')
- const year = Number.parseInt(yearValue ?? '', 10)
- const month = Number.parseInt(monthValue ?? '', 10)
-
- if (!Number.isInteger(year) || !Number.isInteger(month) || month < 1 || month > 12) {
+ const parsed = parsePeriod(period)
+ if (!parsed) {
return period
}
- const date = new Date(Date.UTC(year, month - 1, 1))
- const includeYear = year !== new Date().getUTCFullYear()
+ const date = new Date(Date.UTC(parsed.year, parsed.month - 1, 1))
+ const includeYear = parsed.year !== new Date().getUTCFullYear()
return new Intl.DateTimeFormat(localeTag(locale), {
month: 'long',
...(includeYear ? { year: 'numeric' } : {})
}).format(date)
}
+
+export function formatPeriodDay(period: string, day: number, locale: Locale): string {
+ const parsed = parsePeriod(period)
+ if (!parsed) {
+ return period
+ }
+
+ const safeDay = Math.max(1, Math.min(day, daysInMonth(parsed.year, parsed.month)))
+
+ return (
+ formatCalendarDate(parsed.year, parsed.month, safeDay, locale) ??
+ `${formatCyclePeriod(period, locale)} ${safeDay}`
+ )
+}
+
+export function compareTodayToPeriodDay(
+ period: string,
+ day: number,
+ timezone: string
+): -1 | 0 | 1 | null {
+ const parsed = parsePeriod(period)
+ const today = formatTodayParts(timezone)
+ if (!parsed || !today) {
+ return null
+ }
+
+ const safeDay = Math.max(1, Math.min(day, daysInMonth(parsed.year, parsed.month)))
+ const dueValue = Date.UTC(parsed.year, parsed.month - 1, safeDay)
+ const todayValue = Date.UTC(today.year, today.month - 1, today.day)
+
+ if (todayValue < dueValue) {
+ return -1
+ }
+
+ if (todayValue > dueValue) {
+ return 1
+ }
+
+ return 0
+}
diff --git a/apps/miniapp/src/lib/timezones.ts b/apps/miniapp/src/lib/timezones.ts
index 8f30863..d116e7c 100644
--- a/apps/miniapp/src/lib/timezones.ts
+++ b/apps/miniapp/src/lib/timezones.ts
@@ -2,6 +2,7 @@ const CURATED_TIMEZONES = [
'Asia/Tbilisi',
'Europe/Berlin',
'Europe/London',
+ 'Europe/Moscow',
'Europe/Paris',
'Europe/Warsaw',
'America/New_York',
diff --git a/apps/miniapp/src/miniapp-api.ts b/apps/miniapp/src/miniapp-api.ts
index 369e483..21d1051 100644
--- a/apps/miniapp/src/miniapp-api.ts
+++ b/apps/miniapp/src/miniapp-api.ts
@@ -95,6 +95,9 @@ export interface MiniAppTopicBinding {
export interface MiniAppDashboard {
period: string
currency: 'USD' | 'GEL'
+ timezone: string
+ rentDueDay: number
+ utilitiesDueDay: number
paymentBalanceAdjustmentPolicy: 'utilities' | 'rent' | 'separate'
totalDueMajor: string
totalPaidMajor: string
diff --git a/apps/miniapp/src/screens/balances-screen.tsx b/apps/miniapp/src/screens/balances-screen.tsx
index 78fa57d..e477b69 100644
--- a/apps/miniapp/src/screens/balances-screen.tsx
+++ b/apps/miniapp/src/screens/balances-screen.tsx
@@ -107,46 +107,54 @@ export function BalancesScreen(props: Props) {
purchaseShareLabel: props.copy.purchaseShareLabel ?? ''
}}
/>
-
-
-
- {(member) => (
-
-
- {member.displayName}
-
- {member.remainingMajor} {dashboard().currency}
-
-
-
- {props.copy.baseDue ?? ''}: {props.memberBaseDueMajor(member)}{' '}
- {dashboard().currency}
-
-
- {props.copy.shareRent ?? ''}: {member.rentShareMajor} {dashboard().currency}
-
-
- {props.copy.shareUtilities ?? ''}: {member.utilityShareMajor}{' '}
- {dashboard().currency}
-
-
- {props.copy.shareOffset ?? ''}: {member.purchaseOffsetMajor}{' '}
- {dashboard().currency}
-
-
- {props.copy.paidLabel ?? ''}: {member.paidMajor} {dashboard().currency}
-
-
- {props.copy.remainingLabel ?? ''}: {member.remainingMajor} {dashboard().currency}
-
-
- )}
-
+
+
+ {(member) => (
+
+
+
+ {member.displayName}
+
+ {member.remainingMajor} {dashboard().currency}
+
+
+
+
+ {props.copy.baseDue ?? ''}: {props.memberBaseDueMajor(member)}{' '}
+ {dashboard().currency}
+
+
+ {props.copy.shareRent ?? ''}: {member.rentShareMajor}{' '}
+ {dashboard().currency}
+
+
+ {props.copy.shareUtilities ?? ''}: {member.utilityShareMajor}{' '}
+ {dashboard().currency}
+
+
+ {props.copy.shareOffset ?? ''}: {member.purchaseOffsetMajor}{' '}
+ {dashboard().currency}
+
+
+ {props.copy.paidLabel ?? ''}: {member.paidMajor} {dashboard().currency}
+
+
+
+
+ )}
+
+
+
)}
diff --git a/apps/miniapp/src/screens/home-screen.tsx b/apps/miniapp/src/screens/home-screen.tsx
index 7e60eec..020323c 100644
--- a/apps/miniapp/src/screens/home-screen.tsx
+++ b/apps/miniapp/src/screens/home-screen.tsx
@@ -1,8 +1,8 @@
import { Show } from 'solid-js'
import { FinanceSummaryCards } from '../components/finance/finance-summary-cards'
-import { formatCyclePeriod } from '../lib/dates'
-import { sumMajorStrings } from '../lib/money'
+import { compareTodayToPeriodDay, formatCyclePeriod, formatPeriodDay } from '../lib/dates'
+import { majorStringToMinor, minorToMajorString, sumMajorStrings } from '../lib/money'
import type { MiniAppDashboard } from '../miniapp-api'
type Props = {
@@ -15,6 +15,43 @@ type Props = {
}
export function HomeScreen(props: Props) {
+ const rentPaidMajor = () => {
+ if (!props.dashboard || !props.currentMemberLine) {
+ return '0.00'
+ }
+
+ const totalMinor = props.dashboard.ledger
+ .filter(
+ (entry) =>
+ entry.kind === 'payment' &&
+ entry.memberId === props.currentMemberLine?.memberId &&
+ entry.paymentKind === 'rent'
+ )
+ .reduce((sum, entry) => sum + majorStringToMinor(entry.displayAmountMajor), 0n)
+
+ return minorToMajorString(totalMinor)
+ }
+
+ const utilitiesPaidMajor = () => {
+ if (!props.dashboard || !props.currentMemberLine) {
+ return '0.00'
+ }
+
+ const totalMinor = props.dashboard.ledger
+ .filter(
+ (entry) =>
+ entry.kind === 'payment' &&
+ entry.memberId === props.currentMemberLine?.memberId &&
+ entry.paymentKind === 'utilities'
+ )
+ .reduce((sum, entry) => sum + majorStringToMinor(entry.displayAmountMajor), 0n)
+
+ return minorToMajorString(totalMinor)
+ }
+
+ const hasUtilityBills = () =>
+ Boolean(props.dashboard?.ledger.some((entry) => entry.kind === 'utility'))
+
const adjustedRentMajor = () => {
if (!props.currentMemberLine) {
return null
@@ -37,6 +74,115 @@ export function HomeScreen(props: Props) {
)
}
+ const rentDueMajor = () => {
+ if (!props.currentMemberLine || !props.dashboard) {
+ return null
+ }
+
+ return props.dashboard.paymentBalanceAdjustmentPolicy === 'rent'
+ ? adjustedRentMajor()
+ : props.currentMemberLine.rentShareMajor
+ }
+
+ const utilitiesDueMajor = () => {
+ if (!props.currentMemberLine || !props.dashboard || !hasUtilityBills()) {
+ return null
+ }
+
+ return props.dashboard.paymentBalanceAdjustmentPolicy === 'utilities'
+ ? adjustedUtilitiesMajor()
+ : props.currentMemberLine.utilityShareMajor
+ }
+
+ const separateBalanceMajor = () => {
+ if (
+ !props.currentMemberLine ||
+ props.dashboard?.paymentBalanceAdjustmentPolicy !== 'separate'
+ ) {
+ return null
+ }
+
+ return props.currentMemberLine.purchaseOffsetMajor
+ }
+
+ const heroState = () => {
+ if (!props.dashboard || !props.currentMemberLine) {
+ return {
+ title: props.copy.payNowTitle ?? props.copy.yourBalanceTitle ?? '',
+ label: props.copy.remainingLabel ?? '',
+ amountMajor: '—'
+ }
+ }
+
+ const remainingMinor = majorStringToMinor(props.currentMemberLine.remainingMajor)
+ const paidMinor = majorStringToMinor(props.currentMemberLine.paidMajor)
+ const rentStatus = compareTodayToPeriodDay(
+ props.dashboard.period,
+ props.dashboard.rentDueDay,
+ props.dashboard.timezone
+ )
+ const utilitiesStatus = compareTodayToPeriodDay(
+ props.dashboard.period,
+ props.dashboard.utilitiesDueDay,
+ props.dashboard.timezone
+ )
+ const hasDueNow =
+ (rentStatus !== null &&
+ rentStatus >= 0 &&
+ majorStringToMinor(rentDueMajor() ?? '0.00') > 0n) ||
+ (utilitiesStatus !== null &&
+ utilitiesStatus >= 0 &&
+ majorStringToMinor(utilitiesDueMajor() ?? '0.00') > 0n) ||
+ (props.dashboard.paymentBalanceAdjustmentPolicy === 'separate' &&
+ majorStringToMinor(separateBalanceMajor() ?? '0.00') > 0n)
+
+ if (remainingMinor === 0n && paidMinor > 0n) {
+ return {
+ title: props.copy.homeSettledTitle ?? '',
+ label: props.copy.paidThisCycleLabel ?? props.copy.paidLabel ?? '',
+ amountMajor: props.currentMemberLine.paidMajor
+ }
+ }
+
+ if (hasDueNow) {
+ return {
+ title: props.copy.homeDueTitle ?? props.copy.payNowTitle ?? '',
+ label: props.copy.remainingLabel ?? '',
+ amountMajor: props.currentMemberLine.remainingMajor
+ }
+ }
+
+ return {
+ title: props.copy.payNowTitle ?? props.copy.yourBalanceTitle ?? '',
+ label: props.copy.cycleTotalLabel ?? props.copy.totalDue ?? '',
+ amountMajor: props.currentMemberLine.netDueMajor
+ }
+ }
+
+ const dueLabel = (kind: 'rent' | 'utilities') => {
+ if (!props.dashboard) {
+ return null
+ }
+
+ const day = kind === 'rent' ? props.dashboard.rentDueDay : props.dashboard.utilitiesDueDay
+ const comparison = compareTodayToPeriodDay(
+ props.dashboard.period,
+ day,
+ props.dashboard.timezone
+ )
+ const date = formatPeriodDay(props.dashboard.period, day, props.locale)
+ const template =
+ comparison !== null && comparison < 0
+ ? (props.copy.upcomingLabel ?? '')
+ : (props.copy.dueOnLabel ?? '').replace('{date}', date)
+
+ if (comparison !== null && comparison < 0) {
+ return `${template}${template.length > 0 ? ' ' : ''}${date}`.trim()
+ }
+
+ return template
+ }
+
return (
@@ -86,36 +229,66 @@ export function HomeScreen(props: Props) {
- {props.copy.currentCycleLabel ?? ''}
- {formatCyclePeriod(dashboard().period, props.locale)}
+ {props.copy.remainingLabel ?? ''}
+
+ {member().remainingMajor} {dashboard().currency}
+
-
-
- {dashboard().paymentBalanceAdjustmentPolicy === 'rent'
- ? props.copy.rentAdjustedTotalLabel
- : props.copy.shareRent}
- :{' '}
- {dashboard().paymentBalanceAdjustmentPolicy === 'rent'
- ? adjustedRentMajor()
- : member().rentShareMajor}{' '}
- {dashboard().currency}
-
-
- {dashboard().paymentBalanceAdjustmentPolicy === 'utilities'
- ? props.copy.utilitiesAdjustedTotalLabel
- : props.copy.shareUtilities}
- :{' '}
- {dashboard().paymentBalanceAdjustmentPolicy === 'utilities'
- ? adjustedUtilitiesMajor()
- : member().utilityShareMajor}{' '}
- {dashboard().currency}
-
-
- {props.copy.balanceAdjustmentLabel ?? props.copy.shareOffset}:{' '}
- {member().purchaseOffsetMajor} {dashboard().currency}
-
+
+
+
+
+ {dashboard().paymentBalanceAdjustmentPolicy === 'rent'
+ ? props.copy.rentAdjustedTotalLabel
+ : props.copy.shareRent}
+
+
+ {rentDueMajor()} {dashboard().currency}
+
+ {dueLabel('rent')}
+
+
+ {props.copy.rentPaidLabel ?? props.copy.paidLabel}: {rentPaidMajor()}{' '}
+ {dashboard().currency}
+
+
+
+
+
+
+ {dashboard().paymentBalanceAdjustmentPolicy === 'utilities'
+ ? props.copy.utilitiesAdjustedTotalLabel
+ : (props.copy.utilitiesBalanceLabel ?? props.copy.shareUtilities)}
+
+
+ {utilitiesDueMajor() !== null
+ ? `${utilitiesDueMajor()} ${dashboard().currency}`
+ : (props.copy.notBilledYetLabel ?? '')}
+
+
+ {utilitiesDueMajor() !== null
+ ? dueLabel('utilities')
+ : dueLabel('utilities')}
+
+
+
+ {props.copy.utilitiesPaidLabel ?? props.copy.paidLabel}:{' '}
+ {utilitiesPaidMajor()} {dashboard().currency}
+
+
+
+
+
+
+ {props.copy.balanceAdjustmentLabel ?? props.copy.shareOffset}
+
+ {separateBalanceMajor()} {dashboard().currency}
+
+
+
+
)}
diff --git a/packages/application/src/finance-command-service.ts b/packages/application/src/finance-command-service.ts
index 67a30f4..99239d8 100644
--- a/packages/application/src/finance-command-service.ts
+++ b/packages/application/src/finance-command-service.ts
@@ -140,6 +140,9 @@ export interface FinanceDashboardLedgerEntry {
export interface FinanceDashboard {
period: string
currency: CurrencyCode
+ timezone: string
+ rentDueDay: number
+ utilitiesDueDay: number
paymentBalanceAdjustmentPolicy: 'utilities' | 'rent' | 'separate'
totalDue: Money
totalPaid: Money
@@ -559,6 +562,9 @@ async function buildFinanceDashboard(
return {
period: cycle.period,
currency: cycle.currency,
+ timezone: settings.timezone,
+ rentDueDay: settings.rentDueDay,
+ utilitiesDueDay: settings.utilitiesDueDay,
paymentBalanceAdjustmentPolicy: settings.paymentBalanceAdjustmentPolicy ?? 'utilities',
totalDue: settlement.totalDue,
totalPaid: paymentRecords.reduce(
diff --git a/packages/application/src/payment-confirmation-service.test.ts b/packages/application/src/payment-confirmation-service.test.ts
index eac5ad7..f24a639 100644
--- a/packages/application/src/payment-confirmation-service.test.ts
+++ b/packages/application/src/payment-confirmation-service.test.ts
@@ -112,6 +112,9 @@ describe('createPaymentConfirmationService', () => {
generateDashboard: async () => ({
period: '2026-03',
currency: 'GEL',
+ timezone: 'Asia/Tbilisi',
+ rentDueDay: 20,
+ utilitiesDueDay: 4,
paymentBalanceAdjustmentPolicy: 'utilities',
totalDue: Money.fromMajor('1030', 'GEL'),
totalPaid: Money.zero('GEL'),
@@ -175,6 +178,9 @@ describe('createPaymentConfirmationService', () => {
generateDashboard: async () => ({
period: '2026-03',
currency: 'GEL',
+ timezone: 'Asia/Tbilisi',
+ rentDueDay: 20,
+ utilitiesDueDay: 4,
paymentBalanceAdjustmentPolicy: 'utilities',
totalDue: Money.fromMajor('1030', 'GEL'),
totalPaid: Money.zero('GEL'),