refactor: migrate from ESLint to Biome and extract SQL queries to data.ts
- Replace ESLint with Biome for linting and formatting - Configure Biome with tabs, double quotes, and organized imports - Move all SQL/Drizzle queries from page.tsx files to data.ts files - Create new data.ts files for: ajustes, dashboard, relatorios/categorias - Update existing data.ts files: extrato, fatura (add lancamentos queries) - Remove all drizzle-orm imports from page.tsx files - Update README.md with new tooling info Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,250 +1,249 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
createCardAction,
|
||||
updateCardAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
useTransition,
|
||||
} from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
createCardAction,
|
||||
updateCardAction,
|
||||
} from "@/app/(dashboard)/cartoes/actions";
|
||||
import { LogoPickerDialog, LogoPickerTrigger } from "@/components/logo-picker";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { useControlledState } from "@/hooks/use-controlled-state";
|
||||
import { useFormState } from "@/hooks/use-form-state";
|
||||
import { useLogoSelection } from "@/hooks/use-logo-selection";
|
||||
import { deriveNameFromLogo, normalizeLogo } from "@/lib/logo";
|
||||
import { formatLimitInput } from "@/lib/utils/currency";
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
useTransition,
|
||||
} from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { DEFAULT_CARD_BRANDS, DEFAULT_CARD_STATUS } from "./constants";
|
||||
import { CardFormFields } from "./card-form-fields";
|
||||
import { DEFAULT_CARD_BRANDS, DEFAULT_CARD_STATUS } from "./constants";
|
||||
import type { Card, CardFormValues } from "./types";
|
||||
|
||||
type AccountOption = {
|
||||
id: string;
|
||||
name: string;
|
||||
logo: string | null;
|
||||
id: string;
|
||||
name: string;
|
||||
logo: string | null;
|
||||
};
|
||||
|
||||
interface CardDialogProps {
|
||||
mode: "create" | "update";
|
||||
trigger?: React.ReactNode;
|
||||
logoOptions: string[];
|
||||
accounts: AccountOption[];
|
||||
card?: Card;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
mode: "create" | "update";
|
||||
trigger?: React.ReactNode;
|
||||
logoOptions: string[];
|
||||
accounts: AccountOption[];
|
||||
card?: Card;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const buildInitialValues = ({
|
||||
card,
|
||||
logoOptions,
|
||||
accounts,
|
||||
card,
|
||||
logoOptions,
|
||||
accounts,
|
||||
}: {
|
||||
card?: Card;
|
||||
logoOptions: string[];
|
||||
accounts: AccountOption[];
|
||||
card?: Card;
|
||||
logoOptions: string[];
|
||||
accounts: AccountOption[];
|
||||
}): CardFormValues => {
|
||||
const fallbackLogo = logoOptions[0] ?? "";
|
||||
const selectedLogo = normalizeLogo(card?.logo) || fallbackLogo;
|
||||
const derivedName = deriveNameFromLogo(selectedLogo);
|
||||
const fallbackLogo = logoOptions[0] ?? "";
|
||||
const selectedLogo = normalizeLogo(card?.logo) || fallbackLogo;
|
||||
const derivedName = deriveNameFromLogo(selectedLogo);
|
||||
|
||||
return {
|
||||
name: card?.name ?? derivedName,
|
||||
brand: card?.brand ?? DEFAULT_CARD_BRANDS[0],
|
||||
status: card?.status ?? DEFAULT_CARD_STATUS[0],
|
||||
closingDay: card?.closingDay ?? "01",
|
||||
dueDay: card?.dueDay ?? "10",
|
||||
limit: formatLimitInput(card?.limit ?? null),
|
||||
note: card?.note ?? "",
|
||||
logo: selectedLogo,
|
||||
contaId: card?.contaId ?? accounts[0]?.id ?? "",
|
||||
};
|
||||
return {
|
||||
name: card?.name ?? derivedName,
|
||||
brand: card?.brand ?? DEFAULT_CARD_BRANDS[0],
|
||||
status: card?.status ?? DEFAULT_CARD_STATUS[0],
|
||||
closingDay: card?.closingDay ?? "01",
|
||||
dueDay: card?.dueDay ?? "10",
|
||||
limit: formatLimitInput(card?.limit ?? null),
|
||||
note: card?.note ?? "",
|
||||
logo: selectedLogo,
|
||||
contaId: card?.contaId ?? accounts[0]?.id ?? "",
|
||||
};
|
||||
};
|
||||
|
||||
export function CardDialog({
|
||||
mode,
|
||||
trigger,
|
||||
logoOptions,
|
||||
accounts,
|
||||
card,
|
||||
open,
|
||||
onOpenChange,
|
||||
mode,
|
||||
trigger,
|
||||
logoOptions,
|
||||
accounts,
|
||||
card,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: CardDialogProps) {
|
||||
const [logoDialogOpen, setLogoDialogOpen] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
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
|
||||
);
|
||||
// Use controlled state hook for dialog open state
|
||||
const [dialogOpen, setDialogOpen] = useControlledState(
|
||||
open,
|
||||
false,
|
||||
onOpenChange,
|
||||
);
|
||||
|
||||
const initialState = useMemo(
|
||||
() => buildInitialValues({ card, logoOptions, accounts }),
|
||||
[card, logoOptions, accounts]
|
||||
);
|
||||
const initialState = useMemo(
|
||||
() => buildInitialValues({ card, logoOptions, accounts }),
|
||||
[card, logoOptions, accounts],
|
||||
);
|
||||
|
||||
// Use form state hook for form management
|
||||
const { formState, updateField, updateFields, setFormState } =
|
||||
useFormState<CardFormValues>(initialState);
|
||||
// Use form state hook for form management
|
||||
const { formState, updateField, updateFields, setFormState } =
|
||||
useFormState<CardFormValues>(initialState);
|
||||
|
||||
// Reset form when dialog opens
|
||||
useEffect(() => {
|
||||
if (dialogOpen) {
|
||||
setFormState(initialState);
|
||||
setErrorMessage(null);
|
||||
}
|
||||
}, [dialogOpen, initialState, setFormState]);
|
||||
// Reset form when dialog opens
|
||||
useEffect(() => {
|
||||
if (dialogOpen) {
|
||||
setFormState(initialState);
|
||||
setErrorMessage(null);
|
||||
}
|
||||
}, [dialogOpen, initialState, setFormState]);
|
||||
|
||||
// Close logo dialog when main dialog closes
|
||||
useEffect(() => {
|
||||
if (!dialogOpen) {
|
||||
setErrorMessage(null);
|
||||
setLogoDialogOpen(false);
|
||||
}
|
||||
}, [dialogOpen]);
|
||||
// Close logo dialog when main dialog closes
|
||||
useEffect(() => {
|
||||
if (!dialogOpen) {
|
||||
setErrorMessage(null);
|
||||
setLogoDialogOpen(false);
|
||||
}
|
||||
}, [dialogOpen]);
|
||||
|
||||
// Use logo selection hook
|
||||
const handleLogoSelection = useLogoSelection({
|
||||
mode,
|
||||
currentLogo: formState.logo,
|
||||
currentName: formState.name,
|
||||
onUpdate: (updates) => {
|
||||
updateFields(updates);
|
||||
setLogoDialogOpen(false);
|
||||
},
|
||||
});
|
||||
// Use logo selection hook
|
||||
const handleLogoSelection = useLogoSelection({
|
||||
mode,
|
||||
currentLogo: formState.logo,
|
||||
currentName: formState.name,
|
||||
onUpdate: (updates) => {
|
||||
updateFields(updates);
|
||||
setLogoDialogOpen(false);
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setErrorMessage(null);
|
||||
const handleSubmit = useCallback(
|
||||
(event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setErrorMessage(null);
|
||||
|
||||
if (mode === "update" && !card?.id) {
|
||||
const message = "Cartão inválido.";
|
||||
setErrorMessage(message);
|
||||
toast.error(message);
|
||||
return;
|
||||
}
|
||||
if (mode === "update" && !card?.id) {
|
||||
const message = "Cartão inválido.";
|
||||
setErrorMessage(message);
|
||||
toast.error(message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formState.contaId) {
|
||||
const message = "Selecione a conta vinculada.";
|
||||
setErrorMessage(message);
|
||||
toast.error(message);
|
||||
return;
|
||||
}
|
||||
if (!formState.contaId) {
|
||||
const message = "Selecione a conta vinculada.";
|
||||
setErrorMessage(message);
|
||||
toast.error(message);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = { ...formState };
|
||||
const payload = { ...formState };
|
||||
|
||||
if (!payload.logo) {
|
||||
const message = "Selecione um logo.";
|
||||
setErrorMessage(message);
|
||||
toast.error(message);
|
||||
return;
|
||||
}
|
||||
if (!payload.logo) {
|
||||
const message = "Selecione um logo.";
|
||||
setErrorMessage(message);
|
||||
toast.error(message);
|
||||
return;
|
||||
}
|
||||
|
||||
startTransition(async () => {
|
||||
const result =
|
||||
mode === "create"
|
||||
? await createCardAction(payload)
|
||||
: await updateCardAction({
|
||||
id: card?.id ?? "",
|
||||
...payload,
|
||||
});
|
||||
startTransition(async () => {
|
||||
const result =
|
||||
mode === "create"
|
||||
? await createCardAction(payload)
|
||||
: await updateCardAction({
|
||||
id: card?.id ?? "",
|
||||
...payload,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
toast.success(result.message);
|
||||
setDialogOpen(false);
|
||||
setFormState(initialState);
|
||||
return;
|
||||
}
|
||||
if (result.success) {
|
||||
toast.success(result.message);
|
||||
setDialogOpen(false);
|
||||
setFormState(initialState);
|
||||
return;
|
||||
}
|
||||
|
||||
setErrorMessage(result.error);
|
||||
toast.error(result.error);
|
||||
});
|
||||
},
|
||||
[card?.id, formState, initialState, mode, setDialogOpen, setFormState]
|
||||
);
|
||||
setErrorMessage(result.error);
|
||||
toast.error(result.error);
|
||||
});
|
||||
},
|
||||
[card?.id, formState, initialState, mode, setDialogOpen, setFormState],
|
||||
);
|
||||
|
||||
const title = mode === "create" ? "Novo cartão" : "Editar cartão";
|
||||
const description =
|
||||
mode === "create"
|
||||
? "Inclua um novo cartão de crédito para acompanhar seus gastos."
|
||||
: "Atualize as informações do cartão selecionado.";
|
||||
const submitLabel = mode === "create" ? "Salvar cartão" : "Atualizar cartão";
|
||||
const title = mode === "create" ? "Novo cartão" : "Editar cartão";
|
||||
const description =
|
||||
mode === "create"
|
||||
? "Inclua um novo cartão de crédito para acompanhar seus gastos."
|
||||
: "Atualize as informações do cartão selecionado.";
|
||||
const submitLabel = mode === "create" ? "Salvar cartão" : "Atualizar cartão";
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
{trigger ? <DialogTrigger asChild>{trigger}</DialogTrigger> : null}
|
||||
<DialogContent className="">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
return (
|
||||
<>
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
{trigger ? <DialogTrigger asChild>{trigger}</DialogTrigger> : null}
|
||||
<DialogContent className="">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form className="flex flex-col gap-5" onSubmit={handleSubmit}>
|
||||
<LogoPickerTrigger
|
||||
selectedLogo={formState.logo}
|
||||
disabled={logoOptions.length === 0}
|
||||
helperText="Clique para escolher o logo do cartão"
|
||||
onOpen={() => {
|
||||
if (logoOptions.length > 0) {
|
||||
setLogoDialogOpen(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<form className="flex flex-col gap-5" onSubmit={handleSubmit}>
|
||||
<LogoPickerTrigger
|
||||
selectedLogo={formState.logo}
|
||||
disabled={logoOptions.length === 0}
|
||||
helperText="Clique para escolher o logo do cartão"
|
||||
onOpen={() => {
|
||||
if (logoOptions.length > 0) {
|
||||
setLogoDialogOpen(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<CardFormFields
|
||||
values={formState}
|
||||
accountOptions={accounts}
|
||||
onChange={updateField}
|
||||
/>
|
||||
<CardFormFields
|
||||
values={formState}
|
||||
accountOptions={accounts}
|
||||
onChange={updateField}
|
||||
/>
|
||||
|
||||
{errorMessage && (
|
||||
<p className="text-sm text-destructive">{errorMessage}</p>
|
||||
)}
|
||||
{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}>
|
||||
{isPending ? "Salvando..." : submitLabel}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<DialogFooter className="gap-3">
|
||||
<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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
<LogoPickerDialog
|
||||
open={logoDialogOpen}
|
||||
logos={logoOptions}
|
||||
value={formState.logo}
|
||||
onOpenChange={setLogoDialogOpen}
|
||||
onSelect={handleLogoSelection}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user