fix(miniapp): consolidate member editor actions

This commit is contained in:
2026-03-11 20:13:04 +04:00
parent ac48cece8a
commit 74080c32c6
4 changed files with 139 additions and 81 deletions

View File

@@ -333,14 +333,11 @@ function App() {
const [approvingTelegramUserId, setApprovingTelegramUserId] = createSignal<string | null>(null)
const [promotingMemberId, setPromotingMemberId] = createSignal<string | null>(null)
const [savingOwnDisplayName, setSavingOwnDisplayName] = createSignal(false)
const [savingMemberDisplayNameId, setSavingMemberDisplayNameId] = createSignal<string | null>(
null
)
const [savingRentWeightMemberId, setSavingRentWeightMemberId] = createSignal<string | null>(null)
const [savingMemberStatusId, setSavingMemberStatusId] = createSignal<string | null>(null)
const [savingMemberAbsencePolicyId, setSavingMemberAbsencePolicyId] = createSignal<string | null>(
null
)
const [, setSavingMemberDisplayNameId] = createSignal<string | null>(null)
const [, setSavingRentWeightMemberId] = createSignal<string | null>(null)
const [, setSavingMemberStatusId] = createSignal<string | null>(null)
const [, setSavingMemberAbsencePolicyId] = createSignal<string | null>(null)
const [savingMemberEditorId, setSavingMemberEditorId] = createSignal<string | null>(null)
const [displayNameDraft, setDisplayNameDraft] = createSignal('')
const [memberDisplayNameDrafts, setMemberDisplayNameDrafts] = createSignal<
Record<string, string>
@@ -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}
/>
)

View File

@@ -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: 'Статус участника',

View File

@@ -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) {

View File

@@ -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<void>
onSaveMemberStatus: (memberId: string) => Promise<void>
onSaveMemberAbsencePolicy: (memberId: string) => Promise<void>
onSaveRentWeight: (memberId: string) => Promise<void>
onSaveMemberChanges: (memberId: string) => Promise<void>
onPromoteMember: (memberId: string) => Promise<void>
}
@@ -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 (
<div class="modal-action-row">
<div class="modal-action-row__primary">
<Button variant="ghost" onClick={props.onCloseMemberEditor}>
{props.copy.closeEditorAction ?? ''}
</Button>
<Button
variant="secondary"
disabled={
props.savingMemberDisplayNameId === member.id ||
(props.memberDisplayNameDrafts[member.id] ?? member.displayName).trim()
.length < 2 ||
(props.memberDisplayNameDrafts[member.id] ?? member.displayName).trim() ===
member.displayName
}
onClick={() => void props.onSaveMemberDisplayName(member.id)}
>
{props.savingMemberDisplayNameId === member.id
? props.copy.savingDisplayName
: props.copy.saveDisplayName}
</Button>
<Button
variant="secondary"
disabled={props.savingMemberStatusId === member.id}
onClick={() => void props.onSaveMemberStatus(member.id)}
>
{props.savingMemberStatusId === member.id
? props.copy.savingMemberStatus
: props.copy.saveMemberStatusAction}
</Button>
<Button
variant="secondary"
disabled={
props.savingMemberAbsencePolicyId === member.id ||
(props.memberStatusDrafts[member.id] ?? member.status) !== 'away'
}
onClick={() => void props.onSaveMemberAbsencePolicy(member.id)}
>
{props.savingMemberAbsencePolicyId === member.id
? props.copy.savingAbsencePolicy
: props.copy.saveAbsencePolicyAction}
</Button>
<div class="member-editor-actions">
<Button
variant="ghost"
class="member-editor-actions__close"
onClick={props.onCloseMemberEditor}
>
{props.copy.closeEditorAction ?? ''}
</Button>
<div class="member-editor-actions__grid">
<Button
variant="primary"
disabled={
props.savingRentWeightMemberId === member.id ||
Number(props.rentWeightDrafts[member.id] ?? member.rentShareWeight) <= 0
}
onClick={() => void props.onSaveRentWeight(member.id)}
class="member-editor-actions__button"
disabled={!canSave}
onClick={() => void props.onSaveMemberChanges(member.id)}
>
{props.savingRentWeightMemberId === member.id
? props.copy.savingRentWeight
: props.copy.saveRentWeightAction}
{props.savingMemberEditorId === member.id
? props.copy.savingSettings
: props.copy.saveMemberChangesAction}
</Button>
<Show when={!member.isAdmin}>
<Button
variant="ghost"
variant="secondary"
class="member-editor-actions__button"
disabled={props.promotingMemberId === member.id}
onClick={() => void props.onPromoteMember(member.id)}
>