refactor(miniapp): polish admin house layout

This commit is contained in:
2026-03-10 17:07:57 +04:00
parent 1988521931
commit 2efb18a4de
2 changed files with 564 additions and 430 deletions

View File

@@ -1028,13 +1028,41 @@ function App() {
) )
case 'house': case 'house':
return readySession()?.member.isAdmin ? ( return readySession()?.member.isAdmin ? (
<div class="balance-list"> <div class="admin-layout">
<article class="balance-item"> <article class="balance-item balance-item--accent admin-hero">
<header> <header>
<strong>{copy().householdSettingsTitle}</strong> <strong>{copy().householdSettingsTitle}</strong>
<span>{adminSettings()?.settings.settlementCurrency ?? '—'}</span>
</header> </header>
<p>{copy().householdSettingsBody}</p> <p>{copy().householdSettingsBody}</p>
<div class="admin-summary-grid">
<article class="stat-card">
<span>{copy().billingCycleTitle}</span>
<strong>{cycleState()?.cycle?.period ?? copy().billingCycleEmpty}</strong>
</article> </article>
<article class="stat-card">
<span>{copy().settlementCurrency}</span>
<strong>{adminSettings()?.settings.settlementCurrency ?? '—'}</strong>
</article>
<article class="stat-card">
<span>{copy().membersCount}</span>
<strong>{String(adminSettings()?.members.length ?? 0)}</strong>
</article>
<article class="stat-card">
<span>{copy().pendingRequests}</span>
<strong>{String(pendingMembers().length)}</strong>
</article>
</div>
</article>
<section class="admin-section">
<header class="admin-section__header">
<div>
<h3>{copy().billingCycleTitle}</h3>
<p>{copy().billingSettingsTitle}</p>
</div>
</header>
<div class="admin-grid">
<article class="balance-item"> <article class="balance-item">
<header> <header>
<strong>{copy().billingCycleTitle}</strong> <strong>{copy().billingCycleTitle}</strong>
@@ -1108,65 +1136,6 @@ function App() {
{closingCycle() ? copy().closingCycle : copy().closeCycleAction} {closingCycle() ? copy().closingCycle : copy().closeCycleAction}
</button> </button>
</div> </div>
<div class="settings-grid">
<label class="settings-field">
<span>{copy().utilityCategoryLabel}</span>
<select
value={cycleForm().utilityCategorySlug}
onChange={(event) =>
setCycleForm((current) => ({
...current,
utilityCategorySlug: event.currentTarget.value
}))
}
>
{adminSettings()
?.categories.filter((category) => category.isActive)
.map((category) => (
<option value={category.slug}>{category.name}</option>
))}
</select>
</label>
<label class="settings-field">
<span>{copy().utilityAmount}</span>
<input
value={cycleForm().utilityAmountMajor}
onInput={(event) =>
setCycleForm((current) => ({
...current,
utilityAmountMajor: event.currentTarget.value
}))
}
/>
</label>
</div>
<button
class="ghost-button"
type="button"
disabled={
savingUtilityBill() || cycleForm().utilityAmountMajor.trim().length === 0
}
onClick={() => void handleAddUtilityBill()}
>
{savingUtilityBill() ? copy().savingUtilityBill : copy().addUtilityBillAction}
</button>
<div class="balance-list">
{cycleState()?.utilityBills.length ? (
cycleState()?.utilityBills.map((bill) => (
<article class="ledger-item">
<header>
<strong>{bill.billName}</strong>
<span>
{(Number(bill.amountMinor) / 100).toFixed(2)} {bill.currency}
</span>
</header>
<p>{bill.createdAt.slice(0, 10)}</p>
</article>
))
) : (
<p>{copy().utilityBillsEmpty}</p>
)}
</div>
</> </>
) : ( ) : (
<> <>
@@ -1211,9 +1180,11 @@ function App() {
</> </>
)} )}
</article> </article>
<article class="balance-item"> <article class="balance-item">
<header> <header>
<strong>{copy().billingSettingsTitle}</strong> <strong>{copy().billingSettingsTitle}</strong>
<span>{billingForm().settlementCurrency}</span>
</header> </header>
<div class="settings-grid"> <div class="settings-grid">
<label class="settings-field"> <label class="settings-field">
@@ -1340,11 +1311,13 @@ function App() {
{savingBillingSettings() ? copy().savingSettings : copy().saveSettingsAction} {savingBillingSettings() ? copy().savingSettings : copy().saveSettingsAction}
</button> </button>
</article> </article>
<article class="balance-item"> <article class="balance-item">
<header> <header>
<strong>{copy().householdLanguage}</strong> <strong>{copy().householdLanguage}</strong>
<span>{readySession()?.member.householdDefaultLocale.toUpperCase()}</span> <span>{readySession()?.member.householdDefaultLocale.toUpperCase()}</span>
</header> </header>
<p>{copy().householdSettingsBody}</p>
<div class="locale-switch__buttons"> <div class="locale-switch__buttons">
<button <button
classList={{ classList={{
@@ -1368,12 +1341,89 @@ function App() {
</button> </button>
</div> </div>
</article> </article>
</div>
</section>
<section class="admin-section">
<header class="admin-section__header">
<div>
<h3>{copy().utilityCategoriesTitle}</h3>
<p>{copy().utilityCategoriesBody}</p>
</div>
</header>
<div class="admin-grid">
<article class="balance-item">
<header>
<strong>{copy().utilityLedgerTitle}</strong>
<span>{cycleState()?.cycle?.currency ?? billingForm().settlementCurrency}</span>
</header>
<div class="settings-grid">
<label class="settings-field">
<span>{copy().utilityCategoryLabel}</span>
<select
value={cycleForm().utilityCategorySlug}
onChange={(event) =>
setCycleForm((current) => ({
...current,
utilityCategorySlug: event.currentTarget.value
}))
}
>
{adminSettings()
?.categories.filter((category) => category.isActive)
.map((category) => (
<option value={category.slug}>{category.name}</option>
))}
</select>
</label>
<label class="settings-field">
<span>{copy().utilityAmount}</span>
<input
value={cycleForm().utilityAmountMajor}
onInput={(event) =>
setCycleForm((current) => ({
...current,
utilityAmountMajor: event.currentTarget.value
}))
}
/>
</label>
</div>
<button
class="ghost-button"
type="button"
disabled={
savingUtilityBill() || cycleForm().utilityAmountMajor.trim().length === 0
}
onClick={() => void handleAddUtilityBill()}
>
{savingUtilityBill() ? copy().savingUtilityBill : copy().addUtilityBillAction}
</button>
<div class="balance-list admin-sublist">
{cycleState()?.utilityBills.length ? (
cycleState()?.utilityBills.map((bill) => (
<article class="ledger-item">
<header>
<strong>{bill.billName}</strong>
<span>
{(Number(bill.amountMinor) / 100).toFixed(2)} {bill.currency}
</span>
</header>
<p>{bill.createdAt.slice(0, 10)}</p>
</article>
))
) : (
<p>{copy().utilityBillsEmpty}</p>
)}
</div>
</article>
<article class="balance-item"> <article class="balance-item">
<header> <header>
<strong>{copy().utilityCategoriesTitle}</strong> <strong>{copy().utilityCategoriesTitle}</strong>
<span>{String(adminSettings()?.categories.length ?? 0)}</span>
</header> </header>
<p>{copy().utilityCategoriesBody}</p> <div class="balance-list admin-sublist">
<div class="balance-list">
{adminSettings()?.categories.map((category) => ( {adminSettings()?.categories.map((category) => (
<article class="ledger-item"> <article class="ledger-item">
<header> <header>
@@ -1439,12 +1489,14 @@ function App() {
void handleSaveUtilityCategory({ void handleSaveUtilityCategory({
slug: category.slug, slug: category.slug,
name: name:
adminSettings()?.categories.find((item) => item.slug === category.slug) adminSettings()?.categories.find(
?.name ?? category.name, (item) => item.slug === category.slug
)?.name ?? category.name,
sortOrder: category.sortOrder, sortOrder: category.sortOrder,
isActive: isActive:
adminSettings()?.categories.find((item) => item.slug === category.slug) adminSettings()?.categories.find(
?.isActive ?? category.isActive (item) => item.slug === category.slug
)?.isActive ?? category.isActive
}) })
} }
> >
@@ -1466,7 +1518,8 @@ function App() {
class="ghost-button" class="ghost-button"
type="button" type="button"
disabled={ disabled={
newCategoryName().trim().length === 0 || savingCategorySlug() === '__new__' newCategoryName().trim().length === 0 ||
savingCategorySlug() === '__new__'
} }
onClick={() => onClick={() =>
void handleSaveUtilityCategory({ void handleSaveUtilityCategory({
@@ -1483,23 +1536,37 @@ function App() {
</article> </article>
</div> </div>
</article> </article>
<article class="balance-item"> </div>
</section>
<section class="admin-section">
<header class="admin-section__header">
<div>
<h3>{copy().adminsTitle}</h3>
<p>{copy().adminsBody}</p>
</div>
</header>
<div class="admin-grid">
<article class="balance-item admin-card--wide">
<header> <header>
<strong>{copy().adminsTitle}</strong> <strong>{copy().adminsTitle}</strong>
<span>{String(adminSettings()?.members.length ?? 0)}</span>
</header> </header>
<p>{copy().adminsBody}</p> <div class="balance-list admin-sublist">
<div class="balance-list">
{adminSettings()?.members.map((member) => ( {adminSettings()?.members.map((member) => (
<article class="ledger-item"> <article class="ledger-item">
<header> <header>
<strong>{member.displayName}</strong> <strong>{member.displayName}</strong>
<span>{member.isAdmin ? copy().adminTag : copy().residentTag}</span> <span>{member.isAdmin ? copy().adminTag : copy().residentTag}</span>
</header> </header>
<div class="settings-grid">
<label class="settings-field settings-field--wide"> <label class="settings-field settings-field--wide">
<span>{copy().rentWeightLabel}</span> <span>{copy().rentWeightLabel}</span>
<input <input
inputmode="numeric" inputmode="numeric"
value={rentWeightDrafts()[member.id] ?? String(member.rentShareWeight)} value={
rentWeightDrafts()[member.id] ?? String(member.rentShareWeight)
}
onInput={(event) => onInput={(event) =>
setRentWeightDrafts((current) => ({ setRentWeightDrafts((current) => ({
...current, ...current,
@@ -1508,6 +1575,8 @@ function App() {
} }
/> />
</label> </label>
</div>
<div class="inline-actions">
<button <button
class="ghost-button" class="ghost-button"
type="button" type="button"
@@ -1533,23 +1602,24 @@ function App() {
: copy().promoteAdminAction} : copy().promoteAdminAction}
</button> </button>
) : null} ) : null}
</div>
</article> </article>
))} ))}
</div> </div>
</article> </article>
<article class="balance-item"> <article class="balance-item">
<header> <header>
<strong>{copy().pendingMembersTitle}</strong> <strong>{copy().pendingMembersTitle}</strong>
<span>{String(pendingMembers().length)}</span>
</header> </header>
<p>{copy().pendingMembersBody}</p> <p>{copy().pendingMembersBody}</p>
</article>
{pendingMembers().length === 0 ? ( {pendingMembers().length === 0 ? (
<article class="balance-item">
<p>{copy().pendingMembersEmpty}</p> <p>{copy().pendingMembersEmpty}</p>
</article>
) : ( ) : (
pendingMembers().map((member) => ( <div class="balance-list admin-sublist">
<article class="balance-item"> {pendingMembers().map((member) => (
<article class="ledger-item">
<header> <header>
<strong>{member.displayName}</strong> <strong>{member.displayName}</strong>
<span>{member.telegramUserId}</span> <span>{member.telegramUserId}</span>
@@ -1570,8 +1640,12 @@ function App() {
: copy().approveMemberAction} : copy().approveMemberAction}
</button> </button>
</article> </article>
)) ))}
</div>
)} )}
</article>
</div>
</section>
</div> </div>
) : ( ) : (
<div class="balance-list"> <div class="balance-list">

View File

@@ -229,7 +229,9 @@ button {
.balance-list, .balance-list,
.ledger-list, .ledger-list,
.home-grid { .home-grid,
.admin-layout,
.admin-sublist {
display: grid; display: grid;
gap: 12px; gap: 12px;
} }
@@ -316,6 +318,52 @@ button {
margin-top: 12px; margin-top: 12px;
} }
.admin-layout {
gap: 18px;
}
.admin-hero {
gap: 16px;
}
.admin-summary-grid,
.admin-grid {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 12px;
}
.admin-section {
display: grid;
gap: 12px;
}
.admin-section__header {
display: flex;
align-items: end;
justify-content: space-between;
gap: 12px;
}
.admin-section__header h3 {
margin: 0;
font-family: 'Space Grotesk', 'IBM Plex Sans', sans-serif;
letter-spacing: -0.04em;
font-size: 1.15rem;
}
.admin-section__header p {
margin-top: 6px;
}
.admin-sublist {
margin-top: 12px;
}
.admin-card--wide {
min-width: 0;
}
.settings-field { .settings-field {
display: grid; display: grid;
gap: 6px; gap: 6px;
@@ -374,6 +422,14 @@ button {
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
} }
.admin-summary-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.admin-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.settings-grid { .settings-grid {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
@@ -381,6 +437,10 @@ button {
.panel--wide { .panel--wide {
grid-column: 1 / -1; grid-column: 1 / -1;
} }
.admin-card--wide {
grid-column: 1 / -1;
}
} }
@media (min-width: 980px) { @media (min-width: 980px) {