From 8f9abf998f40f99921febf5b8ed1019a6178f648 Mon Sep 17 00:00:00 2001 From: whekin Date: Tue, 10 Mar 2026 22:04:43 +0400 Subject: [PATCH] feat(miniapp): add billing review tools and house sections --- apps/miniapp/src/App.tsx | 1973 ++++++++++++++++++++----------- apps/miniapp/src/i18n.ts | 41 + apps/miniapp/src/index.css | 34 +- apps/miniapp/src/miniapp-api.ts | 116 ++ 4 files changed, 1468 insertions(+), 696 deletions(-) diff --git a/apps/miniapp/src/App.tsx b/apps/miniapp/src/App.tsx index 69483c8..4892187 100644 --- a/apps/miniapp/src/App.tsx +++ b/apps/miniapp/src/App.tsx @@ -3,8 +3,11 @@ import { Match, Show, Switch, createMemo, createSignal, onMount, type JSX } from import { dictionary, type Locale } from './i18n' import { addMiniAppUtilityBill, + addMiniAppPayment, approveMiniAppPendingMember, closeMiniAppBillingCycle, + deleteMiniAppPayment, + deleteMiniAppPurchase, deleteMiniAppUtilityBill, fetchMiniAppAdminSettings, fetchMiniAppBillingCycle, @@ -20,6 +23,8 @@ import { updateMiniAppLocalePreference, updateMiniAppBillingSettings, updateMiniAppCycleRent, + updateMiniAppPayment, + updateMiniAppPurchase, upsertMiniAppUtilityCategory, updateMiniAppUtilityBill, type MiniAppDashboard, @@ -63,6 +68,7 @@ type SessionState = } type NavigationKey = 'home' | 'balances' | 'ledger' | 'house' +type HouseSectionKey = 'billing' | 'utilities' | 'members' | 'topics' type UtilityBillDraft = { billName: string @@ -70,6 +76,19 @@ type UtilityBillDraft = { currency: 'USD' | 'GEL' } +type PurchaseDraft = { + description: string + amountMajor: string + currency: 'USD' | 'GEL' +} + +type PaymentDraft = { + memberId: string + kind: 'rent' | 'utilities' + amountMajor: string + currency: 'USD' | 'GEL' +} + const demoSession: Extract = { status: 'ready', mode: 'demo', @@ -209,12 +228,48 @@ function cycleUtilityBillDrafts( ) } +function purchaseDrafts( + entries: readonly MiniAppDashboard['ledger'][number][] +): Record { + return Object.fromEntries( + entries + .filter((entry) => entry.kind === 'purchase') + .map((entry) => [ + entry.id, + { + description: entry.title, + amountMajor: entry.amountMajor, + currency: entry.currency + } + ]) + ) +} + +function paymentDrafts( + entries: readonly MiniAppDashboard['ledger'][number][] +): Record { + return Object.fromEntries( + entries + .filter((entry) => entry.kind === 'payment') + .map((entry) => [ + entry.id, + { + memberId: entry.memberId ?? '', + kind: entry.paymentKind ?? 'rent', + amountMajor: entry.amountMajor, + currency: entry.currency + } + ]) + ) +} + function App() { const [locale, setLocale] = createSignal('en') const [session, setSession] = createSignal({ status: 'loading' }) const [activeNav, setActiveNav] = createSignal('home') + const [activeHouseSection, setActiveHouseSection] = createSignal('billing') const [dashboard, setDashboard] = createSignal(null) const [pendingMembers, setPendingMembers] = createSignal([]) const [adminSettings, setAdminSettings] = createSignal(null) @@ -237,6 +292,13 @@ function App() { const [utilityBillDrafts, setUtilityBillDrafts] = createSignal>( {} ) + const [purchaseDraftMap, setPurchaseDraftMap] = createSignal>({}) + const [paymentDraftMap, setPaymentDraftMap] = createSignal>({}) + const [savingPurchaseId, setSavingPurchaseId] = createSignal(null) + const [deletingPurchaseId, setDeletingPurchaseId] = createSignal(null) + const [savingPaymentId, setSavingPaymentId] = createSignal(null) + const [deletingPaymentId, setDeletingPaymentId] = createSignal(null) + const [addingPayment, setAddingPayment] = createSignal(false) const [billingForm, setBillingForm] = createSignal({ settlementCurrency: 'GEL' as 'USD' | 'GEL', rentAmountMajor: '', @@ -256,6 +318,12 @@ function App() { utilityCategorySlug: '', utilityAmountMajor: '' }) + const [paymentForm, setPaymentForm] = createSignal({ + memberId: '', + kind: 'rent', + amountMajor: '', + currency: 'GEL' + }) const copy = createMemo(() => dictionary[locale()]) const onboardingSession = createMemo(() => { @@ -316,13 +384,18 @@ function App() { async function loadDashboard(initData: string) { try { - setDashboard(await fetchMiniAppDashboard(initData)) + const nextDashboard = await fetchMiniAppDashboard(initData) + setDashboard(nextDashboard) + setPurchaseDraftMap(purchaseDrafts(nextDashboard.ledger)) + setPaymentDraftMap(paymentDrafts(nextDashboard.ledger)) } catch (error) { if (import.meta.env.DEV) { console.warn('Failed to load mini app dashboard', error) } setDashboard(null) + setPurchaseDraftMap({}) + setPaymentDraftMap({}) } } @@ -368,6 +441,11 @@ function App() { utilitiesReminderDay: payload.settings.utilitiesReminderDay, timezone: payload.settings.timezone }) + setPaymentForm((current) => ({ + ...current, + memberId: current.memberId || payload.members[0]?.id || '', + currency: payload.settings.settlementCurrency + })) } catch (error) { if (import.meta.env.DEV) { console.warn('Failed to load mini app admin settings', error) @@ -408,6 +486,28 @@ function App() { } } + async function refreshHouseholdData(initData: string, includeAdmin = false) { + await loadDashboard(initData) + + if (includeAdmin) { + await Promise.all([ + loadAdminSettings(initData), + loadCycleState(initData), + loadPendingMembers(initData) + ]) + return + } + + const currentReady = readySession() + if (currentReady?.mode === 'live' && currentReady.member.isAdmin) { + await Promise.all([ + loadAdminSettings(initData), + loadCycleState(initData), + loadPendingMembers(initData) + ]) + } + } + async function bootstrap() { const fallbackLocale = detectLocale() setLocale(fallbackLocale) @@ -513,6 +613,7 @@ function App() { id: 'purchase-1', kind: 'purchase', title: 'Soap', + memberId: 'member-2', paymentKind: null, amountMajor: '30.00', currency: 'GEL', @@ -527,6 +628,7 @@ function App() { id: 'utility-1', kind: 'utility', title: 'Electricity', + memberId: null, paymentKind: null, amountMajor: '120.00', currency: 'GEL', @@ -541,6 +643,7 @@ function App() { id: 'payment-1', kind: 'payment', title: 'rent', + memberId: 'demo-member', paymentKind: 'rent', amountMajor: '501.00', currency: 'GEL', @@ -902,6 +1005,130 @@ function App() { } } + async function handleUpdatePurchase(purchaseId: string) { + const initData = webApp?.initData?.trim() + const currentReady = readySession() + const draft = purchaseDraftMap()[purchaseId] + + if ( + !initData || + currentReady?.mode !== 'live' || + !currentReady.member.isAdmin || + !draft || + draft.description.trim().length === 0 || + draft.amountMajor.trim().length === 0 + ) { + return + } + + setSavingPurchaseId(purchaseId) + + try { + await updateMiniAppPurchase(initData, { + purchaseId, + description: draft.description, + amountMajor: draft.amountMajor, + currency: draft.currency + }) + await refreshHouseholdData(initData, true) + } finally { + setSavingPurchaseId(null) + } + } + + async function handleDeletePurchase(purchaseId: string) { + const initData = webApp?.initData?.trim() + const currentReady = readySession() + if (!initData || currentReady?.mode !== 'live' || !currentReady.member.isAdmin) { + return + } + + setDeletingPurchaseId(purchaseId) + + try { + await deleteMiniAppPurchase(initData, purchaseId) + await refreshHouseholdData(initData, true) + } finally { + setDeletingPurchaseId(null) + } + } + + async function handleAddPayment() { + const initData = webApp?.initData?.trim() + const currentReady = readySession() + const draft = paymentForm() + if ( + !initData || + currentReady?.mode !== 'live' || + !currentReady.member.isAdmin || + draft.memberId.trim().length === 0 || + draft.amountMajor.trim().length === 0 + ) { + return + } + + setAddingPayment(true) + + try { + await addMiniAppPayment(initData, draft) + setPaymentForm((current) => ({ + ...current, + amountMajor: '' + })) + await refreshHouseholdData(initData, true) + } finally { + setAddingPayment(false) + } + } + + async function handleUpdatePayment(paymentId: string) { + const initData = webApp?.initData?.trim() + const currentReady = readySession() + const draft = paymentDraftMap()[paymentId] + if ( + !initData || + currentReady?.mode !== 'live' || + !currentReady.member.isAdmin || + !draft || + draft.memberId.trim().length === 0 || + draft.amountMajor.trim().length === 0 + ) { + return + } + + setSavingPaymentId(paymentId) + + try { + await updateMiniAppPayment(initData, { + paymentId, + memberId: draft.memberId, + kind: draft.kind, + amountMajor: draft.amountMajor, + currency: draft.currency + }) + await refreshHouseholdData(initData, true) + } finally { + setSavingPaymentId(null) + } + } + + async function handleDeletePayment(paymentId: string) { + const initData = webApp?.initData?.trim() + const currentReady = readySession() + if (!initData || currentReady?.mode !== 'live' || !currentReady.member.isAdmin) { + return + } + + setDeletingPaymentId(paymentId) + + try { + await deleteMiniAppPayment(initData, paymentId) + await refreshHouseholdData(initData, true) + } finally { + setDeletingPaymentId(null) + } + } + async function handleSaveUtilityCategory(input: { slug?: string name: string @@ -1106,22 +1333,130 @@ function App() { <>
- {copy().purchasesTitle} + + {readySession()?.member.isAdmin + ? copy().purchaseReviewTitle + : copy().purchasesTitle} +
+ +

{copy().purchaseReviewBody}

+
{purchaseLedger().length === 0 ? (

{copy().purchasesEmpty}

) : (
{purchaseLedger().map((entry) => ( -
+
- {ledgerTitle(entry)} - {ledgerPrimaryAmount(entry)} + + {entry.actorDisplayName ?? copy().ledgerActorFallback} + + {entry.occurredAt?.slice(0, 10) ?? '—'}
- - {(secondary) =>

{secondary()}

} -
-

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

+ {readySession()?.member.isAdmin ? ( + <> +
+ + + +
+
+ + +
+ + ) : ( + <> +

{ledgerPrimaryAmount(entry)}

+ + {(secondary) =>

{secondary()}

} +
+

{entry.title}

+ + )}
))}
@@ -1152,22 +1487,232 @@ function App() {
- {copy().paymentsTitle} + {copy().paymentsAdminTitle}
+ +

{copy().paymentsAdminBody}

+
+ + + + +
+ +
{paymentLedger().length === 0 ? (

{copy().paymentsEmpty}

) : (
{paymentLedger().map((entry) => ( -
+
- {ledgerTitle(entry)} - {ledgerPrimaryAmount(entry)} + + {entry.actorDisplayName ?? copy().ledgerActorFallback} + + {entry.occurredAt?.slice(0, 10) ?? '—'}
- - {(secondary) =>

{secondary()}

} -
-

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

+ {readySession()?.member.isAdmin ? ( + <> +
+ + + + +
+
+ + +
+ + ) : ( + <> +

{ledgerPrimaryAmount(entry)}

+ + {(secondary) =>

{secondary()}

} +
+

{ledgerTitle(entry)}

+ + )}
))}
@@ -1207,644 +1752,85 @@ function App() {
-
-
-
-

{copy().billingCycleTitle}

-

{copy().billingSettingsTitle}

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

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

- - {(data) => ( -

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

- )} -
-
- - -
-
- - -
- - ) : ( - <> -

{copy().billingCycleOpenHint}

-
- -
- {copy().settlementCurrency} -
- {billingForm().settlementCurrency} -
-
-
- - - )} -
+
+ {( + [ + ['billing', copy().houseSectionBilling], + ['utilities', copy().houseSectionUtilities], + ['members', copy().houseSectionMembers], + ['topics', copy().houseSectionTopics] + ] as const + ).map(([key, label]) => ( + + ))} +
-
-
- {copy().billingSettingsTitle} - {billingForm().settlementCurrency} -
-
- - - - - - - - + +
+
+
+

{copy().billingCycleTitle}

+

{copy().billingSettingsTitle}

- -
- -
-
- {copy().householdLanguage} - {readySession()?.member.householdDefaultLocale.toUpperCase()} -
-

{copy().householdSettingsBody}

-
- - -
-
- -
-
- {copy().topicBindingsTitle} - {String(adminSettings()?.topics.length ?? 0)}/4 -
-

{copy().topicBindingsBody}

-
- {(['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().utilityCategoriesTitle}

-

{copy().utilityCategoriesBody}

-
-
-
-
-
- {copy().utilityLedgerTitle} - {cycleForm().utilityCurrency} -
-
- - - -
- -
- {cycleState()?.utilityBills.length ? ( - cycleState()?.utilityBills.map((bill) => ( -
-
- - {utilityBillDrafts()[bill.id]?.billName ?? bill.billName} - - {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().billingCycleTitle} + {cycleState()?.cycle?.period ?? copy().billingCycleEmpty} +
+ {cycleState()?.cycle ? ( + <> +

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

+ + {(data) => ( +

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

+ )} +
-
- -
- ))} -
- - -
-
-
-
-
- -
-
-
-

{copy().adminsTitle}

-

{copy().adminsBody}

-
-
-
-
-
- {copy().adminsTitle} - {String(adminSettings()?.members.length ?? 0)} -
-
- {adminSettings()?.members.map((member) => ( -
-
- {member.displayName} - {member.isAdmin ? copy().adminTag : copy().residentTag} -
-
-
@@ -1852,71 +1838,668 @@ function App() { class="ghost-button" type="button" disabled={ - savingRentWeightMemberId() === member.id || - Number(rentWeightDrafts()[member.id] ?? member.rentShareWeight) <= 0 + savingCycleRent() || cycleForm().rentAmountMajor.trim().length === 0 } - onClick={() => void handleSaveRentWeight(member.id)} + onClick={() => void handleSaveCycleRent()} > - {savingRentWeightMemberId() === member.id - ? copy().savingRentWeight - : copy().saveRentWeightAction} + {savingCycleRent() + ? copy().savingCycleRent + : copy().saveCycleRentAction} - {!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().billingCycleOpenHint}

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

{copy().householdSettingsBody}

+
+ + +
+
+
+
+ + + +
+
+
+

{copy().utilityCategoriesTitle}

+

{copy().utilityCategoriesBody}

+
+
+
+
+
+ {copy().utilityLedgerTitle} + {cycleForm().utilityCurrency} +
+
+ + + +
+ +
+ {cycleState()?.utilityBills.length ? ( + cycleState()?.utilityBills.map((bill) => ( +
+
+ + {utilityBillDrafts()[bill.id]?.billName ?? bill.billName} + + {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().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} +

+
+ ) + })} +
+
+
+
+
) : (
diff --git a/apps/miniapp/src/i18n.ts b/apps/miniapp/src/i18n.ts index 113851d..393091c 100644 --- a/apps/miniapp/src/i18n.ts +++ b/apps/miniapp/src/i18n.ts @@ -35,6 +35,10 @@ export const dictionary = { balances: 'Balances', ledger: 'Ledger', house: 'House', + houseSectionBilling: 'Billing', + houseSectionUtilities: 'Utilities', + houseSectionMembers: 'Members', + houseSectionTopics: 'Topics', welcome: 'Welcome back', adminTag: 'Admin', residentTag: 'Resident', @@ -69,6 +73,22 @@ export const dictionary = { emptyDashboard: 'No billing cycle is ready yet.', latestActivityTitle: 'Latest activity', latestActivityEmpty: 'Recent utility and purchase entries will appear here.', + purchaseReviewTitle: 'Purchases', + purchaseReviewBody: 'Edit or remove purchases if the bot recorded the wrong item.', + paymentsAdminTitle: 'Payments', + paymentsAdminBody: 'Add, fix, or remove payment records for the current cycle.', + paymentsAddAction: 'Add payment', + addingPayment: 'Adding payment…', + paymentKind: 'Payment kind', + paymentAmount: 'Payment amount', + paymentMember: 'Member', + paymentSaveAction: 'Save payment', + paymentDeleteAction: 'Delete payment', + deletingPayment: 'Deleting payment…', + purchaseSaveAction: 'Save purchase', + purchaseDeleteAction: 'Delete purchase', + deletingPurchase: 'Deleting purchase…', + savingPurchase: 'Saving purchase…', householdSettingsTitle: 'Household settings', householdSettingsBody: 'Control household defaults and approve roommates who requested access.', topicBindingsTitle: 'Topic bindings', @@ -171,6 +191,10 @@ export const dictionary = { balances: 'Баланс', ledger: 'Леджер', house: 'Дом', + houseSectionBilling: 'Биллинг', + houseSectionUtilities: 'Коммуналка', + houseSectionMembers: 'Участники', + houseSectionTopics: 'Топики', welcome: 'С возвращением', adminTag: 'Админ', residentTag: 'Житель', @@ -204,6 +228,23 @@ export const dictionary = { emptyDashboard: 'Пока нет готового billing cycle.', latestActivityTitle: 'Последняя активность', latestActivityEmpty: 'Здесь появятся последние коммунальные платежи и покупки.', + purchaseReviewTitle: 'Покупки', + purchaseReviewBody: + 'Здесь можно исправить или удалить покупку, если бот распознал её неправильно.', + paymentsAdminTitle: 'Оплаты', + paymentsAdminBody: 'Добавляй, исправляй или удаляй оплаты за текущий цикл.', + paymentsAddAction: 'Добавить оплату', + addingPayment: 'Добавляем оплату…', + paymentKind: 'Тип оплаты', + paymentAmount: 'Сумма оплаты', + paymentMember: 'Участник', + paymentSaveAction: 'Сохранить оплату', + paymentDeleteAction: 'Удалить оплату', + deletingPayment: 'Удаляем оплату…', + purchaseSaveAction: 'Сохранить покупку', + purchaseDeleteAction: 'Удалить покупку', + deletingPurchase: 'Удаляем покупку…', + savingPurchase: 'Сохраняем покупку…', householdSettingsTitle: 'Настройки household', householdSettingsBody: 'Здесь можно менять язык household и подтверждать новых соседей.', topicBindingsTitle: 'Привязанные топики', diff --git a/apps/miniapp/src/index.css b/apps/miniapp/src/index.css index 60a1323..6151e91 100644 --- a/apps/miniapp/src/index.css +++ b/apps/miniapp/src/index.css @@ -327,6 +327,26 @@ button { margin-top: 12px; } +.section-switch { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; +} + +.section-switch button { + border: 1px solid rgb(255 255 255 / 0.12); + border-radius: 16px; + min-height: 44px; + padding: 10px 12px; + background: rgb(255 255 255 / 0.04); + color: inherit; +} + +.section-switch button.is-active { + border-color: rgb(247 179 137 / 0.7); + background: rgb(247 179 137 / 0.14); +} + .admin-layout { gap: 18px; } @@ -396,7 +416,8 @@ button { padding: 12px 14px; background: rgb(255 255 255 / 0.04); color: inherit; - line-height: 1.35; + font-size: 1rem; + line-height: 1.2; } .settings-field__value { @@ -452,6 +473,13 @@ button { grid-column: 1 / -1; } +.activity-row p, +.ledger-item p, +.utility-bill-row p, +.balance-item p { + overflow-wrap: anywhere; +} + @media (min-width: 760px) { .shell { max-width: 920px; @@ -498,6 +526,10 @@ button { .balance-item--wide { grid-column: 1 / -1; } + + .section-switch { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } } @media (min-width: 980px) { diff --git a/apps/miniapp/src/miniapp-api.ts b/apps/miniapp/src/miniapp-api.ts index f82ddfc..ec7d20e 100644 --- a/apps/miniapp/src/miniapp-api.ts +++ b/apps/miniapp/src/miniapp-api.ts @@ -96,6 +96,7 @@ export interface MiniAppDashboard { id: string kind: 'purchase' | 'utility' | 'payment' title: string + memberId: string | null paymentKind: 'rent' | 'utilities' | null amountMajor: string currency: 'USD' | 'GEL' @@ -731,3 +732,118 @@ export async function deleteMiniAppUtilityBill( return payload.cycleState } + +export async function updateMiniAppPurchase( + initData: string, + input: { + purchaseId: string + description: string + amountMajor: string + currency: 'USD' | 'GEL' + } +): Promise { + const response = await fetch(`${apiBaseUrl()}/api/miniapp/admin/purchases/update`, { + method: 'POST', + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify({ + initData, + ...input + }) + }) + + const payload = (await response.json()) as { ok: boolean; authorized?: boolean; error?: string } + if (!response.ok || !payload.authorized) { + throw new Error(payload.error ?? 'Failed to update purchase') + } +} + +export async function deleteMiniAppPurchase(initData: string, purchaseId: string): Promise { + const response = await fetch(`${apiBaseUrl()}/api/miniapp/admin/purchases/delete`, { + method: 'POST', + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify({ + initData, + purchaseId + }) + }) + + const payload = (await response.json()) as { ok: boolean; authorized?: boolean; error?: string } + if (!response.ok || !payload.authorized) { + throw new Error(payload.error ?? 'Failed to delete purchase') + } +} + +export async function addMiniAppPayment( + initData: string, + input: { + memberId: string + kind: 'rent' | 'utilities' + amountMajor: string + currency: 'USD' | 'GEL' + } +): Promise { + const response = await fetch(`${apiBaseUrl()}/api/miniapp/admin/payments/add`, { + method: 'POST', + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify({ + initData, + ...input + }) + }) + + const payload = (await response.json()) as { ok: boolean; authorized?: boolean; error?: string } + if (!response.ok || !payload.authorized) { + throw new Error(payload.error ?? 'Failed to add payment') + } +} + +export async function updateMiniAppPayment( + initData: string, + input: { + paymentId: string + memberId: string + kind: 'rent' | 'utilities' + amountMajor: string + currency: 'USD' | 'GEL' + } +): Promise { + const response = await fetch(`${apiBaseUrl()}/api/miniapp/admin/payments/update`, { + method: 'POST', + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify({ + initData, + ...input + }) + }) + + const payload = (await response.json()) as { ok: boolean; authorized?: boolean; error?: string } + if (!response.ok || !payload.authorized) { + throw new Error(payload.error ?? 'Failed to update payment') + } +} + +export async function deleteMiniAppPayment(initData: string, paymentId: string): Promise { + const response = await fetch(`${apiBaseUrl()}/api/miniapp/admin/payments/delete`, { + method: 'POST', + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify({ + initData, + paymentId + }) + }) + + const payload = (await response.json()) as { ok: boolean; authorized?: boolean; error?: string } + if (!response.ok || !payload.authorized) { + throw new Error(payload.error ?? 'Failed to delete payment') + } +}