diff --git a/src/components/frontend/banking/ActivityFeed.tsx b/src/components/frontend/banking/ActivityFeed.tsx new file mode 100644 index 0000000..38a4f74 --- /dev/null +++ b/src/components/frontend/banking/ActivityFeed.tsx @@ -0,0 +1,156 @@ +"use client"; + +import Link from "next/link"; +import type { BankAccount, BankTransaction, Resource, User } from "@/payload-types"; +import { + ArrowDownLeftIcon, + ArrowRightLeftIcon, + ArrowUpRightIcon, +} from "lucide-react"; +import { currencyLabel } from "@/lib/banking/format"; +import { + formatTxTimestamp, + txAmountClass, + txAmountPrefix, + txTypeColor, + txTypeFlow, + type TxFlow, +} from "@/lib/banking/ui"; + +interface ActivityFeedProps { + transactions: BankTransaction[]; + currencyResource: Resource | null; +} + +// Icon per type — kept local because the icons are React components used only +// by this feed (the LedgerTable renders a tabular view without icons). +const txTypeIcon: Record = { + deposit: ArrowDownLeftIcon, + withdrawal: ArrowUpRightIcon, + transfer: ArrowRightLeftIcon, + payment: ArrowUpRightIcon, + fee: ArrowDownLeftIcon, + salary: ArrowDownLeftIcon, + adjustment: ArrowRightLeftIcon, +}; + +// Uppercase mono label per type (terminal-aesthetic). +const txTypeLabel: Record = { + deposit: "DEPOSIT", + withdrawal: "WITHDRAW", + transfer: "TRANSFER", + payment: "PAYMENT", + fee: "FEE", + salary: "SALARY", + adjustment: "ADJUST", +}; + +function resolveAccountLabel( + account: (number | null) | BankAccount | undefined, +): { label: string; id: number | null } { + if (!account) return { label: "—", id: null }; + if (typeof account === "object") { + return { label: account.name ?? `#${account.id}`, id: account.id }; + } + return { label: `#${account}`, id: account }; +} + +function resolveActorLabel(actor: (number | null) | User | undefined): string { + if (!actor) return ""; + if (typeof actor === "object") { + return actor.displayName ?? actor.username; + } + return `#${actor}`; +} + +export function ActivityFeed({ transactions, currencyResource }: ActivityFeedProps) { + const cur = currencyLabel(currencyResource); + + if (transactions.length === 0) { + return ( +
+

+ {"> NO TRANSACTIONS YET"} +

+
+ ); + } + + return ( +
+
    + {transactions.map((tx) => { + const flow: TxFlow = txTypeFlow[tx.type] ?? "neutral"; + const colorCls = txTypeColor[tx.type] ?? "text-muted-foreground"; + const Icon = txTypeIcon[tx.type] ?? ArrowRightLeftIcon; + const label = txTypeLabel[tx.type] ?? tx.type.toUpperCase(); + const from = resolveAccountLabel(tx.fromAccount); + const to = resolveAccountLabel(tx.toAccount); + const actor = resolveActorLabel(tx.actor); + const targetAccountId = to.id ?? from.id; + const href = + targetAccountId != null + ? `/logistics/banking/${targetAccountId}` + : null; + const amount = tx.amount ?? 0; + const signed = `${txAmountPrefix(flow)}${amount.toLocaleString()}`; + + const RowInner = ( +
    + +
    +
    + + {formatTxTimestamp(tx.timestamp)} + + + {label} + +
    +
    + {from.label} + → + {to.label} +
    + {tx.memo && ( +

    + {tx.memo} +

    + )} + {actor && ( +

    by {actor}

    + )} +
    +
    + {signed} + {cur && ( + + {cur} + + )} +
    +
    + ); + + return ( +
  • + {href ? ( + + {RowInner} + + ) : ( +
    {RowInner}
    + )} +
  • + ); + })} +
+
+ ); +} \ No newline at end of file diff --git a/src/components/frontend/banking/WalletHero.tsx b/src/components/frontend/banking/WalletHero.tsx new file mode 100644 index 0000000..bf4aa5f --- /dev/null +++ b/src/components/frontend/banking/WalletHero.tsx @@ -0,0 +1,152 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import { ChevronRightIcon, WalletIcon } from "lucide-react"; +import type { BankAccount, Resource } from "@/payload-types"; +import { Button } from "@/components/ui/button"; +import { formatAmount } from "@/lib/banking/format"; +import { + accountStatusBase, + accountStatusClass, + formatLastActivity, + type AccountStatus, +} from "@/lib/banking/ui"; +import { BankTransactionDialog } from "@/components/frontend/banking/BankTransactionDialog"; +import { ensureMyAccount } from "@/app/(frontend)/logistics/banking/actions"; + +interface WalletHeroProps { + account: BankAccount | null; + currentUser: { id: number; username: string; displayName?: string } | null; + currencyResource: Resource | null; +} + +// Shared terminal-aesthetic class for small uppercase mono labels in the hero. +const labelCls = "font-mono text-[10px] uppercase tracking-wider text-muted-foreground"; + +export function WalletHero({ + account, + currentUser, + currencyResource, +}: WalletHeroProps) { + const router = useRouter(); + const [initializing, setInitializing] = useState(false); + const [initError, setInitError] = useState(null); + + async function handleInitialize() { + setInitializing(true); + setInitError(null); + const res = await ensureMyAccount(); + if (res.success) { + router.push(`/logistics/banking/${res.data}`); + router.refresh(); + } else { + setInitError(res.error ?? "Failed to initialize wallet."); + setInitializing(false); + } + } + + // Empty state — terminal-flavored "WALLET NOT INITIALIZED" + if (!account || !currentUser) { + return ( +
+
+
+ + <OPERATOR WALLET> +
+

+ {"> WALLET NOT INITIALIZED"} +

+

+ Run initialization to provision your personal account. +

+ {initError && ( +

+ {"> ERROR: "} + {initError} +

+ )} +
+ +
+
+
+ ); + } + + const accountId = account.id; + const lastActivity = formatLastActivity(account.updatedAt); + const currency = + typeof account.currency === "object" ? account.currency : currencyResource; + const operatorLabel = currentUser.displayName ?? currentUser.username; + const statusKey = (account.status ?? "open") as AccountStatus; + + // otherAccounts requires names for the transfer destination select. + // We only know this wallet here; the BankTransactionDialog receives an empty + // list — transfer mode will prompt the user to use the account detail page. + // Deposits/withdrawals work standalone on the hero. + const otherAccounts: { id: number; name: string }[] = []; + + return ( +
+
+
+
+
+ + <OPERATOR WALLET> +
+
+ + @{operatorLabel} + + + {statusKey} + +
+
+ + ID #{accountId.toString().padStart(6, "0")} + +
+ +
+

+ {formatAmount(account.balance ?? 0, currency)} +

+

+ LAST ACTIVITY {lastActivity} +

+
+ +
+ + + + +
+
+
+ ); +} \ No newline at end of file