forked from git.gladyson/openmonetis
feat: adição de novos ícones SVG e configuração do ambiente
- Adicionados ícones SVG para ChatGPT, Claude, Gemini e OpenRouter - Implementados ícones para modos claro e escuro do ChatGPT - Criado script de inicialização para PostgreSQL com extensão pgcrypto - Adicionado script de configuração de ambiente que faz backup do .env - Configurado tsconfig.json para TypeScript com opções de compilação
This commit is contained in:
223
components/contas/transfer-dialog.tsx
Normal file
223
components/contas/transfer-dialog.tsx
Normal file
@@ -0,0 +1,223 @@
|
||||
"use client";
|
||||
|
||||
import { transferBetweenAccountsAction } from "@/app/(dashboard)/contas/actions";
|
||||
import type { AccountData } from "@/app/(dashboard)/contas/data";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CurrencyInput } from "@/components/ui/currency-input";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useControlledState } from "@/hooks/use-controlled-state";
|
||||
import { formatDateForDb } from "@/lib/utils/date";
|
||||
import { useMemo, useState, useTransition } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface TransferDialogProps {
|
||||
trigger?: React.ReactNode;
|
||||
accounts: AccountData[];
|
||||
fromAccountId: string;
|
||||
currentPeriod: string;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function TransferDialog({
|
||||
trigger,
|
||||
accounts,
|
||||
fromAccountId,
|
||||
currentPeriod,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: TransferDialogProps) {
|
||||
const [dialogOpen, setDialogOpen] = useControlledState(
|
||||
open,
|
||||
false,
|
||||
onOpenChange
|
||||
);
|
||||
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
// Form state
|
||||
const [toAccountId, setToAccountId] = useState("");
|
||||
const [amount, setAmount] = useState("");
|
||||
const [date, setDate] = useState(formatDateForDb(new Date()));
|
||||
const [period, setPeriod] = useState(currentPeriod);
|
||||
|
||||
// Available destination accounts (exclude source account)
|
||||
const availableAccounts = useMemo(
|
||||
() => accounts.filter((account) => account.id !== fromAccountId),
|
||||
[accounts, fromAccountId]
|
||||
);
|
||||
|
||||
// Source account info
|
||||
const fromAccount = useMemo(
|
||||
() => accounts.find((account) => account.id === fromAccountId),
|
||||
[accounts, fromAccountId]
|
||||
);
|
||||
|
||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setErrorMessage(null);
|
||||
|
||||
if (!toAccountId) {
|
||||
setErrorMessage("Selecione a conta de destino.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (toAccountId === fromAccountId) {
|
||||
setErrorMessage("Selecione uma conta de destino diferente da origem.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!amount || parseFloat(amount.replace(",", ".")) <= 0) {
|
||||
setErrorMessage("Informe um valor válido maior que zero.");
|
||||
return;
|
||||
}
|
||||
|
||||
startTransition(async () => {
|
||||
const result = await transferBetweenAccountsAction({
|
||||
fromAccountId,
|
||||
toAccountId,
|
||||
amount,
|
||||
date: new Date(date),
|
||||
period,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
toast.success(result.message);
|
||||
setDialogOpen(false);
|
||||
// Reset form
|
||||
setToAccountId("");
|
||||
setAmount("");
|
||||
setDate(formatDateForDb(new Date()));
|
||||
setPeriod(currentPeriod);
|
||||
return;
|
||||
}
|
||||
|
||||
setErrorMessage(result.error);
|
||||
toast.error(result.error);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
{trigger ? <DialogTrigger asChild>{trigger}</DialogTrigger> : null}
|
||||
<DialogContent className="sm:max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Transferir entre contas</DialogTitle>
|
||||
<DialogDescription>
|
||||
Registre uma transferência de valores entre suas contas.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form className="flex flex-col gap-5" onSubmit={handleSubmit}>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="transfer-date">Data da transferência</Label>
|
||||
<Input
|
||||
id="transfer-date"
|
||||
type="date"
|
||||
value={date}
|
||||
onChange={(e) => setDate(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="transfer-period">Período</Label>
|
||||
<Input
|
||||
id="transfer-period"
|
||||
type="month"
|
||||
value={period}
|
||||
onChange={(e) => setPeriod(e.target.value)}
|
||||
placeholder="AAAA-MM"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 sm:col-span-2">
|
||||
<Label htmlFor="transfer-amount">Valor</Label>
|
||||
<CurrencyInput
|
||||
id="transfer-amount"
|
||||
value={amount}
|
||||
onValueChange={setAmount}
|
||||
placeholder="R$ 0,00"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 sm:col-span-2">
|
||||
<Label htmlFor="from-account">Conta de origem</Label>
|
||||
<Input
|
||||
id="from-account"
|
||||
value={fromAccount?.name || ""}
|
||||
disabled
|
||||
className="bg-muted"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 sm:col-span-2">
|
||||
<Label htmlFor="to-account">Conta de destino</Label>
|
||||
{availableAccounts.length === 0 ? (
|
||||
<div className="rounded-md border border-border bg-muted p-3 text-sm text-muted-foreground">
|
||||
É necessário ter mais de uma conta cadastrada para realizar
|
||||
transferências.
|
||||
</div>
|
||||
) : (
|
||||
<Select value={toAccountId} onValueChange={setToAccountId}>
|
||||
<SelectTrigger id="to-account" className="w-full">
|
||||
<SelectValue placeholder="Selecione a conta de destino" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="w-full">
|
||||
{availableAccounts.map((account) => (
|
||||
<SelectItem key={account.id} value={account.id}>
|
||||
{account.name} - {account.accountType}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{errorMessage && (
|
||||
<p className="text-sm text-destructive">{errorMessage}</p>
|
||||
)}
|
||||
|
||||
<DialogFooter className="gap-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setDialogOpen(false)}
|
||||
disabled={isPending}
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isPending || availableAccounts.length === 0}
|
||||
>
|
||||
{isPending ? "Processando..." : "Confirmar transferência"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user