196 lines
5.5 KiB
TypeScript
196 lines
5.5 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 }[];
|
|
}
|
|
|
|
export function BankTransactionDialog({
|
|
accountId,
|
|
otherAccounts,
|
|
}: BankTransactionDialogProps) {
|
|
const router = useRouter();
|
|
const [mode, setMode] = useState<Mode>("deposit");
|
|
const [open, setOpen] = useState(false);
|
|
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>
|
|
</>
|
|
);
|
|
}
|