1
0
Fork 0
polaris-task-force/src/components/frontend/banking/BankTransactionDialog.tsx
Z8MB1E 4906fc8008 refactor(ui): make dialogs controlled via open/onOpenChange props
Support external open state management so parent components and the
command palette can trigger these dialogs programmatically.
2026-08-18 02:05:20 -04:00

204 lines
5.8 KiB
TypeScript

"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { ArrowDownLeftIcon, ArrowRightLeftIcon, ArrowUpRightIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
depositFunds,
transferFunds,
withdrawFunds,
} from "@/app/(frontend)/logistics/banking/actions";
type Mode = "deposit" | "withdraw" | "transfer";
const modeMeta: Record<
Mode,
{ label: string; verb: string; description: string; icon: typeof ArrowDownLeftIcon }
> = {
deposit: {
label: "Deposit",
verb: "Deposit",
description: "Add funds to this account.",
icon: ArrowDownLeftIcon,
},
withdraw: {
label: "Withdraw",
verb: "Withdraw",
description: "Remove funds from this account.",
icon: ArrowUpRightIcon,
},
transfer: {
label: "Transfer",
verb: "Transfer",
description: "Move funds from this account to another account.",
icon: ArrowRightLeftIcon,
},
};
interface BankTransactionDialogProps {
accountId: number;
otherAccounts: { id: number; name: string }[];
open?: boolean;
onOpenChange?: (open: boolean) => void;
initialMode?: Mode;
}
export function BankTransactionDialog({
accountId,
otherAccounts,
open: externalOpen,
onOpenChange: externalOnOpenChange,
initialMode = "deposit",
}: BankTransactionDialogProps) {
const router = useRouter();
const [mode, setMode] = useState<Mode>(initialMode);
const [internalOpen, setInternalOpen] = useState(false);
const open = externalOpen ?? internalOpen;
const setOpen = externalOnOpenChange ?? setInternalOpen;
const [amount, setAmount] = useState("");
const [toId, setToId] = useState("");
const [memo, setMemo] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
function openMode(next: Mode) {
setMode(next);
setError(null);
setOpen(true);
}
async function handleSubmit() {
const value = Number(amount);
if (!value || value <= 0) {
setError("Enter a valid amount.");
return;
}
if (mode === "transfer" && !toId) {
setError("Select a destination account.");
return;
}
setLoading(true);
setError(null);
const res =
mode === "deposit"
? await depositFunds(accountId, value, memo || undefined)
: mode === "withdraw"
? await withdrawFunds(accountId, value, memo || undefined)
: await transferFunds(accountId, Number(toId), value, memo || undefined);
if (res.success) {
setOpen(false);
setAmount("");
setToId("");
setMemo("");
router.refresh();
} else {
setError(res.error ?? "Transaction failed.");
setLoading(false);
}
}
const meta = modeMeta[mode];
const Icon = meta.icon;
return (
<>
<div className="flex items-center gap-2">
{(Object.keys(modeMeta) as Mode[]).map((m) => {
const M = modeMeta[m];
const Mc = M.icon;
return (
<Button
key={m}
size="sm"
variant={mode === m ? "default" : "outline"}
onClick={() => openMode(m)}
className="font-mono text-xs uppercase tracking-wider"
>
<Mc className="size-4" />
{M.label}
</Button>
);
})}
</div>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Icon className="size-4" />
{meta.verb} Funds
</DialogTitle>
<DialogDescription>{meta.description}</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1.5">
<Label>Amount</Label>
<Input
type="number"
min={1}
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="0"
/>
</div>
{mode === "transfer" && (
<div className="flex flex-col gap-1.5">
<Label>Destination account</Label>
<Select value={toId} onValueChange={setToId}>
<SelectTrigger>
<SelectValue placeholder="Select account" />
</SelectTrigger>
<SelectContent>
{otherAccounts.map((a) => (
<SelectItem key={a.id} value={String(a.id)}>
{a.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
<div className="flex flex-col gap-1.5">
<Label>Memo (optional)</Label>
<Textarea
value={memo}
onChange={(e) => setMemo(e.target.value)}
placeholder="Reason / reference"
rows={2}
/>
</div>
{error && <p className="text-sm text-red-400">{error}</p>}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button onClick={handleSubmit} disabled={loading}>
{loading ? "Processing..." : meta.verb}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}