mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 15:54:03 +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) => ({
|
members: dashboard.members.map((line) => ({
|
||||||
memberId: line.memberId,
|
memberId: line.memberId,
|
||||||
displayName: line.displayName,
|
displayName: line.displayName,
|
||||||
|
predictedUtilityShareMajor: line.predictedUtilityShare?.toMajorString() ?? null,
|
||||||
rentShareMajor: line.rentShare.toMajorString(),
|
rentShareMajor: line.rentShare.toMajorString(),
|
||||||
utilityShareMajor: line.utilityShare.toMajorString(),
|
utilityShareMajor: line.utilityShare.toMajorString(),
|
||||||
purchaseOffsetMajor: line.purchaseOffset.toMajorString(),
|
purchaseOffsetMajor: line.purchaseOffset.toMajorString(),
|
||||||
|
|||||||
@@ -314,6 +314,7 @@ function App() {
|
|||||||
status: 'loading'
|
status: 'loading'
|
||||||
})
|
})
|
||||||
const [activeNav, setActiveNav] = createSignal<NavigationKey>('home')
|
const [activeNav, setActiveNav] = createSignal<NavigationKey>('home')
|
||||||
|
const [selectedBalanceMemberId, setSelectedBalanceMemberId] = createSignal<string | null>(null)
|
||||||
const [dashboard, setDashboard] = createSignal<MiniAppDashboard | null>(null)
|
const [dashboard, setDashboard] = createSignal<MiniAppDashboard | null>(null)
|
||||||
const [pendingMembers, setPendingMembers] = createSignal<readonly MiniAppPendingMember[]>([])
|
const [pendingMembers, setPendingMembers] = createSignal<readonly MiniAppPendingMember[]>([])
|
||||||
const [adminSettings, setAdminSettings] = createSignal<MiniAppAdminSettingsPayload | null>(null)
|
const [adminSettings, setAdminSettings] = createSignal<MiniAppAdminSettingsPayload | null>(null)
|
||||||
@@ -444,6 +445,21 @@ function App() {
|
|||||||
|
|
||||||
return data.members.find((member) => member.memberId === current.member.id) ?? null
|
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(() =>
|
const purchaseLedger = createMemo(() =>
|
||||||
(dashboard()?.ledger ?? []).filter((entry) => entry.kind === 'purchase')
|
(dashboard()?.ledger ?? []).filter((entry) => entry.kind === 'purchase')
|
||||||
)
|
)
|
||||||
@@ -2024,12 +2040,15 @@ function App() {
|
|||||||
locale={locale()}
|
locale={locale()}
|
||||||
dashboard={dashboard()}
|
dashboard={dashboard()}
|
||||||
currentMemberLine={currentMemberLine()}
|
currentMemberLine={currentMemberLine()}
|
||||||
|
inspectedMember={inspectedBalanceMember()}
|
||||||
|
selectedMemberId={inspectedBalanceMember()?.memberId ?? ''}
|
||||||
utilityTotalMajor={utilityTotalMajor()}
|
utilityTotalMajor={utilityTotalMajor()}
|
||||||
purchaseTotalMajor={purchaseTotalMajor()}
|
purchaseTotalMajor={purchaseTotalMajor()}
|
||||||
memberBalanceVisuals={memberBalanceVisuals()}
|
memberBalanceVisuals={memberBalanceVisuals()}
|
||||||
purchaseChart={purchaseInvestmentChart()}
|
purchaseChart={purchaseInvestmentChart()}
|
||||||
memberBaseDueMajor={memberBaseDueMajor}
|
memberBaseDueMajor={memberBaseDueMajor}
|
||||||
memberRemainingClass={memberRemainingClass}
|
memberRemainingClass={memberRemainingClass}
|
||||||
|
onSelectedMemberChange={setSelectedBalanceMemberId}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
case 'ledger':
|
case 'ledger':
|
||||||
@@ -2468,8 +2487,10 @@ function App() {
|
|||||||
locale={locale()}
|
locale={locale()}
|
||||||
dashboard={dashboard()}
|
dashboard={dashboard()}
|
||||||
currentMemberLine={currentMemberLine()}
|
currentMemberLine={currentMemberLine()}
|
||||||
utilityTotalMajor={utilityTotalMajor()}
|
onExplainBalance={() => {
|
||||||
purchaseTotalMajor={purchaseTotalMajor()}
|
setSelectedBalanceMemberId(currentMemberLine()?.memberId ?? null)
|
||||||
|
setActiveNav('balances')
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export const demoDashboard: MiniAppDashboard = {
|
|||||||
{
|
{
|
||||||
memberId: 'demo-member',
|
memberId: 'demo-member',
|
||||||
displayName: 'Stas',
|
displayName: 'Stas',
|
||||||
|
predictedUtilityShareMajor: '78.00',
|
||||||
rentShareMajor: '603.75',
|
rentShareMajor: '603.75',
|
||||||
utilityShareMajor: '78.00',
|
utilityShareMajor: '78.00',
|
||||||
purchaseOffsetMajor: '-66.00',
|
purchaseOffsetMajor: '-66.00',
|
||||||
@@ -53,6 +54,7 @@ export const demoDashboard: MiniAppDashboard = {
|
|||||||
{
|
{
|
||||||
memberId: 'member-chorb',
|
memberId: 'member-chorb',
|
||||||
displayName: 'Chorbanaut',
|
displayName: 'Chorbanaut',
|
||||||
|
predictedUtilityShareMajor: '78.00',
|
||||||
rentShareMajor: '603.75',
|
rentShareMajor: '603.75',
|
||||||
utilityShareMajor: '78.00',
|
utilityShareMajor: '78.00',
|
||||||
purchaseOffsetMajor: '12.00',
|
purchaseOffsetMajor: '12.00',
|
||||||
@@ -64,6 +66,7 @@ export const demoDashboard: MiniAppDashboard = {
|
|||||||
{
|
{
|
||||||
memberId: 'member-el',
|
memberId: 'member-el',
|
||||||
displayName: 'El',
|
displayName: 'El',
|
||||||
|
predictedUtilityShareMajor: '0.00',
|
||||||
rentShareMajor: '1207.50',
|
rentShareMajor: '1207.50',
|
||||||
utilityShareMajor: '0.00',
|
utilityShareMajor: '0.00',
|
||||||
purchaseOffsetMajor: '54.00',
|
purchaseOffsetMajor: '54.00',
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ export const dictionary = {
|
|||||||
payNowBody: '',
|
payNowBody: '',
|
||||||
homeDueTitle: 'Due',
|
homeDueTitle: 'Due',
|
||||||
homeSettledTitle: 'Settled',
|
homeSettledTitle: 'Settled',
|
||||||
|
whyAction: 'Why?',
|
||||||
currentCycleLabel: 'Current cycle',
|
currentCycleLabel: 'Current cycle',
|
||||||
cycleTotalLabel: 'Cycle total',
|
cycleTotalLabel: 'Cycle total',
|
||||||
cycleBillLabel: 'Cycle bill',
|
cycleBillLabel: 'Cycle bill',
|
||||||
@@ -75,8 +76,12 @@ export const dictionary = {
|
|||||||
rentPaidLabel: 'Rent paid',
|
rentPaidLabel: 'Rent paid',
|
||||||
utilitiesPaidLabel: 'Utilities paid',
|
utilitiesPaidLabel: 'Utilities paid',
|
||||||
dueOnLabel: 'Due {date}',
|
dueOnLabel: 'Due {date}',
|
||||||
|
dueTodayLabel: 'Due today',
|
||||||
|
overdueLabel: 'Overdue',
|
||||||
|
daysLeftLabel: '{count}d left',
|
||||||
upcomingLabel: 'Upcoming',
|
upcomingLabel: 'Upcoming',
|
||||||
notBilledYetLabel: 'Not billed yet',
|
notBilledYetLabel: 'Not billed yet',
|
||||||
|
expectedUtilitiesLabel: 'Expected utilities',
|
||||||
baseDue: 'Base due',
|
baseDue: 'Base due',
|
||||||
finalDue: 'Final due',
|
finalDue: 'Final due',
|
||||||
houseSnapshotTitle: 'House totals',
|
houseSnapshotTitle: 'House totals',
|
||||||
@@ -86,6 +91,9 @@ export const dictionary = {
|
|||||||
'This screen only explains your current cycle balance. Older activity stays in the ledger.',
|
'This screen only explains your current cycle balance. Older activity stays in the ledger.',
|
||||||
householdBalancesTitle: 'Household balances',
|
householdBalancesTitle: 'Household balances',
|
||||||
householdBalancesBody: 'Everyone’s current split for this cycle.',
|
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',
|
financeVisualsTitle: 'Visual balance split',
|
||||||
financeVisualsBody:
|
financeVisualsBody:
|
||||||
'Use the bars to see how rent, utilities, and shared-buy adjustments shape each member balance.',
|
'Use the bars to see how rent, utilities, and shared-buy adjustments shape each member balance.',
|
||||||
@@ -351,6 +359,7 @@ export const dictionary = {
|
|||||||
payNowBody: '',
|
payNowBody: '',
|
||||||
homeDueTitle: 'К оплате',
|
homeDueTitle: 'К оплате',
|
||||||
homeSettledTitle: 'Закрыто',
|
homeSettledTitle: 'Закрыто',
|
||||||
|
whyAction: 'Почему?',
|
||||||
currentCycleLabel: 'Текущий цикл',
|
currentCycleLabel: 'Текущий цикл',
|
||||||
cycleTotalLabel: 'Всего за цикл',
|
cycleTotalLabel: 'Всего за цикл',
|
||||||
cycleBillLabel: 'Счёт за цикл',
|
cycleBillLabel: 'Счёт за цикл',
|
||||||
@@ -363,8 +372,12 @@ export const dictionary = {
|
|||||||
rentPaidLabel: 'По аренде оплачено',
|
rentPaidLabel: 'По аренде оплачено',
|
||||||
utilitiesPaidLabel: 'По коммуналке оплачено',
|
utilitiesPaidLabel: 'По коммуналке оплачено',
|
||||||
dueOnLabel: 'Срок {date}',
|
dueOnLabel: 'Срок {date}',
|
||||||
|
dueTodayLabel: 'Срок сегодня',
|
||||||
|
overdueLabel: 'Просрочено',
|
||||||
|
daysLeftLabel: 'Осталось {count} дн.',
|
||||||
upcomingLabel: 'Ещё не срок',
|
upcomingLabel: 'Ещё не срок',
|
||||||
notBilledYetLabel: 'Ещё не начислено',
|
notBilledYetLabel: 'Ещё не начислено',
|
||||||
|
expectedUtilitiesLabel: 'Ожидаемая коммуналка',
|
||||||
baseDue: 'База к оплате',
|
baseDue: 'База к оплате',
|
||||||
finalDue: 'Итог к оплате',
|
finalDue: 'Итог к оплате',
|
||||||
houseSnapshotTitle: 'Сводка по дому',
|
houseSnapshotTitle: 'Сводка по дому',
|
||||||
@@ -374,6 +387,9 @@ export const dictionary = {
|
|||||||
'На этом экране только разбор твоего текущего баланса. Более старые записи остаются в леджере.',
|
'На этом экране только разбор твоего текущего баланса. Более старые записи остаются в леджере.',
|
||||||
householdBalancesTitle: 'Баланс дома',
|
householdBalancesTitle: 'Баланс дома',
|
||||||
householdBalancesBody: 'Текущий расклад по всем участникам за этот цикл.',
|
householdBalancesBody: 'Текущий расклад по всем участникам за этот цикл.',
|
||||||
|
inspectMemberTitle: 'Посмотреть участника',
|
||||||
|
inspectMemberBody: 'Можно быстро проверить чужой баланс без длинного списка карточек.',
|
||||||
|
inspectMemberLabel: 'Участник',
|
||||||
financeVisualsTitle: 'Визуальный разбор баланса',
|
financeVisualsTitle: 'Визуальный разбор баланса',
|
||||||
financeVisualsBody:
|
financeVisualsBody:
|
||||||
'Полосы показывают, как аренда, коммуналка и поправка на общие покупки формируют баланс каждого участника.',
|
'Полосы показывают, как аренда, коммуналка и поправка на общие покупки формируют баланс каждого участника.',
|
||||||
|
|||||||
@@ -461,11 +461,16 @@ button:disabled {
|
|||||||
.home-pay-card,
|
.home-pay-card,
|
||||||
.home-pay-card__header,
|
.home-pay-card__header,
|
||||||
.home-pay-card__copy,
|
.home-pay-card__copy,
|
||||||
.home-pay-card__chips {
|
.home-pay-card__chips,
|
||||||
|
.home-pay-card__actions {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.home-pay-card__actions {
|
||||||
|
justify-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
.stat-card {
|
.stat-card {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -1186,6 +1191,10 @@ button:disabled {
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.balance-section--secondary {
|
||||||
|
background: linear-gradient(180deg, rgb(255 255 255 / 0.04), rgb(255 255 255 / 0.02));
|
||||||
|
}
|
||||||
|
|
||||||
.balance-section__header {
|
.balance-section__header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -1199,6 +1208,10 @@ button:disabled {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.balance-section__field {
|
||||||
|
min-width: min(220px, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
.household-balance-list {
|
.household-balance-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -1208,6 +1221,22 @@ button:disabled {
|
|||||||
margin-top: 12px;
|
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 {
|
.app-context-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -1248,10 +1277,23 @@ button:disabled {
|
|||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
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 {
|
.balance-spotlight__header {
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.home-pay-card__actions {
|
||||||
|
justify-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
.balance-spotlight__stats {
|
.balance-spotlight__stats {
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
@@ -1298,6 +1340,10 @@ button:disabled {
|
|||||||
.member-editor-actions__grid {
|
.member-editor-actions__grid {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.balance-detail-card__rows {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 759px) {
|
@media (max-width: 759px) {
|
||||||
|
|||||||
@@ -156,3 +156,17 @@ export function compareTodayToPeriodDay(
|
|||||||
|
|
||||||
return 0
|
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: {
|
members: {
|
||||||
memberId: string
|
memberId: string
|
||||||
displayName: string
|
displayName: string
|
||||||
|
predictedUtilityShareMajor: string | null
|
||||||
rentShareMajor: string
|
rentShareMajor: string
|
||||||
utilityShareMajor: string
|
utilityShareMajor: string
|
||||||
purchaseOffsetMajor: 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 { FinanceSummaryCards } from '../components/finance/finance-summary-cards'
|
||||||
import { FinanceVisuals } from '../components/finance/finance-visuals'
|
import { FinanceVisuals } from '../components/finance/finance-visuals'
|
||||||
import { MemberBalanceCard } from '../components/finance/member-balance-card'
|
import { MemberBalanceCard } from '../components/finance/member-balance-card'
|
||||||
|
import { Field } from '../components/ui'
|
||||||
import { formatCyclePeriod } from '../lib/dates'
|
import { formatCyclePeriod } from '../lib/dates'
|
||||||
import type { MiniAppDashboard } from '../miniapp-api'
|
import type { MiniAppDashboard } from '../miniapp-api'
|
||||||
|
|
||||||
@@ -11,6 +12,8 @@ type Props = {
|
|||||||
locale: 'en' | 'ru'
|
locale: 'en' | 'ru'
|
||||||
dashboard: MiniAppDashboard | null
|
dashboard: MiniAppDashboard | null
|
||||||
currentMemberLine: MiniAppDashboard['members'][number] | null
|
currentMemberLine: MiniAppDashboard['members'][number] | null
|
||||||
|
inspectedMember: MiniAppDashboard['members'][number] | null
|
||||||
|
selectedMemberId: string
|
||||||
utilityTotalMajor: string
|
utilityTotalMajor: string
|
||||||
purchaseTotalMajor: string
|
purchaseTotalMajor: string
|
||||||
memberBalanceVisuals: {
|
memberBalanceVisuals: {
|
||||||
@@ -39,6 +42,7 @@ type Props = {
|
|||||||
}
|
}
|
||||||
memberBaseDueMajor: (member: MiniAppDashboard['members'][number]) => string
|
memberBaseDueMajor: (member: MiniAppDashboard['members'][number]) => string
|
||||||
memberRemainingClass: (member: MiniAppDashboard['members'][number]) => string
|
memberRemainingClass: (member: MiniAppDashboard['members'][number]) => string
|
||||||
|
onSelectedMemberChange: (memberId: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BalancesScreen(props: Props) {
|
export function BalancesScreen(props: Props) {
|
||||||
@@ -64,13 +68,110 @@ export function BalancesScreen(props: Props) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
<article class="balance-item balance-item--muted">
|
|
||||||
<header>
|
<section class="balance-item balance-item--wide balance-section balance-section--secondary">
|
||||||
<strong>{props.copy.balanceScreenScopeTitle ?? ''}</strong>
|
<header class="balance-section__header">
|
||||||
<span>{formatCyclePeriod(dashboard().period, props.locale)}</span>
|
<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>
|
</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">
|
<article class="balance-item balance-item--wide balance-item--muted">
|
||||||
<header>
|
<header>
|
||||||
<strong>{props.copy.houseSnapshotTitle ?? ''}</strong>
|
<strong>{props.copy.houseSnapshotTitle ?? ''}</strong>
|
||||||
@@ -91,70 +192,6 @@ export function BalancesScreen(props: Props) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { Show } from 'solid-js'
|
import { Show } from 'solid-js'
|
||||||
|
|
||||||
import { FinanceSummaryCards } from '../components/finance/finance-summary-cards'
|
import { Button } from '../components/ui'
|
||||||
import { compareTodayToPeriodDay, formatCyclePeriod, formatPeriodDay } from '../lib/dates'
|
import {
|
||||||
|
compareTodayToPeriodDay,
|
||||||
|
daysUntilPeriodDay,
|
||||||
|
formatCyclePeriod,
|
||||||
|
formatPeriodDay
|
||||||
|
} from '../lib/dates'
|
||||||
import { majorStringToMinor, minorToMajorString, sumMajorStrings } from '../lib/money'
|
import { majorStringToMinor, minorToMajorString, sumMajorStrings } from '../lib/money'
|
||||||
import type { MiniAppDashboard } from '../miniapp-api'
|
import type { MiniAppDashboard } from '../miniapp-api'
|
||||||
|
|
||||||
@@ -10,10 +15,11 @@ type Props = {
|
|||||||
locale: 'en' | 'ru'
|
locale: 'en' | 'ru'
|
||||||
dashboard: MiniAppDashboard | null
|
dashboard: MiniAppDashboard | null
|
||||||
currentMemberLine: MiniAppDashboard['members'][number] | null
|
currentMemberLine: MiniAppDashboard['members'][number] | null
|
||||||
utilityTotalMajor: string
|
onExplainBalance: () => void
|
||||||
purchaseTotalMajor: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HomeMode = 'upcoming' | 'due' | 'settled'
|
||||||
|
|
||||||
export function HomeScreen(props: Props) {
|
export function HomeScreen(props: Props) {
|
||||||
const rentPaidMajor = () => {
|
const rentPaidMajor = () => {
|
||||||
if (!props.dashboard || !props.currentMemberLine) {
|
if (!props.dashboard || !props.currentMemberLine) {
|
||||||
@@ -94,6 +100,14 @@ export function HomeScreen(props: Props) {
|
|||||||
: props.currentMemberLine.utilityShareMajor
|
: props.currentMemberLine.utilityShareMajor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const predictedUtilitiesMajor = () => {
|
||||||
|
if (!props.currentMemberLine) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.currentMemberLine.predictedUtilityShareMajor
|
||||||
|
}
|
||||||
|
|
||||||
const separateBalanceMajor = () => {
|
const separateBalanceMajor = () => {
|
||||||
if (
|
if (
|
||||||
!props.currentMemberLine ||
|
!props.currentMemberLine ||
|
||||||
@@ -105,13 +119,9 @@ export function HomeScreen(props: Props) {
|
|||||||
return props.currentMemberLine.purchaseOffsetMajor
|
return props.currentMemberLine.purchaseOffsetMajor
|
||||||
}
|
}
|
||||||
|
|
||||||
const heroState = () => {
|
const homeMode = (): HomeMode => {
|
||||||
if (!props.dashboard || !props.currentMemberLine) {
|
if (!props.dashboard || !props.currentMemberLine) {
|
||||||
return {
|
return 'upcoming'
|
||||||
title: props.copy.payNowTitle ?? props.copy.yourBalanceTitle ?? '',
|
|
||||||
label: props.copy.remainingLabel ?? '',
|
|
||||||
amountMajor: '—'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const remainingMinor = majorStringToMinor(props.currentMemberLine.remainingMajor)
|
const remainingMinor = majorStringToMinor(props.currentMemberLine.remainingMajor)
|
||||||
@@ -126,6 +136,7 @@ export function HomeScreen(props: Props) {
|
|||||||
props.dashboard.utilitiesDueDay,
|
props.dashboard.utilitiesDueDay,
|
||||||
props.dashboard.timezone
|
props.dashboard.timezone
|
||||||
)
|
)
|
||||||
|
|
||||||
const hasDueNow =
|
const hasDueNow =
|
||||||
(rentStatus !== null &&
|
(rentStatus !== null &&
|
||||||
rentStatus >= 0 &&
|
rentStatus >= 0 &&
|
||||||
@@ -137,50 +148,71 @@ export function HomeScreen(props: Props) {
|
|||||||
majorStringToMinor(separateBalanceMajor() ?? '0.00') > 0n)
|
majorStringToMinor(separateBalanceMajor() ?? '0.00') > 0n)
|
||||||
|
|
||||||
if (remainingMinor === 0n && paidMinor > 0n) {
|
if (remainingMinor === 0n && paidMinor > 0n) {
|
||||||
return {
|
return 'settled'
|
||||||
title: props.copy.homeSettledTitle ?? '',
|
|
||||||
label: props.copy.paidThisCycleLabel ?? props.copy.paidLabel ?? '',
|
|
||||||
amountMajor: props.currentMemberLine.paidMajor
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasDueNow) {
|
return hasDueNow ? 'due' : 'upcoming'
|
||||||
|
}
|
||||||
|
|
||||||
|
const heroState = () => {
|
||||||
|
if (!props.dashboard || !props.currentMemberLine) {
|
||||||
return {
|
return {
|
||||||
title: props.copy.homeDueTitle ?? props.copy.payNowTitle ?? '',
|
title: props.copy.payNowTitle ?? props.copy.yourBalanceTitle ?? '',
|
||||||
label: props.copy.remainingLabel ?? '',
|
label: props.copy.remainingLabel ?? '',
|
||||||
amountMajor: props.currentMemberLine.remainingMajor
|
amountMajor: '—'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
switch (homeMode()) {
|
||||||
title: props.copy.payNowTitle ?? props.copy.yourBalanceTitle ?? '',
|
case 'settled':
|
||||||
label: props.copy.cycleTotalLabel ?? props.copy.totalDue ?? '',
|
return {
|
||||||
amountMajor: props.currentMemberLine.netDueMajor
|
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) {
|
if (!props.dashboard) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const day = kind === 'rent' ? props.dashboard.rentDueDay : props.dashboard.utilitiesDueDay
|
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 date = formatPeriodDay(props.dashboard.period, day, props.locale)
|
||||||
const template =
|
const daysLeft = daysUntilPeriodDay(props.dashboard.period, day, props.dashboard.timezone)
|
||||||
comparison !== null && comparison < 0
|
const dayLabel = dayCountLabel(daysLeft)
|
||||||
? (props.copy.upcomingLabel ?? '')
|
const dueLabel = (props.copy.dueOnLabel ?? '').replace('{date}', date)
|
||||||
: (props.copy.dueOnLabel ?? '').replace('{date}', date)
|
|
||||||
|
|
||||||
if (comparison !== null && comparison < 0) {
|
return dayLabel ? `${dueLabel} · ${dayLabel}` : dueLabel
|
||||||
return `${template}${template.length > 0 ? ' ' : ''}${date}`.trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
return template
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -192,7 +224,6 @@ export function HomeScreen(props: Props) {
|
|||||||
<header class="balance-spotlight__header">
|
<header class="balance-spotlight__header">
|
||||||
<div class="balance-spotlight__copy">
|
<div class="balance-spotlight__copy">
|
||||||
<strong>{props.copy.yourBalanceTitle ?? ''}</strong>
|
<strong>{props.copy.yourBalanceTitle ?? ''}</strong>
|
||||||
<p>{props.copy.yourBalanceBody ?? ''}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="balance-spotlight__hero">
|
<div class="balance-spotlight__hero">
|
||||||
<span>{props.copy.remainingLabel ?? ''}</span>
|
<span>{props.copy.remainingLabel ?? ''}</span>
|
||||||
@@ -213,6 +244,11 @@ export function HomeScreen(props: Props) {
|
|||||||
<strong>{heroState().title}</strong>
|
<strong>{heroState().title}</strong>
|
||||||
<small>{formatCyclePeriod(dashboard().period, props.locale)}</small>
|
<small>{formatCyclePeriod(dashboard().period, props.locale)}</small>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="home-pay-card__actions">
|
||||||
|
<Button variant="ghost" onClick={props.onExplainBalance}>
|
||||||
|
{props.copy.whyAction ?? ''}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<div class="balance-spotlight__hero">
|
<div class="balance-spotlight__hero">
|
||||||
<span>{heroState().label}</span>
|
<span>{heroState().label}</span>
|
||||||
<strong>
|
<strong>
|
||||||
@@ -221,70 +257,129 @@ export function HomeScreen(props: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="balance-spotlight__stats">
|
<Show
|
||||||
<article class="stat-card balance-spotlight__stat">
|
when={homeMode() === 'upcoming'}
|
||||||
<span>{props.copy.paidLabel ?? ''}</span>
|
fallback={
|
||||||
<strong>
|
<div class="balance-spotlight__stats">
|
||||||
{member().paidMajor} {dashboard().currency}
|
<article class="stat-card balance-spotlight__stat">
|
||||||
</strong>
|
<span>{props.copy.paidLabel ?? ''}</span>
|
||||||
</article>
|
<strong>
|
||||||
<article class="stat-card balance-spotlight__stat">
|
{member().paidMajor} {dashboard().currency}
|
||||||
<span>{props.copy.remainingLabel ?? ''}</span>
|
</strong>
|
||||||
<strong>
|
</article>
|
||||||
{member().remainingMajor} {dashboard().currency}
|
<article class="stat-card balance-spotlight__stat">
|
||||||
</strong>
|
<span>{props.copy.remainingLabel ?? ''}</span>
|
||||||
</article>
|
<strong>
|
||||||
</div>
|
{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">
|
<div class="balance-spotlight__rows">
|
||||||
<article class="balance-detail-row">
|
<article class="balance-detail-row">
|
||||||
<div class="balance-detail-row__main">
|
<div class="balance-detail-row__main">
|
||||||
<span>
|
<span>{props.copy.shareRent ?? ''}</span>
|
||||||
{dashboard().paymentBalanceAdjustmentPolicy === 'rent'
|
|
||||||
? props.copy.rentAdjustedTotalLabel
|
|
||||||
: props.copy.shareRent}
|
|
||||||
</span>
|
|
||||||
<strong>
|
<strong>
|
||||||
{rentDueMajor()} {dashboard().currency}
|
{member().rentShareMajor} {dashboard().currency}
|
||||||
</strong>
|
</strong>
|
||||||
<small>{dueLabel('rent')}</small>
|
<small>{scheduleLabel('rent')}</small>
|
||||||
</div>
|
</div>
|
||||||
<span class="mini-chip mini-chip--muted">
|
<Show when={homeMode() !== 'upcoming'}>
|
||||||
{props.copy.rentPaidLabel ?? props.copy.paidLabel}: {rentPaidMajor()}{' '}
|
<span class="mini-chip mini-chip--muted">
|
||||||
{dashboard().currency}
|
{props.copy.rentPaidLabel ?? props.copy.paidLabel}: {rentPaidMajor()}{' '}
|
||||||
</span>
|
{dashboard().currency}
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="balance-detail-row">
|
<article class="balance-detail-row">
|
||||||
<div class="balance-detail-row__main">
|
<div class="balance-detail-row__main">
|
||||||
<span>
|
<span>
|
||||||
{dashboard().paymentBalanceAdjustmentPolicy === 'utilities'
|
{homeMode() === 'upcoming'
|
||||||
? props.copy.utilitiesAdjustedTotalLabel
|
? (props.copy.expectedUtilitiesLabel ?? props.copy.shareUtilities)
|
||||||
: (props.copy.utilitiesBalanceLabel ?? props.copy.shareUtilities)}
|
: (props.copy.pureUtilitiesLabel ?? props.copy.shareUtilities)}
|
||||||
</span>
|
</span>
|
||||||
<strong>
|
<strong>
|
||||||
{utilitiesDueMajor() !== null
|
{homeMode() === 'upcoming'
|
||||||
? `${utilitiesDueMajor()} ${dashboard().currency}`
|
? predictedUtilitiesMajor()
|
||||||
: (props.copy.notBilledYetLabel ?? '')}
|
? `${predictedUtilitiesMajor()} ${dashboard().currency}`
|
||||||
|
: (props.copy.notBilledYetLabel ?? '')
|
||||||
|
: utilitiesDueMajor()
|
||||||
|
? `${member().utilityShareMajor} ${dashboard().currency}`
|
||||||
|
: (props.copy.notBilledYetLabel ?? '')}
|
||||||
</strong>
|
</strong>
|
||||||
<small>
|
<small>{scheduleLabel('utilities')}</small>
|
||||||
{utilitiesDueMajor() !== null
|
|
||||||
? dueLabel('utilities')
|
|
||||||
: dueLabel('utilities')}
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
<span class="mini-chip mini-chip--muted">
|
<Show
|
||||||
{props.copy.utilitiesPaidLabel ?? props.copy.paidLabel}:{' '}
|
when={
|
||||||
{utilitiesPaidMajor()} {dashboard().currency}
|
homeMode() !== 'upcoming' || majorStringToMinor(utilitiesPaidMajor()) > 0n
|
||||||
</span>
|
}
|
||||||
|
>
|
||||||
|
<span class="mini-chip mini-chip--muted">
|
||||||
|
{props.copy.utilitiesPaidLabel ?? props.copy.paidLabel}:{' '}
|
||||||
|
{utilitiesPaidMajor()} {dashboard().currency}
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
</article>
|
</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">
|
<div class="balance-detail-row__main">
|
||||||
<span>{props.copy.balanceAdjustmentLabel ?? props.copy.shareOffset}</span>
|
<span>{props.copy.rentAdjustedTotalLabel ?? ''}</span>
|
||||||
<strong>
|
<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>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
@@ -293,27 +388,6 @@ export function HomeScreen(props: Props) {
|
|||||||
</article>
|
</article>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ function HouseSection(props: {
|
|||||||
<button
|
<button
|
||||||
class="admin-disclosure__summary"
|
class="admin-disclosure__summary"
|
||||||
type="button"
|
type="button"
|
||||||
|
aria-expanded={open()}
|
||||||
onClick={() => setOpen((current) => !current)}
|
onClick={() => setOpen((current) => !current)}
|
||||||
>
|
>
|
||||||
<div class="admin-disclosure__copy">
|
<div class="admin-disclosure__copy">
|
||||||
@@ -296,12 +297,39 @@ export function HouseScreen(props: Props) {
|
|||||||
>
|
>
|
||||||
<section class="admin-section">
|
<section class="admin-section">
|
||||||
<div class="admin-grid">
|
<div class="admin-grid">
|
||||||
<article class="balance-item">
|
<article class="balance-item admin-card--wide">
|
||||||
<header>
|
<header>
|
||||||
<strong>{props.copy.householdNameLabel ?? ''}</strong>
|
<strong>
|
||||||
<span>{props.householdName}</span>
|
{props.copy.householdSettingsTitle ?? props.copy.houseSectionGeneral ?? ''}
|
||||||
|
</strong>
|
||||||
</header>
|
</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">
|
<div class="panel-toolbar">
|
||||||
<Button variant="secondary" onClick={props.onOpenBillingSettingsModal}>
|
<Button variant="secondary" onClick={props.onOpenBillingSettingsModal}>
|
||||||
<SettingsIcon />
|
<SettingsIcon />
|
||||||
@@ -310,31 +338,6 @@ export function HouseScreen(props: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</article>
|
</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">
|
<article class="balance-item">
|
||||||
<header>
|
<header>
|
||||||
<strong>{props.copy.manageProfileAction ?? ''}</strong>
|
<strong>{props.copy.manageProfileAction ?? ''}</strong>
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ export interface FinanceDashboardMemberLine {
|
|||||||
status?: 'active' | 'away' | 'left'
|
status?: 'active' | 'away' | 'left'
|
||||||
absencePolicy?: HouseholdMemberAbsencePolicy
|
absencePolicy?: HouseholdMemberAbsencePolicy
|
||||||
absencePolicyEffectiveFromPeriod?: string | null
|
absencePolicyEffectiveFromPeriod?: string | null
|
||||||
|
predictedUtilityShare?: Money | null
|
||||||
rentShare: Money
|
rentShare: Money
|
||||||
utilityShare: Money
|
utilityShare: Money
|
||||||
purchaseOffset: Money
|
purchaseOffset: Money
|
||||||
@@ -321,6 +322,16 @@ async function buildFinanceDashboard(
|
|||||||
dependencies.repository.listUtilityBillsForCycle(cycle.id)
|
dependencies.repository.listUtilityBillsForCycle(cycle.id)
|
||||||
])
|
])
|
||||||
const paymentRecords = await dependencies.repository.listPaymentRecordsForCycle(cycle.id)
|
const paymentRecords = await dependencies.repository.listPaymentRecordsForCycle(cycle.id)
|
||||||
|
const previousCycle = await dependencies.repository.getCycleByPeriod(period.previous().toString())
|
||||||
|
const previousSnapshotLines = previousCycle
|
||||||
|
? await dependencies.repository.getSettlementSnapshotLines(previousCycle.id)
|
||||||
|
: []
|
||||||
|
const previousUtilityShareByMemberId = new Map(
|
||||||
|
previousSnapshotLines.map((line) => [
|
||||||
|
line.memberId,
|
||||||
|
Money.fromMinor(line.utilityShareMinor, cycle.currency)
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
const convertedRent = await convertIntoCycleCurrency(dependencies, {
|
const convertedRent = await convertIntoCycleCurrency(dependencies, {
|
||||||
cycle,
|
cycle,
|
||||||
@@ -476,6 +487,7 @@ async function buildFinanceDashboard(
|
|||||||
absencePolicy: resolvedAbsencePolicies.get(line.memberId.toString())?.policy ?? 'resident',
|
absencePolicy: resolvedAbsencePolicies.get(line.memberId.toString())?.policy ?? 'resident',
|
||||||
absencePolicyEffectiveFromPeriod:
|
absencePolicyEffectiveFromPeriod:
|
||||||
resolvedAbsencePolicies.get(line.memberId.toString())?.effectiveFromPeriod ?? null,
|
resolvedAbsencePolicies.get(line.memberId.toString())?.effectiveFromPeriod ?? null,
|
||||||
|
predictedUtilityShare: previousUtilityShareByMemberId.get(line.memberId.toString()) ?? null,
|
||||||
rentShare: line.rentShare,
|
rentShare: line.rentShare,
|
||||||
utilityShare: line.utilityShare,
|
utilityShare: line.utilityShare,
|
||||||
purchaseOffset: line.purchaseOffset,
|
purchaseOffset: line.purchaseOffset,
|
||||||
|
|||||||
Reference in New Issue
Block a user