import { For, Show } from 'solid-js' import { Button, CalendarIcon, Field, IconButton, Modal, PencilIcon, PlusIcon, SettingsIcon } from '../components/ui' import { NavigationTabs } from '../components/layout/navigation-tabs' 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 editingCategoryDraft: { name: string isActive: boolean } | 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 savingMemberEditorId: 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 | null) => void onBillingRentWarningDayChange: (value: number | null) => void onBillingUtilitiesDueDayChange: (value: number | null) => void onBillingUtilitiesReminderDayChange: (value: number | null) => 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 onSaveMemberChanges: (memberId: string) => Promise onPromoteMember: (memberId: string) => Promise } export function HouseScreen(props: Props) { function parseBillingDayInput(value: string): number | null { const trimmed = value.trim() if (trimmed.length === 0) { return null } const parsed = Number.parseInt(trimmed, 10) if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < 1 || parsed > 31) { return null } return parsed } const enabledLabel = () => props.copy.onLabel ?? 'ON' const disabledLabel = () => props.copy.offLabel ?? 'OFF' return (
{props.copy.residentHouseTitle ?? ''}

{props.copy.residentHouseBody ?? ''}

} >
{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.cycleState?.cycle ? (
props.onCycleRentAmountChange(event.currentTarget.value)} />
) : (
props.onCyclePeriodChange(event.currentTarget.value)} />
{props.billingForm.settlementCurrency}
)} } >
props.onBillingRentAmountChange(event.currentTarget.value)} /> props.onBillingRentDueDayChange( parseBillingDayInput(event.currentTarget.value) ) } /> props.onBillingRentWarningDayChange( parseBillingDayInput(event.currentTarget.value) ) } /> props.onBillingUtilitiesDueDayChange( parseBillingDayInput(event.currentTarget.value) ) } /> props.onBillingUtilitiesReminderDayChange( parseBillingDayInput(event.currentTarget.value) ) } /> props.onBillingTimezoneChange(event.currentTarget.value)} />
{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 ? enabledLabel() : disabledLabel()}

{props.copy.utilityCategoryName ?? ''}

{category.isActive ? enabledLabel() : disabledLabel()}
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 const draft = props.editingCategoryDraft if (!category || !draft) { return null } return (
props.onEditingCategoryNameChange(event.currentTarget.value) } />
) })() )}
{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 } const nextDisplayName = props.memberDisplayNameDrafts[member.id]?.trim() ?? member.displayName const nextStatus = props.memberStatusDrafts[member.id] ?? member.status const currentPolicy = props.resolvedMemberAbsencePolicy(member.id, member.status) const nextPolicy = props.memberAbsencePolicyDrafts[member.id] ?? currentPolicy.policy const nextWeight = Number( props.rentWeightDrafts[member.id] ?? String(member.rentShareWeight) ) const hasNameChange = nextDisplayName.length >= 2 && nextDisplayName !== member.displayName const hasStatusChange = nextStatus !== member.status const hasPolicyChange = nextStatus === 'away' && nextPolicy !== currentPolicy.policy const hasWeightChange = Number.isInteger(nextWeight) && nextWeight > 0 && nextWeight !== member.rentShareWeight const canSave = props.savingMemberEditorId !== member.id && (hasNameChange || hasStatusChange || hasPolicyChange || hasWeightChange) 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 ?? ''} {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}

) }}
) }