From 74080c32c67150599f4fd3f63b162c22c3f5701e Mon Sep 17 00:00:00 2001 From: whekin Date: Wed, 11 Mar 2026 20:13:04 +0400 Subject: [PATCH] fix(miniapp): consolidate member editor actions --- apps/miniapp/src/App.tsx | 102 +++++++++++++++++----- apps/miniapp/src/i18n.ts | 2 + apps/miniapp/src/index.css | 19 ++++ apps/miniapp/src/screens/house-screen.tsx | 97 +++++++++----------- 4 files changed, 139 insertions(+), 81 deletions(-) diff --git a/apps/miniapp/src/App.tsx b/apps/miniapp/src/App.tsx index 259a44d..3cc8807 100644 --- a/apps/miniapp/src/App.tsx +++ b/apps/miniapp/src/App.tsx @@ -333,14 +333,11 @@ function App() { const [approvingTelegramUserId, setApprovingTelegramUserId] = createSignal(null) const [promotingMemberId, setPromotingMemberId] = createSignal(null) const [savingOwnDisplayName, setSavingOwnDisplayName] = createSignal(false) - const [savingMemberDisplayNameId, setSavingMemberDisplayNameId] = createSignal( - null - ) - const [savingRentWeightMemberId, setSavingRentWeightMemberId] = createSignal(null) - const [savingMemberStatusId, setSavingMemberStatusId] = createSignal(null) - const [savingMemberAbsencePolicyId, setSavingMemberAbsencePolicyId] = createSignal( - null - ) + const [, setSavingMemberDisplayNameId] = createSignal(null) + const [, setSavingRentWeightMemberId] = createSignal(null) + const [, setSavingMemberStatusId] = createSignal(null) + const [, setSavingMemberAbsencePolicyId] = createSignal(null) + const [savingMemberEditorId, setSavingMemberEditorId] = createSignal(null) const [displayNameDraft, setDisplayNameDraft] = createSignal('') const [memberDisplayNameDrafts, setMemberDisplayNameDrafts] = createSignal< Record @@ -1275,7 +1272,7 @@ function App() { } } - async function handleSaveMemberDisplayName(memberId: string) { + async function handleSaveMemberDisplayName(memberId: string, closeEditor = true) { const initData = webApp?.initData?.trim() const currentReady = readySession() const nextDisplayName = memberDisplayNameDrafts()[memberId]?.trim() @@ -1297,6 +1294,9 @@ function App() { nextDisplayName ) syncDisplayName(updatedMember.id, updatedMember.displayName) + if (closeEditor) { + setEditingMemberId(null) + } } finally { setSavingMemberDisplayNameId(null) } @@ -1713,7 +1713,7 @@ function App() { } } - async function handleSaveRentWeight(memberId: string) { + async function handleSaveRentWeight(memberId: string, closeEditor = true) { const initData = webApp?.initData?.trim() const currentReady = readySession() const nextWeight = Number(rentWeightDrafts()[memberId] ?? '') @@ -1743,13 +1743,15 @@ function App() { ...current, [member.id]: String(member.rentShareWeight) })) - setEditingMemberId(null) + if (closeEditor) { + setEditingMemberId(null) + } } finally { setSavingRentWeightMemberId(null) } } - async function handleSaveMemberStatus(memberId: string) { + async function handleSaveMemberStatus(memberId: string, closeEditor = true) { const initData = webApp?.initData?.trim() const currentReady = readySession() const nextStatus = memberStatusDrafts()[memberId] @@ -1780,13 +1782,15 @@ function App() { resolvedMemberAbsencePolicy(member.id, member.status).policy ?? defaultAbsencePolicyForStatus(member.status) })) - setEditingMemberId(null) + if (closeEditor) { + setEditingMemberId(null) + } } finally { setSavingMemberStatusId(null) } } - async function handleSaveMemberAbsencePolicy(memberId: string) { + async function handleSaveMemberAbsencePolicy(memberId: string, closeEditor = true) { const initData = webApp?.initData?.trim() const currentReady = readySession() const member = adminSettings()?.members.find((entry) => entry.id === memberId) @@ -1829,12 +1833,70 @@ function App() { ...current, [memberId]: savedPolicy.policy })) - setEditingMemberId(null) + if (closeEditor) { + setEditingMemberId(null) + } } finally { setSavingMemberAbsencePolicyId(null) } } + async function handleSaveMemberChanges(memberId: string) { + const currentReady = readySession() + const member = adminSettings()?.members.find((entry) => entry.id === memberId) + const nextDisplayName = memberDisplayNameDrafts()[memberId]?.trim() ?? member?.displayName ?? '' + const nextStatus = memberStatusDrafts()[memberId] ?? member?.status + const nextPolicy = memberAbsencePolicyDrafts()[memberId] + const nextWeight = Number(rentWeightDrafts()[memberId] ?? member?.rentShareWeight ?? 0) + + if ( + currentReady?.mode !== 'live' || + !currentReady.member.isAdmin || + !member || + nextDisplayName.length < 2 || + !nextStatus || + !Number.isInteger(nextWeight) || + nextWeight <= 0 || + savingMemberEditorId() === memberId + ) { + return + } + + const currentPolicy = resolvedMemberAbsencePolicy(member.id, member.status).policy + const wantsAwayPolicySave = nextStatus === 'away' && nextPolicy && nextPolicy !== currentPolicy + const hasNameChange = nextDisplayName !== member.displayName + const hasStatusChange = nextStatus !== member.status + const hasWeightChange = nextWeight !== member.rentShareWeight + + if (!hasNameChange && !hasStatusChange && !wantsAwayPolicySave && !hasWeightChange) { + return + } + + setSavingMemberEditorId(memberId) + + try { + if (hasNameChange) { + await handleSaveMemberDisplayName(memberId, false) + } + + if (hasStatusChange) { + await handleSaveMemberStatus(memberId, false) + } + + if (wantsAwayPolicySave) { + await handleSaveMemberAbsencePolicy(memberId, false) + } + + if (hasWeightChange) { + await handleSaveRentWeight(memberId, false) + } + + setEditingMemberId(null) + } finally { + setSavingMemberEditorId(null) + } + } + function purchaseSplitPreview(purchaseId: string): { memberId: string; amountMajor: string }[] { const draft = purchaseDraftMap()[purchaseId] if (!draft || draft.participants.length === 0) { @@ -2044,10 +2106,7 @@ function App() { deletingUtilityBillId={deletingUtilityBillId()} savingCategorySlug={savingCategorySlug()} approvingTelegramUserId={approvingTelegramUserId()} - savingMemberDisplayNameId={savingMemberDisplayNameId()} - savingMemberStatusId={savingMemberStatusId()} - savingMemberAbsencePolicyId={savingMemberAbsencePolicyId()} - savingRentWeightMemberId={savingRentWeightMemberId()} + savingMemberEditorId={savingMemberEditorId()} promotingMemberId={promotingMemberId()} savingHouseholdLocale={savingHouseholdLocale()} minorToMajorString={minorToMajorString} @@ -2264,10 +2323,7 @@ function App() { [memberId]: value })) } - onSaveMemberDisplayName={handleSaveMemberDisplayName} - onSaveMemberStatus={handleSaveMemberStatus} - onSaveMemberAbsencePolicy={handleSaveMemberAbsencePolicy} - onSaveRentWeight={handleSaveRentWeight} + onSaveMemberChanges={handleSaveMemberChanges} onPromoteMember={handlePromoteMember} /> ) diff --git a/apps/miniapp/src/i18n.ts b/apps/miniapp/src/i18n.ts index 33f314c..b8153a2 100644 --- a/apps/miniapp/src/i18n.ts +++ b/apps/miniapp/src/i18n.ts @@ -185,6 +185,7 @@ export const dictionary = { profileEditorBody: 'Keep your own display name in a focused editor instead of the page body.', memberEditorBody: 'Member billing state and admin controls stay grouped in one editor.', editMemberAction: 'Edit member', + saveMemberChangesAction: 'Save changes', saveDisplayName: 'Save name', savingDisplayName: 'Saving name…', memberStatusLabel: 'Member status', @@ -409,6 +410,7 @@ export const dictionary = { 'Своё имя для household лучше менять в отдельном окне, а не на самой странице.', memberEditorBody: 'Статус, политика и админские действия по участнику собраны в одном окне.', editMemberAction: 'Редактировать участника', + saveMemberChangesAction: 'Сохранить изменения', saveDisplayName: 'Сохранить имя', savingDisplayName: 'Сохраняем имя…', memberStatusLabel: 'Статус участника', diff --git a/apps/miniapp/src/index.css b/apps/miniapp/src/index.css index 9d4cad5..78f7eae 100644 --- a/apps/miniapp/src/index.css +++ b/apps/miniapp/src/index.css @@ -856,6 +856,21 @@ button { margin-top: 2px; } +.member-editor-actions { + display: grid; + gap: 10px; +} + +.member-editor-actions__close, +.member-editor-actions__button { + width: 100%; +} + +.member-editor-actions__grid { + display: grid; + gap: 10px; +} + .modal-action-row { display: flex; flex-wrap: wrap; @@ -964,6 +979,10 @@ button { .editor-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + + .member-editor-actions__grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } } @media (max-width: 759px) { diff --git a/apps/miniapp/src/screens/house-screen.tsx b/apps/miniapp/src/screens/house-screen.tsx index 415679a..c16f58e 100644 --- a/apps/miniapp/src/screens/house-screen.tsx +++ b/apps/miniapp/src/screens/house-screen.tsx @@ -73,10 +73,7 @@ type Props = { deletingUtilityBillId: string | null savingCategorySlug: string | null approvingTelegramUserId: string | null - savingMemberDisplayNameId: string | null - savingMemberStatusId: string | null - savingMemberAbsencePolicyId: string | null - savingRentWeightMemberId: string | null + savingMemberEditorId: string | null promotingMemberId: string | null savingHouseholdLocale: boolean minorToMajorString: (value: bigint) => string @@ -149,10 +146,7 @@ type Props = { onMemberStatusDraftChange: (memberId: string, value: 'active' | 'away' | 'left') => void onMemberAbsencePolicyDraftChange: (memberId: string, value: MiniAppMemberAbsencePolicy) => void onRentWeightDraftChange: (memberId: string, value: string) => void - onSaveMemberDisplayName: (memberId: string) => Promise - onSaveMemberStatus: (memberId: string) => Promise - onSaveMemberAbsencePolicy: (memberId: string) => Promise - onSaveRentWeight: (memberId: string) => Promise + onSaveMemberChanges: (memberId: string) => Promise onPromoteMember: (memberId: string) => Promise } @@ -967,63 +961,50 @@ export function HouseScreen(props: Props) { return null } + const nextDisplayName = + props.memberDisplayNameDrafts[member.id]?.trim() ?? member.displayName + const nextStatus = props.memberStatusDrafts[member.id] ?? member.status + const currentPolicy = props.resolvedMemberAbsencePolicy(member.id, member.status) + const nextPolicy = props.memberAbsencePolicyDrafts[member.id] ?? currentPolicy.policy + const nextWeight = Number( + props.rentWeightDrafts[member.id] ?? String(member.rentShareWeight) + ) + const hasNameChange = + nextDisplayName.length >= 2 && nextDisplayName !== member.displayName + const hasStatusChange = nextStatus !== member.status + const hasPolicyChange = nextStatus === 'away' && nextPolicy !== currentPolicy.policy + const hasWeightChange = + Number.isInteger(nextWeight) && + nextWeight > 0 && + nextWeight !== member.rentShareWeight + const canSave = + props.savingMemberEditorId !== member.id && + (hasNameChange || hasStatusChange || hasPolicyChange || hasWeightChange) + return ( -