diff --git a/apps/miniapp/src/App.tsx b/apps/miniapp/src/App.tsx index 2a9f7ca..7398343 100644 --- a/apps/miniapp/src/App.tsx +++ b/apps/miniapp/src/App.tsx @@ -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 { @@ -45,6 +45,7 @@ import { LoadingState } from './components/session/loading-state' import { OnboardingState } from './components/session/onboarding-state' import { BalancesScreen } from './screens/balances-screen' import { HomeScreen } from './screens/home-screen' +import { LedgerScreen } from './screens/ledger-screen' import { demoAdminSettings, demoCycleState, @@ -1868,523 +1869,131 @@ function App() { ) case 'ledger': return ( -
- {copy().emptyDashboard}

} - render={() => ( - <> -
-
- - {readySession()?.member.isAdmin - ? copy().purchaseReviewTitle - : copy().purchasesTitle} - -
- -

{copy().purchaseReviewBody}

-
- {purchaseLedger().length === 0 ? ( -

{copy().purchasesEmpty}

- ) : ( -
- {purchaseLedger().map((entry) => ( -
-
-
- {entry.title} - {entry.occurredAt?.slice(0, 10) ?? '—'} -
-

{entry.actorDisplayName ?? copy().ledgerActorFallback}

-
- {ledgerPrimaryAmount(entry)} - - {(secondary) => ( - {secondary()} - )} - - - - {purchaseParticipantSummary(entry)} - - -
-
- -
- setEditingPurchaseId(entry.id)} - > - ... - -
-
-
- ))} -
- )} -
- setEditingPurchaseId(null)} - footer={(() => { - const entry = editingPurchaseEntry() - - if (!entry) { - return null + setEditingPurchaseId(null)} + onDeletePurchase={handleDeletePurchase} + onSavePurchase={handleUpdatePurchase} + onPurchaseDescriptionChange={(purchaseId, entry, value) => + updatePurchaseDraft(purchaseId, entry, (current) => ({ + ...current, + description: value + })) + } + onPurchaseAmountChange={(purchaseId, entry, value) => + updatePurchaseDraft(purchaseId, entry, (current) => ({ + ...current, + amountMajor: value + })) + } + onPurchaseCurrencyChange={(purchaseId, entry, value) => + updatePurchaseDraft(purchaseId, entry, (current) => ({ + ...current, + currency: value + })) + } + onPurchaseSplitModeChange={(purchaseId, entry, value) => + updatePurchaseDraft(purchaseId, entry, (current) => ({ + ...current, + splitMode: value + })) + } + onTogglePurchaseParticipant={togglePurchaseParticipant} + onPurchaseParticipantShareChange={(purchaseId, entry, memberId, value) => + updatePurchaseDraft(purchaseId, entry, (current) => ({ + ...current, + participants: current.participants.map((participant) => + participant.memberId === memberId + ? { + ...participant, + shareAmountMajor: value } - - return ( - - ) - })()} - > - {(() => { - const entry = editingPurchaseEntry() - - if (!entry) { - return null - } - - const draft = purchaseDraftMap()[entry.id] ?? purchaseDraftForEntry(entry) - const splitPreview = purchaseSplitPreview(entry.id) - - return ( - <> -
- - - updatePurchaseDraft(entry.id, entry, (current) => ({ - ...current, - description: event.currentTarget.value - })) - } - /> - - - - updatePurchaseDraft(entry.id, entry, (current) => ({ - ...current, - amountMajor: event.currentTarget.value - })) - } - /> - - - - -
- -
-
- {copy().purchaseSplitTitle} - - {draft.splitMode === 'custom_amounts' - ? copy().purchaseSplitCustom - : copy().purchaseSplitEqual} - -
-
- - - -
-
- {(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 ( -
-
- {member.displayName} - - {previewAmount} {draft.currency} - -
-
- - - - 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 - ) - })) - } - /> - - -
-
- ) - })} -
-
- - ) - })()} -
-
-
- {copy().utilityLedgerTitle} -
- {utilityLedger().length === 0 ? ( -

{copy().utilityLedgerEmpty}

- ) : ( -
- {utilityLedger().map((entry) => ( -
-
- {ledgerTitle(entry)} - {ledgerPrimaryAmount(entry)} -
- - {(secondary) =>

{secondary()}

} -
-

{entry.actorDisplayName ?? copy().ledgerActorFallback}

-
- ))} -
- )} -
-
-
- {copy().paymentsAdminTitle} -
- -

{copy().paymentsAdminBody}

-
- -
-
- {paymentLedger().length === 0 ? ( -

{copy().paymentsEmpty}

- ) : ( -
- {paymentLedger().map((entry) => ( -
-
-
- {paymentMemberName(entry)} - {entry.occurredAt?.slice(0, 10) ?? '—'} -
-

{ledgerTitle(entry)}

-
- {ledgerPrimaryAmount(entry)} - - {(secondary) => ( - {secondary()} - )} - -
-
- -
- setEditingPaymentId(entry.id)} - > - ... - -
-
-
- ))} -
- )} -
- setAddingPaymentOpen(false)} - footer={ - - } - > -
- - - - - - - - - setPaymentForm((current) => ({ - ...current, - amountMajor: event.currentTarget.value - })) - } - /> - - - - -
-
- setEditingPaymentId(null)} - footer={(() => { - const entry = editingPaymentEntry() - - if (!entry) { - return null - } - - return ( - - ) - })()} - > - {(() => { - const entry = editingPaymentEntry() - - if (!entry) { - return null - } - - const draft = paymentDraftMap()[entry.id] ?? paymentDraftForEntry(entry) - - return ( -
- - - - - - - - - updatePaymentDraft(entry.id, entry, (current) => ({ - ...current, - amountMajor: event.currentTarget.value - })) - } - /> - - - - -
- ) - })()} -
- - )} - /> -
+ : participant + ) + })) + } + onOpenAddPayment={() => setAddingPaymentOpen(true)} + onCloseAddPayment={() => setAddingPaymentOpen(false)} + onAddPayment={handleAddPayment} + onPaymentFormMemberChange={(value) => + setPaymentForm((current) => ({ + ...current, + memberId: value + })) + } + onPaymentFormKindChange={(value) => + setPaymentForm((current) => ({ + ...current, + kind: value + })) + } + onPaymentFormAmountChange={(value) => + setPaymentForm((current) => ({ + ...current, + amountMajor: value + })) + } + onPaymentFormCurrencyChange={(value) => + setPaymentForm((current) => ({ + ...current, + currency: value + })) + } + onOpenPaymentEditor={setEditingPaymentId} + onClosePaymentEditor={() => setEditingPaymentId(null)} + onDeletePayment={handleDeletePayment} + onSavePayment={handleUpdatePayment} + onPaymentDraftMemberChange={(paymentId, entry, value) => + updatePaymentDraft(paymentId, entry, (current) => ({ + ...current, + memberId: value + })) + } + onPaymentDraftKindChange={(paymentId, entry, value) => + updatePaymentDraft(paymentId, entry, (current) => ({ + ...current, + kind: value + })) + } + onPaymentDraftAmountChange={(paymentId, entry, value) => + updatePaymentDraft(paymentId, entry, (current) => ({ + ...current, + amountMajor: value + })) + } + onPaymentDraftCurrencyChange={(paymentId, entry, value) => + updatePaymentDraft(paymentId, entry, (current) => ({ + ...current, + currency: value + })) + } + /> ) case 'house': 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 diff --git a/apps/miniapp/src/screens/ledger-screen.tsx b/apps/miniapp/src/screens/ledger-screen.tsx new file mode 100644 index 0000000..bfd5fe4 --- /dev/null +++ b/apps/miniapp/src/screens/ledger-screen.tsx @@ -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 + 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 + paymentDraftMap: Record + 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 + onSavePurchase: (purchaseId: string) => Promise + 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 + 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 + onSavePayment: (paymentId: string) => Promise + 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 ( +
+

{props.copy.emptyDashboard ?? ''}

+
+ ) + } + + return ( +
+
+
+ + {props.readyIsAdmin ? props.copy.purchaseReviewTitle : props.copy.purchasesTitle} + +
+ +

{props.copy.purchaseReviewBody ?? ''}

+
+ {props.purchaseEntries.length === 0 ? ( +

{props.copy.purchasesEmpty ?? ''}

+ ) : ( +
+ + {(entry) => ( +
+
+
+ {entry.title} + {entry.occurredAt?.slice(0, 10) ?? '—'} +
+

{entry.actorDisplayName ?? props.copy.ledgerActorFallback ?? ''}

+
+ {props.ledgerPrimaryAmount(entry)} + + {(secondary) => ( + {secondary()} + )} + + + {props.purchaseParticipantSummary(entry)} + +
+
+ +
+ props.onOpenPurchaseEditor(entry.id)} + > + ... + +
+
+
+ )} +
+
+ )} +
+ { + const entry = props.editingPurchaseEntry + + if (!entry) { + return null + } + + return ( + + ) + })()} + > + {(() => { + const entry = props.editingPurchaseEntry + + if (!entry) { + return null + } + + const draft = props.purchaseDraftMap[entry.id] ?? props.purchaseDraftForEntry(entry) + const splitPreview = props.purchaseSplitPreview(entry.id) + + return ( + <> +
+ + + props.onPurchaseDescriptionChange(entry.id, entry, event.currentTarget.value) + } + /> + + + + props.onPurchaseAmountChange(entry.id, entry, event.currentTarget.value) + } + /> + + + + +
+ +
+
+ {props.copy.purchaseSplitTitle ?? ''} + + {draft.splitMode === 'custom_amounts' + ? props.copy.purchaseSplitCustom + : props.copy.purchaseSplitEqual} + +
+
+ + + +
+
+ + {(member) => { + const included = draft.participants.some( + (participant) => participant.memberId === member.id + ) + const previewAmount = + splitPreview.find((participant) => participant.memberId === member.id) + ?.amountMajor ?? '0.00' + + return ( +
+
+ {member.displayName} + + {previewAmount} {draft.currency} + +
+
+ + + + participant.memberId === member.id + )?.shareAmountMajor ?? '' + } + onInput={(event) => + props.onPurchaseParticipantShareChange( + entry.id, + entry, + member.id, + event.currentTarget.value + ) + } + /> + + +
+
+ ) + }} +
+
+
+ + ) + })()} +
+
+
+ {props.copy.utilityLedgerTitle ?? ''} +
+ {props.utilityEntries.length === 0 ? ( +

{props.copy.utilityLedgerEmpty ?? ''}

+ ) : ( +
+ + {(entry) => ( +
+
+ {props.ledgerTitle(entry)} + {props.ledgerPrimaryAmount(entry)} +
+ + {(secondary) =>

{secondary()}

} +
+

{entry.actorDisplayName ?? props.copy.ledgerActorFallback ?? ''}

+
+ )} +
+
+ )} +
+
+
+ {props.copy.paymentsAdminTitle ?? ''} +
+ +

{props.copy.paymentsAdminBody ?? ''}

+
+ +
+
+ {props.paymentEntries.length === 0 ? ( +

{props.copy.paymentsEmpty ?? ''}

+ ) : ( +
+ + {(entry) => ( +
+
+
+ {props.paymentMemberName(entry)} + {entry.occurredAt?.slice(0, 10) ?? '—'} +
+

{props.ledgerTitle(entry)}

+
+ {props.ledgerPrimaryAmount(entry)} + + {(secondary) => ( + {secondary()} + )} + +
+
+ +
+ props.onOpenPaymentEditor(entry.id)} + > + ... + +
+
+
+ )} +
+
+ )} +
+ + + +
+ } + > +
+ + + + + + + + props.onPaymentFormAmountChange(event.currentTarget.value)} + /> + + + + +
+ + { + const entry = props.editingPaymentEntry + + if (!entry) { + return null + } + + return ( + + ) + })()} + > + {(() => { + const entry = props.editingPaymentEntry + + if (!entry) { + return null + } + + const draft = props.paymentDraftMap[entry.id] ?? props.paymentDraftForEntry(entry) + + return ( +
+ + + + + + + + + props.onPaymentDraftAmountChange(entry.id, entry, event.currentTarget.value) + } + /> + + + + +
+ ) + })()} +
+ + ) +}