mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 22:54:02 +00:00
feat(miniapp): simplify current-cycle navigation and home summary
This commit is contained in:
@@ -14,7 +14,6 @@ import {
|
|||||||
addMiniAppUtilityBill,
|
addMiniAppUtilityBill,
|
||||||
addMiniAppPayment,
|
addMiniAppPayment,
|
||||||
approveMiniAppPendingMember,
|
approveMiniAppPendingMember,
|
||||||
closeMiniAppBillingCycle,
|
|
||||||
deleteMiniAppPayment,
|
deleteMiniAppPayment,
|
||||||
deleteMiniAppPurchase,
|
deleteMiniAppPurchase,
|
||||||
deleteMiniAppUtilityBill,
|
deleteMiniAppUtilityBill,
|
||||||
@@ -39,7 +38,16 @@ import {
|
|||||||
type MiniAppDashboard,
|
type MiniAppDashboard,
|
||||||
type MiniAppPendingMember
|
type MiniAppPendingMember
|
||||||
} from './miniapp-api'
|
} from './miniapp-api'
|
||||||
import { Button, Field, MiniChip, Modal } from './components/ui'
|
import {
|
||||||
|
Button,
|
||||||
|
Field,
|
||||||
|
HomeIcon,
|
||||||
|
HouseIcon,
|
||||||
|
MiniChip,
|
||||||
|
Modal,
|
||||||
|
ReceiptIcon,
|
||||||
|
WalletIcon
|
||||||
|
} from './components/ui'
|
||||||
import { NavigationTabs } from './components/layout/navigation-tabs'
|
import { NavigationTabs } from './components/layout/navigation-tabs'
|
||||||
import { TopBar } from './components/layout/top-bar'
|
import { TopBar } from './components/layout/top-bar'
|
||||||
import { BlockedState } from './components/session/blocked-state'
|
import { BlockedState } from './components/session/blocked-state'
|
||||||
@@ -96,7 +104,6 @@ type SessionState =
|
|||||||
}
|
}
|
||||||
|
|
||||||
type NavigationKey = 'home' | 'balances' | 'ledger' | 'house'
|
type NavigationKey = 'home' | 'balances' | 'ledger' | 'house'
|
||||||
type HouseSectionKey = 'billing' | 'utilities' | 'members' | 'topics'
|
|
||||||
|
|
||||||
type UtilityBillDraft = {
|
type UtilityBillDraft = {
|
||||||
billName: string
|
billName: string
|
||||||
@@ -124,7 +131,6 @@ type PaymentDraft = {
|
|||||||
|
|
||||||
type TestingRolePreview = 'admin' | 'resident'
|
type TestingRolePreview = 'admin' | 'resident'
|
||||||
|
|
||||||
const chartPalette = ['#f7b389', '#6fd3c0', '#f06a8d', '#94a8ff', '#f3d36f', '#7dc96d'] as const
|
|
||||||
const TESTING_ROLE_TAP_WINDOW_MS = 30 * 60 * 1000
|
const TESTING_ROLE_TAP_WINDOW_MS = 30 * 60 * 1000
|
||||||
|
|
||||||
const demoSession: Extract<SessionState, { status: 'ready' }> = {
|
const demoSession: Extract<SessionState, { status: 'ready' }> = {
|
||||||
@@ -180,30 +186,6 @@ 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}`
|
||||||
}
|
}
|
||||||
@@ -305,7 +287,6 @@ function App() {
|
|||||||
status: 'loading'
|
status: 'loading'
|
||||||
})
|
})
|
||||||
const [activeNav, setActiveNav] = createSignal<NavigationKey>('home')
|
const [activeNav, setActiveNav] = createSignal<NavigationKey>('home')
|
||||||
const [activeHouseSection, setActiveHouseSection] = createSignal<HouseSectionKey>('billing')
|
|
||||||
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)
|
||||||
@@ -335,7 +316,6 @@ function App() {
|
|||||||
const [savingBillingSettings, setSavingBillingSettings] = createSignal(false)
|
const [savingBillingSettings, setSavingBillingSettings] = createSignal(false)
|
||||||
const [savingCategorySlug, setSavingCategorySlug] = createSignal<string | null>(null)
|
const [savingCategorySlug, setSavingCategorySlug] = createSignal<string | null>(null)
|
||||||
const [openingCycle, setOpeningCycle] = createSignal(false)
|
const [openingCycle, setOpeningCycle] = createSignal(false)
|
||||||
const [closingCycle, setClosingCycle] = createSignal(false)
|
|
||||||
const [savingCycleRent, setSavingCycleRent] = createSignal(false)
|
const [savingCycleRent, setSavingCycleRent] = createSignal(false)
|
||||||
const [savingUtilityBill, setSavingUtilityBill] = createSignal(false)
|
const [savingUtilityBill, setSavingUtilityBill] = createSignal(false)
|
||||||
const [savingUtilityBillId, setSavingUtilityBillId] = createSignal<string | null>(null)
|
const [savingUtilityBillId, setSavingUtilityBillId] = createSignal<string | null>(null)
|
||||||
@@ -475,134 +455,6 @@ 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 {
|
||||||
@@ -1395,25 +1247,6 @@ function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleCloseCycle() {
|
|
||||||
const initData = webApp?.initData?.trim()
|
|
||||||
const currentReady = readySession()
|
|
||||||
if (!initData || currentReady?.mode !== 'live' || !currentReady.member.isAdmin) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setClosingCycle(true)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const state = await closeMiniAppBillingCycle(initData, cycleState()?.cycle?.period)
|
|
||||||
setCycleState(state)
|
|
||||||
setUtilityBillDrafts(cycleUtilityBillDrafts(state.utilityBills))
|
|
||||||
setCycleRentOpen(false)
|
|
||||||
} finally {
|
|
||||||
setClosingCycle(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSaveCycleRent() {
|
async function handleSaveCycleRent() {
|
||||||
const initData = webApp?.initData?.trim()
|
const initData = webApp?.initData?.trim()
|
||||||
const currentReady = readySession()
|
const currentReady = readySession()
|
||||||
@@ -1969,12 +1802,6 @@ function App() {
|
|||||||
copy={copy()}
|
copy={copy()}
|
||||||
dashboard={dashboard()}
|
dashboard={dashboard()}
|
||||||
currentMemberLine={currentMemberLine()}
|
currentMemberLine={currentMemberLine()}
|
||||||
utilityTotalMajor={utilityTotalMajor()}
|
|
||||||
purchaseTotalMajor={purchaseTotalMajor()}
|
|
||||||
memberBalanceVisuals={memberBalanceVisuals()}
|
|
||||||
purchaseChart={purchaseInvestmentChart()}
|
|
||||||
memberBaseDueMajor={memberBaseDueMajor}
|
|
||||||
memberRemainingClass={memberRemainingClass}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
case 'ledger':
|
case 'ledger':
|
||||||
@@ -2115,8 +1942,6 @@ function App() {
|
|||||||
adminSettings={adminSettings()}
|
adminSettings={adminSettings()}
|
||||||
cycleState={cycleState()}
|
cycleState={cycleState()}
|
||||||
pendingMembers={pendingMembers()}
|
pendingMembers={pendingMembers()}
|
||||||
activeHouseSection={activeHouseSection()}
|
|
||||||
onChangeHouseSection={setActiveHouseSection}
|
|
||||||
billingForm={billingForm()}
|
billingForm={billingForm()}
|
||||||
cycleForm={cycleForm()}
|
cycleForm={cycleForm()}
|
||||||
newCategoryName={newCategoryName()}
|
newCategoryName={newCategoryName()}
|
||||||
@@ -2134,7 +1959,6 @@ function App() {
|
|||||||
memberAbsencePolicyDrafts={memberAbsencePolicyDrafts()}
|
memberAbsencePolicyDrafts={memberAbsencePolicyDrafts()}
|
||||||
rentWeightDrafts={rentWeightDrafts()}
|
rentWeightDrafts={rentWeightDrafts()}
|
||||||
openingCycle={openingCycle()}
|
openingCycle={openingCycle()}
|
||||||
closingCycle={closingCycle()}
|
|
||||||
savingCycleRent={savingCycleRent()}
|
savingCycleRent={savingCycleRent()}
|
||||||
savingBillingSettings={savingBillingSettings()}
|
savingBillingSettings={savingBillingSettings()}
|
||||||
savingUtilityBill={savingUtilityBill()}
|
savingUtilityBill={savingUtilityBill()}
|
||||||
@@ -2156,7 +1980,6 @@ function App() {
|
|||||||
onCloseCycleModal={() => setCycleRentOpen(false)}
|
onCloseCycleModal={() => setCycleRentOpen(false)}
|
||||||
onSaveCycleRent={handleSaveCycleRent}
|
onSaveCycleRent={handleSaveCycleRent}
|
||||||
onOpenCycle={handleOpenCycle}
|
onOpenCycle={handleOpenCycle}
|
||||||
onCloseCycle={handleCloseCycle}
|
|
||||||
onCycleRentAmountChange={(value) =>
|
onCycleRentAmountChange={(value) =>
|
||||||
setCycleForm((current) => ({
|
setCycleForm((current) => ({
|
||||||
...current,
|
...current,
|
||||||
@@ -2524,20 +2347,21 @@ function App() {
|
|||||||
</Show>
|
</Show>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="content-stack">{panel()}</section>
|
||||||
|
<div class="app-bottom-nav">
|
||||||
<NavigationTabs
|
<NavigationTabs
|
||||||
items={
|
items={
|
||||||
[
|
[
|
||||||
{ key: 'home', label: copy().home },
|
{ key: 'home', label: copy().home, icon: <HomeIcon /> },
|
||||||
{ key: 'balances', label: copy().balances },
|
{ key: 'balances', label: copy().balances, icon: <WalletIcon /> },
|
||||||
{ key: 'ledger', label: copy().ledger },
|
{ key: 'ledger', label: copy().ledger, icon: <ReceiptIcon /> },
|
||||||
{ key: 'house', label: copy().house }
|
{ key: 'house', label: copy().house, icon: <HouseIcon /> }
|
||||||
] as const
|
] as const
|
||||||
}
|
}
|
||||||
active={activeNav()}
|
active={activeNav()}
|
||||||
onChange={setActiveNav}
|
onChange={setActiveNav}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<section class="content-stack">{panel()}</section>
|
|
||||||
<Modal
|
<Modal
|
||||||
open={testingSurfaceOpen()}
|
open={testingSurfaceOpen()}
|
||||||
title={copy().testingSurfaceTitle ?? ''}
|
title={copy().testingSurfaceTitle ?? ''}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { JSX } from 'solid-js'
|
|||||||
type TabItem<T extends string> = {
|
type TabItem<T extends string> = {
|
||||||
key: T
|
key: T
|
||||||
label: string
|
label: string
|
||||||
|
icon?: JSX.Element
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props<T extends string> = {
|
type Props<T extends string> = {
|
||||||
@@ -20,7 +21,8 @@ export function NavigationTabs<T extends string>(props: Props<T>): JSX.Element {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => props.onChange(item.key)}
|
onClick={() => props.onChange(item.key)}
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.icon}
|
||||||
|
<span>{item.label}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -66,6 +66,56 @@ export function GlobeIcon(props: IconProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function HomeIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg {...iconProps(props)}>
|
||||||
|
<path d="M3 10.5 12 3l9 7.5" />
|
||||||
|
<path d="M5 9.5V21h14V9.5" />
|
||||||
|
<path d="M9 21v-6h6v6" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WalletIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg {...iconProps(props)}>
|
||||||
|
<path d="M4 7h14a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V7Z" />
|
||||||
|
<path d="M4 9V7a2 2 0 0 1 2-2h10" />
|
||||||
|
<path d="M15 13h5" />
|
||||||
|
<circle cx="15" cy="13" r=".5" fill="currentColor" stroke="none" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReceiptIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg {...iconProps(props)}>
|
||||||
|
<path d="M7 3h10v18l-2-1.5-2 1.5-2-1.5-2 1.5-2-1.5-2 1.5V3h1Z" />
|
||||||
|
<path d="M9 8h6" />
|
||||||
|
<path d="M9 12h6" />
|
||||||
|
<path d="M9 16h4" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HouseIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg {...iconProps(props)}>
|
||||||
|
<path d="M4 10.5 12 4l8 6.5" />
|
||||||
|
<path d="M6 9.5V20h12V9.5" />
|
||||||
|
<path d="M9 20v-5h6v5" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChevronDownIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg {...iconProps(props)}>
|
||||||
|
<path d="m6 9 6 6 6-6" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function XIcon(props: IconProps) {
|
export function XIcon(props: IconProps) {
|
||||||
return (
|
return (
|
||||||
<svg {...iconProps(props)}>
|
<svg {...iconProps(props)}>
|
||||||
|
|||||||
@@ -56,14 +56,22 @@ export const dictionary = {
|
|||||||
yourBalanceTitle: 'Your balance',
|
yourBalanceTitle: 'Your balance',
|
||||||
yourBalanceBody:
|
yourBalanceBody:
|
||||||
'See rent, pure utilities, purchase balance adjustment, and what is still left to pay.',
|
'See rent, pure utilities, purchase balance adjustment, and what is still left to pay.',
|
||||||
|
payNowTitle: 'Pay now',
|
||||||
|
payNowBody:
|
||||||
|
'Your current-cycle summary stays here so you can see the number that matters first.',
|
||||||
|
currentCycleLabel: 'Current cycle',
|
||||||
cycleBillLabel: 'Cycle bill',
|
cycleBillLabel: 'Cycle bill',
|
||||||
balanceAdjustmentLabel: 'Balance adjustment',
|
balanceAdjustmentLabel: 'Balance adjustment',
|
||||||
pureUtilitiesLabel: 'Pure utilities',
|
pureUtilitiesLabel: 'Pure utilities',
|
||||||
|
rentAdjustedTotalLabel: 'Rent after adjustment',
|
||||||
utilitiesAdjustedTotalLabel: 'Utilities after adjustment',
|
utilitiesAdjustedTotalLabel: 'Utilities after adjustment',
|
||||||
baseDue: 'Base due',
|
baseDue: 'Base due',
|
||||||
finalDue: 'Final due',
|
finalDue: 'Final due',
|
||||||
houseSnapshotTitle: 'House totals',
|
houseSnapshotTitle: 'House totals',
|
||||||
houseSnapshotBody: 'Whole-house totals for the current cycle.',
|
houseSnapshotBody: 'Whole-house totals for the current cycle.',
|
||||||
|
balanceScreenScopeTitle: 'Balance breakdown',
|
||||||
|
balanceScreenScopeBody:
|
||||||
|
'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.',
|
||||||
financeVisualsTitle: 'Visual balance split',
|
financeVisualsTitle: 'Visual balance split',
|
||||||
@@ -309,14 +317,22 @@ export const dictionary = {
|
|||||||
yourBalanceTitle: 'Твой баланс',
|
yourBalanceTitle: 'Твой баланс',
|
||||||
yourBalanceBody:
|
yourBalanceBody:
|
||||||
'Здесь отдельно видно аренду, чистую коммуналку, поправку по покупкам и то, что осталось оплатить.',
|
'Здесь отдельно видно аренду, чистую коммуналку, поправку по покупкам и то, что осталось оплатить.',
|
||||||
|
payNowTitle: 'К оплате сейчас',
|
||||||
|
payNowBody:
|
||||||
|
'Здесь остаётся только короткая сводка по текущему циклу, чтобы сразу видеть нужную сумму.',
|
||||||
|
currentCycleLabel: 'Текущий цикл',
|
||||||
cycleBillLabel: 'Счёт за цикл',
|
cycleBillLabel: 'Счёт за цикл',
|
||||||
balanceAdjustmentLabel: 'Поправка по балансу',
|
balanceAdjustmentLabel: 'Поправка по балансу',
|
||||||
pureUtilitiesLabel: 'Чистая коммуналка',
|
pureUtilitiesLabel: 'Чистая коммуналка',
|
||||||
|
rentAdjustedTotalLabel: 'Аренда после зачёта',
|
||||||
utilitiesAdjustedTotalLabel: 'Коммуналка после зачёта',
|
utilitiesAdjustedTotalLabel: 'Коммуналка после зачёта',
|
||||||
baseDue: 'База к оплате',
|
baseDue: 'База к оплате',
|
||||||
finalDue: 'Итог к оплате',
|
finalDue: 'Итог к оплате',
|
||||||
houseSnapshotTitle: 'Сводка по дому',
|
houseSnapshotTitle: 'Сводка по дому',
|
||||||
houseSnapshotBody: 'Общие суммы по дому за текущий цикл.',
|
houseSnapshotBody: 'Общие суммы по дому за текущий цикл.',
|
||||||
|
balanceScreenScopeTitle: 'Разбор баланса',
|
||||||
|
balanceScreenScopeBody:
|
||||||
|
'На этом экране только разбор твоего текущего баланса. Более старые записи остаются в леджере.',
|
||||||
householdBalancesTitle: 'Баланс household',
|
householdBalancesTitle: 'Баланс household',
|
||||||
householdBalancesBody: 'Текущий расклад по всем участникам за этот цикл.',
|
householdBalancesBody: 'Текущий расклад по всем участникам за этот цикл.',
|
||||||
financeVisualsTitle: 'Визуальный разбор баланса',
|
financeVisualsTitle: 'Визуальный разбор баланса',
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ button {
|
|||||||
position: relative;
|
position: relative;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 24px 18px 32px;
|
padding: 24px 18px 108px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shell__backdrop {
|
.shell__backdrop {
|
||||||
@@ -315,18 +315,37 @@ button {
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
margin-top: 12px;
|
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
border: 1px solid rgb(255 255 255 / 0.08);
|
border: 1px solid rgb(255 255 255 / 0.08);
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
background: rgb(255 255 255 / 0.03);
|
background: rgb(18 26 36 / 0.9);
|
||||||
|
box-shadow: 0 18px 36px rgb(0 0 0 / 0.26);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-grid button {
|
.nav-grid button {
|
||||||
|
display: grid;
|
||||||
|
justify-items: center;
|
||||||
|
gap: 4px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
min-height: 38px;
|
min-height: 52px;
|
||||||
padding: 9px 10px;
|
padding: 8px 6px;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-grid button span {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-bottom-nav {
|
||||||
|
position: fixed;
|
||||||
|
right: 18px;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 18px;
|
||||||
|
z-index: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-grid {
|
.content-grid {
|
||||||
@@ -432,6 +451,14 @@ button {
|
|||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.home-pay-card,
|
||||||
|
.home-pay-card__header,
|
||||||
|
.home-pay-card__copy,
|
||||||
|
.home-pay-card__chips {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.stat-card {
|
.stat-card {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -750,6 +777,52 @@ button {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-disclosure {
|
||||||
|
border: 1px solid rgb(255 255 255 / 0.08);
|
||||||
|
border-radius: 20px;
|
||||||
|
background: rgb(255 255 255 / 0.03);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-disclosure[open] {
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgb(255 255 255 / 0.05), rgb(255 255 255 / 0.02)),
|
||||||
|
rgb(255 255 255 / 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-disclosure__summary {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-disclosure__summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-disclosure__copy {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-disclosure__icon {
|
||||||
|
transition: transform 140ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-disclosure[open] .admin-disclosure__icon {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-disclosure__content {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-sublist {
|
.admin-sublist {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
@@ -1191,7 +1264,7 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-grid {
|
.nav-grid {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.balance-breakdown {
|
.balance-breakdown {
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
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 { MemberBalanceCard } from '../components/finance/member-balance-card'
|
||||||
import type { MiniAppDashboard } from '../miniapp-api'
|
import type { MiniAppDashboard } from '../miniapp-api'
|
||||||
|
|
||||||
@@ -9,34 +7,6 @@ type Props = {
|
|||||||
copy: Record<string, string | undefined>
|
copy: Record<string, string | undefined>
|
||||||
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) {
|
||||||
@@ -61,82 +31,13 @@ export function BalancesScreen(props: Props) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
<article class="balance-item balance-item--wide balance-item--muted">
|
<article class="balance-item balance-item--muted">
|
||||||
<header>
|
<header>
|
||||||
<strong>{props.copy.houseSnapshotTitle ?? ''}</strong>
|
<strong>{props.copy.balanceScreenScopeTitle ?? ''}</strong>
|
||||||
<span>{dashboard().period}</span>
|
<span>{dashboard().period}</span>
|
||||||
</header>
|
</header>
|
||||||
<p>{props.copy.houseSnapshotBody ?? ''}</p>
|
<p>{props.copy.balanceScreenScopeBody ?? ''}</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>
|
</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,7 +1,7 @@
|
|||||||
import { 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 { MemberBalanceCard } from '../components/finance/member-balance-card'
|
import { sumMajorStrings } from '../lib/money'
|
||||||
import type { MiniAppDashboard } from '../miniapp-api'
|
import type { MiniAppDashboard } from '../miniapp-api'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -13,6 +13,28 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function HomeScreen(props: Props) {
|
export function HomeScreen(props: Props) {
|
||||||
|
const adjustedRentMajor = () => {
|
||||||
|
if (!props.currentMemberLine) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return sumMajorStrings(
|
||||||
|
props.currentMemberLine.rentShareMajor,
|
||||||
|
props.currentMemberLine.purchaseOffsetMajor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const adjustedUtilitiesMajor = () => {
|
||||||
|
if (!props.currentMemberLine) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return sumMajorStrings(
|
||||||
|
props.currentMemberLine.utilityShareMajor,
|
||||||
|
props.currentMemberLine.purchaseOffsetMajor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show
|
<Show
|
||||||
when={props.dashboard}
|
when={props.dashboard}
|
||||||
@@ -37,7 +59,63 @@ export function HomeScreen(props: Props) {
|
|||||||
<div class="home-grid">
|
<div class="home-grid">
|
||||||
<Show when={props.currentMemberLine}>
|
<Show when={props.currentMemberLine}>
|
||||||
{(member) => (
|
{(member) => (
|
||||||
<MemberBalanceCard copy={props.copy} dashboard={dashboard()} member={member()} />
|
<article class="balance-item balance-item--accent home-pay-card">
|
||||||
|
<header class="home-pay-card__header">
|
||||||
|
<div class="home-pay-card__copy">
|
||||||
|
<strong>{props.copy.payNowTitle ?? props.copy.yourBalanceTitle ?? ''}</strong>
|
||||||
|
<p>{props.copy.payNowBody ?? ''}</p>
|
||||||
|
</div>
|
||||||
|
<div class="balance-spotlight__hero">
|
||||||
|
<span>{props.copy.remainingLabel ?? ''}</span>
|
||||||
|
<strong>
|
||||||
|
{member().remainingMajor} {dashboard().currency}
|
||||||
|
</strong>
|
||||||
|
<small>
|
||||||
|
{props.copy.totalDue ?? ''}: {member().netDueMajor} {dashboard().currency}
|
||||||
|
</small>
|
||||||
|
</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.currentCycleLabel ?? ''}</span>
|
||||||
|
<strong>{dashboard().period}</strong>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="home-pay-card__chips">
|
||||||
|
<span class="mini-chip">
|
||||||
|
{dashboard().paymentBalanceAdjustmentPolicy === 'rent'
|
||||||
|
? props.copy.rentAdjustedTotalLabel
|
||||||
|
: props.copy.shareRent}
|
||||||
|
:{' '}
|
||||||
|
{dashboard().paymentBalanceAdjustmentPolicy === 'rent'
|
||||||
|
? adjustedRentMajor()
|
||||||
|
: member().rentShareMajor}{' '}
|
||||||
|
{dashboard().currency}
|
||||||
|
</span>
|
||||||
|
<span class="mini-chip mini-chip--muted">
|
||||||
|
{dashboard().paymentBalanceAdjustmentPolicy === 'utilities'
|
||||||
|
? props.copy.utilitiesAdjustedTotalLabel
|
||||||
|
: props.copy.shareUtilities}
|
||||||
|
:{' '}
|
||||||
|
{dashboard().paymentBalanceAdjustmentPolicy === 'utilities'
|
||||||
|
? adjustedUtilitiesMajor()
|
||||||
|
: member().utilityShareMajor}{' '}
|
||||||
|
{dashboard().currency}
|
||||||
|
</span>
|
||||||
|
<span class="mini-chip mini-chip--muted">
|
||||||
|
{props.copy.balanceAdjustmentLabel ?? props.copy.shareOffset}:{' '}
|
||||||
|
{member().purchaseOffsetMajor} {dashboard().currency}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { For, Show } from 'solid-js'
|
import { For, Show, type JSX } from 'solid-js'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
CalendarIcon,
|
CalendarIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
Field,
|
Field,
|
||||||
IconButton,
|
IconButton,
|
||||||
Modal,
|
Modal,
|
||||||
@@ -10,7 +11,6 @@ import {
|
|||||||
PlusIcon,
|
PlusIcon,
|
||||||
SettingsIcon
|
SettingsIcon
|
||||||
} from '../components/ui'
|
} from '../components/ui'
|
||||||
import { NavigationTabs } from '../components/layout/navigation-tabs'
|
|
||||||
import type {
|
import type {
|
||||||
MiniAppAdminCycleState,
|
MiniAppAdminCycleState,
|
||||||
MiniAppAdminSettingsPayload,
|
MiniAppAdminSettingsPayload,
|
||||||
@@ -19,8 +19,6 @@ import type {
|
|||||||
MiniAppPendingMember
|
MiniAppPendingMember
|
||||||
} from '../miniapp-api'
|
} from '../miniapp-api'
|
||||||
|
|
||||||
type HouseSectionKey = 'billing' | 'utilities' | 'members' | 'topics'
|
|
||||||
|
|
||||||
type UtilityBillDraft = {
|
type UtilityBillDraft = {
|
||||||
billName: string
|
billName: string
|
||||||
amountMajor: string
|
amountMajor: string
|
||||||
@@ -58,8 +56,6 @@ type Props = {
|
|||||||
adminSettings: MiniAppAdminSettingsPayload | null
|
adminSettings: MiniAppAdminSettingsPayload | null
|
||||||
cycleState: MiniAppAdminCycleState | null
|
cycleState: MiniAppAdminCycleState | null
|
||||||
pendingMembers: readonly MiniAppPendingMember[]
|
pendingMembers: readonly MiniAppPendingMember[]
|
||||||
activeHouseSection: HouseSectionKey
|
|
||||||
onChangeHouseSection: (section: HouseSectionKey) => void
|
|
||||||
billingForm: BillingForm
|
billingForm: BillingForm
|
||||||
cycleForm: CycleForm
|
cycleForm: CycleForm
|
||||||
newCategoryName: string
|
newCategoryName: string
|
||||||
@@ -81,7 +77,6 @@ type Props = {
|
|||||||
memberAbsencePolicyDrafts: Record<string, MiniAppMemberAbsencePolicy>
|
memberAbsencePolicyDrafts: Record<string, MiniAppMemberAbsencePolicy>
|
||||||
rentWeightDrafts: Record<string, string>
|
rentWeightDrafts: Record<string, string>
|
||||||
openingCycle: boolean
|
openingCycle: boolean
|
||||||
closingCycle: boolean
|
|
||||||
savingCycleRent: boolean
|
savingCycleRent: boolean
|
||||||
savingBillingSettings: boolean
|
savingBillingSettings: boolean
|
||||||
savingUtilityBill: boolean
|
savingUtilityBill: boolean
|
||||||
@@ -107,7 +102,6 @@ type Props = {
|
|||||||
onCloseCycleModal: () => void
|
onCloseCycleModal: () => void
|
||||||
onSaveCycleRent: () => Promise<void>
|
onSaveCycleRent: () => Promise<void>
|
||||||
onOpenCycle: () => Promise<void>
|
onOpenCycle: () => Promise<void>
|
||||||
onCloseCycle: () => Promise<void>
|
|
||||||
onCycleRentAmountChange: (value: string) => void
|
onCycleRentAmountChange: (value: string) => void
|
||||||
onCycleRentCurrencyChange: (value: 'USD' | 'GEL') => void
|
onCycleRentCurrencyChange: (value: 'USD' | 'GEL') => void
|
||||||
onCyclePeriodChange: (value: string) => void
|
onCyclePeriodChange: (value: string) => void
|
||||||
@@ -168,6 +162,26 @@ type Props = {
|
|||||||
onPromoteMember: (memberId: string) => Promise<void>
|
onPromoteMember: (memberId: string) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function HouseSection(props: {
|
||||||
|
title: string
|
||||||
|
body?: string | undefined
|
||||||
|
defaultOpen?: boolean | undefined
|
||||||
|
children: JSX.Element
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<details class="admin-disclosure" open={props.defaultOpen}>
|
||||||
|
<summary class="admin-disclosure__summary">
|
||||||
|
<div class="admin-disclosure__copy">
|
||||||
|
<strong>{props.title}</strong>
|
||||||
|
<Show when={props.body}>{(body) => <p>{body()}</p>}</Show>
|
||||||
|
</div>
|
||||||
|
<ChevronDownIcon class="admin-disclosure__icon" />
|
||||||
|
</summary>
|
||||||
|
<div class="admin-disclosure__content">{props.children}</div>
|
||||||
|
</details>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function HouseScreen(props: Props) {
|
export function HouseScreen(props: Props) {
|
||||||
function parseBillingDayInput(value: string): number | null {
|
function parseBillingDayInput(value: string): number | null {
|
||||||
const trimmed = value.trim()
|
const trimmed = value.trim()
|
||||||
@@ -201,20 +215,11 @@ export function HouseScreen(props: Props) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div class="admin-layout">
|
<div class="admin-layout">
|
||||||
<NavigationTabs
|
<HouseSection
|
||||||
items={
|
title={props.copy.houseSectionBilling ?? ''}
|
||||||
[
|
body={props.copy.billingSettingsEditorBody}
|
||||||
{ key: 'billing', label: props.copy.houseSectionBilling ?? '' },
|
defaultOpen
|
||||||
{ key: 'utilities', label: props.copy.houseSectionUtilities ?? '' },
|
>
|
||||||
{ key: 'members', label: props.copy.houseSectionMembers ?? '' },
|
|
||||||
{ key: 'topics', label: props.copy.houseSectionTopics ?? '' }
|
|
||||||
] as const
|
|
||||||
}
|
|
||||||
active={props.activeHouseSection}
|
|
||||||
onChange={props.onChangeHouseSection}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Show when={props.activeHouseSection === 'billing'}>
|
|
||||||
<section class="admin-section">
|
<section class="admin-section">
|
||||||
<div class="admin-grid">
|
<div class="admin-grid">
|
||||||
<article class="balance-item">
|
<article class="balance-item">
|
||||||
@@ -250,15 +255,6 @@ export function HouseScreen(props: Props) {
|
|||||||
? (props.copy.manageCycleAction ?? '')
|
? (props.copy.manageCycleAction ?? '')
|
||||||
: (props.copy.openCycleAction ?? '')}
|
: (props.copy.openCycleAction ?? '')}
|
||||||
</Button>
|
</Button>
|
||||||
<Show when={props.cycleState?.cycle}>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
disabled={props.closingCycle}
|
|
||||||
onClick={() => void props.onCloseCycle()}
|
|
||||||
>
|
|
||||||
{props.closingCycle ? props.copy.closingCycle : props.copy.closeCycleAction}
|
|
||||||
</Button>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
@@ -541,9 +537,12 @@ export function HouseScreen(props: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
</section>
|
</section>
|
||||||
</Show>
|
</HouseSection>
|
||||||
|
|
||||||
<Show when={props.activeHouseSection === 'utilities'}>
|
<HouseSection
|
||||||
|
title={props.copy.houseSectionUtilities ?? ''}
|
||||||
|
body={props.copy.utilityBillsEditorBody}
|
||||||
|
>
|
||||||
<section class="admin-section">
|
<section class="admin-section">
|
||||||
<div class="admin-grid">
|
<div class="admin-grid">
|
||||||
<article class="balance-item">
|
<article class="balance-item">
|
||||||
@@ -874,9 +873,9 @@ export function HouseScreen(props: Props) {
|
|||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
</section>
|
</section>
|
||||||
</Show>
|
</HouseSection>
|
||||||
|
|
||||||
<Show when={props.activeHouseSection === 'members'}>
|
<HouseSection title={props.copy.houseSectionMembers ?? ''} body={props.copy.adminsBody}>
|
||||||
<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">
|
||||||
@@ -1126,9 +1125,12 @@ export function HouseScreen(props: Props) {
|
|||||||
})()}
|
})()}
|
||||||
</Modal>
|
</Modal>
|
||||||
</section>
|
</section>
|
||||||
</Show>
|
</HouseSection>
|
||||||
|
|
||||||
<Show when={props.activeHouseSection === 'topics'}>
|
<HouseSection
|
||||||
|
title={props.copy.houseSectionTopics ?? ''}
|
||||||
|
body={props.copy.topicBindingsBody}
|
||||||
|
>
|
||||||
<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">
|
||||||
@@ -1162,7 +1164,7 @@ export function HouseScreen(props: Props) {
|
|||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</Show>
|
</HouseSection>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user