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.yourBalanceTitle ?? ''} -

{props.copy.yourBalanceBody ?? ''}

+ {(body) =>

{body()}

}
{props.copy.remainingLabel ?? ''} {props.member.remainingMajor} {props.dashboard.currency} - - {props.copy.totalDue ?? ''}: {props.member.netDueMajor} {props.dashboard.currency} - + 0n}> + + {props.copy.totalDue ?? ''}: {props.member.netDueMajor} {props.dashboard.currency} + +
- {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 ?? '' }} /> -
-
- {props.copy.householdBalancesTitle ?? ''} - {String(dashboard().members.length)} +
+
+
+ {props.copy.householdBalancesTitle ?? ''} +

{props.copy.householdBalancesBody ?? ''}

+
+ + {String(dashboard().members.length)} {props.copy.membersCount ?? ''} +
-

{props.copy.householdBalancesBody ?? ''}

-
- - {(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 (
- {props.copy.payNowTitle ?? props.copy.yourBalanceTitle ?? ''} -

{props.copy.payNowBody ?? ''}

+ {heroState().title} + {formatCyclePeriod(dashboard().period, props.locale)}
- {props.copy.remainingLabel ?? ''} + {heroState().label} - {member().remainingMajor} {dashboard().currency} + {heroState().amountMajor} {dashboard().currency} - - {props.copy.totalDue ?? ''}: {member().netDueMajor} {dashboard().currency} -
@@ -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'),