diff --git a/apps/miniapp/src/App.tsx b/apps/miniapp/src/App.tsx index 7398343..495b8a6 100644 --- a/apps/miniapp/src/App.tsx +++ b/apps/miniapp/src/App.tsx @@ -1,4 +1,4 @@ -import { Match, Show, Switch, createMemo, createSignal, onMount } from 'solid-js' +import { Match, Switch, createMemo, createSignal, onMount } from 'solid-js' import { dictionary, type Locale } from './i18n' import { @@ -35,7 +35,7 @@ import { type MiniAppDashboard, type MiniAppPendingMember } from './miniapp-api' -import { Button, Field, IconButton, Modal } from './components/ui' +import { Button, Field, Modal } from './components/ui' import { HeroBanner } from './components/layout/hero-banner' import { NavigationTabs } from './components/layout/navigation-tabs' import { ProfileCard } from './components/layout/profile-card' @@ -45,6 +45,7 @@ import { LoadingState } from './components/session/loading-state' import { OnboardingState } from './components/session/onboarding-state' import { BalancesScreen } from './screens/balances-screen' import { HomeScreen } from './screens/home-screen' +import { HouseScreen } from './screens/house-screen' import { LedgerScreen } from './screens/ledger-screen' import { demoAdminSettings, @@ -1996,1094 +1997,268 @@ function App() { /> ) case 'house': - return readySession()?.member.isAdmin ? ( -
-
-
- {copy().householdSettingsTitle} - {adminSettings()?.settings.settlementCurrency ?? '—'} -
-

{copy().householdSettingsBody}

-
-
- {copy().billingCycleTitle} - {cycleState()?.cycle?.period ?? copy().billingCycleEmpty} -
-
- {copy().settlementCurrency} - {adminSettings()?.settings.settlementCurrency ?? '—'} -
-
- {copy().membersCount} - {String(adminSettings()?.members.length ?? 0)} -
-
- {copy().pendingRequests} - {String(pendingMembers().length)} -
-
-
+ return ( + + resolvedMemberAbsencePolicy(memberId, status) + } + onChangeHouseholdLocale={handleHouseholdLocaleChange} + onOpenCycleModal={() => setCycleRentOpen(true)} + onCloseCycleModal={() => setCycleRentOpen(false)} + onSaveCycleRent={handleSaveCycleRent} + onOpenCycle={handleOpenCycle} + onCloseCycle={handleCloseCycle} + onCycleRentAmountChange={(value) => + setCycleForm((current) => ({ + ...current, + rentAmountMajor: value + })) + } + onCycleRentCurrencyChange={(value) => + setCycleForm((current) => ({ + ...current, + rentCurrency: value + })) + } + onCyclePeriodChange={(value) => + setCycleForm((current) => ({ + ...current, + period: value + })) + } + onOpenBillingSettingsModal={() => setBillingSettingsOpen(true)} + onCloseBillingSettingsModal={() => setBillingSettingsOpen(false)} + onSaveBillingSettings={handleSaveBillingSettings} + onBillingSettlementCurrencyChange={(value) => + setBillingForm((current) => ({ + ...current, + settlementCurrency: value + })) + } + onBillingAdjustmentPolicyChange={(value) => + setBillingForm((current) => ({ + ...current, + paymentBalanceAdjustmentPolicy: value + })) + } + onBillingRentAmountChange={(value) => + setBillingForm((current) => ({ + ...current, + rentAmountMajor: value + })) + } + onBillingRentCurrencyChange={(value) => + setBillingForm((current) => ({ + ...current, + rentCurrency: value + })) + } + onBillingRentDueDayChange={(value) => + setBillingForm((current) => ({ + ...current, + rentDueDay: value + })) + } + onBillingRentWarningDayChange={(value) => + setBillingForm((current) => ({ + ...current, + rentWarningDay: value + })) + } + onBillingUtilitiesDueDayChange={(value) => + setBillingForm((current) => ({ + ...current, + utilitiesDueDay: value + })) + } + onBillingUtilitiesReminderDayChange={(value) => + setBillingForm((current) => ({ + ...current, + utilitiesReminderDay: value + })) + } + onBillingTimezoneChange={(value) => + setBillingForm((current) => ({ + ...current, + timezone: value + })) + } + onOpenAddUtilityBill={() => setAddingUtilityBillOpen(true)} + onCloseAddUtilityBill={() => setAddingUtilityBillOpen(false)} + onAddUtilityBill={handleAddUtilityBill} + onCycleUtilityCategoryChange={(value) => + setCycleForm((current) => ({ + ...current, + utilityCategorySlug: value + })) + } + onCycleUtilityAmountChange={(value) => + setCycleForm((current) => ({ + ...current, + utilityAmountMajor: value + })) + } + onCycleUtilityCurrencyChange={(value) => + setCycleForm((current) => ({ + ...current, + utilityCurrency: value + })) + } + onOpenUtilityBillEditor={setEditingUtilityBillId} + onCloseUtilityBillEditor={() => setEditingUtilityBillId(null)} + onDeleteUtilityBill={handleDeleteUtilityBill} + onSaveUtilityBill={handleUpdateUtilityBill} + onUtilityBillNameChange={(billId, bill, value) => + updateUtilityBillDraft(billId, bill, (current) => ({ + ...current, + billName: value + })) + } + onUtilityBillAmountChange={(billId, bill, value) => + updateUtilityBillDraft(billId, bill, (current) => ({ + ...current, + amountMajor: value + })) + } + onUtilityBillCurrencyChange={(billId, bill, value) => + updateUtilityBillDraft(billId, bill, (current) => ({ + ...current, + currency: value + })) + } + onOpenCategoryEditor={setEditingCategorySlug} + onCloseCategoryEditor={() => setEditingCategorySlug(null)} + onNewCategoryNameChange={setNewCategoryName} + onSaveNewCategory={() => + handleSaveUtilityCategory({ + name: newCategoryName(), + sortOrder: adminSettings()?.categories.length ?? 0, + isActive: true + }) + } + onSaveExistingCategory={() => { + const category = editingCategory() + if (!category) { + return Promise.resolve() + } -
- {( - [ - ['billing', copy().houseSectionBilling], - ['utilities', copy().houseSectionUtilities], - ['members', copy().houseSectionMembers], - ['topics', copy().houseSectionTopics] - ] as const - ).map(([key, label]) => ( - - ))} -
- - -
-
-
-

{copy().billingCycleTitle}

-

{copy().billingSettingsTitle}

-
-
-
-
-
- {copy().billingCycleTitle} - {cycleState()?.cycle?.period ?? copy().billingCycleEmpty} -
-

- {cycleState()?.cycle - ? copy().billingCycleStatus.replace( - '{currency}', - cycleState()?.cycle?.currency ?? billingForm().settlementCurrency - ) - : copy().billingCycleOpenHint} -

- - {(data) => ( -

- {copy().shareRent}: {data().rentSourceAmountMajor}{' '} - {data().rentSourceCurrency} - {data().rentSourceCurrency !== data().currency - ? ` -> ${data().rentDisplayAmountMajor} ${data().currency}` - : ''} -

- )} -
-
- - - - -
-
- -
-
- {copy().billingSettingsTitle} - {billingForm().settlementCurrency} -
-

- {billingForm().paymentBalanceAdjustmentPolicy === 'utilities' - ? copy().paymentBalanceAdjustmentUtilities - : billingForm().paymentBalanceAdjustmentPolicy === 'rent' - ? copy().paymentBalanceAdjustmentRent - : copy().paymentBalanceAdjustmentSeparate} -

-
- - {copy().rentAmount}: {billingForm().rentAmountMajor || '—'}{' '} - {billingForm().rentCurrency} - - - {copy().timezone}: {billingForm().timezone} - -
-
- -
-
- -
-
- {copy().householdLanguage} - {readySession()?.member.householdDefaultLocale.toUpperCase()} -
-

{copy().householdSettingsBody}

-
- - -
-
-
- setCycleRentOpen(false)} - footer={ - cycleState()?.cycle ? ( - - ) : ( - - ) - } - > - {cycleState()?.cycle ? ( -
- - - setCycleForm((current) => ({ - ...current, - rentAmountMajor: event.currentTarget.value - })) - } - /> - - - - -
- ) : ( -
- - - setCycleForm((current) => ({ - ...current, - period: event.currentTarget.value - })) - } - /> - - -
{billingForm().settlementCurrency}
-
-
- )} -
- setBillingSettingsOpen(false)} - footer={ - - } - > -
- - - - - - - - - setBillingForm((current) => ({ - ...current, - rentAmountMajor: event.currentTarget.value - })) - } - /> - - - - - - - setBillingForm((current) => ({ - ...current, - rentDueDay: Number(event.currentTarget.value) - })) - } - /> - - - - setBillingForm((current) => ({ - ...current, - rentWarningDay: Number(event.currentTarget.value) - })) - } - /> - - - - setBillingForm((current) => ({ - ...current, - utilitiesDueDay: Number(event.currentTarget.value) - })) - } - /> - - - - setBillingForm((current) => ({ - ...current, - utilitiesReminderDay: Number(event.currentTarget.value) - })) - } - /> - - - - setBillingForm((current) => ({ - ...current, - timezone: event.currentTarget.value - })) - } - /> - -
-
-
-
- - -
-
-
-

{copy().utilityCategoriesTitle}

-

{copy().utilityCategoriesBody}

-
-
-
-
-
- {copy().utilityLedgerTitle} - {cycleForm().utilityCurrency} -
-

{copy().utilityBillsEditorBody}

-
- -
-
- {cycleState()?.utilityBills.length ? ( - cycleState()?.utilityBills.map((bill) => ( -
-
-
- {bill.billName} - {bill.createdAt.slice(0, 10)} -
-

{copy().utilityCategoryName}

-
- - {minorToMajorString(BigInt(bill.amountMinor))} {bill.currency} - -
-
-
- setEditingUtilityBillId(bill.id)} - > - ... - -
-
- )) - ) : ( -

{copy().utilityBillsEmpty}

- )} -
-
- -
-
- {copy().utilityCategoriesTitle} - {String(adminSettings()?.categories.length ?? 0)} -
-

{copy().utilityCategoriesBody}

-
- -
-
- {adminSettings()?.categories.map((category) => ( -
-
-
- {category.name} - {category.isActive ? 'ON' : 'OFF'} -
-

{copy().utilityCategoryName}

-
- - {category.isActive ? 'ON' : 'OFF'} - -
-
-
- setEditingCategorySlug(category.slug)} - > - ... - -
-
- ))} -
-
-
- setAddingUtilityBillOpen(false)} - footer={ - - } - > -
- - - - - - setCycleForm((current) => ({ - ...current, - utilityAmountMajor: event.currentTarget.value - })) - } - /> - - - - -
-
- setEditingUtilityBillId(null)} - footer={(() => { - const bill = editingUtilityBill() - if (!bill) { - return null - } - return ( - - ) - })()} - > - {(() => { - const bill = editingUtilityBill() - if (!bill) { - return null - } - const draft = utilityBillDrafts()[bill.id] ?? { - billName: bill.billName, - amountMajor: minorToMajorString(BigInt(bill.amountMinor)), - currency: bill.currency - } - return ( -
- - - updateUtilityBillDraft(bill.id, bill, (current) => ({ - ...current, - billName: event.currentTarget.value - })) + return handleSaveUtilityCategory({ + slug: category.slug, + name: category.name, + sortOrder: category.sortOrder, + isActive: category.isActive + }) + }} + onEditingCategoryNameChange={(value) => + setAdminSettings((current) => + current && editingCategory() + ? { + ...current, + categories: current.categories.map((item) => + item.slug === editingCategory()!.slug + ? { + ...item, + name: value } - /> - - - - updateUtilityBillDraft(bill.id, bill, (current) => ({ - ...current, - amountMajor: event.currentTarget.value - })) - } - /> - - - - -
- ) - })()} -
- setEditingCategorySlug(null)} - footer={(() => { - const category = editingCategory() - const isNew = editingCategorySlug() === '__new__' - return ( - - ) - })()} - > - {editingCategorySlug() === '__new__' ? ( -
- - setNewCategoryName(event.currentTarget.value)} - /> - -
- ) : ( - (() => { - const category = editingCategory() - if (!category) { - return null - } - return ( -
- - - setAdminSettings((current) => - current - ? { - ...current, - categories: current.categories.map((item) => - item.slug === category.slug - ? { - ...item, - name: event.currentTarget.value - } - : item - ) - } - : current - ) - } - /> - - - - -
+ : item ) - })() - )} -
-
-
- - -
-
-
-

{copy().adminsTitle}

-

{copy().adminsBody}

-
-
-
-
-
- {copy().adminsTitle} - {String(adminSettings()?.members.length ?? 0)} -
-
- {adminSettings()?.members.map((member) => ( -
-
-
- {member.displayName} - {member.isAdmin ? copy().adminTag : copy().residentTag} -
-

{memberStatusLabel(member.status)}

-
- - {copy().rentWeightLabel}: {member.rentShareWeight} - - - {resolvedMemberAbsencePolicy(member.id, member.status).policy === - 'away_rent_only' - ? copy().absencePolicyAwayRentOnly - : resolvedMemberAbsencePolicy(member.id, member.status).policy === - 'away_rent_and_utilities' - ? copy().absencePolicyAwayRentAndUtilities - : resolvedMemberAbsencePolicy(member.id, member.status) - .policy === 'inactive' - ? copy().absencePolicyInactive - : copy().absencePolicyResident} - -
-
-
- setEditingMemberId(member.id)} - > - ... - -
-
- ))} -
-
- -
-
- {copy().pendingMembersTitle} - {String(pendingMembers().length)} -
-

{copy().pendingMembersBody}

- {pendingMembers().length === 0 ? ( -

{copy().pendingMembersEmpty}

- ) : ( -
- {pendingMembers().map((member) => ( -
-
- {member.displayName} - {member.telegramUserId} -
-

- {member.username - ? copy().pendingMemberHandle.replace('{username}', member.username) - : (member.languageCode ?? 'Telegram')} -

- -
- ))} -
- )} -
-
- setEditingMemberId(null)} - footer={(() => { - const member = editingMember() - if (!member) { - return null } - - return ( - - ) - })()} - > - {(() => { - const member = editingMember() - if (!member) { - return null + : item + ) } - - return ( -
- - - setMemberDisplayNameDrafts((current) => ({ - ...current, - [member.id]: event.currentTarget.value - })) - } - /> - - - - - - - - - - setRentWeightDrafts((current) => ({ - ...current, - [member.id]: event.currentTarget.value - })) - } - /> - -
- ) - })()} -
-
-
- - -
-
-
-

{copy().topicBindingsTitle}

-

{copy().topicBindingsBody}

-
-
-
-
-
- {copy().topicBindingsTitle} - {String(adminSettings()?.topics.length ?? 0)}/4 -
-
- {(['purchase', 'feedback', 'reminders', 'payments'] as const).map((role) => { - const binding = adminSettings()?.topics.find((topic) => topic.role === role) - - return ( -
-
- {topicRoleLabel(role)} - {binding ? copy().topicBound : copy().topicUnbound} -
-

- {binding - ? `${binding.topicName ?? `Topic #${binding.telegramThreadId}`} · #${binding.telegramThreadId}` - : copy().topicUnbound} -

-
- ) - })} -
-
-
-
-
-
- ) : ( -
-
-
- {copy().residentHouseTitle} -
-

{copy().residentHouseBody}

-
-
+ : current + ) + } + onOpenMemberEditor={setEditingMemberId} + onCloseMemberEditor={() => setEditingMemberId(null)} + onApprovePendingMember={handleApprovePendingMember} + onMemberDisplayNameDraftChange={(memberId, value) => + setMemberDisplayNameDrafts((current) => ({ + ...current, + [memberId]: value + })) + } + onMemberStatusDraftChange={(memberId, value) => + setMemberStatusDrafts((current) => ({ + ...current, + [memberId]: value + })) + } + onMemberAbsencePolicyDraftChange={(memberId, value) => + setMemberAbsencePolicyDrafts((current) => ({ + ...current, + [memberId]: value + })) + } + onRentWeightDraftChange={(memberId, value) => + setRentWeightDrafts((current) => ({ + ...current, + [memberId]: value + })) + } + onSaveMemberDisplayName={handleSaveMemberDisplayName} + onSaveMemberStatus={handleSaveMemberStatus} + onSaveMemberAbsencePolicy={handleSaveMemberAbsencePolicy} + onSaveRentWeight={handleSaveRentWeight} + onPromoteMember={handlePromoteMember} + /> ) default: return ( diff --git a/apps/miniapp/src/screens/house-screen.tsx b/apps/miniapp/src/screens/house-screen.tsx new file mode 100644 index 0000000..415679a --- /dev/null +++ b/apps/miniapp/src/screens/house-screen.tsx @@ -0,0 +1,1166 @@ +import { For, Show } from 'solid-js' + +import { Button, Field, IconButton, Modal } from '../components/ui' +import type { + MiniAppAdminCycleState, + MiniAppAdminSettingsPayload, + MiniAppDashboard, + MiniAppMemberAbsencePolicy, + MiniAppPendingMember +} from '../miniapp-api' + +type HouseSectionKey = 'billing' | 'utilities' | 'members' | 'topics' + +type UtilityBillDraft = { + billName: string + amountMajor: string + currency: 'USD' | 'GEL' +} + +type BillingForm = { + settlementCurrency: 'USD' | 'GEL' + paymentBalanceAdjustmentPolicy: 'utilities' | 'rent' | 'separate' + rentAmountMajor: string + rentCurrency: 'USD' | 'GEL' + rentDueDay: number + rentWarningDay: number + utilitiesDueDay: number + utilitiesReminderDay: number + timezone: string +} + +type CycleForm = { + period: string + rentCurrency: 'USD' | 'GEL' + utilityCurrency: 'USD' | 'GEL' + rentAmountMajor: string + utilityCategorySlug: string + utilityAmountMajor: string +} + +type Props = { + copy: Record + readyIsAdmin: boolean + householdDefaultLocale: 'en' | 'ru' + dashboard: MiniAppDashboard | null + adminSettings: MiniAppAdminSettingsPayload | null + cycleState: MiniAppAdminCycleState | null + pendingMembers: readonly MiniAppPendingMember[] + activeHouseSection: HouseSectionKey + onChangeHouseSection: (section: HouseSectionKey) => void + billingForm: BillingForm + cycleForm: CycleForm + newCategoryName: string + cycleRentOpen: boolean + billingSettingsOpen: boolean + addingUtilityBillOpen: boolean + editingUtilityBill: MiniAppAdminCycleState['utilityBills'][number] | null + editingUtilityBillId: string | null + utilityBillDrafts: Record + editingCategorySlug: string | null + editingCategory: MiniAppAdminSettingsPayload['categories'][number] | null + editingMember: MiniAppAdminSettingsPayload['members'][number] | null + memberDisplayNameDrafts: Record + memberStatusDrafts: Record + memberAbsencePolicyDrafts: Record + rentWeightDrafts: Record + openingCycle: boolean + closingCycle: boolean + savingCycleRent: boolean + savingBillingSettings: boolean + savingUtilityBill: boolean + savingUtilityBillId: string | null + deletingUtilityBillId: string | null + savingCategorySlug: string | null + approvingTelegramUserId: string | null + savingMemberDisplayNameId: string | null + savingMemberStatusId: string | null + savingMemberAbsencePolicyId: string | null + savingRentWeightMemberId: string | null + promotingMemberId: string | null + savingHouseholdLocale: boolean + minorToMajorString: (value: bigint) => string + memberStatusLabel: (status: 'active' | 'away' | 'left') => string + topicRoleLabel: (role: 'purchase' | 'feedback' | 'reminders' | 'payments') => string + resolvedMemberAbsencePolicy: ( + memberId: string, + status: 'active' | 'away' | 'left' + ) => { + policy: MiniAppMemberAbsencePolicy + effectiveFromPeriod: string | null + } + onChangeHouseholdLocale: (locale: 'en' | 'ru') => Promise + onOpenCycleModal: () => void + onCloseCycleModal: () => void + onSaveCycleRent: () => Promise + onOpenCycle: () => Promise + onCloseCycle: () => Promise + onCycleRentAmountChange: (value: string) => void + onCycleRentCurrencyChange: (value: 'USD' | 'GEL') => void + onCyclePeriodChange: (value: string) => void + onOpenBillingSettingsModal: () => void + onCloseBillingSettingsModal: () => void + onSaveBillingSettings: () => Promise + onBillingSettlementCurrencyChange: (value: 'USD' | 'GEL') => void + onBillingAdjustmentPolicyChange: (value: 'utilities' | 'rent' | 'separate') => void + onBillingRentAmountChange: (value: string) => void + onBillingRentCurrencyChange: (value: 'USD' | 'GEL') => void + onBillingRentDueDayChange: (value: number) => void + onBillingRentWarningDayChange: (value: number) => void + onBillingUtilitiesDueDayChange: (value: number) => void + onBillingUtilitiesReminderDayChange: (value: number) => void + onBillingTimezoneChange: (value: string) => void + onOpenAddUtilityBill: () => void + onCloseAddUtilityBill: () => void + onAddUtilityBill: () => Promise + onCycleUtilityCategoryChange: (value: string) => void + onCycleUtilityAmountChange: (value: string) => void + onCycleUtilityCurrencyChange: (value: 'USD' | 'GEL') => void + onOpenUtilityBillEditor: (billId: string) => void + onCloseUtilityBillEditor: () => void + onDeleteUtilityBill: (billId: string) => Promise + onSaveUtilityBill: (billId: string) => Promise + onUtilityBillNameChange: ( + billId: string, + bill: MiniAppAdminCycleState['utilityBills'][number], + value: string + ) => void + onUtilityBillAmountChange: ( + billId: string, + bill: MiniAppAdminCycleState['utilityBills'][number], + value: string + ) => void + onUtilityBillCurrencyChange: ( + billId: string, + bill: MiniAppAdminCycleState['utilityBills'][number], + value: 'USD' | 'GEL' + ) => void + onOpenCategoryEditor: (slug: string) => void + onCloseCategoryEditor: () => void + onNewCategoryNameChange: (value: string) => void + onSaveNewCategory: () => Promise + onSaveExistingCategory: () => Promise + onEditingCategoryNameChange: (value: string) => void + onEditingCategoryActiveChange: (value: boolean) => void + onOpenMemberEditor: (memberId: string) => void + onCloseMemberEditor: () => void + onApprovePendingMember: (telegramUserId: string) => Promise + onMemberDisplayNameDraftChange: (memberId: string, value: string) => void + onMemberStatusDraftChange: (memberId: string, value: 'active' | 'away' | 'left') => void + onMemberAbsencePolicyDraftChange: (memberId: string, value: MiniAppMemberAbsencePolicy) => void + onRentWeightDraftChange: (memberId: string, value: string) => void + onSaveMemberDisplayName: (memberId: string) => Promise + onSaveMemberStatus: (memberId: string) => Promise + onSaveMemberAbsencePolicy: (memberId: string) => Promise + onSaveRentWeight: (memberId: string) => Promise + onPromoteMember: (memberId: string) => Promise +} + +export function HouseScreen(props: Props) { + if (!props.readyIsAdmin) { + return ( +
+
+
+ {props.copy.residentHouseTitle ?? ''} +
+

{props.copy.residentHouseBody ?? ''}

+
+
+ ) + } + + return ( +
+
+
+ {props.copy.householdSettingsTitle ?? ''} + {props.adminSettings?.settings.settlementCurrency ?? '—'} +
+

{props.copy.householdSettingsBody ?? ''}

+
+
+ {props.copy.billingCycleTitle ?? ''} + {props.cycleState?.cycle?.period ?? props.copy.billingCycleEmpty ?? ''} +
+
+ {props.copy.settlementCurrency ?? ''} + {props.adminSettings?.settings.settlementCurrency ?? '—'} +
+
+ {props.copy.membersCount ?? ''} + {String(props.adminSettings?.members.length ?? 0)} +
+
+ {props.copy.pendingRequests ?? ''} + {String(props.pendingMembers.length)} +
+
+
+ +
+ + {([key, label]) => ( + + )} + +
+ + +
+
+
+

{props.copy.billingCycleTitle ?? ''}

+

{props.copy.billingSettingsTitle ?? ''}

+
+
+
+
+
+ {props.copy.billingCycleTitle ?? ''} + {props.cycleState?.cycle?.period ?? props.copy.billingCycleEmpty ?? ''} +
+

+ {props.cycleState?.cycle + ? (props.copy.billingCycleStatus ?? '').replace( + '{currency}', + props.cycleState?.cycle?.currency ?? props.billingForm.settlementCurrency + ) + : props.copy.billingCycleOpenHint} +

+ + {(data) => ( +

+ {props.copy.shareRent ?? ''}: {data().rentSourceAmountMajor}{' '} + {data().rentSourceCurrency} + {data().rentSourceCurrency !== data().currency + ? ` -> ${data().rentDisplayAmountMajor} ${data().currency}` + : ''} +

+ )} +
+
+ + + + +
+
+ +
+
+ {props.copy.billingSettingsTitle ?? ''} + {props.billingForm.settlementCurrency} +
+

+ {props.billingForm.paymentBalanceAdjustmentPolicy === 'utilities' + ? props.copy.paymentBalanceAdjustmentUtilities + : props.billingForm.paymentBalanceAdjustmentPolicy === 'rent' + ? props.copy.paymentBalanceAdjustmentRent + : props.copy.paymentBalanceAdjustmentSeparate} +

+
+ + {props.copy.rentAmount ?? ''}: {props.billingForm.rentAmountMajor || '—'}{' '} + {props.billingForm.rentCurrency} + + + {props.copy.timezone ?? ''}: {props.billingForm.timezone} + +
+
+ +
+
+ +
+
+ {props.copy.householdLanguage ?? ''} + {props.householdDefaultLocale.toUpperCase()} +
+

{props.copy.householdSettingsBody ?? ''}

+
+ + +
+
+
+ + + +
+ ) : ( + + ) + } + > + {props.cycleState?.cycle ? ( +
+ + props.onCycleRentAmountChange(event.currentTarget.value)} + /> + + + + +
+ ) : ( +
+ + props.onCyclePeriodChange(event.currentTarget.value)} + /> + + +
{props.billingForm.settlementCurrency}
+
+
+ )} + + + + + + } + > +
+ + + + + + + + props.onBillingRentAmountChange(event.currentTarget.value)} + /> + + + + + + + props.onBillingRentDueDayChange(Number(event.currentTarget.value)) + } + /> + + + + props.onBillingRentWarningDayChange(Number(event.currentTarget.value)) + } + /> + + + + props.onBillingUtilitiesDueDayChange(Number(event.currentTarget.value)) + } + /> + + + + props.onBillingUtilitiesReminderDayChange(Number(event.currentTarget.value)) + } + /> + + + props.onBillingTimezoneChange(event.currentTarget.value)} + /> + +
+
+ + + + +
+
+
+

{props.copy.utilityCategoriesTitle ?? ''}

+

{props.copy.utilityCategoriesBody ?? ''}

+
+
+
+
+
+ {props.copy.utilityLedgerTitle ?? ''} + {props.cycleForm.utilityCurrency} +
+

{props.copy.utilityBillsEditorBody ?? ''}

+
+ +
+
+ {props.cycleState?.utilityBills.length ? ( + + {(bill) => ( +
+
+
+ {bill.billName} + {bill.createdAt.slice(0, 10)} +
+

{props.copy.utilityCategoryName ?? ''}

+
+ + {props.minorToMajorString(BigInt(bill.amountMinor))} {bill.currency} + +
+
+
+ props.onOpenUtilityBillEditor(bill.id)} + > + ... + +
+
+ )} +
+ ) : ( +

{props.copy.utilityBillsEmpty ?? ''}

+ )} +
+
+ +
+
+ {props.copy.utilityCategoriesTitle ?? ''} + {String(props.adminSettings?.categories.length ?? 0)} +
+

{props.copy.utilityCategoriesBody ?? ''}

+
+ +
+
+ + {(category) => ( +
+
+
+ {category.name} + {category.isActive ? 'ON' : 'OFF'} +
+

{props.copy.utilityCategoryName ?? ''}

+
+ + {category.isActive ? 'ON' : 'OFF'} + +
+
+
+ props.onOpenCategoryEditor(category.slug)} + > + ... + +
+
+ )} +
+
+
+
+ + + + + } + > +
+ + + + + props.onCycleUtilityAmountChange(event.currentTarget.value)} + /> + + + + +
+
+ { + const bill = props.editingUtilityBill + if (!bill) { + return null + } + return ( + + ) + })()} + > + {(() => { + const bill = props.editingUtilityBill + if (!bill) { + return null + } + const draft = props.utilityBillDrafts[bill.id] ?? { + billName: bill.billName, + amountMajor: props.minorToMajorString(BigInt(bill.amountMinor)), + currency: bill.currency + } + + return ( +
+ + + props.onUtilityBillNameChange(bill.id, bill, event.currentTarget.value) + } + /> + + + + props.onUtilityBillAmountChange(bill.id, bill, event.currentTarget.value) + } + /> + + + + +
+ ) + })()} +
+ { + const category = props.editingCategory + const isNew = props.editingCategorySlug === '__new__' + return ( + + ) + })()} + > + {props.editingCategorySlug === '__new__' ? ( +
+ + props.onNewCategoryNameChange(event.currentTarget.value)} + /> + +
+ ) : ( + (() => { + const category = props.editingCategory + if (!category) { + return null + } + return ( +
+ + + props.onEditingCategoryNameChange(event.currentTarget.value) + } + /> + + + + +
+ ) + })() + )} +
+
+
+ + +
+
+
+

{props.copy.adminsTitle ?? ''}

+

{props.copy.adminsBody ?? ''}

+
+
+
+
+
+ {props.copy.adminsTitle ?? ''} + {String(props.adminSettings?.members.length ?? 0)} +
+
+ + {(member) => ( +
+
+
+ {member.displayName} + + {member.isAdmin ? props.copy.adminTag : props.copy.residentTag} + +
+

{props.memberStatusLabel(member.status)}

+
+ + {props.copy.rentWeightLabel}: {member.rentShareWeight} + + + {(() => { + const policy = props.resolvedMemberAbsencePolicy( + member.id, + member.status + ).policy + return policy === 'away_rent_only' + ? props.copy.absencePolicyAwayRentOnly + : policy === 'away_rent_and_utilities' + ? props.copy.absencePolicyAwayRentAndUtilities + : policy === 'inactive' + ? props.copy.absencePolicyInactive + : props.copy.absencePolicyResident + })()} + +
+
+
+ props.onOpenMemberEditor(member.id)} + > + ... + +
+
+ )} +
+
+
+ +
+
+ {props.copy.pendingMembersTitle ?? ''} + {String(props.pendingMembers.length)} +
+

{props.copy.pendingMembersBody ?? ''}

+ {props.pendingMembers.length === 0 ? ( +

{props.copy.pendingMembersEmpty ?? ''}

+ ) : ( +
+ + {(member) => ( +
+
+ {member.displayName} + {member.telegramUserId} +
+

+ {member.username + ? (props.copy.pendingMemberHandle ?? '').replace( + '{username}', + member.username + ) + : (member.languageCode ?? 'Telegram')} +

+ +
+ )} +
+
+ )} +
+
+ { + const member = props.editingMember + if (!member) { + return null + } + + return ( + + ) + })()} + > + {(() => { + const member = props.editingMember + if (!member) { + return null + } + + const resolvedPolicy = props.resolvedMemberAbsencePolicy(member.id, member.status) + + return ( +
+ + + props.onMemberDisplayNameDraftChange(member.id, event.currentTarget.value) + } + /> + + + + + + + + + + props.onRentWeightDraftChange(member.id, event.currentTarget.value) + } + /> + +
+ ) + })()} +
+
+
+ + +
+
+
+

{props.copy.topicBindingsTitle ?? ''}

+

{props.copy.topicBindingsBody ?? ''}

+
+
+
+
+
+ {props.copy.topicBindingsTitle ?? ''} + {String(props.adminSettings?.topics.length ?? 0)}/4 +
+
+ + {(role) => { + const binding = props.adminSettings?.topics.find((topic) => topic.role === role) + + return ( +
+
+ {props.topicRoleLabel(role)} + {binding ? props.copy.topicBound : props.copy.topicUnbound} +
+

+ {binding + ? `${binding.topicName ?? `Topic #${binding.telegramThreadId}`} · #${binding.telegramThreadId}` + : props.copy.topicUnbound} +

+
+ ) + }} +
+
+
+
+
+
+ + ) +}