mirror of
https://github.com/whekin/household-bot.git
synced 2026-04-01 01:54:03 +00:00
feat(miniapp): add pending member admin approval
This commit is contained in:
@@ -2,10 +2,13 @@ import { Match, Switch, createMemo, createSignal, onMount, type JSX } from 'soli
|
||||
|
||||
import { dictionary, type Locale } from './i18n'
|
||||
import {
|
||||
approveMiniAppPendingMember,
|
||||
fetchMiniAppDashboard,
|
||||
fetchMiniAppPendingMembers,
|
||||
fetchMiniAppSession,
|
||||
joinMiniAppHousehold,
|
||||
type MiniAppDashboard
|
||||
type MiniAppDashboard,
|
||||
type MiniAppPendingMember
|
||||
} from './miniapp-api'
|
||||
import { getTelegramWebApp } from './telegram-webapp'
|
||||
|
||||
@@ -106,7 +109,9 @@ function App() {
|
||||
})
|
||||
const [activeNav, setActiveNav] = createSignal<NavigationKey>('home')
|
||||
const [dashboard, setDashboard] = createSignal<MiniAppDashboard | null>(null)
|
||||
const [pendingMembers, setPendingMembers] = createSignal<readonly MiniAppPendingMember[]>([])
|
||||
const [joining, setJoining] = createSignal(false)
|
||||
const [approvingTelegramUserId, setApprovingTelegramUserId] = createSignal<string | null>(null)
|
||||
|
||||
const copy = createMemo(() => dictionary[locale()])
|
||||
const onboardingSession = createMemo(() => {
|
||||
@@ -135,6 +140,18 @@ function App() {
|
||||
}
|
||||
}
|
||||
|
||||
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 bootstrap() {
|
||||
setLocale(detectLocale())
|
||||
|
||||
@@ -183,6 +200,9 @@ function App() {
|
||||
})
|
||||
|
||||
await loadDashboard(initData)
|
||||
if (payload.member.isAdmin) {
|
||||
await loadPendingMembers(initData)
|
||||
}
|
||||
} catch {
|
||||
if (import.meta.env.DEV) {
|
||||
setSession(demoSession)
|
||||
@@ -229,6 +249,14 @@ function App() {
|
||||
}
|
||||
]
|
||||
})
|
||||
setPendingMembers([
|
||||
{
|
||||
telegramUserId: '555777',
|
||||
displayName: 'Mia',
|
||||
username: 'mia',
|
||||
languageCode: 'ru'
|
||||
}
|
||||
])
|
||||
return
|
||||
}
|
||||
|
||||
@@ -263,6 +291,9 @@ function App() {
|
||||
telegramUser: payload.telegramUser
|
||||
})
|
||||
await loadDashboard(initData)
|
||||
if (payload.member.isAdmin) {
|
||||
await loadPendingMembers(initData)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -290,6 +321,24 @@ function App() {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
const renderPanel = () => {
|
||||
switch (activeNav()) {
|
||||
case 'balances':
|
||||
@@ -345,7 +394,47 @@ function App() {
|
||||
</div>
|
||||
)
|
||||
case 'house':
|
||||
return copy().houseEmpty
|
||||
return readySession()?.member.isAdmin ? (
|
||||
<div class="balance-list">
|
||||
<article class="balance-item">
|
||||
<header>
|
||||
<strong>{copy().pendingMembersTitle}</strong>
|
||||
</header>
|
||||
<p>{copy().pendingMembersBody}</p>
|
||||
</article>
|
||||
{pendingMembers().length === 0 ? (
|
||||
<article class="balance-item">
|
||||
<p>{copy().pendingMembersEmpty}</p>
|
||||
</article>
|
||||
) : (
|
||||
pendingMembers().map((member) => (
|
||||
<article class="balance-item">
|
||||
<header>
|
||||
<strong>{member.displayName}</strong>
|
||||
<span>{member.telegramUserId}</span>
|
||||
</header>
|
||||
<p>
|
||||
{member.username
|
||||
? copy().pendingMemberHandle.replace('{username}', member.username)
|
||||
: (member.languageCode ?? 'Telegram')}
|
||||
</p>
|
||||
<button
|
||||
class="ghost-button"
|
||||
type="button"
|
||||
disabled={approvingTelegramUserId() === member.telegramUserId}
|
||||
onClick={() => void handleApprovePendingMember(member.telegramUserId)}
|
||||
>
|
||||
{approvingTelegramUserId() === member.telegramUserId
|
||||
? copy().approvingMember
|
||||
: copy().approveMemberAction}
|
||||
</button>
|
||||
</article>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
copy().houseEmpty
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<ShowDashboard
|
||||
@@ -516,7 +605,7 @@ function App() {
|
||||
<article class="panel panel--wide">
|
||||
<p class="eyebrow">{copy().summaryTitle}</p>
|
||||
<h3>{readySession()?.member.displayName}</h3>
|
||||
<p>{renderPanel()}</p>
|
||||
<div>{renderPanel()}</div>
|
||||
</article>
|
||||
|
||||
<article class="panel">
|
||||
|
||||
@@ -53,6 +53,13 @@ export const dictionary = {
|
||||
sectionTitle: 'Ready for the next features',
|
||||
sectionBody:
|
||||
'This layout is intentionally narrow and mobile-first so it behaves well inside the Telegram webview.',
|
||||
pendingMembersTitle: 'Pending members',
|
||||
pendingMembersBody:
|
||||
'Approve roommates here after they request access from the group join flow.',
|
||||
pendingMembersEmpty: 'No pending member requests right now.',
|
||||
approveMemberAction: 'Approve',
|
||||
approvingMember: 'Approving…',
|
||||
pendingMemberHandle: '@{username}',
|
||||
balancesEmpty: 'Balances will appear here once the dashboard API lands.',
|
||||
ledgerEmpty: 'Ledger entries will appear here after the finance view is connected.',
|
||||
houseEmpty: 'House rules, Wi-Fi info, and practical notes will live here.'
|
||||
@@ -109,6 +116,13 @@ export const dictionary = {
|
||||
sectionTitle: 'Основа готова для следующих функций',
|
||||
sectionBody:
|
||||
'Этот layout специально сделан узким и mobile-first, чтобы хорошо жить внутри Telegram webview.',
|
||||
pendingMembersTitle: 'Ожидающие участники',
|
||||
pendingMembersBody:
|
||||
'Подтверждай соседей здесь после того, как они отправят заявку через кнопку подключения.',
|
||||
pendingMembersEmpty: 'Сейчас нет ожидающих заявок.',
|
||||
approveMemberAction: 'Подтвердить',
|
||||
approvingMember: 'Подтверждаем…',
|
||||
pendingMemberHandle: '@{username}',
|
||||
balancesEmpty: 'Баланс появится здесь, когда подключим dashboard API.',
|
||||
ledgerEmpty: 'Записи леджера появятся здесь после подключения finance view.',
|
||||
houseEmpty: 'Правила дома, Wi-Fi и полезные инструкции будут здесь.'
|
||||
|
||||
@@ -17,6 +17,13 @@ export interface MiniAppSession {
|
||||
}
|
||||
}
|
||||
|
||||
export interface MiniAppPendingMember {
|
||||
telegramUserId: string
|
||||
displayName: string
|
||||
username: string | null
|
||||
languageCode: string | null
|
||||
}
|
||||
|
||||
export interface MiniAppDashboard {
|
||||
period: string
|
||||
currency: 'USD' | 'GEL'
|
||||
@@ -159,3 +166,56 @@ export async function fetchMiniAppDashboard(initData: string): Promise<MiniAppDa
|
||||
|
||||
return payload.dashboard
|
||||
}
|
||||
|
||||
export async function fetchMiniAppPendingMembers(
|
||||
initData: string
|
||||
): Promise<readonly MiniAppPendingMember[]> {
|
||||
const response = await fetch(`${apiBaseUrl()}/api/miniapp/admin/pending-members`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
initData
|
||||
})
|
||||
})
|
||||
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean
|
||||
authorized?: boolean
|
||||
members?: MiniAppPendingMember[]
|
||||
error?: string
|
||||
}
|
||||
|
||||
if (!response.ok || !payload.authorized || !payload.members) {
|
||||
throw new Error(payload.error ?? 'Failed to load pending members')
|
||||
}
|
||||
|
||||
return payload.members
|
||||
}
|
||||
|
||||
export async function approveMiniAppPendingMember(
|
||||
initData: string,
|
||||
pendingTelegramUserId: string
|
||||
): Promise<void> {
|
||||
const response = await fetch(`${apiBaseUrl()}/api/miniapp/admin/approve-member`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
initData,
|
||||
pendingTelegramUserId
|
||||
})
|
||||
})
|
||||
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean
|
||||
authorized?: boolean
|
||||
error?: string
|
||||
}
|
||||
|
||||
if (!response.ok || !payload.authorized) {
|
||||
throw new Error(payload.error ?? 'Failed to approve member')
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user