mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 17:54:02 +00:00
fix(miniapp): consolidate member editor actions
This commit is contained in:
@@ -333,14 +333,11 @@ function App() {
|
|||||||
const [approvingTelegramUserId, setApprovingTelegramUserId] = createSignal<string | null>(null)
|
const [approvingTelegramUserId, setApprovingTelegramUserId] = createSignal<string | null>(null)
|
||||||
const [promotingMemberId, setPromotingMemberId] = createSignal<string | null>(null)
|
const [promotingMemberId, setPromotingMemberId] = createSignal<string | null>(null)
|
||||||
const [savingOwnDisplayName, setSavingOwnDisplayName] = createSignal(false)
|
const [savingOwnDisplayName, setSavingOwnDisplayName] = createSignal(false)
|
||||||
const [savingMemberDisplayNameId, setSavingMemberDisplayNameId] = createSignal<string | null>(
|
const [, setSavingMemberDisplayNameId] = createSignal<string | null>(null)
|
||||||
null
|
const [, setSavingRentWeightMemberId] = createSignal<string | null>(null)
|
||||||
)
|
const [, setSavingMemberStatusId] = createSignal<string | null>(null)
|
||||||
const [savingRentWeightMemberId, setSavingRentWeightMemberId] = createSignal<string | null>(null)
|
const [, setSavingMemberAbsencePolicyId] = createSignal<string | null>(null)
|
||||||
const [savingMemberStatusId, setSavingMemberStatusId] = createSignal<string | null>(null)
|
const [savingMemberEditorId, setSavingMemberEditorId] = createSignal<string | null>(null)
|
||||||
const [savingMemberAbsencePolicyId, setSavingMemberAbsencePolicyId] = createSignal<string | null>(
|
|
||||||
null
|
|
||||||
)
|
|
||||||
const [displayNameDraft, setDisplayNameDraft] = createSignal('')
|
const [displayNameDraft, setDisplayNameDraft] = createSignal('')
|
||||||
const [memberDisplayNameDrafts, setMemberDisplayNameDrafts] = createSignal<
|
const [memberDisplayNameDrafts, setMemberDisplayNameDrafts] = createSignal<
|
||||||
Record<string, string>
|
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 initData = webApp?.initData?.trim()
|
||||||
const currentReady = readySession()
|
const currentReady = readySession()
|
||||||
const nextDisplayName = memberDisplayNameDrafts()[memberId]?.trim()
|
const nextDisplayName = memberDisplayNameDrafts()[memberId]?.trim()
|
||||||
@@ -1297,6 +1294,9 @@ function App() {
|
|||||||
nextDisplayName
|
nextDisplayName
|
||||||
)
|
)
|
||||||
syncDisplayName(updatedMember.id, updatedMember.displayName)
|
syncDisplayName(updatedMember.id, updatedMember.displayName)
|
||||||
|
if (closeEditor) {
|
||||||
|
setEditingMemberId(null)
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setSavingMemberDisplayNameId(null)
|
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 initData = webApp?.initData?.trim()
|
||||||
const currentReady = readySession()
|
const currentReady = readySession()
|
||||||
const nextWeight = Number(rentWeightDrafts()[memberId] ?? '')
|
const nextWeight = Number(rentWeightDrafts()[memberId] ?? '')
|
||||||
@@ -1743,13 +1743,15 @@ function App() {
|
|||||||
...current,
|
...current,
|
||||||
[member.id]: String(member.rentShareWeight)
|
[member.id]: String(member.rentShareWeight)
|
||||||
}))
|
}))
|
||||||
|
if (closeEditor) {
|
||||||
setEditingMemberId(null)
|
setEditingMemberId(null)
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setSavingRentWeightMemberId(null)
|
setSavingRentWeightMemberId(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSaveMemberStatus(memberId: string) {
|
async function handleSaveMemberStatus(memberId: string, closeEditor = true) {
|
||||||
const initData = webApp?.initData?.trim()
|
const initData = webApp?.initData?.trim()
|
||||||
const currentReady = readySession()
|
const currentReady = readySession()
|
||||||
const nextStatus = memberStatusDrafts()[memberId]
|
const nextStatus = memberStatusDrafts()[memberId]
|
||||||
@@ -1780,13 +1782,15 @@ function App() {
|
|||||||
resolvedMemberAbsencePolicy(member.id, member.status).policy ??
|
resolvedMemberAbsencePolicy(member.id, member.status).policy ??
|
||||||
defaultAbsencePolicyForStatus(member.status)
|
defaultAbsencePolicyForStatus(member.status)
|
||||||
}))
|
}))
|
||||||
|
if (closeEditor) {
|
||||||
setEditingMemberId(null)
|
setEditingMemberId(null)
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setSavingMemberStatusId(null)
|
setSavingMemberStatusId(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSaveMemberAbsencePolicy(memberId: string) {
|
async function handleSaveMemberAbsencePolicy(memberId: string, closeEditor = true) {
|
||||||
const initData = webApp?.initData?.trim()
|
const initData = webApp?.initData?.trim()
|
||||||
const currentReady = readySession()
|
const currentReady = readySession()
|
||||||
const member = adminSettings()?.members.find((entry) => entry.id === memberId)
|
const member = adminSettings()?.members.find((entry) => entry.id === memberId)
|
||||||
@@ -1829,12 +1833,70 @@ function App() {
|
|||||||
...current,
|
...current,
|
||||||
[memberId]: savedPolicy.policy
|
[memberId]: savedPolicy.policy
|
||||||
}))
|
}))
|
||||||
|
if (closeEditor) {
|
||||||
setEditingMemberId(null)
|
setEditingMemberId(null)
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setSavingMemberAbsencePolicyId(null)
|
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 }[] {
|
function purchaseSplitPreview(purchaseId: string): { memberId: string; amountMajor: string }[] {
|
||||||
const draft = purchaseDraftMap()[purchaseId]
|
const draft = purchaseDraftMap()[purchaseId]
|
||||||
if (!draft || draft.participants.length === 0) {
|
if (!draft || draft.participants.length === 0) {
|
||||||
@@ -2044,10 +2106,7 @@ function App() {
|
|||||||
deletingUtilityBillId={deletingUtilityBillId()}
|
deletingUtilityBillId={deletingUtilityBillId()}
|
||||||
savingCategorySlug={savingCategorySlug()}
|
savingCategorySlug={savingCategorySlug()}
|
||||||
approvingTelegramUserId={approvingTelegramUserId()}
|
approvingTelegramUserId={approvingTelegramUserId()}
|
||||||
savingMemberDisplayNameId={savingMemberDisplayNameId()}
|
savingMemberEditorId={savingMemberEditorId()}
|
||||||
savingMemberStatusId={savingMemberStatusId()}
|
|
||||||
savingMemberAbsencePolicyId={savingMemberAbsencePolicyId()}
|
|
||||||
savingRentWeightMemberId={savingRentWeightMemberId()}
|
|
||||||
promotingMemberId={promotingMemberId()}
|
promotingMemberId={promotingMemberId()}
|
||||||
savingHouseholdLocale={savingHouseholdLocale()}
|
savingHouseholdLocale={savingHouseholdLocale()}
|
||||||
minorToMajorString={minorToMajorString}
|
minorToMajorString={minorToMajorString}
|
||||||
@@ -2264,10 +2323,7 @@ function App() {
|
|||||||
[memberId]: value
|
[memberId]: value
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
onSaveMemberDisplayName={handleSaveMemberDisplayName}
|
onSaveMemberChanges={handleSaveMemberChanges}
|
||||||
onSaveMemberStatus={handleSaveMemberStatus}
|
|
||||||
onSaveMemberAbsencePolicy={handleSaveMemberAbsencePolicy}
|
|
||||||
onSaveRentWeight={handleSaveRentWeight}
|
|
||||||
onPromoteMember={handlePromoteMember}
|
onPromoteMember={handlePromoteMember}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -185,6 +185,7 @@ export const dictionary = {
|
|||||||
profileEditorBody: 'Keep your own display name in a focused editor instead of the page body.',
|
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.',
|
memberEditorBody: 'Member billing state and admin controls stay grouped in one editor.',
|
||||||
editMemberAction: 'Edit member',
|
editMemberAction: 'Edit member',
|
||||||
|
saveMemberChangesAction: 'Save changes',
|
||||||
saveDisplayName: 'Save name',
|
saveDisplayName: 'Save name',
|
||||||
savingDisplayName: 'Saving name…',
|
savingDisplayName: 'Saving name…',
|
||||||
memberStatusLabel: 'Member status',
|
memberStatusLabel: 'Member status',
|
||||||
@@ -409,6 +410,7 @@ export const dictionary = {
|
|||||||
'Своё имя для household лучше менять в отдельном окне, а не на самой странице.',
|
'Своё имя для household лучше менять в отдельном окне, а не на самой странице.',
|
||||||
memberEditorBody: 'Статус, политика и админские действия по участнику собраны в одном окне.',
|
memberEditorBody: 'Статус, политика и админские действия по участнику собраны в одном окне.',
|
||||||
editMemberAction: 'Редактировать участника',
|
editMemberAction: 'Редактировать участника',
|
||||||
|
saveMemberChangesAction: 'Сохранить изменения',
|
||||||
saveDisplayName: 'Сохранить имя',
|
saveDisplayName: 'Сохранить имя',
|
||||||
savingDisplayName: 'Сохраняем имя…',
|
savingDisplayName: 'Сохраняем имя…',
|
||||||
memberStatusLabel: 'Статус участника',
|
memberStatusLabel: 'Статус участника',
|
||||||
|
|||||||
@@ -856,6 +856,21 @@ button {
|
|||||||
margin-top: 2px;
|
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 {
|
.modal-action-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -964,6 +979,10 @@ button {
|
|||||||
.editor-grid {
|
.editor-grid {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.member-editor-actions__grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 759px) {
|
@media (max-width: 759px) {
|
||||||
|
|||||||
@@ -73,10 +73,7 @@ type Props = {
|
|||||||
deletingUtilityBillId: string | null
|
deletingUtilityBillId: string | null
|
||||||
savingCategorySlug: string | null
|
savingCategorySlug: string | null
|
||||||
approvingTelegramUserId: string | null
|
approvingTelegramUserId: string | null
|
||||||
savingMemberDisplayNameId: string | null
|
savingMemberEditorId: string | null
|
||||||
savingMemberStatusId: string | null
|
|
||||||
savingMemberAbsencePolicyId: string | null
|
|
||||||
savingRentWeightMemberId: string | null
|
|
||||||
promotingMemberId: string | null
|
promotingMemberId: string | null
|
||||||
savingHouseholdLocale: boolean
|
savingHouseholdLocale: boolean
|
||||||
minorToMajorString: (value: bigint) => string
|
minorToMajorString: (value: bigint) => string
|
||||||
@@ -149,10 +146,7 @@ type Props = {
|
|||||||
onMemberStatusDraftChange: (memberId: string, value: 'active' | 'away' | 'left') => void
|
onMemberStatusDraftChange: (memberId: string, value: 'active' | 'away' | 'left') => void
|
||||||
onMemberAbsencePolicyDraftChange: (memberId: string, value: MiniAppMemberAbsencePolicy) => void
|
onMemberAbsencePolicyDraftChange: (memberId: string, value: MiniAppMemberAbsencePolicy) => void
|
||||||
onRentWeightDraftChange: (memberId: string, value: string) => void
|
onRentWeightDraftChange: (memberId: string, value: string) => void
|
||||||
onSaveMemberDisplayName: (memberId: string) => Promise<void>
|
onSaveMemberChanges: (memberId: string) => Promise<void>
|
||||||
onSaveMemberStatus: (memberId: string) => Promise<void>
|
|
||||||
onSaveMemberAbsencePolicy: (memberId: string) => Promise<void>
|
|
||||||
onSaveRentWeight: (memberId: string) => Promise<void>
|
|
||||||
onPromoteMember: (memberId: string) => Promise<void>
|
onPromoteMember: (memberId: string) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -967,63 +961,50 @@ export function HouseScreen(props: Props) {
|
|||||||
return null
|
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 (
|
return (
|
||||||
<div class="modal-action-row">
|
<div class="member-editor-actions">
|
||||||
<div class="modal-action-row__primary">
|
<Button
|
||||||
<Button variant="ghost" onClick={props.onCloseMemberEditor}>
|
variant="ghost"
|
||||||
|
class="member-editor-actions__close"
|
||||||
|
onClick={props.onCloseMemberEditor}
|
||||||
|
>
|
||||||
{props.copy.closeEditorAction ?? ''}
|
{props.copy.closeEditorAction ?? ''}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<div class="member-editor-actions__grid">
|
||||||
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>
|
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
disabled={
|
class="member-editor-actions__button"
|
||||||
props.savingRentWeightMemberId === member.id ||
|
disabled={!canSave}
|
||||||
Number(props.rentWeightDrafts[member.id] ?? member.rentShareWeight) <= 0
|
onClick={() => void props.onSaveMemberChanges(member.id)}
|
||||||
}
|
|
||||||
onClick={() => void props.onSaveRentWeight(member.id)}
|
|
||||||
>
|
>
|
||||||
{props.savingRentWeightMemberId === member.id
|
{props.savingMemberEditorId === member.id
|
||||||
? props.copy.savingRentWeight
|
? props.copy.savingSettings
|
||||||
: props.copy.saveRentWeightAction}
|
: props.copy.saveMemberChangesAction}
|
||||||
</Button>
|
</Button>
|
||||||
<Show when={!member.isAdmin}>
|
<Show when={!member.isAdmin}>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="secondary"
|
||||||
|
class="member-editor-actions__button"
|
||||||
disabled={props.promotingMemberId === member.id}
|
disabled={props.promotingMemberId === member.id}
|
||||||
onClick={() => void props.onPromoteMember(member.id)}
|
onClick={() => void props.onPromoteMember(member.id)}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user