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':
return readySession()?.member.isAdmin ? (
<div class="balance-list">
<article class="balance-item">
<div class="admin-layout">
<article class="balance-item balance-item--accent admin-hero">
<header>
<strong>{copy().householdSettingsTitle}</strong>
<span>{adminSettings()?.settings.settlementCurrency ?? '—'}</span>
</header>
<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 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">
<header>
<strong>{copy().billingCycleTitle}</strong>
@@ -1108,65 +1136,6 @@ function App() {
{closingCycle() ? copy().closingCycle : copy().closeCycleAction}
</button>
</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 class="balance-item">
<header>
<strong>{copy().billingSettingsTitle}</strong>
<span>{billingForm().settlementCurrency}</span>
</header>
<div class="settings-grid">
<label class="settings-field">
@@ -1340,11 +1311,13 @@ function App() {
{savingBillingSettings() ? copy().savingSettings : copy().saveSettingsAction}
</button>
</article>
<article class="balance-item">
<header>
<strong>{copy().householdLanguage}</strong>
<span>{readySession()?.member.householdDefaultLocale.toUpperCase()}</span>
</header>
<p>{copy().householdSettingsBody}</p>
<div class="locale-switch__buttons">
<button
classList={{
@@ -1368,12 +1341,89 @@ function App() {
</button>
</div>
</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">
<header>
<strong>{copy().utilityCategoriesTitle}</strong>
<span>{String(adminSettings()?.categories.length ?? 0)}</span>
</header>
<p>{copy().utilityCategoriesBody}</p>
<div class="balance-list">
<div class="balance-list admin-sublist">
{adminSettings()?.categories.map((category) => (
<article class="ledger-item">
<header>
@@ -1439,12 +1489,14 @@ function App() {
void handleSaveUtilityCategory({
slug: category.slug,
name:
adminSettings()?.categories.find((item) => item.slug === category.slug)
?.name ?? category.name,
adminSettings()?.categories.find(
(item) => item.slug === category.slug
)?.name ?? category.name,
sortOrder: category.sortOrder,
isActive:
adminSettings()?.categories.find((item) => item.slug === category.slug)
?.isActive ?? category.isActive
adminSettings()?.categories.find(
(item) => item.slug === category.slug
)?.isActive ?? category.isActive
})
}
>
@@ -1466,7 +1518,8 @@ function App() {
class="ghost-button"
type="button"
disabled={
newCategoryName().trim().length === 0 || savingCategorySlug() === '__new__'
newCategoryName().trim().length === 0 ||
savingCategorySlug() === '__new__'
}
onClick={() =>
void handleSaveUtilityCategory({
@@ -1483,23 +1536,37 @@ function App() {
</article>
</div>
</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>
<strong>{copy().adminsTitle}</strong>
<span>{String(adminSettings()?.members.length ?? 0)}</span>
</header>
<p>{copy().adminsBody}</p>
<div class="balance-list">
<div class="balance-list admin-sublist">
{adminSettings()?.members.map((member) => (
<article class="ledger-item">
<header>
<strong>{member.displayName}</strong>
<span>{member.isAdmin ? copy().adminTag : copy().residentTag}</span>
</header>
<div class="settings-grid">
<label class="settings-field settings-field--wide">
<span>{copy().rentWeightLabel}</span>
<input
inputmode="numeric"
value={rentWeightDrafts()[member.id] ?? String(member.rentShareWeight)}
value={
rentWeightDrafts()[member.id] ?? String(member.rentShareWeight)
}
onInput={(event) =>
setRentWeightDrafts((current) => ({
...current,
@@ -1508,6 +1575,8 @@ function App() {
}
/>
</label>
</div>
<div class="inline-actions">
<button
class="ghost-button"
type="button"
@@ -1533,23 +1602,24 @@ function App() {
: copy().promoteAdminAction}
</button>
) : null}
</div>
</article>
))}
</div>
</article>
<article class="balance-item">
<header>
<strong>{copy().pendingMembersTitle}</strong>
<span>{String(pendingMembers().length)}</span>
</header>
<p>{copy().pendingMembersBody}</p>
</article>
{pendingMembers().length === 0 ? (
<article class="balance-item">
<p>{copy().pendingMembersEmpty}</p>
</article>
) : (
pendingMembers().map((member) => (
<article class="balance-item">
<div class="balance-list admin-sublist">
{pendingMembers().map((member) => (
<article class="ledger-item">
<header>
<strong>{member.displayName}</strong>
<span>{member.telegramUserId}</span>
@@ -1570,8 +1640,12 @@ function App() {
: copy().approveMemberAction}
</button>
</article>
))
))}
</div>
)}
</article>
</div>
</section>
</div>
) : (
<div class="balance-list">

View File

@@ -229,7 +229,9 @@ button {
.balance-list,
.ledger-list,
.home-grid {
.home-grid,
.admin-layout,
.admin-sublist {
display: grid;
gap: 12px;
}
@@ -316,6 +318,52 @@ button {
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 {
display: grid;
gap: 6px;
@@ -374,6 +422,14 @@ button {
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 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@@ -381,6 +437,10 @@ button {
.panel--wide {
grid-column: 1 / -1;
}
.admin-card--wide {
grid-column: 1 / -1;
}
}
@media (min-width: 980px) {