mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 12:04:02 +00:00
refactor(miniapp): unify settings design with editable-list pattern
This commit is contained in:
@@ -1455,18 +1455,20 @@ a {
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.ledger-actions {
|
||||
/* ── Editable List (used in Ledger, Settings, etc.) ───── */
|
||||
|
||||
.editable-list-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.ledger-list {
|
||||
.editable-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ledger-entry {
|
||||
.editable-list-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
@@ -1484,48 +1486,48 @@ a {
|
||||
transition: background var(--transition-fast);
|
||||
}
|
||||
|
||||
.ledger-entry:hover:not(:disabled) {
|
||||
.editable-list-row:hover:not(:disabled) {
|
||||
background: var(--bg-input);
|
||||
}
|
||||
|
||||
.ledger-entry:disabled {
|
||||
.editable-list-row:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.ledger-entry:last-child {
|
||||
.editable-list-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.ledger-entry__main {
|
||||
.editable-list-row__main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.ledger-entry__title {
|
||||
.editable-list-row__title {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.ledger-entry__actor {
|
||||
.editable-list-row__subtitle {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.ledger-entry__amounts {
|
||||
.editable-list-row__meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.ledger-entry__amounts strong {
|
||||
.editable-list-row__meta strong {
|
||||
font-size: var(--text-sm);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.ledger-entry__secondary {
|
||||
.editable-list-row__secondary {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
font-variant-numeric: tabular-nums;
|
||||
@@ -1617,85 +1619,7 @@ a {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ── Members ──────────────────────────────────────────── */
|
||||
|
||||
.members-list,
|
||||
.pending-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.member-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.member-row.interactive {
|
||||
margin: 0 calc(var(--spacing-lg) * -1);
|
||||
padding: var(--spacing-sm) var(--spacing-lg);
|
||||
cursor: pointer;
|
||||
transition: background var(--transition-fast);
|
||||
}
|
||||
|
||||
.member-row.interactive:hover {
|
||||
background: var(--bg-input);
|
||||
}
|
||||
|
||||
.member-row__info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.member-row__info strong {
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.member-row__badges {
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.member-row__weight {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.pending-member-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pending-member-row__handle {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.pending-member-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
/* ── Topics ───────────────────────────────────────────── */
|
||||
|
||||
.topics-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.topic-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-xs) 0;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
/* ── Settings Route ───────────────────────────────────── */
|
||||
|
||||
/* ── Testing Card ─────────────────────────────────────── */
|
||||
|
||||
|
||||
@@ -571,7 +571,7 @@ export default function LedgerRoute() {
|
||||
defaultOpen
|
||||
>
|
||||
<Show when={effectiveIsAdmin()}>
|
||||
<div class="ledger-actions">
|
||||
<div class="editable-list-actions">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
@@ -603,23 +603,25 @@ export default function LedgerRoute() {
|
||||
when={purchaseLedger().length > 0}
|
||||
fallback={<p class="empty-state">{copy().purchasesEmpty}</p>}
|
||||
>
|
||||
<div class="ledger-list">
|
||||
<div class="editable-list">
|
||||
<For each={purchaseLedger()}>
|
||||
{(entry) => (
|
||||
<button
|
||||
class="ledger-entry"
|
||||
class="editable-list-row"
|
||||
onClick={() => effectiveIsAdmin() && openPurchaseEditor(entry)}
|
||||
disabled={!effectiveIsAdmin()}
|
||||
>
|
||||
<div class="ledger-entry__main">
|
||||
<span class="ledger-entry__title">{entry.title}</span>
|
||||
<span class="ledger-entry__actor">{entry.actorDisplayName}</span>
|
||||
<div class="editable-list-row__main">
|
||||
<span class="editable-list-row__title">{entry.title}</span>
|
||||
<span class="editable-list-row__subtitle">
|
||||
{entry.actorDisplayName}
|
||||
</span>
|
||||
</div>
|
||||
<div class="ledger-entry__amounts">
|
||||
<div class="editable-list-row__meta">
|
||||
<strong>{ledgerPrimaryAmount(entry)}</strong>
|
||||
<Show when={ledgerSecondaryAmount(entry)}>
|
||||
{(secondary) => (
|
||||
<span class="ledger-entry__secondary">{secondary()}</span>
|
||||
<span class="editable-list-row__secondary">{secondary()}</span>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
@@ -633,7 +635,7 @@ export default function LedgerRoute() {
|
||||
{/* ── Utility bills ──────────────────────── */}
|
||||
<Collapsible title={copy().utilityLedgerTitle}>
|
||||
<Show when={effectiveIsAdmin()}>
|
||||
<div class="ledger-actions">
|
||||
<div class="editable-list-actions">
|
||||
<Button variant="primary" size="sm" onClick={() => setAddUtilityOpen(true)}>
|
||||
<Plus size={14} />
|
||||
{copy().addUtilityBillAction}
|
||||
@@ -644,19 +646,21 @@ export default function LedgerRoute() {
|
||||
when={utilityLedger().length > 0}
|
||||
fallback={<p class="empty-state">{copy().utilityLedgerEmpty}</p>}
|
||||
>
|
||||
<div class="ledger-list">
|
||||
<div class="editable-list">
|
||||
<For each={utilityLedger()}>
|
||||
{(entry) => (
|
||||
<button
|
||||
class="ledger-entry"
|
||||
class="editable-list-row"
|
||||
onClick={() => effectiveIsAdmin() && openUtilityEditor(entry)}
|
||||
disabled={!effectiveIsAdmin()}
|
||||
>
|
||||
<div class="ledger-entry__main">
|
||||
<span class="ledger-entry__title">{entry.title}</span>
|
||||
<span class="ledger-entry__actor">{entry.actorDisplayName}</span>
|
||||
<div class="editable-list-row__main">
|
||||
<span class="editable-list-row__title">{entry.title}</span>
|
||||
<span class="editable-list-row__subtitle">
|
||||
{entry.actorDisplayName}
|
||||
</span>
|
||||
</div>
|
||||
<div class="ledger-entry__amounts">
|
||||
<div class="editable-list-row__meta">
|
||||
<strong>{ledgerPrimaryAmount(entry)}</strong>
|
||||
</div>
|
||||
</button>
|
||||
@@ -674,7 +678,7 @@ export default function LedgerRoute() {
|
||||
: {})}
|
||||
>
|
||||
<Show when={effectiveIsAdmin()}>
|
||||
<div class="ledger-actions">
|
||||
<div class="editable-list-actions">
|
||||
<Button variant="primary" size="sm" onClick={() => setAddPaymentOpen(true)}>
|
||||
<Plus size={14} />
|
||||
{copy().paymentsAddAction}
|
||||
@@ -685,23 +689,25 @@ export default function LedgerRoute() {
|
||||
when={paymentLedger().length > 0}
|
||||
fallback={<p class="empty-state">{copy().paymentsEmpty}</p>}
|
||||
>
|
||||
<div class="ledger-list">
|
||||
<div class="editable-list">
|
||||
<For each={paymentLedger()}>
|
||||
{(entry) => (
|
||||
<button
|
||||
class="ledger-entry"
|
||||
class="editable-list-row"
|
||||
onClick={() => effectiveIsAdmin() && openPaymentEditor(entry)}
|
||||
disabled={!effectiveIsAdmin()}
|
||||
>
|
||||
<div class="ledger-entry__main">
|
||||
<span class="ledger-entry__title">
|
||||
<div class="editable-list-row__main">
|
||||
<span class="editable-list-row__title">
|
||||
{entry.paymentKind === 'rent'
|
||||
? copy().paymentLedgerRent
|
||||
: copy().paymentLedgerUtilities}
|
||||
</span>
|
||||
<span class="ledger-entry__actor">{entry.actorDisplayName}</span>
|
||||
<span class="editable-list-row__subtitle">
|
||||
{entry.actorDisplayName}
|
||||
</span>
|
||||
</div>
|
||||
<div class="ledger-entry__amounts">
|
||||
<div class="editable-list-row__meta">
|
||||
<strong>{ledgerPrimaryAmount(entry)}</strong>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Show, For, createSignal } from 'solid-js'
|
||||
import { ArrowLeft, Globe, User } from 'lucide-solid'
|
||||
import { ArrowLeft, Globe, Plus, User } from 'lucide-solid'
|
||||
import { useNavigate } from '@solidjs/router'
|
||||
|
||||
import { useSession } from '../contexts/session-context'
|
||||
@@ -366,6 +366,42 @@ export default function SettingsRoute() {
|
||||
</Card>
|
||||
</Collapsible>
|
||||
|
||||
{/* Utility Categories */}
|
||||
<Collapsible title={copy().utilityCategoriesTitle} body={copy().utilityCategoriesBody}>
|
||||
<div class="editable-list-actions">
|
||||
<Button variant="primary" size="sm" onClick={() => openAddCategory()}>
|
||||
<Plus size={14} />
|
||||
{copy().addCategoryAction}
|
||||
</Button>
|
||||
</div>
|
||||
<Show
|
||||
when={adminSettings()?.categories}
|
||||
fallback={<p class="empty-state">{copy().utilityCategoriesBody}</p>}
|
||||
>
|
||||
{(categories) => (
|
||||
<Show
|
||||
when={categories().length > 0}
|
||||
fallback={<p class="empty-state">{copy().utilityCategoriesBody}</p>}
|
||||
>
|
||||
<div class="editable-list">
|
||||
<For each={categories()}>
|
||||
{(category) => (
|
||||
<button class="editable-list-row" onClick={() => openEditCategory(category)}>
|
||||
<div class="editable-list-row__main">
|
||||
<span class="editable-list-row__title">{category.name}</span>
|
||||
</div>
|
||||
<Badge variant={category.isActive ? 'accent' : 'muted'}>
|
||||
{category.isActive ? copy().onLabel : copy().offLabel}
|
||||
</Badge>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
)}
|
||||
</Show>
|
||||
</Collapsible>
|
||||
|
||||
{/* Billing cycle */}
|
||||
<Collapsible title={copy().billingCycleTitle}>
|
||||
<Card>
|
||||
@@ -395,45 +431,43 @@ export default function SettingsRoute() {
|
||||
when={pendingMembers().length > 0}
|
||||
fallback={<p class="empty-state">{copy().pendingMembersEmpty}</p>}
|
||||
>
|
||||
<div class="pending-list">
|
||||
<div class="editable-list">
|
||||
<For each={pendingMembers()}>
|
||||
{(member) => (
|
||||
<Card>
|
||||
<div class="pending-member-row">
|
||||
<div>
|
||||
<strong>{member.displayName}</strong>
|
||||
<Show when={member.username}>
|
||||
{(username) => (
|
||||
<span class="pending-member-row__handle">@{username()}</span>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
<div class="pending-member-actions">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
loading={rejectingId() === member.telegramUserId}
|
||||
disabled={approvingId() === member.telegramUserId}
|
||||
onClick={() => void handleReject(member.telegramUserId)}
|
||||
>
|
||||
{rejectingId() === member.telegramUserId
|
||||
? copy().rejectingMember
|
||||
: copy().rejectMemberAction}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
loading={approvingId() === member.telegramUserId}
|
||||
disabled={rejectingId() === member.telegramUserId}
|
||||
onClick={() => void handleApprove(member.telegramUserId)}
|
||||
>
|
||||
{approvingId() === member.telegramUserId
|
||||
? copy().approvingMember
|
||||
: copy().approveMemberAction}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="editable-list-row">
|
||||
<div class="editable-list-row__main">
|
||||
<span class="editable-list-row__title">{member.displayName}</span>
|
||||
<Show when={member.username}>
|
||||
{(username) => (
|
||||
<span class="editable-list-row__subtitle">@{username()}</span>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</Card>
|
||||
<div class="editable-list-row__meta">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
loading={rejectingId() === member.telegramUserId}
|
||||
disabled={approvingId() === member.telegramUserId}
|
||||
onClick={() => void handleReject(member.telegramUserId)}
|
||||
>
|
||||
{rejectingId() === member.telegramUserId
|
||||
? copy().rejectingMember
|
||||
: copy().rejectMemberAction}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
loading={approvingId() === member.telegramUserId}
|
||||
disabled={rejectingId() === member.telegramUserId}
|
||||
onClick={() => void handleApprove(member.telegramUserId)}
|
||||
>
|
||||
{approvingId() === member.telegramUserId
|
||||
? copy().approvingMember
|
||||
: copy().approveMemberAction}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
@@ -444,37 +478,29 @@ export default function SettingsRoute() {
|
||||
<Collapsible title={copy().houseSectionMembers} body={copy().membersBody}>
|
||||
<Show when={adminSettings()?.members}>
|
||||
{(members) => (
|
||||
<div class="members-list">
|
||||
<div class="editable-list">
|
||||
<For each={members()}>
|
||||
{(member) => (
|
||||
<Card>
|
||||
<div
|
||||
class="member-row interactive"
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => openEditMember(member)}
|
||||
>
|
||||
<div class="member-row__info">
|
||||
<strong>{member.displayName}</strong>
|
||||
<div class="member-row__badges">
|
||||
<Badge variant={member.isAdmin ? 'accent' : 'muted'}>
|
||||
{member.isAdmin ? copy().adminTag : copy().residentTag}
|
||||
</Badge>
|
||||
<Badge variant="muted">
|
||||
{member.status === 'active'
|
||||
? copy().memberStatusActive
|
||||
: member.status === 'away'
|
||||
? copy().memberStatusAway
|
||||
: copy().memberStatusLeft}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div class="member-row__weight">
|
||||
<span>
|
||||
{copy().rentWeightLabel}: {member.rentShareWeight}
|
||||
</span>
|
||||
</div>
|
||||
<button class="editable-list-row" onClick={() => openEditMember(member)}>
|
||||
<div class="editable-list-row__main">
|
||||
<span class="editable-list-row__title">{member.displayName}</span>
|
||||
<span class="editable-list-row__subtitle">
|
||||
{copy().rentWeightLabel}: {member.rentShareWeight}
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
<div class="editable-list-row__meta">
|
||||
<Badge variant={member.isAdmin ? 'accent' : 'muted'}>
|
||||
{member.isAdmin ? copy().adminTag : copy().residentTag}
|
||||
</Badge>
|
||||
<Badge variant="muted">
|
||||
{member.status === 'active'
|
||||
? copy().memberStatusActive
|
||||
: member.status === 'away'
|
||||
? copy().memberStatusAway
|
||||
: copy().memberStatusLeft}
|
||||
</Badge>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
@@ -486,7 +512,7 @@ export default function SettingsRoute() {
|
||||
<Collapsible title={copy().houseSectionTopics} body={copy().topicBindingsBody}>
|
||||
<Show when={adminSettings()?.topics}>
|
||||
{(topics) => (
|
||||
<div class="topics-list">
|
||||
<div class="editable-list">
|
||||
<For each={topics()}>
|
||||
{(topic) => {
|
||||
const roleLabel = () => {
|
||||
@@ -500,11 +526,15 @@ export default function SettingsRoute() {
|
||||
return labels[topic.role] ?? topic.role
|
||||
}
|
||||
return (
|
||||
<div class="topic-row">
|
||||
<span>{roleLabel()}</span>
|
||||
<Badge variant={topic.telegramThreadId ? 'accent' : 'muted'}>
|
||||
{topic.telegramThreadId ? copy().topicBound : copy().topicUnbound}
|
||||
</Badge>
|
||||
<div class="editable-list-row">
|
||||
<div class="editable-list-row__main">
|
||||
<span class="editable-list-row__title">{roleLabel()}</span>
|
||||
</div>
|
||||
<div class="editable-list-row__meta">
|
||||
<Badge variant={topic.telegramThreadId ? 'accent' : 'muted'}>
|
||||
{topic.telegramThreadId ? copy().topicBound : copy().topicUnbound}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
@@ -513,40 +543,6 @@ export default function SettingsRoute() {
|
||||
)}
|
||||
</Show>
|
||||
</Collapsible>
|
||||
|
||||
{/* Utility Categories */}
|
||||
<Collapsible title={copy().utilityCategoriesTitle} body={copy().utilityCategoriesBody}>
|
||||
<Show
|
||||
when={adminSettings()?.categories}
|
||||
fallback={<p class="empty-state">{copy().utilityCategoriesBody}</p>}
|
||||
>
|
||||
{(categories) => (
|
||||
<div class="categories-list">
|
||||
<For each={categories()}>
|
||||
{(category) => (
|
||||
<Card>
|
||||
<div
|
||||
class="category-row interactive"
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => openEditCategory(category)}
|
||||
>
|
||||
<div class="category-row__info">
|
||||
<strong>{category.name}</strong>
|
||||
<Badge variant={category.isActive ? 'accent' : 'muted'}>
|
||||
{category.isActive ? copy().onLabel : copy().offLabel}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</For>
|
||||
<Button variant="secondary" size="sm" onClick={() => openAddCategory()}>
|
||||
{copy().addCategoryAction}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</Collapsible>
|
||||
</Show>
|
||||
|
||||
{/* ── Billing Settings Editor Modal ────────────── */}
|
||||
|
||||
Reference in New Issue
Block a user