feat(wallets): implement wallet management features including create, update, delete, deposit, transfer, and list movements

- Added createWallet, updateWallet, deleteWallet, depositWallet, transferWallet, and listMovements handlers.
- Created corresponding routes for wallet operations.
- Developed frontend components for wallet management including dialogs for creating, editing, depositing, adjusting balance, transferring, and viewing movements.
- Integrated wallet management into the authenticated routes.
This commit is contained in:
Jose Selesan
2026-06-17 09:36:55 -03:00
parent 1ff2abc16a
commit 1d954217b7
26 changed files with 1643 additions and 0 deletions

View File

@@ -0,0 +1,101 @@
import { useWalletMovements } from "@/lib/queries";
import type { Wallet } from "@/lib/api";
import {
ResponsiveDialog,
ResponsiveDialogContent,
ResponsiveDialogHeader,
ResponsiveDialogTitle,
} from "@/components/ui/responsive-dialog";
const amountFormatter = new Intl.NumberFormat("es-AR", {
minimumFractionDigits: 2,
maximumFractionDigits: 4,
});
const dateFormatter = new Intl.DateTimeFormat("es-AR", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
const typeLabels: Record<string, string> = {
DEPOSIT: "Depósito",
WITHDRAWAL: "Extracción",
TRANSFER_IN: "Transferencia recibida",
TRANSFER_OUT: "Transferencia enviada",
ADJUSTMENT: "Ajuste",
};
interface WalletMovementsDialogProps {
wallet: Wallet;
open: boolean;
onOpenChange: (open: boolean) => void;
}
export function WalletMovementsDialog({
wallet,
open,
onOpenChange,
}: WalletMovementsDialogProps) {
const { data: movements, isLoading } = useWalletMovements(
open ? wallet.id : 0,
);
return (
<ResponsiveDialog open={open} onOpenChange={onOpenChange}>
<ResponsiveDialogContent>
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>
Movimientos de {wallet.name}
</ResponsiveDialogTitle>
</ResponsiveDialogHeader>
{isLoading ? (
<p className="text-sm text-muted-foreground">Cargando...</p>
) : movements && movements.length > 0 ? (
<div className="max-h-96 space-y-1 overflow-y-auto">
{movements.map((m) => (
<div
key={m.id}
className="flex items-center justify-between rounded-md p-2 text-sm hover:bg-muted/50"
>
<div className="flex flex-col">
<span className="font-medium">{typeLabels[m.type] ?? m.type}</span>
{m.description && (
<span className="text-xs text-muted-foreground">
{m.description}
</span>
)}
{m.transferGroupId && (
<span className="text-xs text-muted-foreground font-mono">
ID: {m.transferGroupId.slice(0, 8)}...
</span>
)}
<span className="text-xs text-muted-foreground">
{dateFormatter.format(new Date(m.createdAt))}
</span>
</div>
<span
className={`tabular-nums font-medium ${
m.amount >= 0
? "text-emerald-600 dark:text-emerald-400"
: "text-red-600 dark:text-red-400"
}`}
>
{m.amount >= 0 ? "+" : ""}
{amountFormatter.format(m.amount)}
</span>
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground">
Sin movimientos registrados.
</p>
)}
</ResponsiveDialogContent>
</ResponsiveDialog>
);
}