mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 13:54:02 +00:00
refactor(miniapp): extract ledger screen
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { Match, Show, Switch, createMemo, createSignal, onMount, type JSX } from 'solid-js'
|
import { Match, Show, Switch, createMemo, createSignal, onMount } from 'solid-js'
|
||||||
|
|
||||||
import { dictionary, type Locale } from './i18n'
|
import { dictionary, type Locale } from './i18n'
|
||||||
import {
|
import {
|
||||||
@@ -45,6 +45,7 @@ import { LoadingState } from './components/session/loading-state'
|
|||||||
import { OnboardingState } from './components/session/onboarding-state'
|
import { OnboardingState } from './components/session/onboarding-state'
|
||||||
import { BalancesScreen } from './screens/balances-screen'
|
import { BalancesScreen } from './screens/balances-screen'
|
||||||
import { HomeScreen } from './screens/home-screen'
|
import { HomeScreen } from './screens/home-screen'
|
||||||
|
import { LedgerScreen } from './screens/ledger-screen'
|
||||||
import {
|
import {
|
||||||
demoAdminSettings,
|
demoAdminSettings,
|
||||||
demoCycleState,
|
demoCycleState,
|
||||||
@@ -1868,523 +1869,131 @@ function App() {
|
|||||||
)
|
)
|
||||||
case 'ledger':
|
case 'ledger':
|
||||||
return (
|
return (
|
||||||
<div class="ledger-list">
|
<LedgerScreen
|
||||||
<ShowDashboard
|
copy={copy()}
|
||||||
dashboard={dashboard()}
|
dashboard={dashboard()}
|
||||||
fallback={<p>{copy().emptyDashboard}</p>}
|
readyIsAdmin={readySession()?.member.isAdmin === true}
|
||||||
render={() => (
|
adminMembers={adminSettings()?.members ?? []}
|
||||||
<>
|
purchaseEntries={purchaseLedger()}
|
||||||
<article class="balance-item">
|
utilityEntries={utilityLedger()}
|
||||||
<header>
|
paymentEntries={paymentLedger()}
|
||||||
<strong>
|
editingPurchaseEntry={editingPurchaseEntry()}
|
||||||
{readySession()?.member.isAdmin
|
editingPaymentEntry={editingPaymentEntry()}
|
||||||
? copy().purchaseReviewTitle
|
purchaseDraftMap={purchaseDraftMap()}
|
||||||
: copy().purchasesTitle}
|
paymentDraftMap={paymentDraftMap()}
|
||||||
</strong>
|
paymentForm={paymentForm()}
|
||||||
</header>
|
addingPaymentOpen={addingPaymentOpen()}
|
||||||
<Show when={readySession()?.member.isAdmin}>
|
savingPurchaseId={savingPurchaseId()}
|
||||||
<p>{copy().purchaseReviewBody}</p>
|
deletingPurchaseId={deletingPurchaseId()}
|
||||||
</Show>
|
savingPaymentId={savingPaymentId()}
|
||||||
{purchaseLedger().length === 0 ? (
|
deletingPaymentId={deletingPaymentId()}
|
||||||
<p>{copy().purchasesEmpty}</p>
|
addingPayment={addingPayment()}
|
||||||
) : (
|
ledgerTitle={ledgerTitle}
|
||||||
<div class="ledger-list">
|
ledgerPrimaryAmount={ledgerPrimaryAmount}
|
||||||
{purchaseLedger().map((entry) => (
|
ledgerSecondaryAmount={ledgerSecondaryAmount}
|
||||||
<article class="ledger-compact-card">
|
purchaseParticipantSummary={purchaseParticipantSummary}
|
||||||
<div class="ledger-compact-card__main">
|
purchaseDraftForEntry={purchaseDraftForEntry}
|
||||||
<header>
|
paymentDraftForEntry={paymentDraftForEntry}
|
||||||
<strong>{entry.title}</strong>
|
purchaseSplitPreview={purchaseSplitPreview}
|
||||||
<span>{entry.occurredAt?.slice(0, 10) ?? '—'}</span>
|
paymentMemberName={paymentMemberName}
|
||||||
</header>
|
onOpenPurchaseEditor={setEditingPurchaseId}
|
||||||
<p>{entry.actorDisplayName ?? copy().ledgerActorFallback}</p>
|
onClosePurchaseEditor={() => setEditingPurchaseId(null)}
|
||||||
<div class="ledger-compact-card__meta">
|
onDeletePurchase={handleDeletePurchase}
|
||||||
<span class="mini-chip">{ledgerPrimaryAmount(entry)}</span>
|
onSavePurchase={handleUpdatePurchase}
|
||||||
<Show when={ledgerSecondaryAmount(entry)}>
|
onPurchaseDescriptionChange={(purchaseId, entry, value) =>
|
||||||
{(secondary) => (
|
updatePurchaseDraft(purchaseId, entry, (current) => ({
|
||||||
<span class="mini-chip mini-chip--muted">{secondary()}</span>
|
...current,
|
||||||
)}
|
description: value
|
||||||
</Show>
|
}))
|
||||||
<Show when={entry.kind === 'purchase'}>
|
}
|
||||||
<span class="mini-chip mini-chip--muted">
|
onPurchaseAmountChange={(purchaseId, entry, value) =>
|
||||||
{purchaseParticipantSummary(entry)}
|
updatePurchaseDraft(purchaseId, entry, (current) => ({
|
||||||
</span>
|
...current,
|
||||||
</Show>
|
amountMajor: value
|
||||||
</div>
|
}))
|
||||||
</div>
|
}
|
||||||
<Show when={readySession()?.member.isAdmin}>
|
onPurchaseCurrencyChange={(purchaseId, entry, value) =>
|
||||||
<div class="ledger-compact-card__actions">
|
updatePurchaseDraft(purchaseId, entry, (current) => ({
|
||||||
<IconButton
|
...current,
|
||||||
label={copy().editEntryAction}
|
currency: value
|
||||||
onClick={() => setEditingPurchaseId(entry.id)}
|
}))
|
||||||
>
|
}
|
||||||
...
|
onPurchaseSplitModeChange={(purchaseId, entry, value) =>
|
||||||
</IconButton>
|
updatePurchaseDraft(purchaseId, entry, (current) => ({
|
||||||
</div>
|
...current,
|
||||||
</Show>
|
splitMode: value
|
||||||
</article>
|
}))
|
||||||
))}
|
}
|
||||||
</div>
|
onTogglePurchaseParticipant={togglePurchaseParticipant}
|
||||||
)}
|
onPurchaseParticipantShareChange={(purchaseId, entry, memberId, value) =>
|
||||||
</article>
|
updatePurchaseDraft(purchaseId, entry, (current) => ({
|
||||||
<Modal
|
...current,
|
||||||
open={Boolean(editingPurchaseEntry())}
|
participants: current.participants.map((participant) =>
|
||||||
title={copy().purchaseReviewTitle}
|
participant.memberId === memberId
|
||||||
description={copy().purchaseEditorBody}
|
? {
|
||||||
closeLabel={copy().closeEditorAction}
|
...participant,
|
||||||
onClose={() => setEditingPurchaseId(null)}
|
shareAmountMajor: value
|
||||||
footer={(() => {
|
|
||||||
const entry = editingPurchaseEntry()
|
|
||||||
|
|
||||||
if (!entry) {
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
: participant
|
||||||
return (
|
)
|
||||||
<div class="modal-action-row">
|
}))
|
||||||
<Button
|
}
|
||||||
variant="danger"
|
onOpenAddPayment={() => setAddingPaymentOpen(true)}
|
||||||
onClick={() => void handleDeletePurchase(entry.id)}
|
onCloseAddPayment={() => setAddingPaymentOpen(false)}
|
||||||
>
|
onAddPayment={handleAddPayment}
|
||||||
{deletingPurchaseId() === entry.id
|
onPaymentFormMemberChange={(value) =>
|
||||||
? copy().deletingPurchase
|
setPaymentForm((current) => ({
|
||||||
: copy().purchaseDeleteAction}
|
...current,
|
||||||
</Button>
|
memberId: value
|
||||||
<div class="modal-action-row__primary">
|
}))
|
||||||
<Button variant="ghost" onClick={() => setEditingPurchaseId(null)}>
|
}
|
||||||
{copy().closeEditorAction}
|
onPaymentFormKindChange={(value) =>
|
||||||
</Button>
|
setPaymentForm((current) => ({
|
||||||
<Button
|
...current,
|
||||||
variant="primary"
|
kind: value
|
||||||
disabled={savingPurchaseId() === entry.id}
|
}))
|
||||||
onClick={() => void handleUpdatePurchase(entry.id)}
|
}
|
||||||
>
|
onPaymentFormAmountChange={(value) =>
|
||||||
{savingPurchaseId() === entry.id
|
setPaymentForm((current) => ({
|
||||||
? copy().savingPurchase
|
...current,
|
||||||
: copy().purchaseSaveAction}
|
amountMajor: value
|
||||||
</Button>
|
}))
|
||||||
</div>
|
}
|
||||||
</div>
|
onPaymentFormCurrencyChange={(value) =>
|
||||||
)
|
setPaymentForm((current) => ({
|
||||||
})()}
|
...current,
|
||||||
>
|
currency: value
|
||||||
{(() => {
|
}))
|
||||||
const entry = editingPurchaseEntry()
|
}
|
||||||
|
onOpenPaymentEditor={setEditingPaymentId}
|
||||||
if (!entry) {
|
onClosePaymentEditor={() => setEditingPaymentId(null)}
|
||||||
return null
|
onDeletePayment={handleDeletePayment}
|
||||||
}
|
onSavePayment={handleUpdatePayment}
|
||||||
|
onPaymentDraftMemberChange={(paymentId, entry, value) =>
|
||||||
const draft = purchaseDraftMap()[entry.id] ?? purchaseDraftForEntry(entry)
|
updatePaymentDraft(paymentId, entry, (current) => ({
|
||||||
const splitPreview = purchaseSplitPreview(entry.id)
|
...current,
|
||||||
|
memberId: value
|
||||||
return (
|
}))
|
||||||
<>
|
}
|
||||||
<div class="editor-grid">
|
onPaymentDraftKindChange={(paymentId, entry, value) =>
|
||||||
<Field label={copy().purchaseReviewTitle} wide>
|
updatePaymentDraft(paymentId, entry, (current) => ({
|
||||||
<input
|
...current,
|
||||||
value={draft.description}
|
kind: value
|
||||||
onInput={(event) =>
|
}))
|
||||||
updatePurchaseDraft(entry.id, entry, (current) => ({
|
}
|
||||||
...current,
|
onPaymentDraftAmountChange={(paymentId, entry, value) =>
|
||||||
description: event.currentTarget.value
|
updatePaymentDraft(paymentId, entry, (current) => ({
|
||||||
}))
|
...current,
|
||||||
}
|
amountMajor: value
|
||||||
/>
|
}))
|
||||||
</Field>
|
}
|
||||||
<Field label={copy().paymentAmount}>
|
onPaymentDraftCurrencyChange={(paymentId, entry, value) =>
|
||||||
<input
|
updatePaymentDraft(paymentId, entry, (current) => ({
|
||||||
value={draft.amountMajor}
|
...current,
|
||||||
onInput={(event) =>
|
currency: value
|
||||||
updatePurchaseDraft(entry.id, entry, (current) => ({
|
}))
|
||||||
...current,
|
}
|
||||||
amountMajor: event.currentTarget.value
|
/>
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Field label={copy().settlementCurrency}>
|
|
||||||
<select
|
|
||||||
value={draft.currency}
|
|
||||||
onChange={(event) =>
|
|
||||||
updatePurchaseDraft(entry.id, entry, (current) => ({
|
|
||||||
...current,
|
|
||||||
currency: event.currentTarget.value as 'USD' | 'GEL'
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="GEL">GEL</option>
|
|
||||||
<option value="USD">USD</option>
|
|
||||||
</select>
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<section class="editor-panel">
|
|
||||||
<header class="editor-panel__header">
|
|
||||||
<strong>{copy().purchaseSplitTitle}</strong>
|
|
||||||
<span>
|
|
||||||
{draft.splitMode === 'custom_amounts'
|
|
||||||
? copy().purchaseSplitCustom
|
|
||||||
: copy().purchaseSplitEqual}
|
|
||||||
</span>
|
|
||||||
</header>
|
|
||||||
<div class="editor-grid">
|
|
||||||
<Field label={copy().purchaseSplitModeLabel} wide>
|
|
||||||
<select
|
|
||||||
value={draft.splitMode}
|
|
||||||
onChange={(event) =>
|
|
||||||
updatePurchaseDraft(entry.id, entry, (current) => ({
|
|
||||||
...current,
|
|
||||||
splitMode: event.currentTarget.value as
|
|
||||||
| 'equal'
|
|
||||||
| 'custom_amounts'
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="equal">{copy().purchaseSplitEqual}</option>
|
|
||||||
<option value="custom_amounts">
|
|
||||||
{copy().purchaseSplitCustom}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
<div class="participant-list">
|
|
||||||
{(adminSettings()?.members ?? []).map((member) => {
|
|
||||||
const included = draft.participants.some(
|
|
||||||
(participant) => participant.memberId === member.id
|
|
||||||
)
|
|
||||||
const previewAmount =
|
|
||||||
splitPreview.find(
|
|
||||||
(participant) => participant.memberId === member.id
|
|
||||||
)?.amountMajor ?? '0.00'
|
|
||||||
|
|
||||||
return (
|
|
||||||
<article class="participant-card">
|
|
||||||
<header>
|
|
||||||
<strong>{member.displayName}</strong>
|
|
||||||
<span>
|
|
||||||
{previewAmount} {draft.currency}
|
|
||||||
</span>
|
|
||||||
</header>
|
|
||||||
<div class="participant-card__controls">
|
|
||||||
<Button
|
|
||||||
variant={included ? 'primary' : 'secondary'}
|
|
||||||
onClick={() =>
|
|
||||||
togglePurchaseParticipant(
|
|
||||||
entry.id,
|
|
||||||
entry,
|
|
||||||
member.id,
|
|
||||||
!included
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{included
|
|
||||||
? copy().participantIncluded
|
|
||||||
: copy().participantExcluded}
|
|
||||||
</Button>
|
|
||||||
<Show when={included && draft.splitMode === 'custom_amounts'}>
|
|
||||||
<Field
|
|
||||||
label={copy().purchaseCustomShareLabel}
|
|
||||||
class="participant-card__field"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
value={
|
|
||||||
draft.participants.find(
|
|
||||||
(participant) => participant.memberId === member.id
|
|
||||||
)?.shareAmountMajor ?? ''
|
|
||||||
}
|
|
||||||
onInput={(event) =>
|
|
||||||
updatePurchaseDraft(entry.id, entry, (current) => ({
|
|
||||||
...current,
|
|
||||||
participants: current.participants.map(
|
|
||||||
(participant) =>
|
|
||||||
participant.memberId === member.id
|
|
||||||
? {
|
|
||||||
...participant,
|
|
||||||
shareAmountMajor:
|
|
||||||
event.currentTarget.value
|
|
||||||
}
|
|
||||||
: participant
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
})()}
|
|
||||||
</Modal>
|
|
||||||
<article class="balance-item">
|
|
||||||
<header>
|
|
||||||
<strong>{copy().utilityLedgerTitle}</strong>
|
|
||||||
</header>
|
|
||||||
{utilityLedger().length === 0 ? (
|
|
||||||
<p>{copy().utilityLedgerEmpty}</p>
|
|
||||||
) : (
|
|
||||||
<div class="ledger-list">
|
|
||||||
{utilityLedger().map((entry) => (
|
|
||||||
<article class="ledger-item">
|
|
||||||
<header>
|
|
||||||
<strong>{ledgerTitle(entry)}</strong>
|
|
||||||
<span>{ledgerPrimaryAmount(entry)}</span>
|
|
||||||
</header>
|
|
||||||
<Show when={ledgerSecondaryAmount(entry)}>
|
|
||||||
{(secondary) => <p>{secondary()}</p>}
|
|
||||||
</Show>
|
|
||||||
<p>{entry.actorDisplayName ?? copy().ledgerActorFallback}</p>
|
|
||||||
</article>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</article>
|
|
||||||
<article class="balance-item">
|
|
||||||
<header>
|
|
||||||
<strong>{copy().paymentsAdminTitle}</strong>
|
|
||||||
</header>
|
|
||||||
<Show when={readySession()?.member.isAdmin}>
|
|
||||||
<p>{copy().paymentsAdminBody}</p>
|
|
||||||
<div class="panel-toolbar">
|
|
||||||
<Button variant="secondary" onClick={() => setAddingPaymentOpen(true)}>
|
|
||||||
{copy().paymentsAddAction}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
{paymentLedger().length === 0 ? (
|
|
||||||
<p>{copy().paymentsEmpty}</p>
|
|
||||||
) : (
|
|
||||||
<div class="ledger-list">
|
|
||||||
{paymentLedger().map((entry) => (
|
|
||||||
<article class="ledger-compact-card">
|
|
||||||
<div class="ledger-compact-card__main">
|
|
||||||
<header>
|
|
||||||
<strong>{paymentMemberName(entry)}</strong>
|
|
||||||
<span>{entry.occurredAt?.slice(0, 10) ?? '—'}</span>
|
|
||||||
</header>
|
|
||||||
<p>{ledgerTitle(entry)}</p>
|
|
||||||
<div class="ledger-compact-card__meta">
|
|
||||||
<span class="mini-chip">{ledgerPrimaryAmount(entry)}</span>
|
|
||||||
<Show when={ledgerSecondaryAmount(entry)}>
|
|
||||||
{(secondary) => (
|
|
||||||
<span class="mini-chip mini-chip--muted">{secondary()}</span>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Show when={readySession()?.member.isAdmin}>
|
|
||||||
<div class="ledger-compact-card__actions">
|
|
||||||
<IconButton
|
|
||||||
label={copy().editEntryAction}
|
|
||||||
onClick={() => setEditingPaymentId(entry.id)}
|
|
||||||
>
|
|
||||||
...
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</article>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</article>
|
|
||||||
<Modal
|
|
||||||
open={addingPaymentOpen()}
|
|
||||||
title={copy().paymentsAddAction}
|
|
||||||
description={copy().paymentCreateBody}
|
|
||||||
closeLabel={copy().closeEditorAction}
|
|
||||||
onClose={() => setAddingPaymentOpen(false)}
|
|
||||||
footer={
|
|
||||||
<div class="modal-action-row modal-action-row--single">
|
|
||||||
<Button variant="ghost" onClick={() => setAddingPaymentOpen(false)}>
|
|
||||||
{copy().closeEditorAction}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
disabled={
|
|
||||||
addingPayment() || paymentForm().amountMajor.trim().length === 0
|
|
||||||
}
|
|
||||||
onClick={() => void handleAddPayment()}
|
|
||||||
>
|
|
||||||
{addingPayment() ? copy().addingPayment : copy().paymentsAddAction}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class="editor-grid">
|
|
||||||
<Field label={copy().paymentMember} wide>
|
|
||||||
<select
|
|
||||||
value={paymentForm().memberId}
|
|
||||||
onChange={(event) =>
|
|
||||||
setPaymentForm((current) => ({
|
|
||||||
...current,
|
|
||||||
memberId: event.currentTarget.value
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{adminSettings()?.members.map((member) => (
|
|
||||||
<option value={member.id}>{member.displayName}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</Field>
|
|
||||||
<Field label={copy().paymentKind}>
|
|
||||||
<select
|
|
||||||
value={paymentForm().kind}
|
|
||||||
onChange={(event) =>
|
|
||||||
setPaymentForm((current) => ({
|
|
||||||
...current,
|
|
||||||
kind: event.currentTarget.value as 'rent' | 'utilities'
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="rent">{copy().paymentLedgerRent}</option>
|
|
||||||
<option value="utilities">{copy().paymentLedgerUtilities}</option>
|
|
||||||
</select>
|
|
||||||
</Field>
|
|
||||||
<Field label={copy().paymentAmount}>
|
|
||||||
<input
|
|
||||||
value={paymentForm().amountMajor}
|
|
||||||
onInput={(event) =>
|
|
||||||
setPaymentForm((current) => ({
|
|
||||||
...current,
|
|
||||||
amountMajor: event.currentTarget.value
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Field label={copy().settlementCurrency}>
|
|
||||||
<select
|
|
||||||
value={paymentForm().currency}
|
|
||||||
onChange={(event) =>
|
|
||||||
setPaymentForm((current) => ({
|
|
||||||
...current,
|
|
||||||
currency: event.currentTarget.value as 'USD' | 'GEL'
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="GEL">GEL</option>
|
|
||||||
<option value="USD">USD</option>
|
|
||||||
</select>
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
<Modal
|
|
||||||
open={Boolean(editingPaymentEntry())}
|
|
||||||
title={copy().paymentsAdminTitle}
|
|
||||||
description={copy().paymentEditorBody}
|
|
||||||
closeLabel={copy().closeEditorAction}
|
|
||||||
onClose={() => setEditingPaymentId(null)}
|
|
||||||
footer={(() => {
|
|
||||||
const entry = editingPaymentEntry()
|
|
||||||
|
|
||||||
if (!entry) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="modal-action-row">
|
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
onClick={() => void handleDeletePayment(entry.id)}
|
|
||||||
>
|
|
||||||
{deletingPaymentId() === entry.id
|
|
||||||
? copy().deletingPayment
|
|
||||||
: copy().paymentDeleteAction}
|
|
||||||
</Button>
|
|
||||||
<div class="modal-action-row__primary">
|
|
||||||
<Button variant="ghost" onClick={() => setEditingPaymentId(null)}>
|
|
||||||
{copy().closeEditorAction}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
disabled={savingPaymentId() === entry.id}
|
|
||||||
onClick={() => void handleUpdatePayment(entry.id)}
|
|
||||||
>
|
|
||||||
{savingPaymentId() === entry.id
|
|
||||||
? copy().addingPayment
|
|
||||||
: copy().paymentSaveAction}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})()}
|
|
||||||
>
|
|
||||||
{(() => {
|
|
||||||
const entry = editingPaymentEntry()
|
|
||||||
|
|
||||||
if (!entry) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const draft = paymentDraftMap()[entry.id] ?? paymentDraftForEntry(entry)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="editor-grid">
|
|
||||||
<Field label={copy().paymentMember} wide>
|
|
||||||
<select
|
|
||||||
value={draft.memberId}
|
|
||||||
onChange={(event) =>
|
|
||||||
updatePaymentDraft(entry.id, entry, (current) => ({
|
|
||||||
...current,
|
|
||||||
memberId: event.currentTarget.value
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{adminSettings()?.members.map((member) => (
|
|
||||||
<option value={member.id}>{member.displayName}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</Field>
|
|
||||||
<Field label={copy().paymentKind}>
|
|
||||||
<select
|
|
||||||
value={draft.kind}
|
|
||||||
onChange={(event) =>
|
|
||||||
updatePaymentDraft(entry.id, entry, (current) => ({
|
|
||||||
...current,
|
|
||||||
kind: event.currentTarget.value as 'rent' | 'utilities'
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="rent">{copy().paymentLedgerRent}</option>
|
|
||||||
<option value="utilities">{copy().paymentLedgerUtilities}</option>
|
|
||||||
</select>
|
|
||||||
</Field>
|
|
||||||
<Field label={copy().paymentAmount}>
|
|
||||||
<input
|
|
||||||
value={draft.amountMajor}
|
|
||||||
onInput={(event) =>
|
|
||||||
updatePaymentDraft(entry.id, entry, (current) => ({
|
|
||||||
...current,
|
|
||||||
amountMajor: event.currentTarget.value
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Field label={copy().settlementCurrency}>
|
|
||||||
<select
|
|
||||||
value={draft.currency}
|
|
||||||
onChange={(event) =>
|
|
||||||
updatePaymentDraft(entry.id, entry, (current) => ({
|
|
||||||
...current,
|
|
||||||
currency: event.currentTarget.value as 'USD' | 'GEL'
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="GEL">GEL</option>
|
|
||||||
<option value="USD">USD</option>
|
|
||||||
</select>
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})()}
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
case 'house':
|
case 'house':
|
||||||
return readySession()?.member.isAdmin ? (
|
return readySession()?.member.isAdmin ? (
|
||||||
@@ -3663,12 +3272,4 @@ function App() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ShowDashboard(props: {
|
|
||||||
dashboard: MiniAppDashboard | null
|
|
||||||
fallback: JSX.Element
|
|
||||||
render: (dashboard: MiniAppDashboard) => JSX.Element
|
|
||||||
}) {
|
|
||||||
return <>{props.dashboard ? props.render(props.dashboard) : props.fallback}</>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App
|
export default App
|
||||||
|
|||||||
600
apps/miniapp/src/screens/ledger-screen.tsx
Normal file
600
apps/miniapp/src/screens/ledger-screen.tsx
Normal file
@@ -0,0 +1,600 @@
|
|||||||
|
import { For, Show } from 'solid-js'
|
||||||
|
|
||||||
|
import { Button, Field, IconButton, Modal } from '../components/ui'
|
||||||
|
import type { MiniAppAdminSettingsPayload, MiniAppDashboard } from '../miniapp-api'
|
||||||
|
|
||||||
|
type PurchaseDraft = {
|
||||||
|
description: string
|
||||||
|
amountMajor: string
|
||||||
|
currency: 'USD' | 'GEL'
|
||||||
|
splitMode: 'equal' | 'custom_amounts'
|
||||||
|
participants: {
|
||||||
|
memberId: string
|
||||||
|
shareAmountMajor: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaymentDraft = {
|
||||||
|
memberId: string
|
||||||
|
kind: 'rent' | 'utilities'
|
||||||
|
amountMajor: string
|
||||||
|
currency: 'USD' | 'GEL'
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
copy: Record<string, string | undefined>
|
||||||
|
dashboard: MiniAppDashboard | null
|
||||||
|
readyIsAdmin: boolean
|
||||||
|
adminMembers: readonly MiniAppAdminSettingsPayload['members'][number][]
|
||||||
|
purchaseEntries: readonly MiniAppDashboard['ledger'][number][]
|
||||||
|
utilityEntries: readonly MiniAppDashboard['ledger'][number][]
|
||||||
|
paymentEntries: readonly MiniAppDashboard['ledger'][number][]
|
||||||
|
editingPurchaseEntry: MiniAppDashboard['ledger'][number] | null
|
||||||
|
editingPaymentEntry: MiniAppDashboard['ledger'][number] | null
|
||||||
|
purchaseDraftMap: Record<string, PurchaseDraft>
|
||||||
|
paymentDraftMap: Record<string, PaymentDraft>
|
||||||
|
paymentForm: PaymentDraft
|
||||||
|
addingPaymentOpen: boolean
|
||||||
|
savingPurchaseId: string | null
|
||||||
|
deletingPurchaseId: string | null
|
||||||
|
savingPaymentId: string | null
|
||||||
|
deletingPaymentId: string | null
|
||||||
|
addingPayment: boolean
|
||||||
|
ledgerTitle: (entry: MiniAppDashboard['ledger'][number]) => string
|
||||||
|
ledgerPrimaryAmount: (entry: MiniAppDashboard['ledger'][number]) => string
|
||||||
|
ledgerSecondaryAmount: (entry: MiniAppDashboard['ledger'][number]) => string | null
|
||||||
|
purchaseParticipantSummary: (entry: MiniAppDashboard['ledger'][number]) => string
|
||||||
|
purchaseDraftForEntry: (entry: MiniAppDashboard['ledger'][number]) => PurchaseDraft
|
||||||
|
paymentDraftForEntry: (entry: MiniAppDashboard['ledger'][number]) => PaymentDraft
|
||||||
|
purchaseSplitPreview: (purchaseId: string) => { memberId: string; amountMajor: string }[]
|
||||||
|
paymentMemberName: (entry: MiniAppDashboard['ledger'][number]) => string
|
||||||
|
onOpenPurchaseEditor: (purchaseId: string) => void
|
||||||
|
onClosePurchaseEditor: () => void
|
||||||
|
onDeletePurchase: (purchaseId: string) => Promise<void>
|
||||||
|
onSavePurchase: (purchaseId: string) => Promise<void>
|
||||||
|
onPurchaseDescriptionChange: (
|
||||||
|
purchaseId: string,
|
||||||
|
entry: MiniAppDashboard['ledger'][number],
|
||||||
|
value: string
|
||||||
|
) => void
|
||||||
|
onPurchaseAmountChange: (
|
||||||
|
purchaseId: string,
|
||||||
|
entry: MiniAppDashboard['ledger'][number],
|
||||||
|
value: string
|
||||||
|
) => void
|
||||||
|
onPurchaseCurrencyChange: (
|
||||||
|
purchaseId: string,
|
||||||
|
entry: MiniAppDashboard['ledger'][number],
|
||||||
|
value: 'USD' | 'GEL'
|
||||||
|
) => void
|
||||||
|
onPurchaseSplitModeChange: (
|
||||||
|
purchaseId: string,
|
||||||
|
entry: MiniAppDashboard['ledger'][number],
|
||||||
|
value: 'equal' | 'custom_amounts'
|
||||||
|
) => void
|
||||||
|
onTogglePurchaseParticipant: (
|
||||||
|
purchaseId: string,
|
||||||
|
entry: MiniAppDashboard['ledger'][number],
|
||||||
|
memberId: string,
|
||||||
|
included: boolean
|
||||||
|
) => void
|
||||||
|
onPurchaseParticipantShareChange: (
|
||||||
|
purchaseId: string,
|
||||||
|
entry: MiniAppDashboard['ledger'][number],
|
||||||
|
memberId: string,
|
||||||
|
value: string
|
||||||
|
) => void
|
||||||
|
onOpenAddPayment: () => void
|
||||||
|
onCloseAddPayment: () => void
|
||||||
|
onAddPayment: () => Promise<void>
|
||||||
|
onPaymentFormMemberChange: (value: string) => void
|
||||||
|
onPaymentFormKindChange: (value: 'rent' | 'utilities') => void
|
||||||
|
onPaymentFormAmountChange: (value: string) => void
|
||||||
|
onPaymentFormCurrencyChange: (value: 'USD' | 'GEL') => void
|
||||||
|
onOpenPaymentEditor: (paymentId: string) => void
|
||||||
|
onClosePaymentEditor: () => void
|
||||||
|
onDeletePayment: (paymentId: string) => Promise<void>
|
||||||
|
onSavePayment: (paymentId: string) => Promise<void>
|
||||||
|
onPaymentDraftMemberChange: (
|
||||||
|
paymentId: string,
|
||||||
|
entry: MiniAppDashboard['ledger'][number],
|
||||||
|
value: string
|
||||||
|
) => void
|
||||||
|
onPaymentDraftKindChange: (
|
||||||
|
paymentId: string,
|
||||||
|
entry: MiniAppDashboard['ledger'][number],
|
||||||
|
value: 'rent' | 'utilities'
|
||||||
|
) => void
|
||||||
|
onPaymentDraftAmountChange: (
|
||||||
|
paymentId: string,
|
||||||
|
entry: MiniAppDashboard['ledger'][number],
|
||||||
|
value: string
|
||||||
|
) => void
|
||||||
|
onPaymentDraftCurrencyChange: (
|
||||||
|
paymentId: string,
|
||||||
|
entry: MiniAppDashboard['ledger'][number],
|
||||||
|
value: 'USD' | 'GEL'
|
||||||
|
) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LedgerScreen(props: Props) {
|
||||||
|
if (!props.dashboard) {
|
||||||
|
return (
|
||||||
|
<div class="ledger-list">
|
||||||
|
<p>{props.copy.emptyDashboard ?? ''}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="ledger-list">
|
||||||
|
<article class="balance-item">
|
||||||
|
<header>
|
||||||
|
<strong>
|
||||||
|
{props.readyIsAdmin ? props.copy.purchaseReviewTitle : props.copy.purchasesTitle}
|
||||||
|
</strong>
|
||||||
|
</header>
|
||||||
|
<Show when={props.readyIsAdmin}>
|
||||||
|
<p>{props.copy.purchaseReviewBody ?? ''}</p>
|
||||||
|
</Show>
|
||||||
|
{props.purchaseEntries.length === 0 ? (
|
||||||
|
<p>{props.copy.purchasesEmpty ?? ''}</p>
|
||||||
|
) : (
|
||||||
|
<div class="ledger-list">
|
||||||
|
<For each={props.purchaseEntries}>
|
||||||
|
{(entry) => (
|
||||||
|
<article class="ledger-compact-card">
|
||||||
|
<div class="ledger-compact-card__main">
|
||||||
|
<header>
|
||||||
|
<strong>{entry.title}</strong>
|
||||||
|
<span>{entry.occurredAt?.slice(0, 10) ?? '—'}</span>
|
||||||
|
</header>
|
||||||
|
<p>{entry.actorDisplayName ?? props.copy.ledgerActorFallback ?? ''}</p>
|
||||||
|
<div class="ledger-compact-card__meta">
|
||||||
|
<span class="mini-chip">{props.ledgerPrimaryAmount(entry)}</span>
|
||||||
|
<Show when={props.ledgerSecondaryAmount(entry)}>
|
||||||
|
{(secondary) => (
|
||||||
|
<span class="mini-chip mini-chip--muted">{secondary()}</span>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
<span class="mini-chip mini-chip--muted">
|
||||||
|
{props.purchaseParticipantSummary(entry)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Show when={props.readyIsAdmin}>
|
||||||
|
<div class="ledger-compact-card__actions">
|
||||||
|
<IconButton
|
||||||
|
label={props.copy.editEntryAction ?? ''}
|
||||||
|
onClick={() => props.onOpenPurchaseEditor(entry.id)}
|
||||||
|
>
|
||||||
|
...
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</article>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</article>
|
||||||
|
<Modal
|
||||||
|
open={Boolean(props.editingPurchaseEntry)}
|
||||||
|
title={props.copy.purchaseReviewTitle ?? ''}
|
||||||
|
description={props.copy.purchaseEditorBody ?? ''}
|
||||||
|
closeLabel={props.copy.closeEditorAction ?? ''}
|
||||||
|
onClose={props.onClosePurchaseEditor}
|
||||||
|
footer={(() => {
|
||||||
|
const entry = props.editingPurchaseEntry
|
||||||
|
|
||||||
|
if (!entry) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="modal-action-row">
|
||||||
|
<Button variant="danger" onClick={() => void props.onDeletePurchase(entry.id)}>
|
||||||
|
{props.deletingPurchaseId === entry.id
|
||||||
|
? props.copy.deletingPurchase
|
||||||
|
: props.copy.purchaseDeleteAction}
|
||||||
|
</Button>
|
||||||
|
<div class="modal-action-row__primary">
|
||||||
|
<Button variant="ghost" onClick={props.onClosePurchaseEditor}>
|
||||||
|
{props.copy.closeEditorAction ?? ''}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
disabled={props.savingPurchaseId === entry.id}
|
||||||
|
onClick={() => void props.onSavePurchase(entry.id)}
|
||||||
|
>
|
||||||
|
{props.savingPurchaseId === entry.id
|
||||||
|
? props.copy.savingPurchase
|
||||||
|
: props.copy.purchaseSaveAction}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
>
|
||||||
|
{(() => {
|
||||||
|
const entry = props.editingPurchaseEntry
|
||||||
|
|
||||||
|
if (!entry) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const draft = props.purchaseDraftMap[entry.id] ?? props.purchaseDraftForEntry(entry)
|
||||||
|
const splitPreview = props.purchaseSplitPreview(entry.id)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="editor-grid">
|
||||||
|
<Field label={props.copy.purchaseReviewTitle ?? ''} wide>
|
||||||
|
<input
|
||||||
|
value={draft.description}
|
||||||
|
onInput={(event) =>
|
||||||
|
props.onPurchaseDescriptionChange(entry.id, entry, event.currentTarget.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label={props.copy.paymentAmount ?? ''}>
|
||||||
|
<input
|
||||||
|
value={draft.amountMajor}
|
||||||
|
onInput={(event) =>
|
||||||
|
props.onPurchaseAmountChange(entry.id, entry, event.currentTarget.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label={props.copy.settlementCurrency ?? ''}>
|
||||||
|
<select
|
||||||
|
value={draft.currency}
|
||||||
|
onChange={(event) =>
|
||||||
|
props.onPurchaseCurrencyChange(
|
||||||
|
entry.id,
|
||||||
|
entry,
|
||||||
|
event.currentTarget.value as 'USD' | 'GEL'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="GEL">GEL</option>
|
||||||
|
<option value="USD">USD</option>
|
||||||
|
</select>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="editor-panel">
|
||||||
|
<header class="editor-panel__header">
|
||||||
|
<strong>{props.copy.purchaseSplitTitle ?? ''}</strong>
|
||||||
|
<span>
|
||||||
|
{draft.splitMode === 'custom_amounts'
|
||||||
|
? props.copy.purchaseSplitCustom
|
||||||
|
: props.copy.purchaseSplitEqual}
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
<div class="editor-grid">
|
||||||
|
<Field label={props.copy.purchaseSplitModeLabel ?? ''} wide>
|
||||||
|
<select
|
||||||
|
value={draft.splitMode}
|
||||||
|
onChange={(event) =>
|
||||||
|
props.onPurchaseSplitModeChange(
|
||||||
|
entry.id,
|
||||||
|
entry,
|
||||||
|
event.currentTarget.value as 'equal' | 'custom_amounts'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="equal">{props.copy.purchaseSplitEqual ?? ''}</option>
|
||||||
|
<option value="custom_amounts">{props.copy.purchaseSplitCustom ?? ''}</option>
|
||||||
|
</select>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
<div class="participant-list">
|
||||||
|
<For each={props.adminMembers}>
|
||||||
|
{(member) => {
|
||||||
|
const included = draft.participants.some(
|
||||||
|
(participant) => participant.memberId === member.id
|
||||||
|
)
|
||||||
|
const previewAmount =
|
||||||
|
splitPreview.find((participant) => participant.memberId === member.id)
|
||||||
|
?.amountMajor ?? '0.00'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article class="participant-card">
|
||||||
|
<header>
|
||||||
|
<strong>{member.displayName}</strong>
|
||||||
|
<span>
|
||||||
|
{previewAmount} {draft.currency}
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
<div class="participant-card__controls">
|
||||||
|
<Button
|
||||||
|
variant={included ? 'primary' : 'secondary'}
|
||||||
|
onClick={() =>
|
||||||
|
props.onTogglePurchaseParticipant(
|
||||||
|
entry.id,
|
||||||
|
entry,
|
||||||
|
member.id,
|
||||||
|
!included
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{included
|
||||||
|
? props.copy.participantIncluded
|
||||||
|
: props.copy.participantExcluded}
|
||||||
|
</Button>
|
||||||
|
<Show when={included && draft.splitMode === 'custom_amounts'}>
|
||||||
|
<Field
|
||||||
|
label={props.copy.purchaseCustomShareLabel ?? ''}
|
||||||
|
class="participant-card__field"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
value={
|
||||||
|
draft.participants.find(
|
||||||
|
(participant) => participant.memberId === member.id
|
||||||
|
)?.shareAmountMajor ?? ''
|
||||||
|
}
|
||||||
|
onInput={(event) =>
|
||||||
|
props.onPurchaseParticipantShareChange(
|
||||||
|
entry.id,
|
||||||
|
entry,
|
||||||
|
member.id,
|
||||||
|
event.currentTarget.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
</Modal>
|
||||||
|
<article class="balance-item">
|
||||||
|
<header>
|
||||||
|
<strong>{props.copy.utilityLedgerTitle ?? ''}</strong>
|
||||||
|
</header>
|
||||||
|
{props.utilityEntries.length === 0 ? (
|
||||||
|
<p>{props.copy.utilityLedgerEmpty ?? ''}</p>
|
||||||
|
) : (
|
||||||
|
<div class="ledger-list">
|
||||||
|
<For each={props.utilityEntries}>
|
||||||
|
{(entry) => (
|
||||||
|
<article class="ledger-item">
|
||||||
|
<header>
|
||||||
|
<strong>{props.ledgerTitle(entry)}</strong>
|
||||||
|
<span>{props.ledgerPrimaryAmount(entry)}</span>
|
||||||
|
</header>
|
||||||
|
<Show when={props.ledgerSecondaryAmount(entry)}>
|
||||||
|
{(secondary) => <p>{secondary()}</p>}
|
||||||
|
</Show>
|
||||||
|
<p>{entry.actorDisplayName ?? props.copy.ledgerActorFallback ?? ''}</p>
|
||||||
|
</article>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</article>
|
||||||
|
<article class="balance-item">
|
||||||
|
<header>
|
||||||
|
<strong>{props.copy.paymentsAdminTitle ?? ''}</strong>
|
||||||
|
</header>
|
||||||
|
<Show when={props.readyIsAdmin}>
|
||||||
|
<p>{props.copy.paymentsAdminBody ?? ''}</p>
|
||||||
|
<div class="panel-toolbar">
|
||||||
|
<Button variant="secondary" onClick={props.onOpenAddPayment}>
|
||||||
|
{props.copy.paymentsAddAction ?? ''}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
{props.paymentEntries.length === 0 ? (
|
||||||
|
<p>{props.copy.paymentsEmpty ?? ''}</p>
|
||||||
|
) : (
|
||||||
|
<div class="ledger-list">
|
||||||
|
<For each={props.paymentEntries}>
|
||||||
|
{(entry) => (
|
||||||
|
<article class="ledger-compact-card">
|
||||||
|
<div class="ledger-compact-card__main">
|
||||||
|
<header>
|
||||||
|
<strong>{props.paymentMemberName(entry)}</strong>
|
||||||
|
<span>{entry.occurredAt?.slice(0, 10) ?? '—'}</span>
|
||||||
|
</header>
|
||||||
|
<p>{props.ledgerTitle(entry)}</p>
|
||||||
|
<div class="ledger-compact-card__meta">
|
||||||
|
<span class="mini-chip">{props.ledgerPrimaryAmount(entry)}</span>
|
||||||
|
<Show when={props.ledgerSecondaryAmount(entry)}>
|
||||||
|
{(secondary) => (
|
||||||
|
<span class="mini-chip mini-chip--muted">{secondary()}</span>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Show when={props.readyIsAdmin}>
|
||||||
|
<div class="ledger-compact-card__actions">
|
||||||
|
<IconButton
|
||||||
|
label={props.copy.editEntryAction ?? ''}
|
||||||
|
onClick={() => props.onOpenPaymentEditor(entry.id)}
|
||||||
|
>
|
||||||
|
...
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</article>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</article>
|
||||||
|
<Modal
|
||||||
|
open={props.addingPaymentOpen}
|
||||||
|
title={props.copy.paymentsAddAction ?? ''}
|
||||||
|
description={props.copy.paymentCreateBody ?? ''}
|
||||||
|
closeLabel={props.copy.closeEditorAction ?? ''}
|
||||||
|
onClose={props.onCloseAddPayment}
|
||||||
|
footer={
|
||||||
|
<div class="modal-action-row modal-action-row--single">
|
||||||
|
<Button variant="ghost" onClick={props.onCloseAddPayment}>
|
||||||
|
{props.copy.closeEditorAction ?? ''}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
disabled={props.addingPayment || props.paymentForm.amountMajor.trim().length === 0}
|
||||||
|
onClick={() => void props.onAddPayment()}
|
||||||
|
>
|
||||||
|
{props.addingPayment ? props.copy.addingPayment : props.copy.paymentsAddAction}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div class="editor-grid">
|
||||||
|
<Field label={props.copy.paymentMember ?? ''} wide>
|
||||||
|
<select
|
||||||
|
value={props.paymentForm.memberId}
|
||||||
|
onChange={(event) => props.onPaymentFormMemberChange(event.currentTarget.value)}
|
||||||
|
>
|
||||||
|
<For each={props.adminMembers}>
|
||||||
|
{(member) => <option value={member.id}>{member.displayName}</option>}
|
||||||
|
</For>
|
||||||
|
</select>
|
||||||
|
</Field>
|
||||||
|
<Field label={props.copy.paymentKind ?? ''}>
|
||||||
|
<select
|
||||||
|
value={props.paymentForm.kind}
|
||||||
|
onChange={(event) =>
|
||||||
|
props.onPaymentFormKindChange(event.currentTarget.value as 'rent' | 'utilities')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="rent">{props.copy.paymentLedgerRent ?? ''}</option>
|
||||||
|
<option value="utilities">{props.copy.paymentLedgerUtilities ?? ''}</option>
|
||||||
|
</select>
|
||||||
|
</Field>
|
||||||
|
<Field label={props.copy.paymentAmount ?? ''}>
|
||||||
|
<input
|
||||||
|
value={props.paymentForm.amountMajor}
|
||||||
|
onInput={(event) => props.onPaymentFormAmountChange(event.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label={props.copy.settlementCurrency ?? ''}>
|
||||||
|
<select
|
||||||
|
value={props.paymentForm.currency}
|
||||||
|
onChange={(event) =>
|
||||||
|
props.onPaymentFormCurrencyChange(event.currentTarget.value as 'USD' | 'GEL')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="GEL">GEL</option>
|
||||||
|
<option value="USD">USD</option>
|
||||||
|
</select>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
<Modal
|
||||||
|
open={Boolean(props.editingPaymentEntry)}
|
||||||
|
title={props.copy.paymentsAdminTitle ?? ''}
|
||||||
|
description={props.copy.paymentEditorBody ?? ''}
|
||||||
|
closeLabel={props.copy.closeEditorAction ?? ''}
|
||||||
|
onClose={props.onClosePaymentEditor}
|
||||||
|
footer={(() => {
|
||||||
|
const entry = props.editingPaymentEntry
|
||||||
|
|
||||||
|
if (!entry) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="modal-action-row">
|
||||||
|
<Button variant="danger" onClick={() => void props.onDeletePayment(entry.id)}>
|
||||||
|
{props.deletingPaymentId === entry.id
|
||||||
|
? props.copy.deletingPayment
|
||||||
|
: props.copy.paymentDeleteAction}
|
||||||
|
</Button>
|
||||||
|
<div class="modal-action-row__primary">
|
||||||
|
<Button variant="ghost" onClick={props.onClosePaymentEditor}>
|
||||||
|
{props.copy.closeEditorAction ?? ''}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
disabled={props.savingPaymentId === entry.id}
|
||||||
|
onClick={() => void props.onSavePayment(entry.id)}
|
||||||
|
>
|
||||||
|
{props.savingPaymentId === entry.id
|
||||||
|
? props.copy.addingPayment
|
||||||
|
: props.copy.paymentSaveAction}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
>
|
||||||
|
{(() => {
|
||||||
|
const entry = props.editingPaymentEntry
|
||||||
|
|
||||||
|
if (!entry) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const draft = props.paymentDraftMap[entry.id] ?? props.paymentDraftForEntry(entry)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="editor-grid">
|
||||||
|
<Field label={props.copy.paymentMember ?? ''} wide>
|
||||||
|
<select
|
||||||
|
value={draft.memberId}
|
||||||
|
onChange={(event) =>
|
||||||
|
props.onPaymentDraftMemberChange(entry.id, entry, event.currentTarget.value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<For each={props.adminMembers}>
|
||||||
|
{(member) => <option value={member.id}>{member.displayName}</option>}
|
||||||
|
</For>
|
||||||
|
</select>
|
||||||
|
</Field>
|
||||||
|
<Field label={props.copy.paymentKind ?? ''}>
|
||||||
|
<select
|
||||||
|
value={draft.kind}
|
||||||
|
onChange={(event) =>
|
||||||
|
props.onPaymentDraftKindChange(
|
||||||
|
entry.id,
|
||||||
|
entry,
|
||||||
|
event.currentTarget.value as 'rent' | 'utilities'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="rent">{props.copy.paymentLedgerRent ?? ''}</option>
|
||||||
|
<option value="utilities">{props.copy.paymentLedgerUtilities ?? ''}</option>
|
||||||
|
</select>
|
||||||
|
</Field>
|
||||||
|
<Field label={props.copy.paymentAmount ?? ''}>
|
||||||
|
<input
|
||||||
|
value={draft.amountMajor}
|
||||||
|
onInput={(event) =>
|
||||||
|
props.onPaymentDraftAmountChange(entry.id, entry, event.currentTarget.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label={props.copy.settlementCurrency ?? ''}>
|
||||||
|
<select
|
||||||
|
value={draft.currency}
|
||||||
|
onChange={(event) =>
|
||||||
|
props.onPaymentDraftCurrencyChange(
|
||||||
|
entry.id,
|
||||||
|
entry,
|
||||||
|
event.currentTarget.value as 'USD' | 'GEL'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="GEL">GEL</option>
|
||||||
|
<option value="USD">USD</option>
|
||||||
|
</select>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user