feat(miniapp): add member rent weight controls

This commit is contained in:
2026-03-10 02:48:12 +04:00
parent 6a04b9d7f5
commit 4b4f7d46e5
10 changed files with 377 additions and 1 deletions

View File

@@ -13,6 +13,7 @@ import {
joinMiniAppHousehold,
openMiniAppBillingCycle,
promoteMiniAppMember,
updateMiniAppMemberRentWeight,
type MiniAppAdminCycleState,
type MiniAppAdminSettingsPayload,
updateMiniAppLocalePreference,
@@ -143,6 +144,8 @@ function App() {
const [joining, setJoining] = createSignal(false)
const [approvingTelegramUserId, setApprovingTelegramUserId] = createSignal<string | null>(null)
const [promotingMemberId, setPromotingMemberId] = createSignal<string | null>(null)
const [savingRentWeightMemberId, setSavingRentWeightMemberId] = createSignal<string | null>(null)
const [rentWeightDrafts, setRentWeightDrafts] = createSignal<Record<string, string>>({})
const [savingMemberLocale, setSavingMemberLocale] = createSignal(false)
const [savingHouseholdLocale, setSavingHouseholdLocale] = createSignal(false)
const [savingBillingSettings, setSavingBillingSettings] = createSignal(false)
@@ -212,6 +215,11 @@ function App() {
try {
const payload = await fetchMiniAppAdminSettings(initData)
setAdminSettings(payload)
setRentWeightDrafts(
Object.fromEntries(
payload.members.map((member) => [member.id, String(member.rentShareWeight)])
)
)
setCycleForm((current) => ({
...current,
utilityCategorySlug:
@@ -721,11 +729,50 @@ function App() {
}
: 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':
@@ -1223,6 +1270,32 @@ function App() {
<strong>{member.displayName}</strong>
<span>{member.isAdmin ? copy().adminTag : copy().residentTag}</span>
</header>
<label class="settings-field settings-field--wide">
<span>{copy().rentWeightLabel}</span>
<input
inputmode="numeric"
value={rentWeightDrafts()[member.id] ?? String(member.rentShareWeight)}
onInput={(event) =>
setRentWeightDrafts((current) => ({
...current,
[member.id]: event.currentTarget.value
}))
}
/>
</label>
<button
class="ghost-button"
type="button"
disabled={
savingRentWeightMemberId() === member.id ||
Number(rentWeightDrafts()[member.id] ?? member.rentShareWeight) <= 0
}
onClick={() => void handleSaveRentWeight(member.id)}
>
{savingRentWeightMemberId() === member.id
? copy().savingRentWeight
: copy().saveRentWeightAction}
</button>
{!member.isAdmin ? (
<button
class="ghost-button"

View File

@@ -88,6 +88,9 @@ export const dictionary = {
savingCategory: 'Saving…',
adminsTitle: 'Admins',
adminsBody: 'Promote trusted household members so they can manage billing and approvals.',
rentWeightLabel: 'Rent weight',
saveRentWeightAction: 'Save rent weight',
savingRentWeight: 'Saving weight…',
promoteAdminAction: 'Promote to admin',
promotingAdmin: 'Promoting…',
residentHouseTitle: 'Household access',
@@ -192,6 +195,9 @@ export const dictionary = {
adminsTitle: 'Админы',
adminsBody:
'Повышай доверенных участников, чтобы они могли управлять биллингом и подтверждениями.',
rentWeightLabel: 'Вес аренды',
saveRentWeightAction: 'Сохранить вес аренды',
savingRentWeight: 'Сохраняем вес…',
promoteAdminAction: 'Сделать админом',
promotingAdmin: 'Повышаем…',
residentHouseTitle: 'Доступ к household',

View File

@@ -37,6 +37,7 @@ export interface MiniAppPendingMember {
export interface MiniAppMember {
id: string
displayName: string
rentShareWeight: number
isAdmin: boolean
}
@@ -452,6 +453,37 @@ export async function promoteMiniAppMember(
return payload.member
}
export async function updateMiniAppMemberRentWeight(
initData: string,
memberId: string,
rentShareWeight: number
): Promise<MiniAppMember> {
const response = await fetch(`${apiBaseUrl()}/api/miniapp/admin/members/rent-weight`, {
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
initData,
memberId,
rentShareWeight
})
})
const payload = (await response.json()) as {
ok: boolean
authorized?: boolean
member?: MiniAppMember
error?: string
}
if (!response.ok || !payload.member) {
throw new Error(payload.error ?? 'Failed to update member rent weight')
}
return payload.member
}
export async function fetchMiniAppBillingCycle(initData: string): Promise<MiniAppAdminCycleState> {
const response = await fetch(`${apiBaseUrl()}/api/miniapp/admin/billing-cycle`, {
method: 'POST',