feat(miniapp): show topic routing in admin settings

This commit is contained in:
2026-03-10 17:18:53 +04:00
parent c292518760
commit 3168356431
7 changed files with 109 additions and 6 deletions

View File

@@ -41,7 +41,14 @@ function onboardingRepository(): HouseholdConfigurationRepository {
}) satisfies HouseholdTopicBindingRecord, }) satisfies HouseholdTopicBindingRecord,
getHouseholdTopicBinding: async () => null, getHouseholdTopicBinding: async () => null,
findHouseholdTopicByTelegramContext: async () => null, findHouseholdTopicByTelegramContext: async () => null,
listHouseholdTopicBindings: async () => [], listHouseholdTopicBindings: async () => [
{
householdId: household.householdId,
role: 'purchase',
telegramThreadId: '2',
topicName: 'Общие покупки'
}
],
listReminderTargets: async () => [], listReminderTargets: async () => [],
upsertHouseholdJoinToken: async (input) => ({ upsertHouseholdJoinToken: async (input) => ({
householdId: household.householdId, householdId: household.householdId,
@@ -381,6 +388,14 @@ describe('createMiniAppSettingsHandler', () => {
utilitiesReminderDay: 3, utilitiesReminderDay: 3,
timezone: 'Asia/Tbilisi' timezone: 'Asia/Tbilisi'
}, },
topics: [
{
householdId: 'household-1',
role: 'purchase',
telegramThreadId: '2',
topicName: 'Общие покупки'
}
],
categories: [], categories: [],
members: [ members: [
{ {

View File

@@ -384,6 +384,7 @@ export function createMiniAppSettingsHandler(options: {
ok: true, ok: true,
authorized: true, authorized: true,
settings: serializeBillingSettings(result.settings), settings: serializeBillingSettings(result.settings),
topics: result.topics,
categories: result.categories, categories: result.categories,
members: result.members members: result.members
}, },

View File

@@ -272,6 +272,19 @@ function App() {
: copy().paymentLedgerRent : copy().paymentLedgerRent
} }
function topicRoleLabel(role: 'purchase' | 'feedback' | 'reminders' | 'payments'): string {
switch (role) {
case 'purchase':
return copy().topicPurchase
case 'feedback':
return copy().topicFeedback
case 'reminders':
return copy().topicReminders
case 'payments':
return copy().topicPayments
}
}
async function loadDashboard(initData: string) { async function loadDashboard(initData: string) {
try { try {
setDashboard(await fetchMiniAppDashboard(initData)) setDashboard(await fetchMiniAppDashboard(initData))
@@ -1393,6 +1406,33 @@ function App() {
</button> </button>
</div> </div>
</article> </article>
<article class="balance-item">
<header>
<strong>{copy().topicBindingsTitle}</strong>
<span>{String(adminSettings()?.topics.length ?? 0)}/4</span>
</header>
<p>{copy().topicBindingsBody}</p>
<div class="balance-list admin-sublist">
{(['purchase', 'feedback', 'reminders', 'payments'] as const).map((role) => {
const binding = adminSettings()?.topics.find((topic) => topic.role === role)
return (
<article class="ledger-item">
<header>
<strong>{topicRoleLabel(role)}</strong>
<span>{binding ? copy().topicBound : copy().topicUnbound}</span>
</header>
<p>
{binding
? `${binding.topicName ?? `Topic #${binding.telegramThreadId}`} · #${binding.telegramThreadId}`
: copy().topicUnbound}
</p>
</article>
)
})}
</div>
</article>
</div> </div>
</section> </section>

View File

@@ -71,6 +71,15 @@ export const dictionary = {
latestActivityEmpty: 'Recent utility and purchase entries will appear here.', latestActivityEmpty: 'Recent utility and purchase entries will appear here.',
householdSettingsTitle: 'Household settings', householdSettingsTitle: 'Household settings',
householdSettingsBody: 'Control household defaults and approve roommates who requested access.', householdSettingsBody: 'Control household defaults and approve roommates who requested access.',
topicBindingsTitle: 'Topic bindings',
topicBindingsBody:
'Review which Telegram topics are currently connected for purchases, feedback, reminders, and payments.',
topicPurchase: 'Purchases',
topicFeedback: 'Feedback',
topicReminders: 'Reminders',
topicPayments: 'Payments',
topicBound: 'Bound',
topicUnbound: 'Unbound',
billingSettingsTitle: 'Billing settings', billingSettingsTitle: 'Billing settings',
settlementCurrency: 'Settlement currency', settlementCurrency: 'Settlement currency',
billingCycleTitle: 'Current billing cycle', billingCycleTitle: 'Current billing cycle',
@@ -194,6 +203,15 @@ export const dictionary = {
latestActivityEmpty: 'Здесь появятся последние коммунальные платежи и покупки.', latestActivityEmpty: 'Здесь появятся последние коммунальные платежи и покупки.',
householdSettingsTitle: 'Настройки household', householdSettingsTitle: 'Настройки household',
householdSettingsBody: 'Здесь можно менять язык household и подтверждать новых соседей.', householdSettingsBody: 'Здесь можно менять язык household и подтверждать новых соседей.',
topicBindingsTitle: 'Привязанные топики',
topicBindingsBody:
'Проверь, какие Telegram-топики сейчас подключены для покупок, обратной связи, напоминаний и оплат.',
topicPurchase: 'Покупки',
topicFeedback: 'Обратная связь',
topicReminders: 'Напоминания',
topicPayments: 'Оплаты',
topicBound: 'Привязан',
topicUnbound: 'Не привязан',
billingSettingsTitle: 'Настройки биллинга', billingSettingsTitle: 'Настройки биллинга',
settlementCurrency: 'Валюта расчёта', settlementCurrency: 'Валюта расчёта',
billingCycleTitle: 'Текущий billing cycle', billingCycleTitle: 'Текущий billing cycle',

View File

@@ -64,6 +64,12 @@ export interface MiniAppUtilityCategory {
isActive: boolean isActive: boolean
} }
export interface MiniAppTopicBinding {
role: 'purchase' | 'feedback' | 'reminders' | 'payments'
telegramThreadId: string
topicName: string | null
}
export interface MiniAppDashboard { export interface MiniAppDashboard {
period: string period: string
currency: 'USD' | 'GEL' currency: 'USD' | 'GEL'
@@ -104,6 +110,7 @@ export interface MiniAppDashboard {
export interface MiniAppAdminSettingsPayload { export interface MiniAppAdminSettingsPayload {
settings: MiniAppBillingSettings settings: MiniAppBillingSettings
topics: readonly MiniAppTopicBinding[]
categories: readonly MiniAppUtilityCategory[] categories: readonly MiniAppUtilityCategory[]
members: readonly MiniAppMember[] members: readonly MiniAppMember[]
} }
@@ -349,6 +356,7 @@ export async function fetchMiniAppAdminSettings(
ok: boolean ok: boolean
authorized?: boolean authorized?: boolean
settings?: MiniAppBillingSettings settings?: MiniAppBillingSettings
topics?: MiniAppTopicBinding[]
categories?: MiniAppUtilityCategory[] categories?: MiniAppUtilityCategory[]
members?: MiniAppMember[] members?: MiniAppMember[]
error?: string error?: string
@@ -358,6 +366,7 @@ export async function fetchMiniAppAdminSettings(
!response.ok || !response.ok ||
!payload.authorized || !payload.authorized ||
!payload.settings || !payload.settings ||
!payload.topics ||
!payload.categories || !payload.categories ||
!payload.members !payload.members
) { ) {
@@ -366,6 +375,7 @@ export async function fetchMiniAppAdminSettings(
return { return {
settings: payload.settings, settings: payload.settings,
topics: payload.topics,
categories: payload.categories, categories: payload.categories,
members: payload.members members: payload.members
} }

View File

@@ -27,7 +27,14 @@ function repository(): HouseholdConfigurationRepository {
}), }),
getHouseholdTopicBinding: async () => null, getHouseholdTopicBinding: async () => null,
findHouseholdTopicByTelegramContext: async () => null, findHouseholdTopicByTelegramContext: async () => null,
listHouseholdTopicBindings: async () => [], listHouseholdTopicBindings: async () => [
{
householdId: 'household-1',
role: 'purchase',
telegramThreadId: '2',
topicName: 'Общие покупки'
}
],
listReminderTargets: async () => [], listReminderTargets: async () => [],
upsertHouseholdJoinToken: async () => ({ upsertHouseholdJoinToken: async () => ({
householdId: 'household-1', householdId: 'household-1',
@@ -167,7 +174,7 @@ function repository(): HouseholdConfigurationRepository {
} }
describe('createMiniAppAdminService', () => { describe('createMiniAppAdminService', () => {
test('returns billing settings, utility categories, and members for admins', async () => { test('returns billing settings, topic bindings, utility categories, and members for admins', async () => {
const service = createMiniAppAdminService(repository()) const service = createMiniAppAdminService(repository())
const result = await service.getSettings({ const result = await service.getSettings({
@@ -188,6 +195,14 @@ describe('createMiniAppAdminService', () => {
utilitiesReminderDay: 3, utilitiesReminderDay: 3,
timezone: 'Asia/Tbilisi' timezone: 'Asia/Tbilisi'
}, },
topics: [
{
householdId: 'household-1',
role: 'purchase',
telegramThreadId: '2',
topicName: 'Общие покупки'
}
],
categories: [], categories: [],
members: [] members: []
}) })

View File

@@ -3,6 +3,7 @@ import type {
HouseholdConfigurationRepository, HouseholdConfigurationRepository,
HouseholdMemberRecord, HouseholdMemberRecord,
HouseholdPendingMemberRecord, HouseholdPendingMemberRecord,
HouseholdTopicBindingRecord,
HouseholdUtilityCategoryRecord HouseholdUtilityCategoryRecord
} from '@household/ports' } from '@household/ports'
import { Money, type CurrencyCode } from '@household/domain' import { Money, type CurrencyCode } from '@household/domain'
@@ -27,6 +28,7 @@ export interface MiniAppAdminService {
settings: HouseholdBillingSettingsRecord settings: HouseholdBillingSettingsRecord
categories: readonly HouseholdUtilityCategoryRecord[] categories: readonly HouseholdUtilityCategoryRecord[]
members: readonly HouseholdMemberRecord[] members: readonly HouseholdMemberRecord[]
topics: readonly HouseholdTopicBindingRecord[]
} }
| { | {
status: 'rejected' status: 'rejected'
@@ -138,17 +140,19 @@ export function createMiniAppAdminService(
} }
} }
const [settings, categories, members] = await Promise.all([ const [settings, categories, members, topics] = await Promise.all([
repository.getHouseholdBillingSettings(input.householdId), repository.getHouseholdBillingSettings(input.householdId),
repository.listHouseholdUtilityCategories(input.householdId), repository.listHouseholdUtilityCategories(input.householdId),
repository.listHouseholdMembers(input.householdId) repository.listHouseholdMembers(input.householdId),
repository.listHouseholdTopicBindings(input.householdId)
]) ])
return { return {
status: 'ok', status: 'ok',
settings, settings,
categories, categories,
members members,
topics
} }
}, },