import { Match, Show, Switch, createMemo, createSignal, onMount, type JSX } from 'solid-js' import { dictionary, type Locale } from './i18n' import { addMiniAppUtilityBill, approveMiniAppPendingMember, closeMiniAppBillingCycle, fetchMiniAppAdminSettings, fetchMiniAppBillingCycle, fetchMiniAppDashboard, fetchMiniAppPendingMembers, fetchMiniAppSession, joinMiniAppHousehold, openMiniAppBillingCycle, promoteMiniAppMember, updateMiniAppMemberRentWeight, type MiniAppAdminCycleState, type MiniAppAdminSettingsPayload, updateMiniAppLocalePreference, updateMiniAppBillingSettings, updateMiniAppCycleRent, upsertMiniAppUtilityCategory, type MiniAppDashboard, type MiniAppPendingMember } from './miniapp-api' import { getTelegramWebApp } from './telegram-webapp' type SessionState = | { status: 'loading' } | { status: 'blocked' reason: 'telegram_only' | 'error' } | { status: 'onboarding' mode: 'join_required' | 'pending' | 'open_from_group' householdName?: string telegramUser: { firstName: string | null username: string | null languageCode: string | null } } | { status: 'ready' mode: 'live' | 'demo' member: { id: string displayName: string isAdmin: boolean preferredLocale: Locale | null householdDefaultLocale: Locale } telegramUser: { firstName: string | null username: string | null languageCode: string | null } } type NavigationKey = 'home' | 'balances' | 'ledger' | 'house' const demoSession: Extract = { status: 'ready', mode: 'demo', member: { id: 'demo-member', displayName: 'Demo Resident', isAdmin: false, preferredLocale: 'en', householdDefaultLocale: 'en' }, telegramUser: { firstName: 'Demo', username: 'demo_user', languageCode: 'en' } } function detectLocale(): Locale { const telegramLocale = getTelegramWebApp()?.initDataUnsafe?.user?.language_code const browserLocale = navigator.language.toLowerCase() return (telegramLocale ?? browserLocale).startsWith('ru') ? 'ru' : 'en' } function joinContext(): { joinToken?: string botUsername?: string } { if (typeof window === 'undefined') { return {} } const params = new URLSearchParams(window.location.search) const joinToken = params.get('join')?.trim() const botUsername = params.get('bot')?.trim() return { ...(joinToken ? { joinToken } : {}), ...(botUsername ? { botUsername } : {}) } } function joinDeepLink(): string | null { const context = joinContext() if (!context.botUsername || !context.joinToken) { return null } return `https://t.me/${context.botUsername}?start=join_${encodeURIComponent(context.joinToken)}` } function dashboardMemberCount(dashboard: MiniAppDashboard | null): string { return dashboard ? String(dashboard.members.length) : '—' } function dashboardLedgerCount(dashboard: MiniAppDashboard | null): string { return dashboard ? String(dashboard.ledger.length) : '—' } function defaultCyclePeriod(): string { return new Date().toISOString().slice(0, 7) } function majorStringToMinor(value: string): bigint { const trimmed = value.trim() const negative = trimmed.startsWith('-') const normalized = negative ? trimmed.slice(1) : trimmed const [whole = '0', fraction = ''] = normalized.split('.') const major = BigInt(whole || '0') const cents = BigInt((fraction.padEnd(2, '0').slice(0, 2) || '00').replace(/\D/g, '') || '0') const minor = major * 100n + cents return negative ? -minor : minor } function minorToMajorString(value: bigint): string { const negative = value < 0n const absolute = negative ? -value : value const whole = absolute / 100n const fraction = String(absolute % 100n).padStart(2, '0') return `${negative ? '-' : ''}${whole.toString()}.${fraction}` } 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 { return `${entry.displayAmountMajor} ${entry.displayCurrency}` } function ledgerSecondaryAmount(entry: MiniAppDashboard['ledger'][number]): string | null { if (entry.currency === entry.displayCurrency && entry.amountMajor === entry.displayAmountMajor) { return null } return `${entry.amountMajor} ${entry.currency}` } function App() { const [locale, setLocale] = createSignal('en') const [session, setSession] = createSignal({ status: 'loading' }) const [activeNav, setActiveNav] = createSignal('home') const [dashboard, setDashboard] = createSignal(null) const [pendingMembers, setPendingMembers] = createSignal([]) const [adminSettings, setAdminSettings] = createSignal(null) const [cycleState, setCycleState] = createSignal(null) const [joining, setJoining] = createSignal(false) const [approvingTelegramUserId, setApprovingTelegramUserId] = createSignal(null) const [promotingMemberId, setPromotingMemberId] = createSignal(null) const [savingRentWeightMemberId, setSavingRentWeightMemberId] = createSignal(null) const [rentWeightDrafts, setRentWeightDrafts] = createSignal>({}) const [savingMemberLocale, setSavingMemberLocale] = createSignal(false) const [savingHouseholdLocale, setSavingHouseholdLocale] = createSignal(false) const [savingBillingSettings, setSavingBillingSettings] = createSignal(false) const [savingCategorySlug, setSavingCategorySlug] = createSignal(null) const [openingCycle, setOpeningCycle] = createSignal(false) const [closingCycle, setClosingCycle] = createSignal(false) const [savingCycleRent, setSavingCycleRent] = createSignal(false) const [savingUtilityBill, setSavingUtilityBill] = createSignal(false) const [billingForm, setBillingForm] = createSignal({ settlementCurrency: 'GEL' as 'USD' | 'GEL', rentAmountMajor: '', rentCurrency: 'USD' as 'USD' | 'GEL', rentDueDay: 20, rentWarningDay: 17, utilitiesDueDay: 4, utilitiesReminderDay: 3, timezone: 'Asia/Tbilisi' }) const [newCategoryName, setNewCategoryName] = createSignal('') const [cycleForm, setCycleForm] = createSignal({ period: defaultCyclePeriod(), currency: 'GEL' as 'USD' | 'GEL', rentAmountMajor: '', utilityCategorySlug: '', utilityAmountMajor: '' }) const copy = createMemo(() => dictionary[locale()]) const onboardingSession = createMemo(() => { const current = session() return current.status === 'onboarding' ? current : null }) const blockedSession = createMemo(() => { const current = session() return current.status === 'blocked' ? current : null }) const readySession = createMemo(() => { const current = session() return current.status === 'ready' ? current : null }) const currentMemberLine = createMemo(() => { const current = readySession() const data = dashboard() if (!current || !data) { return null } return data.members.find((member) => member.memberId === current.member.id) ?? null }) const purchaseLedger = createMemo(() => (dashboard()?.ledger ?? []).filter((entry) => entry.kind === 'purchase') ) const utilityLedger = createMemo(() => (dashboard()?.ledger ?? []).filter((entry) => entry.kind === 'utility') ) const paymentLedger = createMemo(() => (dashboard()?.ledger ?? []).filter((entry) => entry.kind === 'payment') ) const webApp = getTelegramWebApp() function ledgerTitle(entry: MiniAppDashboard['ledger'][number]): string { if (entry.kind !== 'payment') { return entry.title } return entry.paymentKind === 'utilities' ? copy().paymentLedgerUtilities : copy().paymentLedgerRent } async function loadDashboard(initData: string) { try { setDashboard(await fetchMiniAppDashboard(initData)) } catch (error) { if (import.meta.env.DEV) { console.warn('Failed to load mini app dashboard', error) } setDashboard(null) } } async function loadPendingMembers(initData: string) { try { setPendingMembers(await fetchMiniAppPendingMembers(initData)) } catch (error) { if (import.meta.env.DEV) { console.warn('Failed to load pending mini app members', error) } setPendingMembers([]) } } async function loadAdminSettings(initData: string) { try { const payload = await fetchMiniAppAdminSettings(initData) setAdminSettings(payload) setRentWeightDrafts( Object.fromEntries( payload.members.map((member) => [member.id, String(member.rentShareWeight)]) ) ) setCycleForm((current) => ({ ...current, currency: current.currency || payload.settings.settlementCurrency, utilityCategorySlug: current.utilityCategorySlug || payload.categories.find((category) => category.isActive)?.slug || '' })) setBillingForm({ settlementCurrency: payload.settings.settlementCurrency, rentAmountMajor: payload.settings.rentAmountMinor ? (Number(payload.settings.rentAmountMinor) / 100).toFixed(2) : '', rentCurrency: payload.settings.rentCurrency, rentDueDay: payload.settings.rentDueDay, rentWarningDay: payload.settings.rentWarningDay, utilitiesDueDay: payload.settings.utilitiesDueDay, utilitiesReminderDay: payload.settings.utilitiesReminderDay, timezone: payload.settings.timezone }) } catch (error) { if (import.meta.env.DEV) { console.warn('Failed to load mini app admin settings', error) } setAdminSettings(null) } } async function loadCycleState(initData: string) { try { const payload = await fetchMiniAppBillingCycle(initData) setCycleState(payload) setCycleForm((current) => ({ ...current, period: payload.cycle?.period ?? current.period, currency: payload.cycle?.currency ?? adminSettings()?.settings.settlementCurrency ?? current.currency, rentAmountMajor: payload.rentRule ? (Number(payload.rentRule.amountMinor) / 100).toFixed(2) : '', utilityCategorySlug: current.utilityCategorySlug || adminSettings()?.categories.find((category) => category.isActive)?.slug || '', utilityAmountMajor: current.utilityAmountMajor })) } catch (error) { if (import.meta.env.DEV) { console.warn('Failed to load mini app billing cycle', error) } setCycleState(null) } } async function bootstrap() { const fallbackLocale = detectLocale() setLocale(fallbackLocale) webApp?.ready?.() webApp?.expand?.() const initData = webApp?.initData?.trim() if (!initData) { if (import.meta.env.DEV) { setSession(demoSession) return } setSession({ status: 'blocked', reason: 'telegram_only' }) return } try { const payload = await fetchMiniAppSession(initData, joinContext().joinToken) if (!payload.authorized || !payload.member || !payload.telegramUser) { setLocale( payload.onboarding?.householdDefaultLocale ?? ((payload.telegramUser?.languageCode ?? fallbackLocale).startsWith('ru') ? 'ru' : 'en') ) setSession({ status: 'onboarding', mode: payload.onboarding?.status ?? 'open_from_group', ...(payload.onboarding?.householdName ? { householdName: payload.onboarding.householdName } : {}), telegramUser: payload.telegramUser ?? { firstName: null, username: null, languageCode: null } }) return } setLocale(payload.member.preferredLocale ?? payload.member.householdDefaultLocale) setSession({ status: 'ready', mode: 'live', member: payload.member, telegramUser: payload.telegramUser }) await loadDashboard(initData) if (payload.member.isAdmin) { await loadPendingMembers(initData) await loadAdminSettings(initData) await loadCycleState(initData) } else { setAdminSettings(null) setCycleState(null) } } catch { if (import.meta.env.DEV) { setSession(demoSession) setDashboard({ period: '2026-03', currency: 'GEL', totalDueMajor: '1030.00', totalPaidMajor: '501.00', totalRemainingMajor: '529.00', rentSourceAmountMajor: '700.00', rentSourceCurrency: 'USD', rentDisplayAmountMajor: '1932.00', rentFxRateMicros: '2760000', rentFxEffectiveDate: '2026-03-17', members: [ { memberId: 'demo-member', displayName: 'Demo Resident', rentShareMajor: '483.00', utilityShareMajor: '32.00', purchaseOffsetMajor: '-14.00', netDueMajor: '501.00', paidMajor: '501.00', remainingMajor: '0.00', explanations: ['Equal utility split', 'Shared purchase offset'] }, { memberId: 'member-2', displayName: 'Alice', rentShareMajor: '483.00', utilityShareMajor: '32.00', purchaseOffsetMajor: '14.00', netDueMajor: '529.00', paidMajor: '0.00', remainingMajor: '529.00', explanations: ['Equal utility split'] } ], ledger: [ { id: 'purchase-1', kind: 'purchase', title: 'Soap', paymentKind: null, amountMajor: '30.00', currency: 'GEL', displayAmountMajor: '30.00', displayCurrency: 'GEL', fxRateMicros: null, fxEffectiveDate: null, actorDisplayName: 'Alice', occurredAt: '2026-03-12T11:00:00.000Z' }, { id: 'utility-1', kind: 'utility', title: 'Electricity', paymentKind: null, amountMajor: '120.00', currency: 'GEL', displayAmountMajor: '120.00', displayCurrency: 'GEL', fxRateMicros: null, fxEffectiveDate: null, actorDisplayName: 'Alice', occurredAt: '2026-03-12T12:00:00.000Z' }, { id: 'payment-1', kind: 'payment', title: 'rent', paymentKind: 'rent', amountMajor: '501.00', currency: 'GEL', displayAmountMajor: '501.00', displayCurrency: 'GEL', fxRateMicros: null, fxEffectiveDate: null, actorDisplayName: 'Demo Resident', occurredAt: '2026-03-18T15:10:00.000Z' } ] }) setPendingMembers([ { telegramUserId: '555777', displayName: 'Mia', username: 'mia', languageCode: 'ru' } ]) return } setSession({ status: 'blocked', reason: 'error' }) } } onMount(() => { void bootstrap() }) async function handleJoinHousehold() { const initData = webApp?.initData?.trim() const joinToken = joinContext().joinToken if (!initData || !joinToken || joining()) { return } setJoining(true) try { const payload = await joinMiniAppHousehold(initData, joinToken) if (payload.authorized && payload.member && payload.telegramUser) { setLocale(payload.member.preferredLocale ?? payload.member.householdDefaultLocale) setSession({ status: 'ready', mode: 'live', member: payload.member, telegramUser: payload.telegramUser }) await loadDashboard(initData) if (payload.member.isAdmin) { await loadPendingMembers(initData) await loadAdminSettings(initData) await loadCycleState(initData) } else { setAdminSettings(null) setCycleState(null) } return } setLocale( payload.onboarding?.householdDefaultLocale ?? ((payload.telegramUser?.languageCode ?? locale()).startsWith('ru') ? 'ru' : 'en') ) setSession({ status: 'onboarding', mode: payload.onboarding?.status ?? 'pending', ...(payload.onboarding?.householdName ? { householdName: payload.onboarding.householdName } : {}), telegramUser: payload.telegramUser ?? { firstName: null, username: null, languageCode: null } }) } catch { setSession({ status: 'blocked', reason: 'error' }) } finally { setJoining(false) } } async function handleApprovePendingMember(pendingTelegramUserId: string) { const initData = webApp?.initData?.trim() if (!initData || approvingTelegramUserId()) { return } setApprovingTelegramUserId(pendingTelegramUserId) try { await approveMiniAppPendingMember(initData, pendingTelegramUserId) setPendingMembers((current) => current.filter((member) => member.telegramUserId !== pendingTelegramUserId) ) } finally { setApprovingTelegramUserId(null) } } async function handleMemberLocaleChange(nextLocale: Locale) { const initData = webApp?.initData?.trim() const currentReady = readySession() setLocale(nextLocale) if (!initData || currentReady?.mode !== 'live') { return } setSavingMemberLocale(true) try { const updated = await updateMiniAppLocalePreference(initData, nextLocale, 'member') setSession((current) => current.status === 'ready' ? { ...current, member: { ...current.member, preferredLocale: updated.memberPreferredLocale, householdDefaultLocale: updated.householdDefaultLocale } } : current ) setLocale(updated.effectiveLocale) } finally { setSavingMemberLocale(false) } } async function handleHouseholdLocaleChange(nextLocale: Locale) { const initData = webApp?.initData?.trim() const currentReady = readySession() if (!initData || currentReady?.mode !== 'live' || !currentReady.member.isAdmin) { return } setSavingHouseholdLocale(true) try { const updated = await updateMiniAppLocalePreference(initData, nextLocale, 'household') setSession((current) => current.status === 'ready' ? { ...current, member: { ...current.member, householdDefaultLocale: updated.householdDefaultLocale } } : current ) if (!currentReady.member.preferredLocale) { setLocale(updated.effectiveLocale) } } finally { setSavingHouseholdLocale(false) } } async function handleSaveBillingSettings() { const initData = webApp?.initData?.trim() const currentReady = readySession() if (!initData || currentReady?.mode !== 'live' || !currentReady.member.isAdmin) { return } setSavingBillingSettings(true) try { const settings = await updateMiniAppBillingSettings(initData, billingForm()) setAdminSettings((current) => current ? { ...current, settings } : current ) setCycleForm((current) => ({ ...current, currency: cycleState()?.cycle?.currency ?? settings.settlementCurrency })) } finally { setSavingBillingSettings(false) } } async function handleOpenCycle() { const initData = webApp?.initData?.trim() const currentReady = readySession() if (!initData || currentReady?.mode !== 'live' || !currentReady.member.isAdmin) { return } setOpeningCycle(true) try { const state = await openMiniAppBillingCycle(initData, { period: cycleForm().period, currency: cycleForm().currency }) setCycleState(state) setCycleForm((current) => ({ ...current, period: state.cycle?.period ?? current.period, currency: state.cycle?.currency ?? current.currency })) } finally { setOpeningCycle(false) } } 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) } finally { setClosingCycle(false) } } async function handleSaveCycleRent() { const initData = webApp?.initData?.trim() const currentReady = readySession() if (!initData || currentReady?.mode !== 'live' || !currentReady.member.isAdmin) { return } setSavingCycleRent(true) try { const state = await updateMiniAppCycleRent(initData, { amountMajor: cycleForm().rentAmountMajor, currency: cycleForm().currency, ...(cycleState()?.cycle?.period ? { period: cycleState()!.cycle!.period } : {}) }) setCycleState(state) } finally { setSavingCycleRent(false) } } async function handleAddUtilityBill() { const initData = webApp?.initData?.trim() const currentReady = readySession() if (!initData || currentReady?.mode !== 'live' || !currentReady.member.isAdmin) { return } const selectedCategory = adminSettings()?.categories.find( (category) => category.slug === cycleForm().utilityCategorySlug ) ?? adminSettings()?.categories.find((category) => category.isActive) if (!selectedCategory || cycleForm().utilityAmountMajor.trim().length === 0) { return } setSavingUtilityBill(true) try { const state = await addMiniAppUtilityBill(initData, { billName: selectedCategory.name, amountMajor: cycleForm().utilityAmountMajor, currency: cycleForm().currency }) setCycleState(state) setCycleForm((current) => ({ ...current, utilityAmountMajor: '' })) } finally { setSavingUtilityBill(false) } } async function handleSaveUtilityCategory(input: { slug?: string name: string sortOrder: number isActive: boolean }) { const initData = webApp?.initData?.trim() const currentReady = readySession() if (!initData || currentReady?.mode !== 'live' || !currentReady.member.isAdmin) { return } setSavingCategorySlug(input.slug ?? '__new__') try { const category = await upsertMiniAppUtilityCategory(initData, input) setAdminSettings((current) => { if (!current) { return current } const categories = current.categories.some((item) => item.slug === category.slug) ? current.categories.map((item) => (item.slug === category.slug ? category : item)) : [...current.categories, category] return { ...current, categories: [...categories].sort((left, right) => left.sortOrder - right.sortOrder) } }) if (!input.slug) { setNewCategoryName('') } } finally { setSavingCategorySlug(null) } } async function handlePromoteMember(memberId: string) { const initData = webApp?.initData?.trim() const currentReady = readySession() if (!initData || currentReady?.mode !== 'live' || !currentReady.member.isAdmin) { return } setPromotingMemberId(memberId) try { const member = await promoteMiniAppMember(initData, memberId) setAdminSettings((current) => current ? { ...current, members: current.members.map((item) => (item.id === member.id ? member : item)) } : current ) setRentWeightDrafts((current) => ({ ...current, [member.id]: String(member.rentShareWeight) })) } finally { setPromotingMemberId(null) } } async function handleSaveRentWeight(memberId: string) { const initData = webApp?.initData?.trim() const currentReady = readySession() const nextWeight = Number(rentWeightDrafts()[memberId] ?? '') if ( !initData || currentReady?.mode !== 'live' || !currentReady.member.isAdmin || !Number.isInteger(nextWeight) || nextWeight <= 0 ) { return } setSavingRentWeightMemberId(memberId) try { const member = await updateMiniAppMemberRentWeight(initData, memberId, nextWeight) setAdminSettings((current) => current ? { ...current, members: current.members.map((item) => (item.id === member.id ? member : item)) } : current ) setRentWeightDrafts((current) => ({ ...current, [member.id]: String(member.rentShareWeight) })) } finally { setSavingRentWeightMemberId(null) } } const renderPanel = () => { switch (activeNav()) { case 'balances': return (
{copy().emptyDashboard}

} render={(data) => ( <> {currentMemberLine() ? (
{copy().yourBalanceTitle} {currentMemberLine()!.netDueMajor} {data.currency}

{copy().yourBalanceBody}

{copy().baseDue} {memberBaseDueMajor(currentMemberLine()!)} {data.currency}
{copy().shareOffset} {currentMemberLine()!.purchaseOffsetMajor} {data.currency}
{copy().finalDue} {currentMemberLine()!.netDueMajor} {data.currency}
{copy().paidLabel} {currentMemberLine()!.paidMajor} {data.currency}
{copy().remainingLabel} {currentMemberLine()!.remainingMajor} {data.currency}
) : null}
{copy().householdBalancesTitle}

{copy().householdBalancesBody}

{data.members.map((member) => (
{member.displayName} {member.remainingMajor} {data.currency}

{copy().baseDue}: {memberBaseDueMajor(member)} {data.currency}

{copy().shareRent}: {member.rentShareMajor} {data.currency}

{copy().shareUtilities}: {member.utilityShareMajor} {data.currency}

{copy().shareOffset}: {member.purchaseOffsetMajor} {data.currency}

{copy().paidLabel}: {member.paidMajor} {data.currency}

{copy().remainingLabel}: {member.remainingMajor} {data.currency}

))} )} />
) case 'ledger': return (
{copy().emptyDashboard}

} render={() => ( <>
{copy().purchasesTitle}
{purchaseLedger().length === 0 ? (

{copy().purchasesEmpty}

) : (
{purchaseLedger().map((entry) => (
{ledgerTitle(entry)} {ledgerPrimaryAmount(entry)}
{(secondary) =>

{secondary()}

}

{entry.actorDisplayName ?? copy().ledgerActorFallback}

))}
)}
{copy().utilityLedgerTitle}
{utilityLedger().length === 0 ? (

{copy().utilityLedgerEmpty}

) : (
{utilityLedger().map((entry) => (
{ledgerTitle(entry)} {ledgerPrimaryAmount(entry)}
{(secondary) =>

{secondary()}

}

{entry.actorDisplayName ?? copy().ledgerActorFallback}

))}
)}
{copy().paymentsTitle}
{paymentLedger().length === 0 ? (

{copy().paymentsEmpty}

) : (
{paymentLedger().map((entry) => (
{ledgerTitle(entry)} {ledgerPrimaryAmount(entry)}
{(secondary) =>

{secondary()}

}

{entry.actorDisplayName ?? copy().ledgerActorFallback}

))}
)}
)} />
) 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)}

{copy().billingCycleTitle}

{copy().billingSettingsTitle}

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

{copy().billingCycleStatus.replace( '{currency}', cycleState()?.cycle?.currency ?? cycleForm().currency )}

{(data) => (

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

)}
) : ( <>

{copy().billingCycleOpenHint}

)}
{copy().billingSettingsTitle} {billingForm().settlementCurrency}
{copy().householdLanguage} {readySession()?.member.householdDefaultLocale.toUpperCase()}

{copy().householdSettingsBody}

{copy().utilityCategoriesTitle}

{copy().utilityCategoriesBody}

{copy().utilityLedgerTitle} {cycleState()?.cycle?.currency ?? billingForm().settlementCurrency}
{cycleState()?.utilityBills.length ? ( cycleState()?.utilityBills.map((bill) => (
{bill.billName} {(Number(bill.amountMinor) / 100).toFixed(2)} {bill.currency}

{bill.createdAt.slice(0, 10)}

)) ) : (

{copy().utilityBillsEmpty}

)}
{copy().utilityCategoriesTitle} {String(adminSettings()?.categories.length ?? 0)}
{adminSettings()?.categories.map((category) => (
{category.name} {category.isActive ? 'ON' : 'OFF'}
))}

{copy().adminsTitle}

{copy().adminsBody}

{copy().adminsTitle} {String(adminSettings()?.members.length ?? 0)}
{adminSettings()?.members.map((member) => (
{member.displayName} {member.isAdmin ? copy().adminTag : copy().residentTag}
{!member.isAdmin ? ( ) : null}
))}
{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')}

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

{copy().residentHouseBody}

) default: return (
{copy().totalDue} {dashboard() ? `${dashboard()!.totalDueMajor} ${dashboard()!.currency}` : '—'}
{copy().paidLabel} {dashboard() ? `${dashboard()!.totalPaidMajor} ${dashboard()!.currency}` : '—'}
{copy().remainingLabel} {dashboard() ? `${dashboard()!.totalRemainingMajor} ${dashboard()!.currency}` : '—'}
{copy().membersCount} {dashboardMemberCount(dashboard())}
{copy().ledgerEntries} {dashboardLedgerCount(dashboard())}
{copy().purchasesTitle} {String(purchaseLedger().length)}
{readySession()?.member.isAdmin ? (
{copy().pendingRequests} {String(pendingMembers().length)}
) : null} {currentMemberLine() ? (
{copy().yourBalanceTitle} {currentMemberLine()!.remainingMajor} {dashboard()?.currency ?? ''}

{copy().yourBalanceBody}

(

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

)} />
{copy().baseDue} {memberBaseDueMajor(currentMemberLine()!)} {dashboard()?.currency ?? ''}
{copy().shareOffset} {currentMemberLine()!.purchaseOffsetMajor} {dashboard()?.currency ?? ''}
{copy().finalDue} {currentMemberLine()!.netDueMajor} {dashboard()?.currency ?? ''}
{copy().paidLabel} {currentMemberLine()!.paidMajor} {dashboard()?.currency ?? ''}
{copy().remainingLabel} {currentMemberLine()!.remainingMajor} {dashboard()?.currency ?? ''}
) : (
{copy().overviewTitle}

{copy().overviewBody}

)}
{copy().latestActivityTitle}
{copy().latestActivityEmpty}

} render={(data) => data.ledger.length === 0 ? (

{copy().latestActivityEmpty}

) : (
{data.ledger.slice(0, 3).map((entry) => (
{ledgerTitle(entry)} {ledgerPrimaryAmount(entry)}
{(secondary) =>

{secondary()}

}

{entry.actorDisplayName ?? copy().ledgerActorFallback}

))}
) } />
) } } return (

{copy().appSubtitle}

{copy().appTitle}

{copy().loadingBadge}

{copy().loadingTitle}

{copy().loadingBody}

{copy().loadingBadge}

{blockedSession()?.reason === 'telegram_only' ? copy().telegramOnlyTitle : copy().unexpectedErrorTitle}

{blockedSession()?.reason === 'telegram_only' ? copy().telegramOnlyBody : copy().unexpectedErrorBody}

{copy().loadingBadge}

{onboardingSession()?.mode === 'pending' ? copy().pendingTitle : onboardingSession()?.mode === 'open_from_group' ? copy().openFromGroupTitle : copy().joinTitle}

{onboardingSession()?.mode === 'pending' ? copy().pendingBody.replace( '{household}', onboardingSession()?.householdName ?? copy().householdFallback ) : onboardingSession()?.mode === 'open_from_group' ? copy().openFromGroupBody : copy().joinBody.replace( '{household}', onboardingSession()?.householdName ?? copy().householdFallback )}

{readySession()?.mode === 'demo' ? copy().demoBadge : copy().liveBadge} {readySession()?.member.isAdmin ? copy().adminTag : copy().residentTag}

{copy().welcome},{' '} {readySession()?.telegramUser.firstName ?? readySession()?.member.displayName}

{copy().overviewBody}

{copy().overviewTitle}

{readySession()?.member.displayName}

{renderPanel()}
) } function ShowDashboard(props: { dashboard: MiniAppDashboard | null fallback: JSX.Element render: (dashboard: MiniAppDashboard) => JSX.Element }) { return <>{props.dashboard ? props.render(props.dashboard) : props.fallback} } export default App