feat(miniapp): redesign balance and due-state flows

This commit is contained in:
2026-03-12 14:08:55 +04:00
parent 6053379f31
commit 0d2065fd5e
11 changed files with 434 additions and 206 deletions

View File

@@ -103,6 +103,7 @@ export function createMiniAppDashboardHandler(options: {
members: dashboard.members.map((line) => ({
memberId: line.memberId,
displayName: line.displayName,
predictedUtilityShareMajor: line.predictedUtilityShare?.toMajorString() ?? null,
rentShareMajor: line.rentShare.toMajorString(),
utilityShareMajor: line.utilityShare.toMajorString(),
purchaseOffsetMajor: line.purchaseOffset.toMajorString(),

View File

@@ -314,6 +314,7 @@ function App() {
status: 'loading'
})
const [activeNav, setActiveNav] = createSignal<NavigationKey>('home')
const [selectedBalanceMemberId, setSelectedBalanceMemberId] = createSignal<string | null>(null)
const [dashboard, setDashboard] = createSignal<MiniAppDashboard | null>(null)
const [pendingMembers, setPendingMembers] = createSignal<readonly MiniAppPendingMember[]>([])
const [adminSettings, setAdminSettings] = createSignal<MiniAppAdminSettingsPayload | null>(null)
@@ -444,6 +445,21 @@ function App() {
return data.members.find((member) => member.memberId === current.member.id) ?? null
})
const inspectedBalanceMember = createMemo(() => {
const data = dashboard()
if (!data) {
return null
}
const selected = selectedBalanceMemberId()
return (
data.members.find((member) => member.memberId === selected) ??
currentMemberLine() ??
data.members[0] ??
null
)
})
const purchaseLedger = createMemo(() =>
(dashboard()?.ledger ?? []).filter((entry) => entry.kind === 'purchase')
)
@@ -2024,12 +2040,15 @@ function App() {
locale={locale()}
dashboard={dashboard()}
currentMemberLine={currentMemberLine()}
inspectedMember={inspectedBalanceMember()}
selectedMemberId={inspectedBalanceMember()?.memberId ?? ''}
utilityTotalMajor={utilityTotalMajor()}
purchaseTotalMajor={purchaseTotalMajor()}
memberBalanceVisuals={memberBalanceVisuals()}
purchaseChart={purchaseInvestmentChart()}
memberBaseDueMajor={memberBaseDueMajor}
memberRemainingClass={memberRemainingClass}
onSelectedMemberChange={setSelectedBalanceMemberId}
/>
)
case 'ledger':
@@ -2468,8 +2487,10 @@ function App() {
locale={locale()}
dashboard={dashboard()}
currentMemberLine={currentMemberLine()}
utilityTotalMajor={utilityTotalMajor()}
purchaseTotalMajor={purchaseTotalMajor()}
onExplainBalance={() => {
setSelectedBalanceMemberId(currentMemberLine()?.memberId ?? null)
setActiveNav('balances')
}}
/>
)
}

View File

@@ -42,6 +42,7 @@ export const demoDashboard: MiniAppDashboard = {
{
memberId: 'demo-member',
displayName: 'Stas',
predictedUtilityShareMajor: '78.00',
rentShareMajor: '603.75',
utilityShareMajor: '78.00',
purchaseOffsetMajor: '-66.00',
@@ -53,6 +54,7 @@ export const demoDashboard: MiniAppDashboard = {
{
memberId: 'member-chorb',
displayName: 'Chorbanaut',
predictedUtilityShareMajor: '78.00',
rentShareMajor: '603.75',
utilityShareMajor: '78.00',
purchaseOffsetMajor: '12.00',
@@ -64,6 +66,7 @@ export const demoDashboard: MiniAppDashboard = {
{
memberId: 'member-el',
displayName: 'El',
predictedUtilityShareMajor: '0.00',
rentShareMajor: '1207.50',
utilityShareMajor: '0.00',
purchaseOffsetMajor: '54.00',

View File

@@ -63,6 +63,7 @@ export const dictionary = {
payNowBody: '',
homeDueTitle: 'Due',
homeSettledTitle: 'Settled',
whyAction: 'Why?',
currentCycleLabel: 'Current cycle',
cycleTotalLabel: 'Cycle total',
cycleBillLabel: 'Cycle bill',
@@ -75,8 +76,12 @@ export const dictionary = {
rentPaidLabel: 'Rent paid',
utilitiesPaidLabel: 'Utilities paid',
dueOnLabel: 'Due {date}',
dueTodayLabel: 'Due today',
overdueLabel: 'Overdue',
daysLeftLabel: '{count}d left',
upcomingLabel: 'Upcoming',
notBilledYetLabel: 'Not billed yet',
expectedUtilitiesLabel: 'Expected utilities',
baseDue: 'Base due',
finalDue: 'Final due',
houseSnapshotTitle: 'House totals',
@@ -86,6 +91,9 @@ export const dictionary = {
'This screen only explains your current cycle balance. Older activity stays in the ledger.',
householdBalancesTitle: 'Household balances',
householdBalancesBody: 'Everyones current split for this cycle.',
inspectMemberTitle: 'Inspect member',
inspectMemberBody: 'Check another member balance without opening a long list.',
inspectMemberLabel: 'Member',
financeVisualsTitle: 'Visual balance split',
financeVisualsBody:
'Use the bars to see how rent, utilities, and shared-buy adjustments shape each member balance.',
@@ -351,6 +359,7 @@ export const dictionary = {
payNowBody: '',
homeDueTitle: 'К оплате',
homeSettledTitle: 'Закрыто',
whyAction: 'Почему?',
currentCycleLabel: 'Текущий цикл',
cycleTotalLabel: 'Всего за цикл',
cycleBillLabel: 'Счёт за цикл',
@@ -363,8 +372,12 @@ export const dictionary = {
rentPaidLabel: 'По аренде оплачено',
utilitiesPaidLabel: 'По коммуналке оплачено',
dueOnLabel: 'Срок {date}',
dueTodayLabel: 'Срок сегодня',
overdueLabel: 'Просрочено',
daysLeftLabel: 'Осталось {count} дн.',
upcomingLabel: 'Ещё не срок',
notBilledYetLabel: 'Ещё не начислено',
expectedUtilitiesLabel: 'Ожидаемая коммуналка',
baseDue: 'База к оплате',
finalDue: 'Итог к оплате',
houseSnapshotTitle: 'Сводка по дому',
@@ -374,6 +387,9 @@ export const dictionary = {
'На этом экране только разбор твоего текущего баланса. Более старые записи остаются в леджере.',
householdBalancesTitle: 'Баланс дома',
householdBalancesBody: 'Текущий расклад по всем участникам за этот цикл.',
inspectMemberTitle: 'Посмотреть участника',
inspectMemberBody: 'Можно быстро проверить чужой баланс без длинного списка карточек.',
inspectMemberLabel: 'Участник',
financeVisualsTitle: 'Визуальный разбор баланса',
financeVisualsBody:
'Полосы показывают, как аренда, коммуналка и поправка на общие покупки формируют баланс каждого участника.',

View File

@@ -461,11 +461,16 @@ button:disabled {
.home-pay-card,
.home-pay-card__header,
.home-pay-card__copy,
.home-pay-card__chips {
.home-pay-card__chips,
.home-pay-card__actions {
display: grid;
gap: 12px;
}
.home-pay-card__actions {
justify-items: start;
}
.stat-card {
display: grid;
gap: 8px;
@@ -1186,6 +1191,10 @@ button:disabled {
gap: 16px;
}
.balance-section--secondary {
background: linear-gradient(180deg, rgb(255 255 255 / 0.04), rgb(255 255 255 / 0.02));
}
.balance-section__header {
display: flex;
flex-wrap: wrap;
@@ -1199,6 +1208,10 @@ button:disabled {
gap: 8px;
}
.balance-section__field {
min-width: min(220px, 100%);
}
.household-balance-list {
display: grid;
gap: 12px;
@@ -1208,6 +1221,22 @@ button:disabled {
margin-top: 12px;
}
.balance-detail-card,
.balance-detail-card__rows {
display: grid;
gap: 12px;
}
.balance-detail-card__header {
display: grid;
gap: 10px;
}
.balance-detail-card__copy {
display: grid;
gap: 6px;
}
.app-context-row {
display: flex;
flex-wrap: wrap;
@@ -1248,10 +1277,23 @@ button:disabled {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.home-pay-card__header {
grid-template-columns: minmax(0, 1fr) auto;
align-items: start;
}
.balance-spotlight__hero {
grid-column: 1 / -1;
}
.balance-spotlight__header {
align-items: start;
}
.home-pay-card__actions {
justify-items: end;
}
.balance-spotlight__stats {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
@@ -1298,6 +1340,10 @@ button:disabled {
.member-editor-actions__grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.balance-detail-card__rows {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 759px) {

View File

@@ -156,3 +156,17 @@ export function compareTodayToPeriodDay(
return 0
}
export function daysUntilPeriodDay(period: string, day: number, timezone: string): number | 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)
return Math.round((dueValue - todayValue) / 86_400_000)
}

View File

@@ -110,6 +110,7 @@ export interface MiniAppDashboard {
members: {
memberId: string
displayName: string
predictedUtilityShareMajor: string | null
rentShareMajor: string
utilityShareMajor: string
purchaseOffsetMajor: string

View File

@@ -1,8 +1,9 @@
import { For, Show } from 'solid-js'
import { Show } from 'solid-js'
import { FinanceSummaryCards } from '../components/finance/finance-summary-cards'
import { FinanceVisuals } from '../components/finance/finance-visuals'
import { MemberBalanceCard } from '../components/finance/member-balance-card'
import { Field } from '../components/ui'
import { formatCyclePeriod } from '../lib/dates'
import type { MiniAppDashboard } from '../miniapp-api'
@@ -11,6 +12,8 @@ type Props = {
locale: 'en' | 'ru'
dashboard: MiniAppDashboard | null
currentMemberLine: MiniAppDashboard['members'][number] | null
inspectedMember: MiniAppDashboard['members'][number] | null
selectedMemberId: string
utilityTotalMajor: string
purchaseTotalMajor: string
memberBalanceVisuals: {
@@ -39,6 +42,7 @@ type Props = {
}
memberBaseDueMajor: (member: MiniAppDashboard['members'][number]) => string
memberRemainingClass: (member: MiniAppDashboard['members'][number]) => string
onSelectedMemberChange: (memberId: string) => void
}
export function BalancesScreen(props: Props) {
@@ -64,13 +68,110 @@ export function BalancesScreen(props: Props) {
/>
)}
</Show>
<article class="balance-item balance-item--muted">
<header>
<strong>{props.copy.balanceScreenScopeTitle ?? ''}</strong>
<span>{formatCyclePeriod(dashboard().period, props.locale)}</span>
<section class="balance-item balance-item--wide balance-section balance-section--secondary">
<header class="balance-section__header">
<div class="balance-section__copy">
<strong>{props.copy.inspectMemberTitle ?? ''}</strong>
<p>{props.copy.inspectMemberBody ?? ''}</p>
</div>
<Field label={props.copy.inspectMemberLabel ?? ''} class="balance-section__field">
<select
value={props.selectedMemberId}
onChange={(event) => props.onSelectedMemberChange(event.currentTarget.value)}
>
{dashboard().members.map((member) => (
<option value={member.memberId}>{member.displayName}</option>
))}
</select>
</Field>
</header>
<p>{props.copy.balanceScreenScopeBody ?? ''}</p>
</article>
<Show when={props.inspectedMember}>
{(member) => (
<article class="balance-detail-card">
<header class="balance-detail-card__header">
<div class="balance-detail-card__copy">
<strong>{member().displayName}</strong>
<small>{formatCyclePeriod(dashboard().period, props.locale)}</small>
</div>
<span class={`balance-status ${props.memberRemainingClass(member())}`}>
{member().remainingMajor} {dashboard().currency}
</span>
</header>
<div class="balance-detail-card__rows">
<article class="balance-detail-row">
<div class="balance-detail-row__main">
<span>{props.copy.baseDue ?? ''}</span>
<strong>
{props.memberBaseDueMajor(member())} {dashboard().currency}
</strong>
</div>
</article>
<article class="balance-detail-row">
<div class="balance-detail-row__main">
<span>{props.copy.shareRent ?? ''}</span>
<strong>
{member().rentShareMajor} {dashboard().currency}
</strong>
</div>
</article>
<article class="balance-detail-row">
<div class="balance-detail-row__main">
<span>{props.copy.shareUtilities ?? ''}</span>
<strong>
{member().utilityShareMajor} {dashboard().currency}
</strong>
</div>
</article>
<article class="balance-detail-row">
<div class="balance-detail-row__main">
<span>{props.copy.shareOffset ?? ''}</span>
<strong>
{member().purchaseOffsetMajor} {dashboard().currency}
</strong>
</div>
</article>
<article class="balance-detail-row">
<div class="balance-detail-row__main">
<span>{props.copy.paidLabel ?? ''}</span>
<strong>
{member().paidMajor} {dashboard().currency}
</strong>
</div>
</article>
<article class="balance-detail-row balance-detail-row--accent">
<div class="balance-detail-row__main">
<span>{props.copy.remainingLabel ?? ''}</span>
<strong>
{member().remainingMajor} {dashboard().currency}
</strong>
</div>
</article>
</div>
</article>
)}
</Show>
</section>
<FinanceVisuals
dashboard={dashboard()}
memberVisuals={props.memberBalanceVisuals}
purchaseChart={props.purchaseChart}
remainingClass={props.memberRemainingClass}
labels={{
financeVisualsTitle: props.copy.financeVisualsTitle ?? '',
financeVisualsBody: props.copy.financeVisualsBody ?? '',
membersCount: props.copy.membersCount ?? '',
purchaseInvestmentsTitle: props.copy.purchaseInvestmentsTitle ?? '',
purchaseInvestmentsBody: props.copy.purchaseInvestmentsBody ?? '',
purchaseInvestmentsEmpty: props.copy.purchaseInvestmentsEmpty ?? '',
purchaseTotalLabel: props.copy.purchaseTotalLabel ?? '',
purchaseShareLabel: props.copy.purchaseShareLabel ?? ''
}}
/>
<article class="balance-item balance-item--wide balance-item--muted">
<header>
<strong>{props.copy.houseSnapshotTitle ?? ''}</strong>
@@ -91,70 +192,6 @@ export function BalancesScreen(props: Props) {
/>
</div>
</article>
<FinanceVisuals
dashboard={dashboard()}
memberVisuals={props.memberBalanceVisuals}
purchaseChart={props.purchaseChart}
remainingClass={props.memberRemainingClass}
labels={{
financeVisualsTitle: props.copy.financeVisualsTitle ?? '',
financeVisualsBody: props.copy.financeVisualsBody ?? '',
membersCount: props.copy.membersCount ?? '',
purchaseInvestmentsTitle: props.copy.purchaseInvestmentsTitle ?? '',
purchaseInvestmentsBody: props.copy.purchaseInvestmentsBody ?? '',
purchaseInvestmentsEmpty: props.copy.purchaseInvestmentsEmpty ?? '',
purchaseTotalLabel: props.copy.purchaseTotalLabel ?? '',
purchaseShareLabel: props.copy.purchaseShareLabel ?? ''
}}
/>
<section class="balance-item balance-item--wide balance-section">
<header class="balance-section__header">
<div class="balance-section__copy">
<strong>{props.copy.householdBalancesTitle ?? ''}</strong>
<p>{props.copy.householdBalancesBody ?? ''}</p>
</div>
<span class="mini-chip mini-chip--muted">
{String(dashboard().members.length)} {props.copy.membersCount ?? ''}
</span>
</header>
<div class="household-balance-list">
<For each={dashboard().members}>
{(member) => (
<article class="ledger-compact-card household-balance-list__card">
<div class="ledger-compact-card__main">
<header>
<strong>{member.displayName}</strong>
<span class={`balance-status ${props.memberRemainingClass(member)}`}>
{member.remainingMajor} {dashboard().currency}
</span>
</header>
<div class="ledger-compact-card__meta">
<span class="mini-chip mini-chip--muted">
{props.copy.baseDue ?? ''}: {props.memberBaseDueMajor(member)}{' '}
{dashboard().currency}
</span>
<span class="mini-chip mini-chip--muted">
{props.copy.shareRent ?? ''}: {member.rentShareMajor}{' '}
{dashboard().currency}
</span>
<span class="mini-chip mini-chip--muted">
{props.copy.shareUtilities ?? ''}: {member.utilityShareMajor}{' '}
{dashboard().currency}
</span>
<span class="mini-chip mini-chip--muted">
{props.copy.shareOffset ?? ''}: {member.purchaseOffsetMajor}{' '}
{dashboard().currency}
</span>
<span class="mini-chip mini-chip--muted">
{props.copy.paidLabel ?? ''}: {member.paidMajor} {dashboard().currency}
</span>
</div>
</div>
</article>
)}
</For>
</div>
</section>
</div>
)}
</Show>

View File

@@ -1,7 +1,12 @@
import { Show } from 'solid-js'
import { FinanceSummaryCards } from '../components/finance/finance-summary-cards'
import { compareTodayToPeriodDay, formatCyclePeriod, formatPeriodDay } from '../lib/dates'
import { Button } from '../components/ui'
import {
compareTodayToPeriodDay,
daysUntilPeriodDay,
formatCyclePeriod,
formatPeriodDay
} from '../lib/dates'
import { majorStringToMinor, minorToMajorString, sumMajorStrings } from '../lib/money'
import type { MiniAppDashboard } from '../miniapp-api'
@@ -10,10 +15,11 @@ type Props = {
locale: 'en' | 'ru'
dashboard: MiniAppDashboard | null
currentMemberLine: MiniAppDashboard['members'][number] | null
utilityTotalMajor: string
purchaseTotalMajor: string
onExplainBalance: () => void
}
type HomeMode = 'upcoming' | 'due' | 'settled'
export function HomeScreen(props: Props) {
const rentPaidMajor = () => {
if (!props.dashboard || !props.currentMemberLine) {
@@ -94,6 +100,14 @@ export function HomeScreen(props: Props) {
: props.currentMemberLine.utilityShareMajor
}
const predictedUtilitiesMajor = () => {
if (!props.currentMemberLine) {
return null
}
return props.currentMemberLine.predictedUtilityShareMajor
}
const separateBalanceMajor = () => {
if (
!props.currentMemberLine ||
@@ -105,13 +119,9 @@ export function HomeScreen(props: Props) {
return props.currentMemberLine.purchaseOffsetMajor
}
const heroState = () => {
const homeMode = (): HomeMode => {
if (!props.dashboard || !props.currentMemberLine) {
return {
title: props.copy.payNowTitle ?? props.copy.yourBalanceTitle ?? '',
label: props.copy.remainingLabel ?? '',
amountMajor: '—'
}
return 'upcoming'
}
const remainingMinor = majorStringToMinor(props.currentMemberLine.remainingMajor)
@@ -126,6 +136,7 @@ export function HomeScreen(props: Props) {
props.dashboard.utilitiesDueDay,
props.dashboard.timezone
)
const hasDueNow =
(rentStatus !== null &&
rentStatus >= 0 &&
@@ -137,50 +148,71 @@ export function HomeScreen(props: Props) {
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
}
return 'settled'
}
if (hasDueNow) {
return hasDueNow ? 'due' : 'upcoming'
}
const heroState = () => {
if (!props.dashboard || !props.currentMemberLine) {
return {
title: props.copy.homeDueTitle ?? props.copy.payNowTitle ?? '',
title: props.copy.payNowTitle ?? props.copy.yourBalanceTitle ?? '',
label: props.copy.remainingLabel ?? '',
amountMajor: props.currentMemberLine.remainingMajor
amountMajor: '—'
}
}
return {
title: props.copy.payNowTitle ?? props.copy.yourBalanceTitle ?? '',
label: props.copy.cycleTotalLabel ?? props.copy.totalDue ?? '',
amountMajor: props.currentMemberLine.netDueMajor
switch (homeMode()) {
case 'settled':
return {
title: props.copy.homeSettledTitle ?? '',
label: props.copy.paidThisCycleLabel ?? props.copy.paidLabel ?? '',
amountMajor: props.currentMemberLine.paidMajor
}
case 'due':
return {
title: props.copy.homeDueTitle ?? props.copy.payNowTitle ?? '',
label: props.copy.remainingLabel ?? '',
amountMajor: props.currentMemberLine.remainingMajor
}
default:
return {
title: props.copy.payNowTitle ?? props.copy.yourBalanceTitle ?? '',
label: props.copy.cycleTotalLabel ?? props.copy.totalDue ?? '',
amountMajor: props.currentMemberLine.netDueMajor
}
}
}
const dueLabel = (kind: 'rent' | 'utilities') => {
const dayCountLabel = (daysLeft: number | null) => {
if (daysLeft === null) {
return null
}
if (daysLeft < 0) {
return props.copy.overdueLabel ?? ''
}
if (daysLeft === 0) {
return props.copy.dueTodayLabel ?? ''
}
return (props.copy.daysLeftLabel ?? '').replace('{count}', String(daysLeft))
}
const scheduleLabel = (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)
const daysLeft = daysUntilPeriodDay(props.dashboard.period, day, props.dashboard.timezone)
const dayLabel = dayCountLabel(daysLeft)
const dueLabel = (props.copy.dueOnLabel ?? '').replace('{date}', date)
if (comparison !== null && comparison < 0) {
return `${template}${template.length > 0 ? ' ' : ''}${date}`.trim()
}
return template
return dayLabel ? `${dueLabel} · ${dayLabel}` : dueLabel
}
return (
@@ -192,7 +224,6 @@ export function HomeScreen(props: Props) {
<header class="balance-spotlight__header">
<div class="balance-spotlight__copy">
<strong>{props.copy.yourBalanceTitle ?? ''}</strong>
<p>{props.copy.yourBalanceBody ?? ''}</p>
</div>
<div class="balance-spotlight__hero">
<span>{props.copy.remainingLabel ?? ''}</span>
@@ -213,6 +244,11 @@ export function HomeScreen(props: Props) {
<strong>{heroState().title}</strong>
<small>{formatCyclePeriod(dashboard().period, props.locale)}</small>
</div>
<div class="home-pay-card__actions">
<Button variant="ghost" onClick={props.onExplainBalance}>
{props.copy.whyAction ?? ''}
</Button>
</div>
<div class="balance-spotlight__hero">
<span>{heroState().label}</span>
<strong>
@@ -221,70 +257,129 @@ export function HomeScreen(props: Props) {
</div>
</header>
<div class="balance-spotlight__stats">
<article class="stat-card balance-spotlight__stat">
<span>{props.copy.paidLabel ?? ''}</span>
<strong>
{member().paidMajor} {dashboard().currency}
</strong>
</article>
<article class="stat-card balance-spotlight__stat">
<span>{props.copy.remainingLabel ?? ''}</span>
<strong>
{member().remainingMajor} {dashboard().currency}
</strong>
</article>
</div>
<Show
when={homeMode() === 'upcoming'}
fallback={
<div class="balance-spotlight__stats">
<article class="stat-card balance-spotlight__stat">
<span>{props.copy.paidLabel ?? ''}</span>
<strong>
{member().paidMajor} {dashboard().currency}
</strong>
</article>
<article class="stat-card balance-spotlight__stat">
<span>{props.copy.remainingLabel ?? ''}</span>
<strong>
{member().remainingMajor} {dashboard().currency}
</strong>
</article>
</div>
}
>
<div class="balance-spotlight__stats">
<article class="stat-card balance-spotlight__stat">
<span>{props.copy.shareRent ?? ''}</span>
<strong>{scheduleLabel('rent')}</strong>
</article>
<article class="stat-card balance-spotlight__stat">
<span>{props.copy.shareUtilities ?? ''}</span>
<strong>{scheduleLabel('utilities')}</strong>
</article>
</div>
</Show>
<div class="balance-spotlight__rows">
<article class="balance-detail-row">
<div class="balance-detail-row__main">
<span>
{dashboard().paymentBalanceAdjustmentPolicy === 'rent'
? props.copy.rentAdjustedTotalLabel
: props.copy.shareRent}
</span>
<span>{props.copy.shareRent ?? ''}</span>
<strong>
{rentDueMajor()} {dashboard().currency}
{member().rentShareMajor} {dashboard().currency}
</strong>
<small>{dueLabel('rent')}</small>
<small>{scheduleLabel('rent')}</small>
</div>
<span class="mini-chip mini-chip--muted">
{props.copy.rentPaidLabel ?? props.copy.paidLabel}: {rentPaidMajor()}{' '}
{dashboard().currency}
</span>
<Show when={homeMode() !== 'upcoming'}>
<span class="mini-chip mini-chip--muted">
{props.copy.rentPaidLabel ?? props.copy.paidLabel}: {rentPaidMajor()}{' '}
{dashboard().currency}
</span>
</Show>
</article>
<article class="balance-detail-row">
<div class="balance-detail-row__main">
<span>
{dashboard().paymentBalanceAdjustmentPolicy === 'utilities'
? props.copy.utilitiesAdjustedTotalLabel
: (props.copy.utilitiesBalanceLabel ?? props.copy.shareUtilities)}
{homeMode() === 'upcoming'
? (props.copy.expectedUtilitiesLabel ?? props.copy.shareUtilities)
: (props.copy.pureUtilitiesLabel ?? props.copy.shareUtilities)}
</span>
<strong>
{utilitiesDueMajor() !== null
? `${utilitiesDueMajor()} ${dashboard().currency}`
: (props.copy.notBilledYetLabel ?? '')}
{homeMode() === 'upcoming'
? predictedUtilitiesMajor()
? `${predictedUtilitiesMajor()} ${dashboard().currency}`
: (props.copy.notBilledYetLabel ?? '')
: utilitiesDueMajor()
? `${member().utilityShareMajor} ${dashboard().currency}`
: (props.copy.notBilledYetLabel ?? '')}
</strong>
<small>
{utilitiesDueMajor() !== null
? dueLabel('utilities')
: dueLabel('utilities')}
</small>
<small>{scheduleLabel('utilities')}</small>
</div>
<span class="mini-chip mini-chip--muted">
{props.copy.utilitiesPaidLabel ?? props.copy.paidLabel}:{' '}
{utilitiesPaidMajor()} {dashboard().currency}
</span>
<Show
when={
homeMode() !== 'upcoming' || majorStringToMinor(utilitiesPaidMajor()) > 0n
}
>
<span class="mini-chip mini-chip--muted">
{props.copy.utilitiesPaidLabel ?? props.copy.paidLabel}:{' '}
{utilitiesPaidMajor()} {dashboard().currency}
</span>
</Show>
</article>
<Show when={dashboard().paymentBalanceAdjustmentPolicy === 'separate'}>
<article class="balance-detail-row">
<article class="balance-detail-row">
<div class="balance-detail-row__main">
<span>{props.copy.balanceAdjustmentLabel ?? props.copy.shareOffset}</span>
<strong>
{member().purchaseOffsetMajor} {dashboard().currency}
</strong>
<small>{props.copy.currentCycleLabel ?? ''}</small>
</div>
</article>
<Show when={dashboard().paymentBalanceAdjustmentPolicy === 'rent'}>
<article class="balance-detail-row balance-detail-row--accent">
<div class="balance-detail-row__main">
<span>{props.copy.balanceAdjustmentLabel ?? props.copy.shareOffset}</span>
<span>{props.copy.rentAdjustedTotalLabel ?? ''}</span>
<strong>
{separateBalanceMajor()} {dashboard().currency}
{adjustedRentMajor()} {dashboard().currency}
</strong>
</div>
</article>
</Show>
<Show when={dashboard().paymentBalanceAdjustmentPolicy === 'utilities'}>
<article class="balance-detail-row balance-detail-row--accent">
<div class="balance-detail-row__main">
<span>{props.copy.utilitiesAdjustedTotalLabel ?? ''}</span>
<strong>
{homeMode() === 'upcoming'
? predictedUtilitiesMajor()
? `${sumMajorStrings(
predictedUtilitiesMajor() ?? '0.00',
member().purchaseOffsetMajor
)} ${dashboard().currency}`
: (props.copy.notBilledYetLabel ?? '')
: `${adjustedUtilitiesMajor()} ${dashboard().currency}`}
</strong>
</div>
</article>
</Show>
<Show when={dashboard().paymentBalanceAdjustmentPolicy === 'separate'}>
<article class="balance-detail-row balance-detail-row--accent">
<div class="balance-detail-row__main">
<span>{props.copy.finalDue ?? props.copy.remainingLabel}</span>
<strong>
{member().remainingMajor} {dashboard().currency}
</strong>
</div>
</article>
@@ -293,27 +388,6 @@ export function HomeScreen(props: Props) {
</article>
)}
</Show>
<article class="balance-item balance-item--wide balance-item--muted">
<header>
<strong>{props.copy.houseSnapshotTitle ?? ''}</strong>
<span>{formatCyclePeriod(dashboard().period, props.locale)}</span>
</header>
<p>{props.copy.houseSnapshotBody ?? ''}</p>
<div class="summary-card-grid summary-card-grid--secondary">
<FinanceSummaryCards
dashboard={dashboard()}
utilityTotalMajor={props.utilityTotalMajor}
purchaseTotalMajor={props.purchaseTotalMajor}
labels={{
remaining: props.copy.remainingLabel ?? '',
rent: props.copy.shareRent ?? '',
utilities: props.copy.shareUtilities ?? '',
purchases: props.copy.purchasesTitle ?? ''
}}
/>
</div>
</article>
</div>
)}
</Show>

View File

@@ -184,6 +184,7 @@ function HouseSection(props: {
<button
class="admin-disclosure__summary"
type="button"
aria-expanded={open()}
onClick={() => setOpen((current) => !current)}
>
<div class="admin-disclosure__copy">
@@ -296,12 +297,39 @@ export function HouseScreen(props: Props) {
>
<section class="admin-section">
<div class="admin-grid">
<article class="balance-item">
<article class="balance-item admin-card--wide">
<header>
<strong>{props.copy.householdNameLabel ?? ''}</strong>
<span>{props.householdName}</span>
<strong>
{props.copy.householdSettingsTitle ?? props.copy.houseSectionGeneral ?? ''}
</strong>
</header>
<p>{props.copy.householdNameHint ?? ''}</p>
<div class="settings-grid">
<div class="settings-field">
<span>{props.copy.householdNameLabel ?? ''}</span>
<div class="settings-field__value">{props.householdName}</div>
</div>
<div class="settings-field">
<span>{props.copy.householdLanguage ?? ''}</span>
<div class="locale-switch__buttons locale-switch__buttons--inline">
<button
classList={{ 'is-active': props.householdDefaultLocale === 'en' }}
type="button"
disabled={props.savingHouseholdLocale}
onClick={() => void props.onChangeHouseholdLocale('en')}
>
EN
</button>
<button
classList={{ 'is-active': props.householdDefaultLocale === 'ru' }}
type="button"
disabled={props.savingHouseholdLocale}
onClick={() => void props.onChangeHouseholdLocale('ru')}
>
RU
</button>
</div>
</div>
</div>
<div class="panel-toolbar">
<Button variant="secondary" onClick={props.onOpenBillingSettingsModal}>
<SettingsIcon />
@@ -310,31 +338,6 @@ export function HouseScreen(props: Props) {
</div>
</article>
<article class="balance-item">
<header>
<strong>{props.copy.householdLanguage ?? ''}</strong>
<span>{props.householdDefaultLocale.toUpperCase()}</span>
</header>
<div class="locale-switch__buttons locale-switch__buttons--inline">
<button
classList={{ 'is-active': props.householdDefaultLocale === 'en' }}
type="button"
disabled={props.savingHouseholdLocale}
onClick={() => void props.onChangeHouseholdLocale('en')}
>
EN
</button>
<button
classList={{ 'is-active': props.householdDefaultLocale === 'ru' }}
type="button"
disabled={props.savingHouseholdLocale}
onClick={() => void props.onChangeHouseholdLocale('ru')}
>
RU
</button>
</div>
</article>
<article class="balance-item">
<header>
<strong>{props.copy.manageProfileAction ?? ''}</strong>