mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 21:04:03 +00:00
fix(miniapp): tighten house layout and restore balance details
This commit is contained in:
@@ -106,6 +106,8 @@ type SessionState =
|
|||||||
|
|
||||||
type NavigationKey = 'home' | 'balances' | 'ledger' | 'house'
|
type NavigationKey = 'home' | 'balances' | 'ledger' | 'house'
|
||||||
|
|
||||||
|
const chartPalette = ['#f7b389', '#6fd3c0', '#f06a8d', '#94a8ff', '#f3d36f', '#7dc96d'] as const
|
||||||
|
|
||||||
type UtilityBillDraft = {
|
type UtilityBillDraft = {
|
||||||
billName: string
|
billName: string
|
||||||
amountMajor: string
|
amountMajor: string
|
||||||
@@ -187,6 +189,30 @@ function defaultCyclePeriod(): string {
|
|||||||
return new Date().toISOString().slice(0, 7)
|
return new Date().toISOString().slice(0, 7)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function absoluteMinor(value: bigint): bigint {
|
||||||
|
return value < 0n ? -value : value
|
||||||
|
}
|
||||||
|
|
||||||
|
function memberBaseDueMajor(member: MiniAppDashboard['members'][number]): string {
|
||||||
|
return minorToMajorString(
|
||||||
|
majorStringToMinor(member.rentShareMajor) + majorStringToMinor(member.utilityShareMajor)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function memberRemainingClass(member: MiniAppDashboard['members'][number]): string {
|
||||||
|
const remainingMinor = majorStringToMinor(member.remainingMajor)
|
||||||
|
|
||||||
|
if (remainingMinor < 0n) {
|
||||||
|
return 'is-credit'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainingMinor === 0n) {
|
||||||
|
return 'is-settled'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'is-due'
|
||||||
|
}
|
||||||
|
|
||||||
function ledgerPrimaryAmount(entry: MiniAppDashboard['ledger'][number]): string {
|
function ledgerPrimaryAmount(entry: MiniAppDashboard['ledger'][number]): string {
|
||||||
return `${entry.displayAmountMajor} ${entry.displayCurrency}`
|
return `${entry.displayAmountMajor} ${entry.displayCurrency}`
|
||||||
}
|
}
|
||||||
@@ -458,6 +484,134 @@ function App() {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
const memberBalanceVisuals = createMemo(() => {
|
||||||
|
const data = dashboard()
|
||||||
|
if (!data) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const totals = data.members.map((member) => {
|
||||||
|
const rentMinor = absoluteMinor(majorStringToMinor(member.rentShareMajor))
|
||||||
|
const utilityMinor = absoluteMinor(majorStringToMinor(member.utilityShareMajor))
|
||||||
|
const purchaseMinor = absoluteMinor(majorStringToMinor(member.purchaseOffsetMajor))
|
||||||
|
|
||||||
|
return {
|
||||||
|
member,
|
||||||
|
totalMinor: rentMinor + utilityMinor + purchaseMinor,
|
||||||
|
segments: [
|
||||||
|
{
|
||||||
|
key: 'rent',
|
||||||
|
label: copy().shareRent,
|
||||||
|
amountMajor: member.rentShareMajor,
|
||||||
|
amountMinor: rentMinor
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'utilities',
|
||||||
|
label: copy().shareUtilities,
|
||||||
|
amountMajor: member.utilityShareMajor,
|
||||||
|
amountMinor: utilityMinor
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key:
|
||||||
|
majorStringToMinor(member.purchaseOffsetMajor) < 0n
|
||||||
|
? 'purchase-credit'
|
||||||
|
: 'purchase-debit',
|
||||||
|
label: copy().shareOffset,
|
||||||
|
amountMajor: member.purchaseOffsetMajor,
|
||||||
|
amountMinor: purchaseMinor
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const maxTotalMinor = totals.reduce(
|
||||||
|
(max, item) => (item.totalMinor > max ? item.totalMinor : max),
|
||||||
|
0n
|
||||||
|
)
|
||||||
|
|
||||||
|
return totals
|
||||||
|
.sort((left, right) => {
|
||||||
|
const leftRemaining = majorStringToMinor(left.member.remainingMajor)
|
||||||
|
const rightRemaining = majorStringToMinor(right.member.remainingMajor)
|
||||||
|
|
||||||
|
if (rightRemaining === leftRemaining) {
|
||||||
|
return left.member.displayName.localeCompare(right.member.displayName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rightRemaining > leftRemaining ? 1 : -1
|
||||||
|
})
|
||||||
|
.map((item) => ({
|
||||||
|
...item,
|
||||||
|
barWidthPercent:
|
||||||
|
maxTotalMinor > 0n ? (Number(item.totalMinor) / Number(maxTotalMinor)) * 100 : 0,
|
||||||
|
segments: item.segments.map((segment) => ({
|
||||||
|
...segment,
|
||||||
|
widthPercent:
|
||||||
|
item.totalMinor > 0n ? (Number(segment.amountMinor) / Number(item.totalMinor)) * 100 : 0
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
const purchaseInvestmentChart = createMemo(() => {
|
||||||
|
const data = dashboard()
|
||||||
|
if (!data) {
|
||||||
|
return {
|
||||||
|
totalMajor: '0.00',
|
||||||
|
slices: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const membersById = new Map(data.members.map((member) => [member.memberId, member.displayName]))
|
||||||
|
const totals = new Map<string, { label: string; amountMinor: bigint }>()
|
||||||
|
|
||||||
|
for (const entry of purchaseLedger()) {
|
||||||
|
const key = entry.memberId ?? entry.actorDisplayName ?? entry.id
|
||||||
|
const label =
|
||||||
|
(entry.memberId ? membersById.get(entry.memberId) : null) ??
|
||||||
|
entry.actorDisplayName ??
|
||||||
|
copy().ledgerActorFallback
|
||||||
|
const current = totals.get(key) ?? {
|
||||||
|
label,
|
||||||
|
amountMinor: 0n
|
||||||
|
}
|
||||||
|
|
||||||
|
totals.set(key, {
|
||||||
|
label,
|
||||||
|
amountMinor:
|
||||||
|
current.amountMinor + absoluteMinor(majorStringToMinor(entry.displayAmountMajor))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = [...totals.entries()]
|
||||||
|
.map(([key, value], index) => ({
|
||||||
|
key,
|
||||||
|
label: value.label,
|
||||||
|
amountMinor: value.amountMinor,
|
||||||
|
amountMajor: minorToMajorString(value.amountMinor),
|
||||||
|
color: chartPalette[index % chartPalette.length]!
|
||||||
|
}))
|
||||||
|
.filter((item) => item.amountMinor > 0n)
|
||||||
|
.sort((left, right) => (right.amountMinor > left.amountMinor ? 1 : -1))
|
||||||
|
|
||||||
|
const totalMinor = items.reduce((sum, item) => sum + item.amountMinor, 0n)
|
||||||
|
const circumference = 2 * Math.PI * 42
|
||||||
|
let offset = 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalMajor: minorToMajorString(totalMinor),
|
||||||
|
slices: items.map((item) => {
|
||||||
|
const ratio = totalMinor > 0n ? Number(item.amountMinor) / Number(totalMinor) : 0
|
||||||
|
const dash = ratio * circumference
|
||||||
|
const slice = {
|
||||||
|
...item,
|
||||||
|
percentage: Math.round(ratio * 100),
|
||||||
|
dasharray: `${dash} ${Math.max(circumference - dash, 0)}`,
|
||||||
|
dashoffset: `${-offset}`
|
||||||
|
}
|
||||||
|
offset += dash
|
||||||
|
return slice
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
const webApp = getTelegramWebApp()
|
const webApp = getTelegramWebApp()
|
||||||
|
|
||||||
function ledgerTitle(entry: MiniAppDashboard['ledger'][number]): string {
|
function ledgerTitle(entry: MiniAppDashboard['ledger'][number]): string {
|
||||||
@@ -1870,6 +2024,12 @@ function App() {
|
|||||||
locale={locale()}
|
locale={locale()}
|
||||||
dashboard={dashboard()}
|
dashboard={dashboard()}
|
||||||
currentMemberLine={currentMemberLine()}
|
currentMemberLine={currentMemberLine()}
|
||||||
|
utilityTotalMajor={utilityTotalMajor()}
|
||||||
|
purchaseTotalMajor={purchaseTotalMajor()}
|
||||||
|
memberBalanceVisuals={memberBalanceVisuals()}
|
||||||
|
purchaseChart={purchaseInvestmentChart()}
|
||||||
|
memberBaseDueMajor={memberBaseDueMajor}
|
||||||
|
memberRemainingClass={memberRemainingClass}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
case 'ledger':
|
case 'ledger':
|
||||||
|
|||||||
@@ -30,8 +30,7 @@ export const dictionary = {
|
|||||||
reload: 'Retry',
|
reload: 'Retry',
|
||||||
language: 'Language',
|
language: 'Language',
|
||||||
householdLanguage: 'Household language',
|
householdLanguage: 'Household language',
|
||||||
generalSettingsBody:
|
generalSettingsBody: 'Household name, default language, and your profile controls live here.',
|
||||||
'Household identity, default language, and personal profile controls live here.',
|
|
||||||
householdNameLabel: 'Household name',
|
householdNameLabel: 'Household name',
|
||||||
householdNameHint: 'This appears in the mini app, join flow, and bot responses.',
|
householdNameHint: 'This appears in the mini app, join flow, and bot responses.',
|
||||||
savingLanguage: 'Saving…',
|
savingLanguage: 'Saving…',
|
||||||
@@ -237,6 +236,8 @@ export const dictionary = {
|
|||||||
editCategoryAction: 'Edit category',
|
editCategoryAction: 'Edit category',
|
||||||
adminsTitle: 'Admins',
|
adminsTitle: 'Admins',
|
||||||
adminsBody: 'Promote trusted household members so they can manage billing and approvals.',
|
adminsBody: 'Promote trusted household members so they can manage billing and approvals.',
|
||||||
|
membersTitle: 'Members',
|
||||||
|
membersBody: 'Review roles, billing weights, and status for everyone in the household.',
|
||||||
displayNameLabel: 'Household display name',
|
displayNameLabel: 'Household display name',
|
||||||
displayNameHint: 'This name appears in balances, ledger entries, and assistant replies.',
|
displayNameHint: 'This name appears in balances, ledger entries, and assistant replies.',
|
||||||
manageProfileAction: 'Edit profile',
|
manageProfileAction: 'Edit profile',
|
||||||
@@ -310,7 +311,7 @@ export const dictionary = {
|
|||||||
reload: 'Повторить',
|
reload: 'Повторить',
|
||||||
language: 'Язык',
|
language: 'Язык',
|
||||||
householdLanguage: 'Язык дома',
|
householdLanguage: 'Язык дома',
|
||||||
generalSettingsBody: 'Здесь живут имя дома, язык по умолчанию и доступ к личному профилю.',
|
generalSettingsBody: 'Здесь настраиваются название дома, язык по умолчанию и твой профиль.',
|
||||||
householdNameLabel: 'Название дома',
|
householdNameLabel: 'Название дома',
|
||||||
householdNameHint: 'Показывается в приложении, при вступлении и в ответах бота.',
|
householdNameHint: 'Показывается в приложении, при вступлении и в ответах бота.',
|
||||||
savingLanguage: 'Сохраняем…',
|
savingLanguage: 'Сохраняем…',
|
||||||
@@ -518,6 +519,8 @@ export const dictionary = {
|
|||||||
adminsTitle: 'Админы',
|
adminsTitle: 'Админы',
|
||||||
adminsBody:
|
adminsBody:
|
||||||
'Повышай доверенных участников, чтобы они могли управлять биллингом и подтверждениями.',
|
'Повышай доверенных участников, чтобы они могли управлять биллингом и подтверждениями.',
|
||||||
|
membersTitle: 'Участники',
|
||||||
|
membersBody: 'Здесь собраны роли, веса аренды и статусы всех участников дома.',
|
||||||
displayNameLabel: 'Имя в доме',
|
displayNameLabel: 'Имя в доме',
|
||||||
displayNameHint: 'Это имя будет видно в балансе, леджере и ответах ассистента.',
|
displayNameHint: 'Это имя будет видно в балансе, леджере и ответах ассистента.',
|
||||||
manageProfileAction: 'Редактировать профиль',
|
manageProfileAction: 'Редактировать профиль',
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ body {
|
|||||||
|
|
||||||
button {
|
button {
|
||||||
font: inherit;
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
@@ -44,7 +49,7 @@ button {
|
|||||||
position: relative;
|
position: relative;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 24px 18px 108px;
|
padding: 24px 18px calc(140px + env(safe-area-inset-bottom));
|
||||||
}
|
}
|
||||||
|
|
||||||
.shell__backdrop {
|
.shell__backdrop {
|
||||||
@@ -343,7 +348,7 @@ button {
|
|||||||
.app-bottom-nav {
|
.app-bottom-nav {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 18px;
|
right: 18px;
|
||||||
bottom: 20px;
|
bottom: calc(12px + env(safe-area-inset-bottom));
|
||||||
left: 18px;
|
left: 18px;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
}
|
}
|
||||||
@@ -358,6 +363,7 @@ button {
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
|
padding-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-card-grid {
|
.summary-card-grid {
|
||||||
@@ -387,6 +393,7 @@ button {
|
|||||||
.activity-row,
|
.activity-row,
|
||||||
.utility-bill-row {
|
.utility-bill-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
align-content: start;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
border: 1px solid rgb(255 255 255 / 0.08);
|
border: 1px solid rgb(255 255 255 / 0.08);
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
@@ -769,25 +776,23 @@ button {
|
|||||||
.admin-grid {
|
.admin-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
align-items: start;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-section {
|
.admin-section {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
align-items: start;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-disclosure {
|
.admin-disclosure {
|
||||||
border: 1px solid rgb(255 255 255 / 0.08);
|
border: 1px solid rgb(255 255 255 / 0.08);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
background: rgb(255 255 255 / 0.03);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-disclosure[open] {
|
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgb(255 255 255 / 0.05), rgb(255 255 255 / 0.02)),
|
linear-gradient(180deg, rgb(255 255 255 / 0.05), rgb(255 255 255 / 0.02)),
|
||||||
rgb(255 255 255 / 0.03);
|
rgb(255 255 255 / 0.03);
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-disclosure__summary {
|
.admin-disclosure__summary {
|
||||||
@@ -795,13 +800,11 @@ button {
|
|||||||
grid-template-columns: minmax(0, 1fr) auto;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
cursor: pointer;
|
text-align: left;
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-disclosure__summary::-webkit-details-marker {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-disclosure__copy {
|
.admin-disclosure__copy {
|
||||||
@@ -813,7 +816,7 @@ button {
|
|||||||
transition: transform 140ms ease;
|
transition: transform 140ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-disclosure[open] .admin-disclosure__icon {
|
.admin-disclosure__icon.is-open {
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -966,7 +969,7 @@ button {
|
|||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
padding: 0;
|
padding: 6px 10px;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -979,6 +982,7 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.timezone-chip {
|
.timezone-chip {
|
||||||
|
padding: 6px 10px;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1194,7 +1198,7 @@ button {
|
|||||||
.shell {
|
.shell {
|
||||||
max-width: 920px;
|
max-width: 920px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 32px 24px 40px;
|
padding: 32px 24px 136px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-card-grid {
|
.summary-card-grid {
|
||||||
@@ -1266,7 +1270,7 @@ button {
|
|||||||
|
|
||||||
@media (max-width: 759px) {
|
@media (max-width: 759px) {
|
||||||
.shell {
|
.shell {
|
||||||
padding: 18px 14px 28px;
|
padding: 18px 14px calc(132px + env(safe-area-inset-bottom));
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar {
|
.topbar {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Show } from 'solid-js'
|
import { For, 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 { MemberBalanceCard } from '../components/finance/member-balance-card'
|
||||||
import { formatCyclePeriod } from '../lib/dates'
|
import { formatCyclePeriod } from '../lib/dates'
|
||||||
import type { MiniAppDashboard } from '../miniapp-api'
|
import type { MiniAppDashboard } from '../miniapp-api'
|
||||||
@@ -9,6 +11,34 @@ 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
|
||||||
|
purchaseTotalMajor: string
|
||||||
|
memberBalanceVisuals: {
|
||||||
|
member: MiniAppDashboard['members'][number]
|
||||||
|
totalMinor: bigint
|
||||||
|
barWidthPercent: number
|
||||||
|
segments: {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
amountMajor: string
|
||||||
|
amountMinor: bigint
|
||||||
|
widthPercent: number
|
||||||
|
}[]
|
||||||
|
}[]
|
||||||
|
purchaseChart: {
|
||||||
|
totalMajor: string
|
||||||
|
slices: {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
amountMajor: string
|
||||||
|
color: string
|
||||||
|
percentage: number
|
||||||
|
dasharray: string
|
||||||
|
dashoffset: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
memberBaseDueMajor: (member: MiniAppDashboard['members'][number]) => string
|
||||||
|
memberRemainingClass: (member: MiniAppDashboard['members'][number]) => string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BalancesScreen(props: Props) {
|
export function BalancesScreen(props: Props) {
|
||||||
@@ -41,6 +71,82 @@ export function BalancesScreen(props: Props) {
|
|||||||
</header>
|
</header>
|
||||||
<p>{props.copy.balanceScreenScopeBody ?? ''}</p>
|
<p>{props.copy.balanceScreenScopeBody ?? ''}</p>
|
||||||
</article>
|
</article>
|
||||||
|
<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>
|
||||||
|
<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">
|
||||||
|
<header>
|
||||||
|
<strong>{props.copy.householdBalancesTitle ?? ''}</strong>
|
||||||
|
<span>{String(dashboard().members.length)}</span>
|
||||||
|
</header>
|
||||||
|
<p>{props.copy.householdBalancesBody ?? ''}</p>
|
||||||
|
</article>
|
||||||
|
<For each={dashboard().members}>
|
||||||
|
{(member) => (
|
||||||
|
<article class="balance-item">
|
||||||
|
<header>
|
||||||
|
<strong>{member.displayName}</strong>
|
||||||
|
<span>
|
||||||
|
{member.remainingMajor} {dashboard().currency}
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
<p>
|
||||||
|
{props.copy.baseDue ?? ''}: {props.memberBaseDueMajor(member)}{' '}
|
||||||
|
{dashboard().currency}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{props.copy.shareRent ?? ''}: {member.rentShareMajor} {dashboard().currency}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{props.copy.shareUtilities ?? ''}: {member.utilityShareMajor}{' '}
|
||||||
|
{dashboard().currency}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{props.copy.shareOffset ?? ''}: {member.purchaseOffsetMajor}{' '}
|
||||||
|
{dashboard().currency}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{props.copy.paidLabel ?? ''}: {member.paidMajor} {dashboard().currency}
|
||||||
|
</p>
|
||||||
|
<p class={`balance-status ${props.memberRemainingClass(member)}`}>
|
||||||
|
{props.copy.remainingLabel ?? ''}: {member.remainingMajor} {dashboard().currency}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { For, Show, createMemo, type JSX } from 'solid-js'
|
import { For, Show, createMemo, createSignal, type JSX } from 'solid-js'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -177,17 +177,25 @@ function HouseSection(props: {
|
|||||||
defaultOpen?: boolean | undefined
|
defaultOpen?: boolean | undefined
|
||||||
children: JSX.Element
|
children: JSX.Element
|
||||||
}) {
|
}) {
|
||||||
|
const [open, setOpen] = createSignal(props.defaultOpen ?? false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<details class="admin-disclosure" open={props.defaultOpen}>
|
<section class="admin-disclosure">
|
||||||
<summary class="admin-disclosure__summary">
|
<button
|
||||||
|
class="admin-disclosure__summary"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpen((current) => !current)}
|
||||||
|
>
|
||||||
<div class="admin-disclosure__copy">
|
<div class="admin-disclosure__copy">
|
||||||
<strong>{props.title}</strong>
|
<strong>{props.title}</strong>
|
||||||
<Show when={props.body}>{(body) => <p>{body()}</p>}</Show>
|
<Show when={props.body}>{(body) => <p>{body()}</p>}</Show>
|
||||||
</div>
|
</div>
|
||||||
<ChevronDownIcon class="admin-disclosure__icon" />
|
<ChevronDownIcon class={`admin-disclosure__icon${open() ? ' is-open' : ''}`} />
|
||||||
</summary>
|
</button>
|
||||||
<div class="admin-disclosure__content">{props.children}</div>
|
<Show when={open()}>
|
||||||
</details>
|
<div class="admin-disclosure__content">{props.children}</div>
|
||||||
|
</Show>
|
||||||
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1047,13 +1055,16 @@ export function HouseScreen(props: Props) {
|
|||||||
</section>
|
</section>
|
||||||
</HouseSection>
|
</HouseSection>
|
||||||
|
|
||||||
<HouseSection title={props.copy.houseSectionMembers ?? ''} body={props.copy.adminsBody}>
|
<HouseSection title={props.copy.houseSectionMembers ?? ''} body={props.copy.membersBody}>
|
||||||
<section class="admin-section">
|
<section class="admin-section">
|
||||||
<div class="admin-grid">
|
<div class="admin-grid">
|
||||||
<article class="balance-item admin-card--wide">
|
<article class="balance-item admin-card--wide">
|
||||||
<header>
|
<header>
|
||||||
<strong>{props.copy.adminsTitle ?? ''}</strong>
|
<strong>{props.copy.membersTitle ?? props.copy.houseSectionMembers ?? ''}</strong>
|
||||||
<span>{String(props.adminSettings?.members.length ?? 0)}</span>
|
<span class="mini-chip mini-chip--muted">
|
||||||
|
{String(props.adminSettings?.members.length ?? 0)}{' '}
|
||||||
|
{props.copy.membersCount ?? ''}
|
||||||
|
</span>
|
||||||
</header>
|
</header>
|
||||||
<div class="ledger-list">
|
<div class="ledger-list">
|
||||||
<For each={props.adminSettings?.members ?? []}>
|
<For each={props.adminSettings?.members ?? []}>
|
||||||
@@ -1146,7 +1157,7 @@ export function HouseScreen(props: Props) {
|
|||||||
</div>
|
</div>
|
||||||
<Modal
|
<Modal
|
||||||
open={Boolean(props.editingMember)}
|
open={Boolean(props.editingMember)}
|
||||||
title={props.copy.adminsTitle ?? ''}
|
title={props.copy.membersTitle ?? props.copy.houseSectionMembers ?? ''}
|
||||||
description={props.copy.memberEditorBody ?? ''}
|
description={props.copy.memberEditorBody ?? ''}
|
||||||
closeLabel={props.copy.closeEditorAction ?? ''}
|
closeLabel={props.copy.closeEditorAction ?? ''}
|
||||||
onClose={props.onCloseMemberEditor}
|
onClose={props.onCloseMemberEditor}
|
||||||
|
|||||||
@@ -406,12 +406,10 @@ export function LedgerScreen(props: Props) {
|
|||||||
<Show when={props.readyIsAdmin}>
|
<Show when={props.readyIsAdmin}>
|
||||||
<p>{props.copy.paymentsAdminBody ?? ''}</p>
|
<p>{props.copy.paymentsAdminBody ?? ''}</p>
|
||||||
<div class="panel-toolbar">
|
<div class="panel-toolbar">
|
||||||
<IconButton
|
<Button variant="secondary" onClick={props.onOpenAddPayment}>
|
||||||
label={props.copy.paymentsAddAction ?? ''}
|
|
||||||
onClick={props.onOpenAddPayment}
|
|
||||||
>
|
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
</IconButton>
|
{props.copy.paymentsAddAction ?? ''}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
{props.paymentEntries.length === 0 ? (
|
{props.paymentEntries.length === 0 ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user