mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 17:44:03 +00:00
feat(miniapp): improve mobile billing and utility controls
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
addMiniAppUtilityBill,
|
||||
approveMiniAppPendingMember,
|
||||
closeMiniAppBillingCycle,
|
||||
deleteMiniAppUtilityBill,
|
||||
fetchMiniAppAdminSettings,
|
||||
fetchMiniAppBillingCycle,
|
||||
fetchMiniAppDashboard,
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
updateMiniAppBillingSettings,
|
||||
updateMiniAppCycleRent,
|
||||
upsertMiniAppUtilityCategory,
|
||||
updateMiniAppUtilityBill,
|
||||
type MiniAppDashboard,
|
||||
type MiniAppPendingMember
|
||||
} from './miniapp-api'
|
||||
@@ -62,6 +64,12 @@ type SessionState =
|
||||
|
||||
type NavigationKey = 'home' | 'balances' | 'ledger' | 'house'
|
||||
|
||||
type UtilityBillDraft = {
|
||||
billName: string
|
||||
amountMajor: string
|
||||
currency: 'USD' | 'GEL'
|
||||
}
|
||||
|
||||
const demoSession: Extract<SessionState, { status: 'ready' }> = {
|
||||
status: 'ready',
|
||||
mode: 'demo',
|
||||
@@ -186,6 +194,21 @@ function ledgerSecondaryAmount(entry: MiniAppDashboard['ledger'][number]): strin
|
||||
return `${entry.amountMajor} ${entry.currency}`
|
||||
}
|
||||
|
||||
function cycleUtilityBillDrafts(
|
||||
bills: MiniAppAdminCycleState['utilityBills']
|
||||
): Record<string, UtilityBillDraft> {
|
||||
return Object.fromEntries(
|
||||
bills.map((bill) => [
|
||||
bill.id,
|
||||
{
|
||||
billName: bill.billName,
|
||||
amountMajor: minorToMajorString(BigInt(bill.amountMinor)),
|
||||
currency: bill.currency
|
||||
}
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [locale, setLocale] = createSignal<Locale>('en')
|
||||
const [session, setSession] = createSignal<SessionState>({
|
||||
@@ -209,6 +232,11 @@ function App() {
|
||||
const [closingCycle, setClosingCycle] = createSignal(false)
|
||||
const [savingCycleRent, setSavingCycleRent] = createSignal(false)
|
||||
const [savingUtilityBill, setSavingUtilityBill] = createSignal(false)
|
||||
const [savingUtilityBillId, setSavingUtilityBillId] = createSignal<string | null>(null)
|
||||
const [deletingUtilityBillId, setDeletingUtilityBillId] = createSignal<string | null>(null)
|
||||
const [utilityBillDrafts, setUtilityBillDrafts] = createSignal<Record<string, UtilityBillDraft>>(
|
||||
{}
|
||||
)
|
||||
const [billingForm, setBillingForm] = createSignal({
|
||||
settlementCurrency: 'GEL' as 'USD' | 'GEL',
|
||||
rentAmountMajor: '',
|
||||
@@ -222,7 +250,8 @@ function App() {
|
||||
const [newCategoryName, setNewCategoryName] = createSignal('')
|
||||
const [cycleForm, setCycleForm] = createSignal({
|
||||
period: defaultCyclePeriod(),
|
||||
currency: 'GEL' as 'USD' | 'GEL',
|
||||
rentCurrency: 'USD' as 'USD' | 'GEL',
|
||||
utilityCurrency: 'GEL' as 'USD' | 'GEL',
|
||||
rentAmountMajor: '',
|
||||
utilityCategorySlug: '',
|
||||
utilityAmountMajor: ''
|
||||
@@ -320,7 +349,8 @@ function App() {
|
||||
)
|
||||
setCycleForm((current) => ({
|
||||
...current,
|
||||
currency: current.currency || payload.settings.settlementCurrency,
|
||||
rentCurrency: payload.settings.rentCurrency,
|
||||
utilityCurrency: payload.settings.settlementCurrency,
|
||||
utilityCategorySlug:
|
||||
current.utilityCategorySlug ||
|
||||
payload.categories.find((category) => category.isActive)?.slug ||
|
||||
@@ -351,13 +381,15 @@ function App() {
|
||||
try {
|
||||
const payload = await fetchMiniAppBillingCycle(initData)
|
||||
setCycleState(payload)
|
||||
setUtilityBillDrafts(cycleUtilityBillDrafts(payload.utilityBills))
|
||||
setCycleForm((current) => ({
|
||||
...current,
|
||||
period: payload.cycle?.period ?? current.period,
|
||||
currency:
|
||||
payload.cycle?.currency ??
|
||||
adminSettings()?.settings.settlementCurrency ??
|
||||
current.currency,
|
||||
rentCurrency:
|
||||
payload.rentRule?.currency ??
|
||||
adminSettings()?.settings.rentCurrency ??
|
||||
current.rentCurrency,
|
||||
utilityCurrency: adminSettings()?.settings.settlementCurrency ?? current.utilityCurrency,
|
||||
rentAmountMajor: payload.rentRule
|
||||
? (Number(payload.rentRule.amountMinor) / 100).toFixed(2)
|
||||
: '',
|
||||
@@ -707,7 +739,8 @@ function App() {
|
||||
)
|
||||
setCycleForm((current) => ({
|
||||
...current,
|
||||
currency: cycleState()?.cycle?.currency ?? settings.settlementCurrency
|
||||
rentCurrency: settings.rentCurrency,
|
||||
utilityCurrency: settings.settlementCurrency
|
||||
}))
|
||||
} finally {
|
||||
setSavingBillingSettings(false)
|
||||
@@ -726,13 +759,14 @@ function App() {
|
||||
try {
|
||||
const state = await openMiniAppBillingCycle(initData, {
|
||||
period: cycleForm().period,
|
||||
currency: cycleForm().currency
|
||||
currency: billingForm().settlementCurrency
|
||||
})
|
||||
setCycleState(state)
|
||||
setUtilityBillDrafts(cycleUtilityBillDrafts(state.utilityBills))
|
||||
setCycleForm((current) => ({
|
||||
...current,
|
||||
period: state.cycle?.period ?? current.period,
|
||||
currency: state.cycle?.currency ?? current.currency
|
||||
utilityCurrency: billingForm().settlementCurrency
|
||||
}))
|
||||
} finally {
|
||||
setOpeningCycle(false)
|
||||
@@ -751,6 +785,7 @@ function App() {
|
||||
try {
|
||||
const state = await closeMiniAppBillingCycle(initData, cycleState()?.cycle?.period)
|
||||
setCycleState(state)
|
||||
setUtilityBillDrafts(cycleUtilityBillDrafts(state.utilityBills))
|
||||
} finally {
|
||||
setClosingCycle(false)
|
||||
}
|
||||
@@ -768,7 +803,7 @@ function App() {
|
||||
try {
|
||||
const state = await updateMiniAppCycleRent(initData, {
|
||||
amountMajor: cycleForm().rentAmountMajor,
|
||||
currency: cycleForm().currency,
|
||||
currency: cycleForm().rentCurrency,
|
||||
...(cycleState()?.cycle?.period
|
||||
? {
|
||||
period: cycleState()!.cycle!.period
|
||||
@@ -776,6 +811,7 @@ function App() {
|
||||
: {})
|
||||
})
|
||||
setCycleState(state)
|
||||
setUtilityBillDrafts(cycleUtilityBillDrafts(state.utilityBills))
|
||||
} finally {
|
||||
setSavingCycleRent(false)
|
||||
}
|
||||
@@ -803,9 +839,10 @@ function App() {
|
||||
const state = await addMiniAppUtilityBill(initData, {
|
||||
billName: selectedCategory.name,
|
||||
amountMajor: cycleForm().utilityAmountMajor,
|
||||
currency: cycleForm().currency
|
||||
currency: cycleForm().utilityCurrency
|
||||
})
|
||||
setCycleState(state)
|
||||
setUtilityBillDrafts(cycleUtilityBillDrafts(state.utilityBills))
|
||||
setCycleForm((current) => ({
|
||||
...current,
|
||||
utilityAmountMajor: ''
|
||||
@@ -815,6 +852,56 @@ function App() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdateUtilityBill(billId: string) {
|
||||
const initData = webApp?.initData?.trim()
|
||||
const currentReady = readySession()
|
||||
const draft = utilityBillDrafts()[billId]
|
||||
|
||||
if (
|
||||
!initData ||
|
||||
currentReady?.mode !== 'live' ||
|
||||
!currentReady.member.isAdmin ||
|
||||
!draft ||
|
||||
draft.billName.trim().length === 0 ||
|
||||
draft.amountMajor.trim().length === 0
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
setSavingUtilityBillId(billId)
|
||||
|
||||
try {
|
||||
const state = await updateMiniAppUtilityBill(initData, {
|
||||
billId,
|
||||
billName: draft.billName,
|
||||
amountMajor: draft.amountMajor,
|
||||
currency: draft.currency
|
||||
})
|
||||
setCycleState(state)
|
||||
setUtilityBillDrafts(cycleUtilityBillDrafts(state.utilityBills))
|
||||
} finally {
|
||||
setSavingUtilityBillId(null)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteUtilityBill(billId: string) {
|
||||
const initData = webApp?.initData?.trim()
|
||||
const currentReady = readySession()
|
||||
if (!initData || currentReady?.mode !== 'live' || !currentReady.member.isAdmin) {
|
||||
return
|
||||
}
|
||||
|
||||
setDeletingUtilityBillId(billId)
|
||||
|
||||
try {
|
||||
const state = await deleteMiniAppUtilityBill(initData, billId)
|
||||
setCycleState(state)
|
||||
setUtilityBillDrafts(cycleUtilityBillDrafts(state.utilityBills))
|
||||
} finally {
|
||||
setDeletingUtilityBillId(null)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveUtilityCategory(input: {
|
||||
slug?: string
|
||||
name: string
|
||||
@@ -1138,7 +1225,7 @@ function App() {
|
||||
<p>
|
||||
{copy().billingCycleStatus.replace(
|
||||
'{currency}',
|
||||
cycleState()?.cycle?.currency ?? cycleForm().currency
|
||||
cycleState()?.cycle?.currency ?? billingForm().settlementCurrency
|
||||
)}
|
||||
</p>
|
||||
<Show when={dashboard()}>
|
||||
@@ -1168,11 +1255,11 @@ function App() {
|
||||
<label class="settings-field">
|
||||
<span>{copy().shareRent}</span>
|
||||
<select
|
||||
value={cycleForm().currency}
|
||||
value={cycleForm().rentCurrency}
|
||||
onChange={(event) =>
|
||||
setCycleForm((current) => ({
|
||||
...current,
|
||||
currency: event.currentTarget.value as 'USD' | 'GEL'
|
||||
rentCurrency: event.currentTarget.value as 'USD' | 'GEL'
|
||||
}))
|
||||
}
|
||||
>
|
||||
@@ -1218,21 +1305,12 @@ function App() {
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label class="settings-field">
|
||||
<div class="settings-field">
|
||||
<span>{copy().settlementCurrency}</span>
|
||||
<select
|
||||
value={cycleForm().currency}
|
||||
onChange={(event) =>
|
||||
setCycleForm((current) => ({
|
||||
...current,
|
||||
currency: event.currentTarget.value as 'USD' | 'GEL'
|
||||
}))
|
||||
}
|
||||
>
|
||||
<option value="USD">USD</option>
|
||||
<option value="GEL">GEL</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="settings-field__value">
|
||||
{billingForm().settlementCurrency}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="ghost-button"
|
||||
@@ -1447,7 +1525,7 @@ function App() {
|
||||
<article class="balance-item">
|
||||
<header>
|
||||
<strong>{copy().utilityLedgerTitle}</strong>
|
||||
<span>{cycleState()?.cycle?.currency ?? billingForm().settlementCurrency}</span>
|
||||
<span>{cycleForm().utilityCurrency}</span>
|
||||
</header>
|
||||
<div class="settings-grid">
|
||||
<label class="settings-field">
|
||||
@@ -1480,6 +1558,21 @@ function App() {
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label class="settings-field">
|
||||
<span>{copy().settlementCurrency}</span>
|
||||
<select
|
||||
value={cycleForm().utilityCurrency}
|
||||
onChange={(event) =>
|
||||
setCycleForm((current) => ({
|
||||
...current,
|
||||
utilityCurrency: event.currentTarget.value as 'USD' | 'GEL'
|
||||
}))
|
||||
}
|
||||
>
|
||||
<option value="GEL">GEL</option>
|
||||
<option value="USD">USD</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<button
|
||||
class="ghost-button"
|
||||
@@ -1491,17 +1584,103 @@ function App() {
|
||||
>
|
||||
{savingUtilityBill() ? copy().savingUtilityBill : copy().addUtilityBillAction}
|
||||
</button>
|
||||
<div class="balance-list admin-sublist">
|
||||
<div class="admin-sublist admin-sublist--plain">
|
||||
{cycleState()?.utilityBills.length ? (
|
||||
cycleState()?.utilityBills.map((bill) => (
|
||||
<article class="ledger-item">
|
||||
<article class="utility-bill-row">
|
||||
<header>
|
||||
<strong>{bill.billName}</strong>
|
||||
<span>
|
||||
{(Number(bill.amountMinor) / 100).toFixed(2)} {bill.currency}
|
||||
</span>
|
||||
<strong>
|
||||
{utilityBillDrafts()[bill.id]?.billName ?? bill.billName}
|
||||
</strong>
|
||||
<span>{bill.createdAt.slice(0, 10)}</span>
|
||||
</header>
|
||||
<p>{bill.createdAt.slice(0, 10)}</p>
|
||||
<div class="settings-grid">
|
||||
<label class="settings-field settings-field--wide">
|
||||
<span>{copy().utilityCategoryName}</span>
|
||||
<input
|
||||
value={utilityBillDrafts()[bill.id]?.billName ?? bill.billName}
|
||||
onInput={(event) =>
|
||||
setUtilityBillDrafts((current) => ({
|
||||
...current,
|
||||
[bill.id]: {
|
||||
...(current[bill.id] ?? {
|
||||
billName: bill.billName,
|
||||
amountMajor: minorToMajorString(BigInt(bill.amountMinor)),
|
||||
currency: bill.currency
|
||||
}),
|
||||
billName: event.currentTarget.value
|
||||
}
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label class="settings-field">
|
||||
<span>{copy().utilityAmount}</span>
|
||||
<input
|
||||
value={
|
||||
utilityBillDrafts()[bill.id]?.amountMajor ??
|
||||
minorToMajorString(BigInt(bill.amountMinor))
|
||||
}
|
||||
onInput={(event) =>
|
||||
setUtilityBillDrafts((current) => ({
|
||||
...current,
|
||||
[bill.id]: {
|
||||
...(current[bill.id] ?? {
|
||||
billName: bill.billName,
|
||||
amountMajor: minorToMajorString(BigInt(bill.amountMinor)),
|
||||
currency: bill.currency
|
||||
}),
|
||||
amountMajor: event.currentTarget.value
|
||||
}
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label class="settings-field">
|
||||
<span>{copy().settlementCurrency}</span>
|
||||
<select
|
||||
value={utilityBillDrafts()[bill.id]?.currency ?? bill.currency}
|
||||
onChange={(event) =>
|
||||
setUtilityBillDrafts((current) => ({
|
||||
...current,
|
||||
[bill.id]: {
|
||||
...(current[bill.id] ?? {
|
||||
billName: bill.billName,
|
||||
amountMajor: minorToMajorString(BigInt(bill.amountMinor)),
|
||||
currency: bill.currency
|
||||
}),
|
||||
currency: event.currentTarget.value as 'USD' | 'GEL'
|
||||
}
|
||||
}))
|
||||
}
|
||||
>
|
||||
<option value="GEL">GEL</option>
|
||||
<option value="USD">USD</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="inline-actions">
|
||||
<button
|
||||
class="ghost-button"
|
||||
type="button"
|
||||
disabled={savingUtilityBillId() === bill.id}
|
||||
onClick={() => void handleUpdateUtilityBill(bill.id)}
|
||||
>
|
||||
{savingUtilityBillId() === bill.id
|
||||
? copy().savingUtilityBill
|
||||
: copy().saveUtilityBillAction}
|
||||
</button>
|
||||
<button
|
||||
class="ghost-button ghost-button--danger"
|
||||
type="button"
|
||||
disabled={deletingUtilityBillId() === bill.id}
|
||||
onClick={() => void handleDeleteUtilityBill(bill.id)}
|
||||
>
|
||||
{deletingUtilityBillId() === bill.id
|
||||
? copy().deletingUtilityBill
|
||||
: copy().deleteUtilityBillAction}
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
))
|
||||
) : (
|
||||
@@ -1515,9 +1694,9 @@ function App() {
|
||||
<strong>{copy().utilityCategoriesTitle}</strong>
|
||||
<span>{String(adminSettings()?.categories.length ?? 0)}</span>
|
||||
</header>
|
||||
<div class="balance-list admin-sublist">
|
||||
<div class="admin-sublist admin-sublist--plain">
|
||||
{adminSettings()?.categories.map((category) => (
|
||||
<article class="ledger-item">
|
||||
<article class="utility-bill-row">
|
||||
<header>
|
||||
<strong>{category.name}</strong>
|
||||
<span>{category.isActive ? 'ON' : 'OFF'}</span>
|
||||
@@ -1646,7 +1825,7 @@ function App() {
|
||||
</header>
|
||||
<div class="balance-list admin-sublist">
|
||||
{adminSettings()?.members.map((member) => (
|
||||
<article class="ledger-item">
|
||||
<article class="utility-bill-row">
|
||||
<header>
|
||||
<strong>{member.displayName}</strong>
|
||||
<span>{member.isAdmin ? copy().adminTag : copy().residentTag}</span>
|
||||
@@ -1709,7 +1888,7 @@ function App() {
|
||||
{pendingMembers().length === 0 ? (
|
||||
<p>{copy().pendingMembersEmpty}</p>
|
||||
) : (
|
||||
<div class="balance-list admin-sublist">
|
||||
<div class="admin-sublist admin-sublist--plain">
|
||||
{pendingMembers().map((member) => (
|
||||
<article class="ledger-item">
|
||||
<header>
|
||||
@@ -1751,7 +1930,7 @@ function App() {
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<div class="home-grid">
|
||||
<div class="home-grid home-grid--summary">
|
||||
<article class="stat-card">
|
||||
<span>{copy().totalDue}</span>
|
||||
<strong>
|
||||
@@ -1852,7 +2031,7 @@ function App() {
|
||||
</article>
|
||||
)}
|
||||
|
||||
<article class="balance-item">
|
||||
<article class="balance-item balance-item--wide">
|
||||
<header>
|
||||
<strong>{copy().latestActivityTitle}</strong>
|
||||
</header>
|
||||
@@ -1863,9 +2042,9 @@ function App() {
|
||||
data.ledger.length === 0 ? (
|
||||
<p>{copy().latestActivityEmpty}</p>
|
||||
) : (
|
||||
<div class="ledger-list">
|
||||
<div class="activity-list">
|
||||
{data.ledger.slice(0, 3).map((entry) => (
|
||||
<article class="ledger-item">
|
||||
<article class="activity-row">
|
||||
<header>
|
||||
<strong>{ledgerTitle(entry)}</strong>
|
||||
<span>{ledgerPrimaryAmount(entry)}</span>
|
||||
|
||||
@@ -97,6 +97,9 @@ export const dictionary = {
|
||||
utilityAmount: 'Utility amount',
|
||||
addUtilityBillAction: 'Add utility bill',
|
||||
savingUtilityBill: 'Saving utility bill…',
|
||||
saveUtilityBillAction: 'Save utility bill',
|
||||
deleteUtilityBillAction: 'Delete utility bill',
|
||||
deletingUtilityBill: 'Deleting utility bill…',
|
||||
utilityBillsEmpty: 'No utility bills recorded for this cycle yet.',
|
||||
rentAmount: 'Rent amount',
|
||||
rentDueDay: 'Rent due day',
|
||||
@@ -229,6 +232,9 @@ export const dictionary = {
|
||||
utilityAmount: 'Сумма коммуналки',
|
||||
addUtilityBillAction: 'Добавить коммунальный счёт',
|
||||
savingUtilityBill: 'Сохраняем счёт…',
|
||||
saveUtilityBillAction: 'Сохранить счёт',
|
||||
deleteUtilityBillAction: 'Удалить счёт',
|
||||
deletingUtilityBill: 'Удаляем счёт…',
|
||||
utilityBillsEmpty: 'Для этого цикла пока нет коммунальных счетов.',
|
||||
rentAmount: 'Сумма аренды',
|
||||
rentDueDay: 'День оплаты аренды',
|
||||
|
||||
@@ -231,14 +231,17 @@ button {
|
||||
.ledger-list,
|
||||
.home-grid,
|
||||
.admin-layout,
|
||||
.admin-sublist {
|
||||
.admin-sublist,
|
||||
.activity-list {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.balance-item,
|
||||
.ledger-item,
|
||||
.stat-card {
|
||||
.stat-card,
|
||||
.activity-row,
|
||||
.utility-bill-row {
|
||||
border: 1px solid rgb(255 255 255 / 0.08);
|
||||
border-radius: 18px;
|
||||
padding: 14px;
|
||||
@@ -253,7 +256,9 @@ button {
|
||||
}
|
||||
|
||||
.balance-item header,
|
||||
.ledger-item header {
|
||||
.ledger-item header,
|
||||
.activity-row header,
|
||||
.utility-bill-row header {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: start;
|
||||
@@ -262,13 +267,17 @@ button {
|
||||
}
|
||||
|
||||
.balance-item strong,
|
||||
.ledger-item strong {
|
||||
.ledger-item strong,
|
||||
.activity-row strong,
|
||||
.utility-bill-row strong {
|
||||
font-size: 1rem;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.balance-item p,
|
||||
.ledger-item p {
|
||||
.ledger-item p,
|
||||
.activity-row p,
|
||||
.utility-bill-row p {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
@@ -285,7 +294,7 @@ button {
|
||||
}
|
||||
|
||||
.home-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
@@ -360,6 +369,10 @@ button {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.admin-sublist--plain {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.admin-card--wide {
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -379,6 +392,19 @@ button {
|
||||
width: 100%;
|
||||
border: 1px solid rgb(255 255 255 / 0.12);
|
||||
border-radius: 14px;
|
||||
min-height: 48px;
|
||||
padding: 12px 14px;
|
||||
background: rgb(255 255 255 / 0.04);
|
||||
color: inherit;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.settings-field__value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 48px;
|
||||
border: 1px solid rgb(255 255 255 / 0.12);
|
||||
border-radius: 14px;
|
||||
padding: 12px 14px;
|
||||
background: rgb(255 255 255 / 0.04);
|
||||
color: inherit;
|
||||
@@ -399,10 +425,33 @@ button {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.ghost-button--danger {
|
||||
border-color: rgb(247 115 115 / 0.28);
|
||||
color: #ffc5c5;
|
||||
}
|
||||
|
||||
.panel--wide {
|
||||
min-height: 170px;
|
||||
}
|
||||
|
||||
.activity-row,
|
||||
.utility-bill-row {
|
||||
background: rgb(255 255 255 / 0.02);
|
||||
}
|
||||
|
||||
.activity-row header,
|
||||
.utility-bill-row header {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.activity-row strong {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.balance-item--wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
@media (min-width: 760px) {
|
||||
.shell {
|
||||
max-width: 920px;
|
||||
@@ -418,6 +467,10 @@ button {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.home-grid--summary {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.balance-breakdown {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
@@ -441,6 +494,10 @@ button {
|
||||
.admin-card--wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.balance-item--wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 980px) {
|
||||
@@ -448,3 +505,37 @@ button {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 759px) {
|
||||
.shell {
|
||||
padding: 18px 14px 28px;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.locale-switch {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.nav-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.balance-breakdown {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.admin-section__header {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.activity-row header,
|
||||
.ledger-item header,
|
||||
.utility-bill-row header,
|
||||
.balance-item header {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -668,3 +668,66 @@ export async function addMiniAppUtilityBill(
|
||||
|
||||
return payload.cycleState
|
||||
}
|
||||
|
||||
export async function updateMiniAppUtilityBill(
|
||||
initData: string,
|
||||
input: {
|
||||
billId: string
|
||||
billName: string
|
||||
amountMajor: string
|
||||
currency: 'USD' | 'GEL'
|
||||
}
|
||||
): Promise<MiniAppAdminCycleState> {
|
||||
const response = await fetch(`${apiBaseUrl()}/api/miniapp/admin/utility-bills/update`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
initData,
|
||||
...input
|
||||
})
|
||||
})
|
||||
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean
|
||||
authorized?: boolean
|
||||
cycleState?: MiniAppAdminCycleState
|
||||
error?: string
|
||||
}
|
||||
|
||||
if (!response.ok || !payload.authorized || !payload.cycleState) {
|
||||
throw new Error(payload.error ?? 'Failed to update utility bill')
|
||||
}
|
||||
|
||||
return payload.cycleState
|
||||
}
|
||||
|
||||
export async function deleteMiniAppUtilityBill(
|
||||
initData: string,
|
||||
billId: string
|
||||
): Promise<MiniAppAdminCycleState> {
|
||||
const response = await fetch(`${apiBaseUrl()}/api/miniapp/admin/utility-bills/delete`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
initData,
|
||||
billId
|
||||
})
|
||||
})
|
||||
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean
|
||||
authorized?: boolean
|
||||
cycleState?: MiniAppAdminCycleState
|
||||
error?: string
|
||||
}
|
||||
|
||||
if (!response.ok || !payload.authorized || !payload.cycleState) {
|
||||
throw new Error(payload.error ?? 'Failed to delete utility bill')
|
||||
}
|
||||
|
||||
return payload.cycleState
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user