mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 15:44:02 +00:00
feat(miniapp): redesign balance and due-state flows
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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')
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: 'Everyone’s 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:
|
||||
'Полосы показывают, как аренда, коммуналка и поправка на общие покупки формируют баланс каждого участника.',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -110,6 +110,7 @@ export interface MiniAppDashboard {
|
||||
members: {
|
||||
memberId: string
|
||||
displayName: string
|
||||
predictedUtilityShareMajor: string | null
|
||||
rentShareMajor: string
|
||||
utilityShareMajor: string
|
||||
purchaseOffsetMajor: string
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user