mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 21:04:03 +00:00
feat(miniapp): add household general settings
This commit is contained in:
@@ -469,6 +469,7 @@ describe('createMiniAppSettingsHandler', () => {
|
|||||||
expect(await response.json()).toEqual({
|
expect(await response.json()).toEqual({
|
||||||
ok: true,
|
ok: true,
|
||||||
authorized: true,
|
authorized: true,
|
||||||
|
householdName: 'Kojori House',
|
||||||
settings: {
|
settings: {
|
||||||
householdId: 'household-1',
|
householdId: 'household-1',
|
||||||
settlementCurrency: 'GEL',
|
settlementCurrency: 'GEL',
|
||||||
@@ -570,6 +571,7 @@ describe('createMiniAppUpdateSettingsHandler', () => {
|
|||||||
expect(await response.json()).toEqual({
|
expect(await response.json()).toEqual({
|
||||||
ok: true,
|
ok: true,
|
||||||
authorized: true,
|
authorized: true,
|
||||||
|
householdName: 'Kojori House',
|
||||||
settings: {
|
settings: {
|
||||||
householdId: 'household-1',
|
householdId: 'household-1',
|
||||||
settlementCurrency: 'GEL',
|
settlementCurrency: 'GEL',
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ async function readApprovalPayload(request: Request): Promise<{
|
|||||||
|
|
||||||
async function readSettingsUpdatePayload(request: Request): Promise<{
|
async function readSettingsUpdatePayload(request: Request): Promise<{
|
||||||
initData: string
|
initData: string
|
||||||
|
householdName?: string
|
||||||
settlementCurrency?: string
|
settlementCurrency?: string
|
||||||
paymentBalanceAdjustmentPolicy?: string
|
paymentBalanceAdjustmentPolicy?: string
|
||||||
rentAmountMajor?: string
|
rentAmountMajor?: string
|
||||||
@@ -69,6 +70,7 @@ async function readSettingsUpdatePayload(request: Request): Promise<{
|
|||||||
|
|
||||||
const text = await clonedRequest.text()
|
const text = await clonedRequest.text()
|
||||||
let parsed: {
|
let parsed: {
|
||||||
|
householdName?: string
|
||||||
settlementCurrency?: string
|
settlementCurrency?: string
|
||||||
paymentBalanceAdjustmentPolicy?: string
|
paymentBalanceAdjustmentPolicy?: string
|
||||||
rentAmountMajor?: string
|
rentAmountMajor?: string
|
||||||
@@ -99,6 +101,11 @@ async function readSettingsUpdatePayload(request: Request): Promise<{
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
initData: payload.initData,
|
initData: payload.initData,
|
||||||
|
...(typeof parsed.householdName === 'string'
|
||||||
|
? {
|
||||||
|
householdName: parsed.householdName
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
...(typeof parsed.rentAmountMajor === 'string'
|
...(typeof parsed.rentAmountMajor === 'string'
|
||||||
? {
|
? {
|
||||||
rentAmountMajor: parsed.rentAmountMajor
|
rentAmountMajor: parsed.rentAmountMajor
|
||||||
@@ -545,6 +552,7 @@ export function createMiniAppSettingsHandler(options: {
|
|||||||
{
|
{
|
||||||
ok: true,
|
ok: true,
|
||||||
authorized: true,
|
authorized: true,
|
||||||
|
householdName: result.householdName,
|
||||||
settings: serializeBillingSettings(result.settings),
|
settings: serializeBillingSettings(result.settings),
|
||||||
assistantConfig: serializeAssistantConfig(result.assistantConfig),
|
assistantConfig: serializeAssistantConfig(result.assistantConfig),
|
||||||
topics: result.topics,
|
topics: result.topics,
|
||||||
@@ -620,6 +628,11 @@ export function createMiniAppUpdateSettingsHandler(options: {
|
|||||||
const result = await options.miniAppAdminService.updateSettings({
|
const result = await options.miniAppAdminService.updateSettings({
|
||||||
householdId: session.member.householdId,
|
householdId: session.member.householdId,
|
||||||
actorIsAdmin: session.member.isAdmin,
|
actorIsAdmin: session.member.isAdmin,
|
||||||
|
...(payload.householdName !== undefined
|
||||||
|
? {
|
||||||
|
householdName: payload.householdName
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
...(payload.settlementCurrency
|
...(payload.settlementCurrency
|
||||||
? {
|
? {
|
||||||
settlementCurrency: payload.settlementCurrency
|
settlementCurrency: payload.settlementCurrency
|
||||||
@@ -675,6 +688,7 @@ export function createMiniAppUpdateSettingsHandler(options: {
|
|||||||
{
|
{
|
||||||
ok: true,
|
ok: true,
|
||||||
authorized: true,
|
authorized: true,
|
||||||
|
householdName: result.householdName,
|
||||||
settings: serializeBillingSettings(result.settings),
|
settings: serializeBillingSettings(result.settings),
|
||||||
assistantConfig: serializeAssistantConfig(result.assistantConfig)
|
assistantConfig: serializeAssistantConfig(result.assistantConfig)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ export interface MiniAppSessionResult {
|
|||||||
member?: {
|
member?: {
|
||||||
id: string
|
id: string
|
||||||
householdId: string
|
householdId: string
|
||||||
|
householdName: string
|
||||||
displayName: string
|
displayName: string
|
||||||
status: 'active' | 'away' | 'left'
|
status: 'active' | 'away' | 'left'
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ type SessionState =
|
|||||||
mode: 'live' | 'demo'
|
mode: 'live' | 'demo'
|
||||||
member: {
|
member: {
|
||||||
id: string
|
id: string
|
||||||
|
householdName: string
|
||||||
displayName: string
|
displayName: string
|
||||||
status: 'active' | 'away' | 'left'
|
status: 'active' | 'away' | 'left'
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
@@ -348,6 +349,7 @@ function App() {
|
|||||||
const [testingRolePreview, setTestingRolePreview] = createSignal<TestingRolePreview | null>(null)
|
const [testingRolePreview, setTestingRolePreview] = createSignal<TestingRolePreview | null>(null)
|
||||||
const [addingPayment, setAddingPayment] = createSignal(false)
|
const [addingPayment, setAddingPayment] = createSignal(false)
|
||||||
const [billingForm, setBillingForm] = createSignal({
|
const [billingForm, setBillingForm] = createSignal({
|
||||||
|
householdName: '',
|
||||||
settlementCurrency: 'GEL' as 'USD' | 'GEL',
|
settlementCurrency: 'GEL' as 'USD' | 'GEL',
|
||||||
paymentBalanceAdjustmentPolicy: 'utilities' as 'utilities' | 'rent' | 'separate',
|
paymentBalanceAdjustmentPolicy: 'utilities' as 'utilities' | 'rent' | 'separate',
|
||||||
rentAmountMajor: '',
|
rentAmountMajor: '',
|
||||||
@@ -762,6 +764,7 @@ function App() {
|
|||||||
''
|
''
|
||||||
}))
|
}))
|
||||||
setBillingForm({
|
setBillingForm({
|
||||||
|
householdName: payload.householdName,
|
||||||
settlementCurrency: payload.settings.settlementCurrency,
|
settlementCurrency: payload.settings.settlementCurrency,
|
||||||
paymentBalanceAdjustmentPolicy: payload.settings.paymentBalanceAdjustmentPolicy,
|
paymentBalanceAdjustmentPolicy: payload.settings.paymentBalanceAdjustmentPolicy,
|
||||||
rentAmountMajor: payload.settings.rentAmountMinor
|
rentAmountMajor: payload.settings.rentAmountMinor
|
||||||
@@ -883,6 +886,7 @@ function App() {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
setBillingForm({
|
setBillingForm({
|
||||||
|
householdName: demoAdminSettings.householdName,
|
||||||
settlementCurrency: demoAdminSettings.settings.settlementCurrency,
|
settlementCurrency: demoAdminSettings.settings.settlementCurrency,
|
||||||
paymentBalanceAdjustmentPolicy: demoAdminSettings.settings.paymentBalanceAdjustmentPolicy,
|
paymentBalanceAdjustmentPolicy: demoAdminSettings.settings.paymentBalanceAdjustmentPolicy,
|
||||||
rentAmountMajor: demoAdminSettings.settings.rentAmountMinor
|
rentAmountMajor: demoAdminSettings.settings.rentAmountMinor
|
||||||
@@ -1200,7 +1204,7 @@ function App() {
|
|||||||
setSavingBillingSettings(true)
|
setSavingBillingSettings(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { settings, assistantConfig } = await updateMiniAppBillingSettings(
|
const { householdName, settings, assistantConfig } = await updateMiniAppBillingSettings(
|
||||||
initData,
|
initData,
|
||||||
billingForm()
|
billingForm()
|
||||||
)
|
)
|
||||||
@@ -1208,11 +1212,27 @@ function App() {
|
|||||||
current
|
current
|
||||||
? {
|
? {
|
||||||
...current,
|
...current,
|
||||||
|
householdName,
|
||||||
settings,
|
settings,
|
||||||
assistantConfig
|
assistantConfig
|
||||||
}
|
}
|
||||||
: current
|
: current
|
||||||
)
|
)
|
||||||
|
setBillingForm((current) => ({
|
||||||
|
...current,
|
||||||
|
householdName
|
||||||
|
}))
|
||||||
|
setSession((current) =>
|
||||||
|
current.status === 'ready'
|
||||||
|
? {
|
||||||
|
...current,
|
||||||
|
member: {
|
||||||
|
...current.member,
|
||||||
|
householdName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: current
|
||||||
|
)
|
||||||
setCycleForm((current) => ({
|
setCycleForm((current) => ({
|
||||||
...current,
|
...current,
|
||||||
rentCurrency: settings.rentCurrency,
|
rentCurrency: settings.rentCurrency,
|
||||||
@@ -1992,6 +2012,8 @@ function App() {
|
|||||||
locale={locale()}
|
locale={locale()}
|
||||||
readyIsAdmin={effectiveIsAdmin()}
|
readyIsAdmin={effectiveIsAdmin()}
|
||||||
householdDefaultLocale={readySession()?.member.householdDefaultLocale ?? 'en'}
|
householdDefaultLocale={readySession()?.member.householdDefaultLocale ?? 'en'}
|
||||||
|
householdName={readySession()?.member.householdName ?? billingForm().householdName}
|
||||||
|
profileDisplayName={readySession()?.member.displayName ?? displayNameDraft()}
|
||||||
dashboard={dashboard()}
|
dashboard={dashboard()}
|
||||||
adminSettings={adminSettings()}
|
adminSettings={adminSettings()}
|
||||||
cycleState={cycleState()}
|
cycleState={cycleState()}
|
||||||
@@ -2030,6 +2052,7 @@ function App() {
|
|||||||
resolvedMemberAbsencePolicy(memberId, status)
|
resolvedMemberAbsencePolicy(memberId, status)
|
||||||
}
|
}
|
||||||
onChangeHouseholdLocale={handleHouseholdLocaleChange}
|
onChangeHouseholdLocale={handleHouseholdLocaleChange}
|
||||||
|
onOpenProfileEditor={() => setProfileEditorOpen(true)}
|
||||||
onOpenCycleModal={() => setCycleRentOpen(true)}
|
onOpenCycleModal={() => setCycleRentOpen(true)}
|
||||||
onCloseCycleModal={() => setCycleRentOpen(false)}
|
onCloseCycleModal={() => setCycleRentOpen(false)}
|
||||||
onSaveCycleRent={handleSaveCycleRent}
|
onSaveCycleRent={handleSaveCycleRent}
|
||||||
@@ -2061,6 +2084,12 @@ function App() {
|
|||||||
settlementCurrency: value
|
settlementCurrency: value
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
onBillingHouseholdNameChange={(value) =>
|
||||||
|
setBillingForm((current) => ({
|
||||||
|
...current,
|
||||||
|
householdName: value
|
||||||
|
}))
|
||||||
|
}
|
||||||
onBillingAdjustmentPolicyChange={(value) =>
|
onBillingAdjustmentPolicyChange={(value) =>
|
||||||
setBillingForm((current) => ({
|
setBillingForm((current) => ({
|
||||||
...current,
|
...current,
|
||||||
@@ -2290,7 +2319,11 @@ function App() {
|
|||||||
|
|
||||||
<TopBar
|
<TopBar
|
||||||
subtitle={copy().appSubtitle}
|
subtitle={copy().appSubtitle}
|
||||||
title={copy().appTitle}
|
title={
|
||||||
|
readySession()?.member.householdName ??
|
||||||
|
onboardingSession()?.householdName ??
|
||||||
|
copy().appTitle
|
||||||
|
}
|
||||||
languageLabel={copy().language}
|
languageLabel={copy().language}
|
||||||
locale={locale()}
|
locale={locale()}
|
||||||
saving={savingMemberLocale()}
|
saving={savingMemberLocale()}
|
||||||
@@ -2391,15 +2424,6 @@ function App() {
|
|||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<Show when={readySession()?.mode === 'live'}>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
class="app-context-row__action"
|
|
||||||
onClick={() => setProfileEditorOpen(true)}
|
|
||||||
>
|
|
||||||
{copy().manageProfileAction}
|
|
||||||
</Button>
|
|
||||||
</Show>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="content-stack">{panel()}</section>
|
<section class="content-stack">{panel()}</section>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type {
|
|||||||
export const demoMember: NonNullable<MiniAppSession['member']> = {
|
export const demoMember: NonNullable<MiniAppSession['member']> = {
|
||||||
id: 'demo-member',
|
id: 'demo-member',
|
||||||
householdId: 'demo-household',
|
householdId: 'demo-household',
|
||||||
|
householdName: 'Kojori House',
|
||||||
displayName: 'Stas',
|
displayName: 'Stas',
|
||||||
status: 'active',
|
status: 'active',
|
||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
@@ -191,6 +192,7 @@ export const demoPendingMembers: readonly MiniAppPendingMember[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export const demoAdminSettings: MiniAppAdminSettingsPayload = {
|
export const demoAdminSettings: MiniAppAdminSettingsPayload = {
|
||||||
|
householdName: 'Kojori House',
|
||||||
settings: {
|
settings: {
|
||||||
householdId: 'demo-household',
|
householdId: 'demo-household',
|
||||||
settlementCurrency: 'GEL',
|
settlementCurrency: 'GEL',
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ export const dictionary = {
|
|||||||
reload: 'Retry',
|
reload: 'Retry',
|
||||||
language: 'Language',
|
language: 'Language',
|
||||||
householdLanguage: 'Household language',
|
householdLanguage: 'Household language',
|
||||||
|
generalSettingsBody:
|
||||||
|
'Household identity, default language, and personal profile controls live here.',
|
||||||
|
householdNameLabel: 'Household name',
|
||||||
|
householdNameHint: 'This appears in the mini app, join flow, and bot responses.',
|
||||||
savingLanguage: 'Saving…',
|
savingLanguage: 'Saving…',
|
||||||
onLabel: 'On',
|
onLabel: 'On',
|
||||||
offLabel: 'Off',
|
offLabel: 'Off',
|
||||||
@@ -37,6 +41,7 @@ export const dictionary = {
|
|||||||
balances: 'Balances',
|
balances: 'Balances',
|
||||||
ledger: 'Ledger',
|
ledger: 'Ledger',
|
||||||
house: 'House',
|
house: 'House',
|
||||||
|
houseSectionGeneral: 'General',
|
||||||
houseSectionBilling: 'Billing',
|
houseSectionBilling: 'Billing',
|
||||||
houseSectionUtilities: 'Utilities',
|
houseSectionUtilities: 'Utilities',
|
||||||
houseSectionMembers: 'Members',
|
houseSectionMembers: 'Members',
|
||||||
@@ -292,6 +297,9 @@ export const dictionary = {
|
|||||||
reload: 'Повторить',
|
reload: 'Повторить',
|
||||||
language: 'Язык',
|
language: 'Язык',
|
||||||
householdLanguage: 'Язык дома',
|
householdLanguage: 'Язык дома',
|
||||||
|
generalSettingsBody: 'Здесь живут имя дома, язык по умолчанию и доступ к личному профилю.',
|
||||||
|
householdNameLabel: 'Название дома',
|
||||||
|
householdNameHint: 'Показывается в mini app, при вступлении и в ответах бота.',
|
||||||
savingLanguage: 'Сохраняем…',
|
savingLanguage: 'Сохраняем…',
|
||||||
onLabel: 'Вкл',
|
onLabel: 'Вкл',
|
||||||
offLabel: 'Выкл',
|
offLabel: 'Выкл',
|
||||||
@@ -299,6 +307,7 @@ export const dictionary = {
|
|||||||
balances: 'Баланс',
|
balances: 'Баланс',
|
||||||
ledger: 'Леджер',
|
ledger: 'Леджер',
|
||||||
house: 'Дом',
|
house: 'Дом',
|
||||||
|
houseSectionGeneral: 'Общее',
|
||||||
houseSectionBilling: 'Биллинг',
|
houseSectionBilling: 'Биллинг',
|
||||||
houseSectionUtilities: 'Коммуналка',
|
houseSectionUtilities: 'Коммуналка',
|
||||||
houseSectionMembers: 'Участники',
|
houseSectionMembers: 'Участники',
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export interface MiniAppSession {
|
|||||||
member?: {
|
member?: {
|
||||||
id: string
|
id: string
|
||||||
householdId: string
|
householdId: string
|
||||||
|
householdName: string
|
||||||
displayName: string
|
displayName: string
|
||||||
status: 'active' | 'away' | 'left'
|
status: 'active' | 'away' | 'left'
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
@@ -138,6 +139,7 @@ export interface MiniAppDashboard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface MiniAppAdminSettingsPayload {
|
export interface MiniAppAdminSettingsPayload {
|
||||||
|
householdName: string
|
||||||
settings: MiniAppBillingSettings
|
settings: MiniAppBillingSettings
|
||||||
assistantConfig: MiniAppAssistantConfig
|
assistantConfig: MiniAppAssistantConfig
|
||||||
topics: readonly MiniAppTopicBinding[]
|
topics: readonly MiniAppTopicBinding[]
|
||||||
@@ -386,6 +388,7 @@ export async function fetchMiniAppAdminSettings(
|
|||||||
const payload = (await response.json()) as {
|
const payload = (await response.json()) as {
|
||||||
ok: boolean
|
ok: boolean
|
||||||
authorized?: boolean
|
authorized?: boolean
|
||||||
|
householdName?: string
|
||||||
settings?: MiniAppBillingSettings
|
settings?: MiniAppBillingSettings
|
||||||
assistantConfig?: MiniAppAssistantConfig
|
assistantConfig?: MiniAppAssistantConfig
|
||||||
topics?: MiniAppTopicBinding[]
|
topics?: MiniAppTopicBinding[]
|
||||||
@@ -398,6 +401,7 @@ export async function fetchMiniAppAdminSettings(
|
|||||||
if (
|
if (
|
||||||
!response.ok ||
|
!response.ok ||
|
||||||
!payload.authorized ||
|
!payload.authorized ||
|
||||||
|
!payload.householdName ||
|
||||||
!payload.settings ||
|
!payload.settings ||
|
||||||
!payload.assistantConfig ||
|
!payload.assistantConfig ||
|
||||||
!payload.topics ||
|
!payload.topics ||
|
||||||
@@ -409,6 +413,7 @@ export async function fetchMiniAppAdminSettings(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
householdName: payload.householdName,
|
||||||
settings: payload.settings,
|
settings: payload.settings,
|
||||||
assistantConfig: payload.assistantConfig,
|
assistantConfig: payload.assistantConfig,
|
||||||
topics: payload.topics,
|
topics: payload.topics,
|
||||||
@@ -423,6 +428,7 @@ export async function updateMiniAppBillingSettings(
|
|||||||
input: {
|
input: {
|
||||||
settlementCurrency?: 'USD' | 'GEL'
|
settlementCurrency?: 'USD' | 'GEL'
|
||||||
paymentBalanceAdjustmentPolicy?: 'utilities' | 'rent' | 'separate'
|
paymentBalanceAdjustmentPolicy?: 'utilities' | 'rent' | 'separate'
|
||||||
|
householdName?: string
|
||||||
rentAmountMajor?: string
|
rentAmountMajor?: string
|
||||||
rentCurrency: 'USD' | 'GEL'
|
rentCurrency: 'USD' | 'GEL'
|
||||||
rentDueDay: number
|
rentDueDay: number
|
||||||
@@ -434,6 +440,7 @@ export async function updateMiniAppBillingSettings(
|
|||||||
assistantTone?: string
|
assistantTone?: string
|
||||||
}
|
}
|
||||||
): Promise<{
|
): Promise<{
|
||||||
|
householdName: string
|
||||||
settings: MiniAppBillingSettings
|
settings: MiniAppBillingSettings
|
||||||
assistantConfig: MiniAppAssistantConfig
|
assistantConfig: MiniAppAssistantConfig
|
||||||
}> {
|
}> {
|
||||||
@@ -451,16 +458,24 @@ export async function updateMiniAppBillingSettings(
|
|||||||
const payload = (await response.json()) as {
|
const payload = (await response.json()) as {
|
||||||
ok: boolean
|
ok: boolean
|
||||||
authorized?: boolean
|
authorized?: boolean
|
||||||
|
householdName?: string
|
||||||
settings?: MiniAppBillingSettings
|
settings?: MiniAppBillingSettings
|
||||||
assistantConfig?: MiniAppAssistantConfig
|
assistantConfig?: MiniAppAssistantConfig
|
||||||
error?: string
|
error?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.ok || !payload.authorized || !payload.settings || !payload.assistantConfig) {
|
if (
|
||||||
|
!response.ok ||
|
||||||
|
!payload.authorized ||
|
||||||
|
!payload.householdName ||
|
||||||
|
!payload.settings ||
|
||||||
|
!payload.assistantConfig
|
||||||
|
) {
|
||||||
throw new Error(payload.error ?? 'Failed to update billing settings')
|
throw new Error(payload.error ?? 'Failed to update billing settings')
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
householdName: payload.householdName,
|
||||||
settings: payload.settings,
|
settings: payload.settings,
|
||||||
assistantConfig: payload.assistantConfig
|
assistantConfig: payload.assistantConfig
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ type UtilityBillDraft = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type BillingForm = {
|
type BillingForm = {
|
||||||
|
householdName: string
|
||||||
settlementCurrency: 'USD' | 'GEL'
|
settlementCurrency: 'USD' | 'GEL'
|
||||||
paymentBalanceAdjustmentPolicy: 'utilities' | 'rent' | 'separate'
|
paymentBalanceAdjustmentPolicy: 'utilities' | 'rent' | 'separate'
|
||||||
rentAmountMajor: string
|
rentAmountMajor: string
|
||||||
@@ -55,6 +56,8 @@ type Props = {
|
|||||||
locale: 'en' | 'ru'
|
locale: 'en' | 'ru'
|
||||||
readyIsAdmin: boolean
|
readyIsAdmin: boolean
|
||||||
householdDefaultLocale: 'en' | 'ru'
|
householdDefaultLocale: 'en' | 'ru'
|
||||||
|
householdName: string
|
||||||
|
profileDisplayName: string
|
||||||
dashboard: MiniAppDashboard | null
|
dashboard: MiniAppDashboard | null
|
||||||
adminSettings: MiniAppAdminSettingsPayload | null
|
adminSettings: MiniAppAdminSettingsPayload | null
|
||||||
cycleState: MiniAppAdminCycleState | null
|
cycleState: MiniAppAdminCycleState | null
|
||||||
@@ -101,6 +104,7 @@ type Props = {
|
|||||||
effectiveFromPeriod: string | null
|
effectiveFromPeriod: string | null
|
||||||
}
|
}
|
||||||
onChangeHouseholdLocale: (locale: 'en' | 'ru') => Promise<void>
|
onChangeHouseholdLocale: (locale: 'en' | 'ru') => Promise<void>
|
||||||
|
onOpenProfileEditor: () => void
|
||||||
onOpenCycleModal: () => void
|
onOpenCycleModal: () => void
|
||||||
onCloseCycleModal: () => void
|
onCloseCycleModal: () => void
|
||||||
onSaveCycleRent: () => Promise<void>
|
onSaveCycleRent: () => Promise<void>
|
||||||
@@ -111,6 +115,7 @@ type Props = {
|
|||||||
onOpenBillingSettingsModal: () => void
|
onOpenBillingSettingsModal: () => void
|
||||||
onCloseBillingSettingsModal: () => void
|
onCloseBillingSettingsModal: () => void
|
||||||
onSaveBillingSettings: () => Promise<void>
|
onSaveBillingSettings: () => Promise<void>
|
||||||
|
onBillingHouseholdNameChange: (value: string) => void
|
||||||
onBillingSettlementCurrencyChange: (value: 'USD' | 'GEL') => void
|
onBillingSettlementCurrencyChange: (value: 'USD' | 'GEL') => void
|
||||||
onBillingAdjustmentPolicyChange: (value: 'utilities' | 'rent' | 'separate') => void
|
onBillingAdjustmentPolicyChange: (value: 'utilities' | 'rent' | 'separate') => void
|
||||||
onBillingRentAmountChange: (value: string) => void
|
onBillingRentAmountChange: (value: string) => void
|
||||||
@@ -213,15 +218,83 @@ export function HouseScreen(props: Props) {
|
|||||||
<strong>{props.copy.residentHouseTitle ?? ''}</strong>
|
<strong>{props.copy.residentHouseTitle ?? ''}</strong>
|
||||||
</header>
|
</header>
|
||||||
<p>{props.copy.residentHouseBody ?? ''}</p>
|
<p>{props.copy.residentHouseBody ?? ''}</p>
|
||||||
|
<div class="panel-toolbar">
|
||||||
|
<Button variant="secondary" onClick={props.onOpenProfileEditor}>
|
||||||
|
<PencilIcon />
|
||||||
|
{props.copy.manageProfileAction ?? ''}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div class="admin-layout">
|
<div class="admin-layout">
|
||||||
|
<HouseSection
|
||||||
|
title={props.copy.houseSectionGeneral ?? ''}
|
||||||
|
body={props.copy.generalSettingsBody}
|
||||||
|
defaultOpen
|
||||||
|
>
|
||||||
|
<section class="admin-section">
|
||||||
|
<div class="admin-grid">
|
||||||
|
<article class="balance-item">
|
||||||
|
<header>
|
||||||
|
<strong>{props.copy.householdNameLabel ?? ''}</strong>
|
||||||
|
<span>{props.householdName}</span>
|
||||||
|
</header>
|
||||||
|
<p>{props.copy.householdNameHint ?? ''}</p>
|
||||||
|
<div class="panel-toolbar">
|
||||||
|
<Button variant="secondary" onClick={props.onOpenBillingSettingsModal}>
|
||||||
|
<SettingsIcon />
|
||||||
|
{props.copy.manageSettingsAction ?? ''}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="balance-item">
|
||||||
|
<header>
|
||||||
|
<strong>{props.copy.householdLanguage ?? ''}</strong>
|
||||||
|
<span>{props.householdDefaultLocale.toUpperCase()}</span>
|
||||||
|
</header>
|
||||||
|
<div class="locale-switch__buttons locale-switch__buttons--inline">
|
||||||
|
<button
|
||||||
|
classList={{ 'is-active': props.householdDefaultLocale === 'en' }}
|
||||||
|
type="button"
|
||||||
|
disabled={props.savingHouseholdLocale}
|
||||||
|
onClick={() => void props.onChangeHouseholdLocale('en')}
|
||||||
|
>
|
||||||
|
EN
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
classList={{ 'is-active': props.householdDefaultLocale === 'ru' }}
|
||||||
|
type="button"
|
||||||
|
disabled={props.savingHouseholdLocale}
|
||||||
|
onClick={() => void props.onChangeHouseholdLocale('ru')}
|
||||||
|
>
|
||||||
|
RU
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="balance-item">
|
||||||
|
<header>
|
||||||
|
<strong>{props.copy.manageProfileAction ?? ''}</strong>
|
||||||
|
<span>{props.profileDisplayName}</span>
|
||||||
|
</header>
|
||||||
|
<p>{props.copy.profileEditorBody ?? ''}</p>
|
||||||
|
<div class="panel-toolbar">
|
||||||
|
<Button variant="secondary" onClick={props.onOpenProfileEditor}>
|
||||||
|
<PencilIcon />
|
||||||
|
{props.copy.manageProfileAction ?? ''}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</HouseSection>
|
||||||
|
|
||||||
<HouseSection
|
<HouseSection
|
||||||
title={props.copy.houseSectionBilling ?? ''}
|
title={props.copy.houseSectionBilling ?? ''}
|
||||||
body={props.copy.billingSettingsEditorBody}
|
body={props.copy.billingSettingsEditorBody}
|
||||||
defaultOpen
|
|
||||||
>
|
>
|
||||||
<section class="admin-section">
|
<section class="admin-section">
|
||||||
<div class="admin-grid">
|
<div class="admin-grid">
|
||||||
@@ -290,31 +363,6 @@ export function HouseScreen(props: Props) {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="balance-item">
|
|
||||||
<header>
|
|
||||||
<strong>{props.copy.householdLanguage ?? ''}</strong>
|
|
||||||
<span>{props.householdDefaultLocale.toUpperCase()}</span>
|
|
||||||
</header>
|
|
||||||
<div class="locale-switch__buttons locale-switch__buttons--inline">
|
|
||||||
<button
|
|
||||||
classList={{ 'is-active': props.householdDefaultLocale === 'en' }}
|
|
||||||
type="button"
|
|
||||||
disabled={props.savingHouseholdLocale}
|
|
||||||
onClick={() => void props.onChangeHouseholdLocale('en')}
|
|
||||||
>
|
|
||||||
EN
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
classList={{ 'is-active': props.householdDefaultLocale === 'ru' }}
|
|
||||||
type="button"
|
|
||||||
disabled={props.savingHouseholdLocale}
|
|
||||||
onClick={() => void props.onChangeHouseholdLocale('ru')}
|
|
||||||
>
|
|
||||||
RU
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</div>
|
</div>
|
||||||
<Modal
|
<Modal
|
||||||
open={props.cycleRentOpen}
|
open={props.cycleRentOpen}
|
||||||
@@ -414,6 +462,18 @@ export function HouseScreen(props: Props) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div class="editor-grid">
|
<div class="editor-grid">
|
||||||
|
<Field
|
||||||
|
label={props.copy.householdNameLabel ?? ''}
|
||||||
|
hint={props.copy.householdNameHint ?? ''}
|
||||||
|
wide
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
value={props.billingForm.householdName}
|
||||||
|
onInput={(event) =>
|
||||||
|
props.onBillingHouseholdNameChange(event.currentTarget.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
<Field label={props.copy.settlementCurrency ?? ''}>
|
<Field label={props.copy.settlementCurrency ?? ''}>
|
||||||
<select
|
<select
|
||||||
value={props.billingForm.settlementCurrency}
|
value={props.billingForm.settlementCurrency}
|
||||||
|
|||||||
@@ -1320,6 +1320,35 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async updateHouseholdName(householdId, householdName) {
|
||||||
|
const updatedHouseholds = await db
|
||||||
|
.update(schema.households)
|
||||||
|
.set({
|
||||||
|
name: householdName
|
||||||
|
})
|
||||||
|
.where(eq(schema.households.id, householdId))
|
||||||
|
.returning({
|
||||||
|
id: schema.households.id,
|
||||||
|
name: schema.households.name,
|
||||||
|
defaultLocale: schema.households.defaultLocale
|
||||||
|
})
|
||||||
|
|
||||||
|
const household = updatedHouseholds[0]
|
||||||
|
if (!household) {
|
||||||
|
throw new Error('Failed to update household name')
|
||||||
|
}
|
||||||
|
|
||||||
|
const chat = await this.getHouseholdChatByHouseholdId(householdId)
|
||||||
|
if (!chat) {
|
||||||
|
throw new Error('Failed to resolve household chat after name update')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...chat,
|
||||||
|
householdName: household.name
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async updateMemberPreferredLocale(householdId, telegramUserId, locale) {
|
async updateMemberPreferredLocale(householdId, telegramUserId, locale) {
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.update(schema.members)
|
.update(schema.members)
|
||||||
|
|||||||
@@ -338,6 +338,7 @@ describe('createHouseholdOnboardingService', () => {
|
|||||||
member: {
|
member: {
|
||||||
id: 'member-42',
|
id: 'member-42',
|
||||||
householdId: 'household-1',
|
householdId: 'household-1',
|
||||||
|
householdName: 'Kojori House',
|
||||||
displayName: 'Stan',
|
displayName: 'Stan',
|
||||||
status: 'active',
|
status: 'active',
|
||||||
preferredLocale: null,
|
preferredLocale: null,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export type HouseholdMiniAppAccess =
|
|||||||
member: {
|
member: {
|
||||||
id: string
|
id: string
|
||||||
householdId: string
|
householdId: string
|
||||||
|
householdName: string
|
||||||
displayName: string
|
displayName: string
|
||||||
status: HouseholdMemberRecord['status']
|
status: HouseholdMemberRecord['status']
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
@@ -68,6 +69,7 @@ export interface HouseholdOnboardingService {
|
|||||||
member: {
|
member: {
|
||||||
id: string
|
id: string
|
||||||
householdId: string
|
householdId: string
|
||||||
|
householdName: string
|
||||||
displayName: string
|
displayName: string
|
||||||
status: HouseholdMemberRecord['status']
|
status: HouseholdMemberRecord['status']
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
@@ -161,9 +163,19 @@ export function createHouseholdOnboardingService(options: {
|
|||||||
) ?? null)
|
) ?? null)
|
||||||
|
|
||||||
if (matchingActiveMember) {
|
if (matchingActiveMember) {
|
||||||
|
const household = await options.repository.getHouseholdChatByHouseholdId(
|
||||||
|
matchingActiveMember.householdId
|
||||||
|
)
|
||||||
|
if (!household) {
|
||||||
|
throw new Error('Failed to resolve household for active mini app member')
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: 'active',
|
status: 'active',
|
||||||
member: toMember(matchingActiveMember)
|
member: {
|
||||||
|
...toMember(matchingActiveMember),
|
||||||
|
householdName: household.householdName
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,9 +244,19 @@ export function createHouseholdOnboardingService(options: {
|
|||||||
).find((member) => member.householdId === household.householdId)
|
).find((member) => member.householdId === household.householdId)
|
||||||
|
|
||||||
if (activeMember) {
|
if (activeMember) {
|
||||||
|
const householdRecord = await options.repository.getHouseholdChatByHouseholdId(
|
||||||
|
activeMember.householdId
|
||||||
|
)
|
||||||
|
if (!householdRecord) {
|
||||||
|
throw new Error('Failed to resolve household after mini app join')
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: 'active',
|
status: 'active',
|
||||||
member: toMember(activeMember)
|
member: {
|
||||||
|
...toMember(activeMember),
|
||||||
|
householdName: householdRecord.householdName
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,14 @@ function repository(): HouseholdConfigurationRepository {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
getTelegramHouseholdChat: async () => null,
|
getTelegramHouseholdChat: async () => null,
|
||||||
getHouseholdChatByHouseholdId: async () => null,
|
getHouseholdChatByHouseholdId: async () => ({
|
||||||
|
householdId: 'household-1',
|
||||||
|
householdName: 'Kojori House',
|
||||||
|
telegramChatId: '-100123',
|
||||||
|
telegramChatType: 'supergroup',
|
||||||
|
title: 'Kojori House',
|
||||||
|
defaultLocale: 'ru'
|
||||||
|
}),
|
||||||
bindHouseholdTopic: async (input) => ({
|
bindHouseholdTopic: async (input) => ({
|
||||||
householdId: input.householdId,
|
householdId: input.householdId,
|
||||||
role: input.role,
|
role: input.role,
|
||||||
@@ -268,6 +275,7 @@ describe('createMiniAppAdminService', () => {
|
|||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
|
householdName: 'Kojori House',
|
||||||
settings: {
|
settings: {
|
||||||
householdId: 'household-1',
|
householdId: 'household-1',
|
||||||
settlementCurrency: 'GEL',
|
settlementCurrency: 'GEL',
|
||||||
@@ -327,6 +335,7 @@ describe('createMiniAppAdminService', () => {
|
|||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
|
householdName: 'Kojori House',
|
||||||
settings: {
|
settings: {
|
||||||
householdId: 'household-1',
|
householdId: 'household-1',
|
||||||
settlementCurrency: 'GEL',
|
settlementCurrency: 'GEL',
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export interface MiniAppAdminService {
|
|||||||
getSettings(input: { householdId: string; actorIsAdmin: boolean }): Promise<
|
getSettings(input: { householdId: string; actorIsAdmin: boolean }): Promise<
|
||||||
| {
|
| {
|
||||||
status: 'ok'
|
status: 'ok'
|
||||||
|
householdName: string
|
||||||
settings: HouseholdBillingSettingsRecord
|
settings: HouseholdBillingSettingsRecord
|
||||||
assistantConfig: HouseholdAssistantConfigRecord
|
assistantConfig: HouseholdAssistantConfigRecord
|
||||||
categories: readonly HouseholdUtilityCategoryRecord[]
|
categories: readonly HouseholdUtilityCategoryRecord[]
|
||||||
@@ -44,6 +45,7 @@ export interface MiniAppAdminService {
|
|||||||
updateSettings(input: {
|
updateSettings(input: {
|
||||||
householdId: string
|
householdId: string
|
||||||
actorIsAdmin: boolean
|
actorIsAdmin: boolean
|
||||||
|
householdName?: string
|
||||||
settlementCurrency?: string
|
settlementCurrency?: string
|
||||||
paymentBalanceAdjustmentPolicy?: string
|
paymentBalanceAdjustmentPolicy?: string
|
||||||
rentAmountMajor?: string
|
rentAmountMajor?: string
|
||||||
@@ -58,6 +60,7 @@ export interface MiniAppAdminService {
|
|||||||
}): Promise<
|
}): Promise<
|
||||||
| {
|
| {
|
||||||
status: 'ok'
|
status: 'ok'
|
||||||
|
householdName: string
|
||||||
settings: HouseholdBillingSettingsRecord
|
settings: HouseholdBillingSettingsRecord
|
||||||
assistantConfig: HouseholdAssistantConfigRecord
|
assistantConfig: HouseholdAssistantConfigRecord
|
||||||
}
|
}
|
||||||
@@ -215,6 +218,19 @@ function normalizeDisplayName(raw: string): string | null {
|
|||||||
return trimmed.replace(/\s+/g, ' ')
|
return trimmed.replace(/\s+/g, ' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeHouseholdName(raw: string | undefined): string | null | undefined {
|
||||||
|
if (raw === undefined) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmed = raw.trim()
|
||||||
|
if (trimmed.length < 2 || trimmed.length > 120) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmed.replace(/\s+/g, ' ')
|
||||||
|
}
|
||||||
|
|
||||||
function defaultAssistantConfig(householdId: string): HouseholdAssistantConfigRecord {
|
function defaultAssistantConfig(householdId: string): HouseholdAssistantConfigRecord {
|
||||||
return {
|
return {
|
||||||
householdId,
|
householdId,
|
||||||
@@ -255,6 +271,11 @@ export function createMiniAppAdminService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const household = await repository.getHouseholdChatByHouseholdId(input.householdId)
|
||||||
|
if (!household) {
|
||||||
|
throw new Error('Failed to resolve household chat for mini app settings')
|
||||||
|
}
|
||||||
|
|
||||||
const [settings, assistantConfig, categories, members, memberAbsencePolicies, topics] =
|
const [settings, assistantConfig, categories, members, memberAbsencePolicies, topics] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
repository.getHouseholdBillingSettings(input.householdId),
|
repository.getHouseholdBillingSettings(input.householdId),
|
||||||
@@ -269,6 +290,7 @@ export function createMiniAppAdminService(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
|
householdName: household.householdName,
|
||||||
settings,
|
settings,
|
||||||
assistantConfig,
|
assistantConfig,
|
||||||
categories,
|
categories,
|
||||||
@@ -303,8 +325,11 @@ export function createMiniAppAdminService(
|
|||||||
|
|
||||||
const assistantContext = normalizeAssistantText(input.assistantContext, 1200)
|
const assistantContext = normalizeAssistantText(input.assistantContext, 1200)
|
||||||
const assistantTone = normalizeAssistantText(input.assistantTone, 160)
|
const assistantTone = normalizeAssistantText(input.assistantTone, 160)
|
||||||
|
const householdName = normalizeHouseholdName(input.householdName)
|
||||||
|
const nextHouseholdName = householdName ?? undefined
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
(input.householdName !== undefined && householdName === null) ||
|
||||||
(input.assistantContext !== undefined &&
|
(input.assistantContext !== undefined &&
|
||||||
assistantContext === null &&
|
assistantContext === null &&
|
||||||
input.assistantContext.trim().length > 0) ||
|
input.assistantContext.trim().length > 0) ||
|
||||||
@@ -349,7 +374,7 @@ export function createMiniAppAdminService(
|
|||||||
const shouldUpdateAssistantConfig =
|
const shouldUpdateAssistantConfig =
|
||||||
assistantContext !== undefined || assistantTone !== undefined
|
assistantContext !== undefined || assistantTone !== undefined
|
||||||
|
|
||||||
const [settings, nextAssistantConfig] = await Promise.all([
|
const [settings, nextAssistantConfig, household] = await Promise.all([
|
||||||
repository.updateHouseholdBillingSettings({
|
repository.updateHouseholdBillingSettings({
|
||||||
householdId: input.householdId,
|
householdId: input.householdId,
|
||||||
...(settlementCurrency
|
...(settlementCurrency
|
||||||
@@ -398,11 +423,19 @@ export function createMiniAppAdminService(
|
|||||||
householdId: input.householdId,
|
householdId: input.householdId,
|
||||||
assistantContext: assistantContext ?? null,
|
assistantContext: assistantContext ?? null,
|
||||||
assistantTone: assistantTone ?? null
|
assistantTone: assistantTone ?? null
|
||||||
})
|
}),
|
||||||
|
nextHouseholdName !== undefined && repository.updateHouseholdName
|
||||||
|
? repository.updateHouseholdName(input.householdId, nextHouseholdName)
|
||||||
|
: repository.getHouseholdChatByHouseholdId(input.householdId)
|
||||||
])
|
])
|
||||||
|
|
||||||
|
if (!household) {
|
||||||
|
throw new Error('Failed to resolve household chat after settings update')
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
|
householdName: household.householdName,
|
||||||
settings,
|
settings,
|
||||||
assistantConfig: nextAssistantConfig
|
assistantConfig: nextAssistantConfig
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,6 +213,10 @@ export interface HouseholdConfigurationRepository {
|
|||||||
householdId: string,
|
householdId: string,
|
||||||
locale: SupportedLocale
|
locale: SupportedLocale
|
||||||
): Promise<HouseholdTelegramChatRecord>
|
): Promise<HouseholdTelegramChatRecord>
|
||||||
|
updateHouseholdName?(
|
||||||
|
householdId: string,
|
||||||
|
householdName: string
|
||||||
|
): Promise<HouseholdTelegramChatRecord>
|
||||||
updateMemberPreferredLocale(
|
updateMemberPreferredLocale(
|
||||||
householdId: string,
|
householdId: string,
|
||||||
telegramUserId: string,
|
telegramUserId: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user