UI — @kioskgaming/ui / portal-cash-settlement
3 mockup screens (sell modal, list, detail view-only) shared across Admin / Super Agent / Agent portals → implement in packages/ui, each portal only wires API + route + opens modal from entity detail.
Mockup: prototype · Schema: schema-cash-transactions
Reference pattern
Same as existing modules:
| Module | Example |
|---|---|
portal-credit-wallet | PortalCreditTransactionsLedger — list + filter + table |
portal-usd-wallet | Summary grid, filters bar, ledger table |
portal-ledger | Detail modal, presentation helpers |
Cash settlement follows the same approach: UI + format + form state in package; fetch/submit injected via props/callback.
Suggested directory structure
packages/ui/src/portal-cash-settlement/
├── index.ts
├── types.ts # CashTransactionRow, filters, sell form values, flow context
├── constants.ts # flow/status/payment_method labels (EN)
├── formatters.ts # formatCash, formatCredits, formatCostRate
├── cashTransactionPresentation.ts # badge tone, flow label, status label
├── useSellCreditsCashForm.ts # expected cash calc, validation
├── SellCreditsCashModal.tsx # Sell credits modal (entity detail)
├── PortalCashTransactionSummaryGrid.tsx
├── PortalCashTransactionFiltersBar.tsx
├── PortalCashTransactionTable.tsx
├── PortalCashTransactionsLedger.tsx # Compose list page (summary + filter + table + pagination)
├── PortalCashTransactionDetail.tsx # View-only detail page body
└── *.test.ts
Export from packages/ui/src/index.ts:
export { … } from './portal-cash-settlement';
Component map ↔ mockup
| Mockup | Package component | Notes |
|---|---|---|
| sell-credit.html | SellCreditsCashModal | Context bar: flow, buyer name, cost rate — no buyer picker |
| cash-transaction-list.html | PortalCashTransactionsLedger | No Sell button; export via onExport |
| cash-transaction-detail.html | PortalCashTransactionDetail | Read-only; banner + KV + linked credit + audit |
SellCreditsCashModal
Props (draft)
type SellCreditsCashModalProps = {
open: boolean;
onClose: () => void;
flow: CashTransactionFlow;
buyerName: string;
buyerCostRate: number; // decimal 0–1
currency?: string; // default USD
submitting?: boolean;
error?: string | null;
onSubmit: (values: SellCreditsCashFormValues) => void | Promise<void>;
};
type SellCreditsCashFormValues = {
creditAmount: number;
cashReceived: number;
paymentMethod: CashPaymentMethod;
receivedAt: string; // ISO
externalReference?: string;
note?: string;
};
Behavior
- Section 1: credits, cost rate (read-only), expected cash (auto)
- Section 2: cash received, payment method, received at, currency
- Section 3: V1 hides status — always
paid_fullon submit - Section 4: external ref, note
- Uses
Modal/Form/FormField/Input/Select/Textareafrom@kioskgaming/ui - Copy in English (matches mockup)
PortalCashTransactionsLedger
Pattern like PortalCreditTransactionsLedger:
type PortalCashTransactionsLedgerProps = {
title?: string;
description?: string;
loading?: boolean;
data: CashTransactionListResponse | null;
onFetch: (params: { filters: CashTransactionFilterState; page: number; limit: number }) => void | Promise<void>;
onViewDetail?: (row: CashTransactionRow) => void;
onExport?: (params: { filters: CashTransactionFilterState }) => void | Promise<void>;
flowFilterOptions?: CashTransactionFlow[]; // portal limits visible flows
headerExtra?: React.ReactNode;
};
Sub-components split for testing and layout reuse.
PortalCashTransactionDetail
type PortalCashTransactionDetailProps = {
loading?: boolean;
transaction: CashTransactionDetail | null;
readOnly?: boolean; // default true (V1)
onBack?: () => void;
};
- Summary cards + KV table + linked credit block + audit timeline
- No action buttons (cancel, update, add note, open credit tx link) — V1 view-only
- Built-in
readOnlybanner
Portal integration (thin layer)
Each app only needs:
| Portal | Sell modal opens from | List route | Detail route |
|---|---|---|---|
| Admin | Super Agent detail | /cash-transactions | /cash-transactions/:id |
| Super Agent | Agent detail | /cash-transactions | /cash-transactions/:id |
| Agent | Shop detail | /cash-transactions | /cash-transactions/:id |
Example Admin Super Agent detail:
<SellCreditsCashModal
open={sellOpen}
onClose={() => setSellOpen(false)}
flow="admin_super_agent"
buyerName={superAgent.name}
buyerCostRate={superAgent.costRate}
submitting={mutation.isPending}
onSubmit={(values) => mutation.mutateAsync({ superAgentId, ...values })}
/>
Admin list page:
<PortalCashTransactionsLedger
data={listQuery.data}
loading={listQuery.isLoading}
onFetch={(p) => listQuery.refetch(/* map p */)}
onViewDetail={(row) => navigate(`/cash-transactions/${row.id}`)}
onExport={exportCsv}
/>
Shared types (package)
Align with DB schema:
CashTransactionFlow:admin_super_agent|super_agent_agent|agent_shopCashTransactionStatus:paid_full| …CashPaymentMethod:cash|bank_transfer|check|other
Portal API response maps → CashTransactionRow / CashTransactionDetail in types.ts (do not leak Sequelize models into UI).
Task breakdown (UI package)
| # | Task |
|---|---|
| UI.1 | Scaffold portal-cash-settlement/ + export index.ts |
| UI.2 | types.ts, constants.ts, formatters.ts, cashTransactionPresentation.ts |
| UI.3 | useSellCreditsCashForm + unit tests (expected cash, defaults) |
| UI.4 | SellCreditsCashModal |
| UI.5 | PortalCashTransactionSummaryGrid, FiltersBar, Table |
| UI.6 | PortalCashTransactionsLedger (compose + pagination) |
| UI.7 | PortalCashTransactionDetail (view-only) |
| UI.8 | Storybook / dev preview in packages/dev (optional) |
| UI.9 | Jest tests presentation + form hook |
After UI.1–UI.7, portal tasks only need API + routing wiring.
Suggested implementation order
1. UI package (UI.1–UI.7) ← parallel with backend Phase 1
2. Admin wire (tasks flow 1 Phase 2–4)
3. Super Agent wire (tasks flow 2)
4. Agent wire (tasks flow 3)
Flows 2–3 do not copy UI — only add API hook + open modal with different flow / buyer context.