mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-09 11:01:45 +00:00
refactor(core): move app para src e padroniza estrutura
This commit is contained in:
313
src/features/accounts/components/account-dialog.tsx
Normal file
313
src/features/accounts/components/account-dialog.tsx
Normal file
@@ -0,0 +1,313 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState, useTransition } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
createAccountAction,
|
||||
updateAccountAction,
|
||||
} from "@/features/accounts/actions";
|
||||
import {
|
||||
LogoPickerDialog,
|
||||
LogoPickerTrigger,
|
||||
} from "@/shared/components/logo-picker";
|
||||
import { useLogoSelection } from "@/shared/components/logo-picker/use-logo-selection";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/shared/components/ui/dialog";
|
||||
import { useControlledState } from "@/shared/hooks/use-controlled-state";
|
||||
import { useFormState } from "@/shared/hooks/use-form-state";
|
||||
import { deriveNameFromLogo, normalizeLogo } from "@/shared/lib/logo";
|
||||
import {
|
||||
formatInitialBalanceInput,
|
||||
normalizeDecimalInput,
|
||||
} from "@/shared/utils/currency";
|
||||
|
||||
import { AccountFormFields } from "./account-form-fields";
|
||||
import type { Account, AccountFormValues } from "./types";
|
||||
|
||||
const DEFAULT_ACCOUNT_TYPES = [
|
||||
"Conta Corrente",
|
||||
"Conta Poupança",
|
||||
"Carteira Digital",
|
||||
"Conta Investimento",
|
||||
"Pré-Pago | VR/VA",
|
||||
] as const;
|
||||
|
||||
const DEFAULT_ACCOUNT_STATUS = ["Ativa", "Inativa"] as const;
|
||||
|
||||
interface AccountDialogProps {
|
||||
mode: "create" | "update";
|
||||
trigger?: React.ReactNode;
|
||||
logoOptions: string[];
|
||||
account?: Account;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const buildInitialValues = ({
|
||||
account,
|
||||
logoOptions,
|
||||
accountTypes,
|
||||
accountStatuses,
|
||||
}: {
|
||||
account?: Account;
|
||||
logoOptions: string[];
|
||||
accountTypes: string[];
|
||||
accountStatuses: string[];
|
||||
}): AccountFormValues => {
|
||||
const fallbackLogo = logoOptions[0] ?? "";
|
||||
const selectedLogo = normalizeLogo(account?.logo) || fallbackLogo;
|
||||
const derivedName = deriveNameFromLogo(selectedLogo);
|
||||
|
||||
return {
|
||||
name: account?.name ?? derivedName,
|
||||
accountType: account?.accountType ?? accountTypes[0] ?? "",
|
||||
status: account?.status ?? accountStatuses[0] ?? "",
|
||||
note: account?.note ?? "",
|
||||
logo: selectedLogo,
|
||||
initialBalance: formatInitialBalanceInput(account?.initialBalance ?? 0),
|
||||
excludeFromBalance: account?.excludeFromBalance ?? false,
|
||||
excludeInitialBalanceFromIncome:
|
||||
account?.excludeInitialBalanceFromIncome ?? false,
|
||||
};
|
||||
};
|
||||
|
||||
export function AccountDialog({
|
||||
mode,
|
||||
trigger,
|
||||
logoOptions,
|
||||
account,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: AccountDialogProps) {
|
||||
const [logoDialogOpen, setLogoDialogOpen] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
// Use controlled state hook for dialog open state
|
||||
const [dialogOpen, setDialogOpen] = useControlledState(
|
||||
open,
|
||||
false,
|
||||
onOpenChange,
|
||||
);
|
||||
|
||||
const accountTypes = useMemo(() => {
|
||||
const values = new Set<string>(DEFAULT_ACCOUNT_TYPES);
|
||||
if (account?.accountType) {
|
||||
values.add(account.accountType);
|
||||
}
|
||||
return Array.from(values);
|
||||
}, [account?.accountType]);
|
||||
|
||||
const accountStatuses = useMemo(() => {
|
||||
const values = new Set<string>(DEFAULT_ACCOUNT_STATUS);
|
||||
if (account?.status) {
|
||||
values.add(account.status);
|
||||
}
|
||||
return Array.from(values);
|
||||
}, [account?.status]);
|
||||
|
||||
const initialState = useMemo(
|
||||
() =>
|
||||
buildInitialValues({
|
||||
account,
|
||||
logoOptions,
|
||||
accountTypes,
|
||||
accountStatuses,
|
||||
}),
|
||||
[account, logoOptions, accountTypes, accountStatuses],
|
||||
);
|
||||
|
||||
// Use form state hook for form management
|
||||
const { formState, resetForm, updateField, updateFields } =
|
||||
useFormState<AccountFormValues>(initialState);
|
||||
|
||||
// Reset form when dialog opens
|
||||
useEffect(() => {
|
||||
if (dialogOpen) {
|
||||
resetForm(initialState);
|
||||
setErrorMessage(null);
|
||||
}
|
||||
}, [dialogOpen, initialState, resetForm]);
|
||||
|
||||
// Close logo dialog when main dialog closes
|
||||
useEffect(() => {
|
||||
if (!dialogOpen) {
|
||||
setErrorMessage(null);
|
||||
setLogoDialogOpen(false);
|
||||
}
|
||||
}, [dialogOpen]);
|
||||
|
||||
type AccountCreatePayload = Parameters<typeof createAccountAction>[0];
|
||||
|
||||
// Use logo selection hook
|
||||
const handleLogoSelection = useLogoSelection({
|
||||
mode,
|
||||
currentLogo: formState.logo,
|
||||
currentName: formState.name,
|
||||
onUpdate: (updates) => {
|
||||
updateFields(updates);
|
||||
// Delay closing to avoid race condition on mobile
|
||||
requestAnimationFrame(() => {
|
||||
setLogoDialogOpen(false);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setErrorMessage(null);
|
||||
const accountId = account?.id;
|
||||
|
||||
if (mode === "update" && !accountId) {
|
||||
const message = "Conta inválida.";
|
||||
setErrorMessage(message);
|
||||
toast.error(message);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: AccountCreatePayload = {
|
||||
name: formState.name.trim(),
|
||||
accountType: formState.accountType,
|
||||
status: formState.status,
|
||||
note: formState.note.trim() || null,
|
||||
logo: formState.logo,
|
||||
initialBalance: Number(normalizeDecimalInput(formState.initialBalance)),
|
||||
excludeFromBalance: formState.excludeFromBalance,
|
||||
excludeInitialBalanceFromIncome:
|
||||
formState.excludeInitialBalanceFromIncome,
|
||||
};
|
||||
|
||||
if (!payload.logo) {
|
||||
setErrorMessage("Selecione um logo.");
|
||||
return;
|
||||
}
|
||||
|
||||
startTransition(async () => {
|
||||
if (mode === "create") {
|
||||
const result = await createAccountAction(payload);
|
||||
|
||||
if (result.success) {
|
||||
toast.success(result.message);
|
||||
setDialogOpen(false);
|
||||
resetForm(initialState);
|
||||
return;
|
||||
}
|
||||
|
||||
setErrorMessage(result.error);
|
||||
toast.error(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!accountId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await updateAccountAction({
|
||||
id: accountId,
|
||||
...payload,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
toast.success(result.message);
|
||||
setDialogOpen(false);
|
||||
resetForm(initialState);
|
||||
return;
|
||||
}
|
||||
|
||||
setErrorMessage(result.error);
|
||||
toast.error(result.error);
|
||||
});
|
||||
};
|
||||
|
||||
const title = mode === "create" ? "Nova conta" : "Editar conta";
|
||||
const description =
|
||||
mode === "create"
|
||||
? "Cadastre uma nova conta para organizar seus lançamentos."
|
||||
: "Atualize as informações da conta selecionada.";
|
||||
const submitLabel = mode === "create" ? "Salvar conta" : "Atualizar conta";
|
||||
|
||||
const handleMainDialogOpenChange = (open: boolean) => {
|
||||
if (!open && logoDialogOpen) {
|
||||
return;
|
||||
}
|
||||
setDialogOpen(open);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={dialogOpen} onOpenChange={handleMainDialogOpenChange}>
|
||||
{trigger ? <DialogTrigger asChild>{trigger}</DialogTrigger> : null}
|
||||
<DialogContent
|
||||
className="sm:max-w-xl"
|
||||
onPointerDownOutside={(e) => {
|
||||
if (logoDialogOpen) e.preventDefault();
|
||||
}}
|
||||
onInteractOutside={(e) => {
|
||||
if (logoDialogOpen) e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form className="flex flex-col gap-5" onSubmit={handleSubmit}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<LogoPickerTrigger
|
||||
selectedLogo={formState.logo}
|
||||
disabled={logoOptions.length === 0}
|
||||
onOpen={() => {
|
||||
if (logoOptions.length > 0) {
|
||||
setLogoDialogOpen(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AccountFormFields
|
||||
values={formState}
|
||||
accountTypes={accountTypes}
|
||||
accountStatuses={accountStatuses}
|
||||
onChange={updateField}
|
||||
showInitialBalance={mode === "create"}
|
||||
/>
|
||||
|
||||
{errorMessage && (
|
||||
<p className="text-sm text-destructive">{errorMessage}</p>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setDialogOpen(false)}
|
||||
disabled={isPending}
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={isPending}>
|
||||
{isPending ? "Salvando..." : submitLabel}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<LogoPickerDialog
|
||||
open={logoDialogOpen}
|
||||
logos={logoOptions}
|
||||
value={formState.logo}
|
||||
onOpenChange={setLogoDialogOpen}
|
||||
onSelect={handleLogoSelection}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user