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:
Felipe Coutinho
2026-01-27 13:15:37 +00:00
parent 8ffe61c59b
commit a7f63fb77a
442 changed files with 66141 additions and 69292 deletions

View File

@@ -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}
/>
</>
);
}

View File

@@ -4,211 +4,212 @@ import { CurrencyInput } from "@/components/ui/currency-input";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import {
DAYS_IN_MONTH,
DEFAULT_CARD_BRANDS,
DEFAULT_CARD_STATUS,
AccountSelectContent,
BrandSelectContent,
StatusSelectContent,
} from "./card-select-items";
import {
DAYS_IN_MONTH,
DEFAULT_CARD_BRANDS,
DEFAULT_CARD_STATUS,
} from "./constants";
import type { CardFormValues } from "./types";
import {
BrandSelectContent,
StatusSelectContent,
AccountSelectContent,
} from "./card-select-items";
interface AccountOption {
id: string;
name: string;
logo: string | null;
id: string;
name: string;
logo: string | null;
}
interface CardFormFieldsProps {
values: CardFormValues;
accountOptions: AccountOption[];
onChange: (field: keyof CardFormValues, value: string) => void;
values: CardFormValues;
accountOptions: AccountOption[];
onChange: (field: keyof CardFormValues, value: string) => void;
}
const ensureOption = (options: string[], value: string) => {
if (!value) {
return options;
}
return options.includes(value) ? options : [value, ...options];
if (!value) {
return options;
}
return options.includes(value) ? options : [value, ...options];
};
export function CardFormFields({
values,
accountOptions,
onChange,
values,
accountOptions,
onChange,
}: CardFormFieldsProps) {
const brands = ensureOption(
DEFAULT_CARD_BRANDS as unknown as string[],
values.brand
);
const statuses = ensureOption(
DEFAULT_CARD_STATUS as unknown as string[],
values.status
);
const brands = ensureOption(
DEFAULT_CARD_BRANDS as unknown as string[],
values.brand,
);
const statuses = ensureOption(
DEFAULT_CARD_STATUS as unknown as string[],
values.status,
);
return (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="flex flex-col gap-2">
<Label htmlFor="card-name">Nome do cartão</Label>
<Input
id="card-name"
value={values.name}
onChange={(event) => onChange("name", event.target.value)}
placeholder="Ex.: Nubank Platinum"
required
/>
</div>
return (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="flex flex-col gap-2">
<Label htmlFor="card-name">Nome do cartão</Label>
<Input
id="card-name"
value={values.name}
onChange={(event) => onChange("name", event.target.value)}
placeholder="Ex.: Nubank Platinum"
required
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="card-brand">Bandeira</Label>
<Select
value={values.brand}
onValueChange={(value) => onChange("brand", value)}
>
<SelectTrigger id="card-brand" className="w-full">
<SelectValue placeholder="Selecione a bandeira">
{values.brand && <BrandSelectContent label={values.brand} />}
</SelectValue>
</SelectTrigger>
<SelectContent>
{brands.map((brand) => (
<SelectItem key={brand} value={brand}>
<BrandSelectContent label={brand} />
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="card-brand">Bandeira</Label>
<Select
value={values.brand}
onValueChange={(value) => onChange("brand", value)}
>
<SelectTrigger id="card-brand" className="w-full">
<SelectValue placeholder="Selecione a bandeira">
{values.brand && <BrandSelectContent label={values.brand} />}
</SelectValue>
</SelectTrigger>
<SelectContent>
{brands.map((brand) => (
<SelectItem key={brand} value={brand}>
<BrandSelectContent label={brand} />
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="card-status">Status</Label>
<Select
value={values.status}
onValueChange={(value) => onChange("status", value)}
>
<SelectTrigger id="card-status" className="w-full">
<SelectValue placeholder="Selecione o status">
{values.status && <StatusSelectContent label={values.status} />}
</SelectValue>
</SelectTrigger>
<SelectContent>
{statuses.map((status) => (
<SelectItem key={status} value={status}>
<StatusSelectContent label={status} />
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="card-status">Status</Label>
<Select
value={values.status}
onValueChange={(value) => onChange("status", value)}
>
<SelectTrigger id="card-status" className="w-full">
<SelectValue placeholder="Selecione o status">
{values.status && <StatusSelectContent label={values.status} />}
</SelectValue>
</SelectTrigger>
<SelectContent>
{statuses.map((status) => (
<SelectItem key={status} value={status}>
<StatusSelectContent label={status} />
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="card-limit">Limite (R$)</Label>
<CurrencyInput
id="card-limit"
value={values.limit}
onValueChange={(value) => onChange("limit", value)}
placeholder="R$ 0,00"
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="card-limit">Limite (R$)</Label>
<CurrencyInput
id="card-limit"
value={values.limit}
onValueChange={(value) => onChange("limit", value)}
placeholder="R$ 0,00"
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="card-closing-day">Dia de fechamento</Label>
<Select
value={values.closingDay}
onValueChange={(value) => onChange("closingDay", value)}
>
<SelectTrigger id="card-closing-day" className="w-full">
<SelectValue placeholder="Dia de fechamento" />
</SelectTrigger>
<SelectContent>
{DAYS_IN_MONTH.map((day) => (
<SelectItem key={day} value={day}>
Dia {day}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="card-closing-day">Dia de fechamento</Label>
<Select
value={values.closingDay}
onValueChange={(value) => onChange("closingDay", value)}
>
<SelectTrigger id="card-closing-day" className="w-full">
<SelectValue placeholder="Dia de fechamento" />
</SelectTrigger>
<SelectContent>
{DAYS_IN_MONTH.map((day) => (
<SelectItem key={day} value={day}>
Dia {day}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="card-due-day">Dia de vencimento</Label>
<Select
value={values.dueDay}
onValueChange={(value) => onChange("dueDay", value)}
>
<SelectTrigger id="card-due-day" className="w-full">
<SelectValue placeholder="Dia de vencimento" />
</SelectTrigger>
<SelectContent>
{DAYS_IN_MONTH.map((day) => (
<SelectItem key={day} value={day}>
Dia {day}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="card-due-day">Dia de vencimento</Label>
<Select
value={values.dueDay}
onValueChange={(value) => onChange("dueDay", value)}
>
<SelectTrigger id="card-due-day" className="w-full">
<SelectValue placeholder="Dia de vencimento" />
</SelectTrigger>
<SelectContent>
{DAYS_IN_MONTH.map((day) => (
<SelectItem key={day} value={day}>
Dia {day}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2 sm:col-span-2">
<Label htmlFor="card-account">Conta vinculada</Label>
<Select
value={values.contaId}
onValueChange={(value) => onChange("contaId", value)}
disabled={accountOptions.length === 0}
>
<SelectTrigger id="card-account" className="w-full">
<SelectValue
placeholder={
accountOptions.length === 0
? "Cadastre uma conta primeiro"
: "Selecione a conta"
}
>
{values.contaId && (() => {
const selectedAccount = accountOptions.find(
(acc) => acc.id === values.contaId
);
return selectedAccount ? (
<AccountSelectContent
label={selectedAccount.name}
logo={selectedAccount.logo}
/>
) : null;
})()}
</SelectValue>
</SelectTrigger>
<SelectContent>
{accountOptions.map((account) => (
<SelectItem key={account.id} value={account.id}>
<AccountSelectContent
label={account.name}
logo={account.logo}
/>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2 sm:col-span-2">
<Label htmlFor="card-account">Conta vinculada</Label>
<Select
value={values.contaId}
onValueChange={(value) => onChange("contaId", value)}
disabled={accountOptions.length === 0}
>
<SelectTrigger id="card-account" className="w-full">
<SelectValue
placeholder={
accountOptions.length === 0
? "Cadastre uma conta primeiro"
: "Selecione a conta"
}
>
{values.contaId &&
(() => {
const selectedAccount = accountOptions.find(
(acc) => acc.id === values.contaId,
);
return selectedAccount ? (
<AccountSelectContent
label={selectedAccount.name}
logo={selectedAccount.logo}
/>
) : null;
})()}
</SelectValue>
</SelectTrigger>
<SelectContent>
{accountOptions.map((account) => (
<SelectItem key={account.id} value={account.id}>
<AccountSelectContent
label={account.name}
logo={account.logo}
/>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2 sm:col-span-2">
<Label htmlFor="card-note">Anotação</Label>
<Textarea
id="card-note"
value={values.note}
onChange={(event) => onChange("note", event.target.value)}
placeholder="Observações sobre este cartão"
/>
</div>
</div>
);
<div className="flex flex-col gap-2 sm:col-span-2">
<Label htmlFor="card-note">Anotação</Label>
<Textarea
id="card-note"
value={values.note}
onChange={(event) => onChange("note", event.target.value)}
placeholder="Observações sobre este cartão"
/>
</div>
</div>
);
}

View File

@@ -1,307 +1,307 @@
"use client";
import {
Card,
CardContent,
CardFooter,
CardHeader,
} from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils/ui";
import {
RiChat3Line,
RiDeleteBin5Line,
RiEyeLine,
RiPencilLine,
RiChat3Line,
RiDeleteBin5Line,
RiEyeLine,
RiPencilLine,
} from "@remixicon/react";
import Image from "next/image";
import { useMemo } from "react";
import {
Card,
CardContent,
CardFooter,
CardHeader,
} from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils/ui";
import MoneyValues from "../money-values";
interface CardItemProps {
name: string;
brand: string;
status: string;
closingDay: string;
dueDay: string;
limit: number | null;
limitInUse?: number | null;
limitAvailable?: number | null;
contaName: string;
logo?: string | null;
note?: string | null;
onEdit?: () => void;
onInvoice?: () => void;
onRemove?: () => void;
name: string;
brand: string;
status: string;
closingDay: string;
dueDay: string;
limit: number | null;
limitInUse?: number | null;
limitAvailable?: number | null;
contaName: string;
logo?: string | null;
note?: string | null;
onEdit?: () => void;
onInvoice?: () => void;
onRemove?: () => void;
}
const BRAND_ASSETS: Record<string, string> = {
visa: "/bandeiras/visa.svg",
mastercard: "/bandeiras/mastercard.svg",
amex: "/bandeiras/amex.svg",
american: "/bandeiras/amex.svg",
elo: "/bandeiras/elo.svg",
hipercard: "/bandeiras/hipercard.svg",
hiper: "/bandeiras/hipercard.svg",
visa: "/bandeiras/visa.svg",
mastercard: "/bandeiras/mastercard.svg",
amex: "/bandeiras/amex.svg",
american: "/bandeiras/amex.svg",
elo: "/bandeiras/elo.svg",
hipercard: "/bandeiras/hipercard.svg",
hiper: "/bandeiras/hipercard.svg",
};
const resolveBrandAsset = (brand: string) => {
const normalized = brand.trim().toLowerCase();
const normalized = brand.trim().toLowerCase();
const match = (
Object.keys(BRAND_ASSETS) as Array<keyof typeof BRAND_ASSETS>
).find((entry) => normalized.includes(entry));
const match = (
Object.keys(BRAND_ASSETS) as Array<keyof typeof BRAND_ASSETS>
).find((entry) => normalized.includes(entry));
return match ? BRAND_ASSETS[match] : null;
return match ? BRAND_ASSETS[match] : null;
};
const formatDay = (value: string) => value.padStart(2, "0");
export function CardItem({
name,
brand,
status,
closingDay,
dueDay,
limit,
limitInUse,
limitAvailable,
contaName: _contaName,
logo,
note,
onEdit,
onInvoice,
onRemove,
name,
brand,
status,
closingDay,
dueDay,
limit,
limitInUse,
limitAvailable,
contaName: _contaName,
logo,
note,
onEdit,
onInvoice,
onRemove,
}: CardItemProps) {
void _contaName;
void _contaName;
const limitTotal = limit ?? null;
const used =
limitInUse ??
(limitTotal !== null && limitAvailable !== null
? Math.max(limitTotal - limitAvailable, 0)
: limitTotal !== null
? 0
: null);
const limitTotal = limit ?? null;
const used =
limitInUse ??
(limitTotal !== null && limitAvailable !== null
? Math.max(limitTotal - limitAvailable, 0)
: limitTotal !== null
? 0
: null);
const available =
limitAvailable ??
(limitTotal !== null && used !== null
? Math.max(limitTotal - used, 0)
: null);
const available =
limitAvailable ??
(limitTotal !== null && used !== null
? Math.max(limitTotal - used, 0)
: null);
const usagePercent =
limitTotal && limitTotal > 0 && used !== null
? Math.min(Math.max((used / limitTotal) * 100, 0), 100)
: 0;
const usagePercent =
limitTotal && limitTotal > 0 && used !== null
? Math.min(Math.max((used / limitTotal) * 100, 0), 100)
: 0;
const logoPath = useMemo(() => {
if (!logo) {
return null;
}
const logoPath = useMemo(() => {
if (!logo) {
return null;
}
if (
logo.startsWith("http://") ||
logo.startsWith("https://") ||
logo.startsWith("data:")
) {
return logo;
}
if (
logo.startsWith("http://") ||
logo.startsWith("https://") ||
logo.startsWith("data:")
) {
return logo;
}
return logo.startsWith("/") ? logo : `/logos/${logo}`;
}, [logo]);
return logo.startsWith("/") ? logo : `/logos/${logo}`;
}, [logo]);
const brandAsset = useMemo(() => resolveBrandAsset(brand), [brand]);
const brandAsset = useMemo(() => resolveBrandAsset(brand), [brand]);
const isInactive = useMemo(
() => status?.toLowerCase() === "inativo",
[status]
);
const isInactive = useMemo(
() => status?.toLowerCase() === "inativo",
[status],
);
const metrics = useMemo(() => {
if (limitTotal === null) return null;
const metrics = useMemo(() => {
if (limitTotal === null) return null;
return [
{ label: "Limite Total", value: limitTotal },
{ label: "Em uso", value: used },
{ label: "Disponível", value: available },
];
}, [available, limitTotal, used]);
return [
{ label: "Limite Total", value: limitTotal },
{ label: "Em uso", value: used },
{ label: "Disponível", value: available },
];
}, [available, limitTotal, used]);
const actions = useMemo(
() => [
{
label: "editar",
icon: <RiPencilLine className="size-4" aria-hidden />,
onClick: onEdit,
className: "text-primary",
},
{
label: "ver fatura",
icon: <RiEyeLine className="size-4" aria-hidden />,
onClick: onInvoice,
className: "text-primary",
},
{
label: "remover",
icon: <RiDeleteBin5Line className="size-4" aria-hidden />,
onClick: onRemove,
className: "text-destructive",
},
],
[onEdit, onInvoice, onRemove]
);
const actions = useMemo(
() => [
{
label: "editar",
icon: <RiPencilLine className="size-4" aria-hidden />,
onClick: onEdit,
className: "text-primary",
},
{
label: "ver fatura",
icon: <RiEyeLine className="size-4" aria-hidden />,
onClick: onInvoice,
className: "text-primary",
},
{
label: "remover",
icon: <RiDeleteBin5Line className="size-4" aria-hidden />,
onClick: onRemove,
className: "text-destructive",
},
],
[onEdit, onInvoice, onRemove],
);
return (
<Card className="flex p-6 h-[300px] w-[440px]">
<CardHeader className="space-y-2 px-0 pb-0">
<div className="flex items-start justify-between gap-2">
<div className="flex flex-1 items-center gap-2">
{logoPath ? (
<div className="flex size-10 shrink-0 items-center justify-center overflow-hidden">
<Image
src={logoPath}
alt={`Logo do cartão ${name}`}
width={42}
height={42}
className={cn(
"rounded-lg",
isInactive && "grayscale opacity-40"
)}
/>
</div>
) : null}
return (
<Card className="flex p-6 h-[300px] w-[440px]">
<CardHeader className="space-y-2 px-0 pb-0">
<div className="flex items-start justify-between gap-2">
<div className="flex flex-1 items-center gap-2">
{logoPath ? (
<div className="flex size-10 shrink-0 items-center justify-center overflow-hidden">
<Image
src={logoPath}
alt={`Logo do cartão ${name}`}
width={42}
height={42}
className={cn(
"rounded-lg",
isInactive && "grayscale opacity-40",
)}
/>
</div>
) : null}
<div className="min-w-0">
<div className="flex items-center gap-1.5">
<h3 className="truncate text-sm font-semibold text-foreground sm:text-base">
{name}
</h3>
{note ? (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="text-muted-foreground/70 transition-colors hover:text-foreground"
aria-label="Observações do cartão"
>
<RiChat3Line className="size-3.5" />
</button>
</TooltipTrigger>
<TooltipContent side="top" align="start">
{note}
</TooltipContent>
</Tooltip>
) : null}
</div>
<div className="min-w-0">
<div className="flex items-center gap-1.5">
<h3 className="truncate text-sm font-semibold text-foreground sm:text-base">
{name}
</h3>
{note ? (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="text-muted-foreground/70 transition-colors hover:text-foreground"
aria-label="Observações do cartão"
>
<RiChat3Line className="size-3.5" />
</button>
</TooltipTrigger>
<TooltipContent side="top" align="start">
{note}
</TooltipContent>
</Tooltip>
) : null}
</div>
{status ? (
<span className="text-xs tracking-wide text-muted-foreground">
{status}
</span>
) : null}
</div>
</div>
{status ? (
<span className="text-xs tracking-wide text-muted-foreground">
{status}
</span>
) : null}
</div>
</div>
{brandAsset ? (
<div className="flex items-center justify-center rounded-lg py-1">
<Image
src={brandAsset}
alt={`Bandeira ${brand}`}
width={42}
height={42}
className={cn(
"h-6 w-auto rounded",
isInactive && "grayscale opacity-40"
)}
/>
</div>
) : (
<span className="text-sm font-medium text-muted-foreground">
{brand}
</span>
)}
</div>
{brandAsset ? (
<div className="flex items-center justify-center rounded-lg py-1">
<Image
src={brandAsset}
alt={`Bandeira ${brand}`}
width={42}
height={42}
className={cn(
"h-6 w-auto rounded",
isInactive && "grayscale opacity-40",
)}
/>
</div>
) : (
<span className="text-sm font-medium text-muted-foreground">
{brand}
</span>
)}
</div>
<div className="flex items-center justify-between border-y border-dashed py-3 text-xs font-medium text-muted-foreground sm:text-sm">
<span>
Fecha dia{" "}
<span className="font-semibold text-foreground">
{formatDay(closingDay)}
</span>
</span>
<span>
Vence dia{" "}
<span className="font-semibold text-foreground">
{formatDay(dueDay)}
</span>
</span>
</div>
</CardHeader>
<div className="flex items-center justify-between border-y border-dashed py-3 text-xs font-medium text-muted-foreground sm:text-sm">
<span>
Fecha dia{" "}
<span className="font-semibold text-foreground">
{formatDay(closingDay)}
</span>
</span>
<span>
Vence dia{" "}
<span className="font-semibold text-foreground">
{formatDay(dueDay)}
</span>
</span>
</div>
</CardHeader>
<CardContent className="flex flex-1 flex-col gap-5 px-0">
{metrics ? (
<>
<div className="grid grid-cols-3 gap-4">
<div className="flex flex-col items-start gap-1">
<p className="text-sm font-semibold text-foreground">
<MoneyValues amount={metrics[0].value} />
</p>
<span className="text-xs font-medium text-muted-foreground">
{metrics[0].label}
</span>
</div>
<CardContent className="flex flex-1 flex-col gap-5 px-0">
{metrics ? (
<>
<div className="grid grid-cols-3 gap-4">
<div className="flex flex-col items-start gap-1">
<p className="text-sm font-semibold text-foreground">
<MoneyValues amount={metrics[0].value} />
</p>
<span className="text-xs font-medium text-muted-foreground">
{metrics[0].label}
</span>
</div>
<div className="flex flex-col items-center gap-1">
<p className="flex items-center gap-1.5 text-sm font-semibold text-foreground">
<span className="size-2 rounded-full bg-primary" />
<MoneyValues amount={metrics[1].value} />
</p>
<span className="text-xs font-medium text-muted-foreground">
{metrics[1].label}
</span>
</div>
<div className="flex flex-col items-center gap-1">
<p className="flex items-center gap-1.5 text-sm font-semibold text-foreground">
<span className="size-2 rounded-full bg-primary" />
<MoneyValues amount={metrics[1].value} />
</p>
<span className="text-xs font-medium text-muted-foreground">
{metrics[1].label}
</span>
</div>
<div className="flex flex-col items-end gap-1">
<p className="text-sm font-semibold text-foreground">
<MoneyValues amount={metrics[2].value} />
</p>
<span className="text-xs font-medium text-muted-foreground">
{metrics[2].label}
</span>
</div>
</div>
<div className="flex flex-col items-end gap-1">
<p className="text-sm font-semibold text-foreground">
<MoneyValues amount={metrics[2].value} />
</p>
<span className="text-xs font-medium text-muted-foreground">
{metrics[2].label}
</span>
</div>
</div>
<Progress value={usagePercent} className="h-3" />
</>
) : (
<p className="text-sm text-muted-foreground">
Ainda não limite registrado para este cartão.
</p>
)}
</CardContent>
<Progress value={usagePercent} className="h-3" />
</>
) : (
<p className="text-sm text-muted-foreground">
Ainda não limite registrado para este cartão.
</p>
)}
</CardContent>
<CardFooter className="mt-auto flex flex-wrap gap-4 px-0 text-sm">
{actions.map(({ label, icon, onClick, className }) => (
<button
key={label}
type="button"
onClick={onClick}
className={cn(
"flex items-center gap-1 font-medium transition-opacity hover:opacity-80",
className
)}
>
{icon}
{label}
</button>
))}
</CardFooter>
</Card>
);
<CardFooter className="mt-auto flex flex-wrap gap-4 px-0 text-sm">
{actions.map(({ label, icon, onClick, className }) => (
<button
key={label}
type="button"
onClick={onClick}
className={cn(
"flex items-center gap-1 font-medium transition-opacity hover:opacity-80",
className,
)}
>
{icon}
{label}
</button>
))}
</CardFooter>
</Card>
);
}

View File

@@ -1,89 +1,89 @@
"use client";
import DotIcon from "@/components/dot-icon";
import Image from "next/image";
import { RiBankLine } from "@remixicon/react";
import Image from "next/image";
import DotIcon from "@/components/dot-icon";
type SelectItemContentProps = {
label: string;
logo?: string | null;
label: string;
logo?: string | null;
};
const resolveLogoSrc = (logo: string | null) => {
if (!logo) {
return null;
}
if (!logo) {
return null;
}
const fileName = logo.split("/").filter(Boolean).pop() ?? logo;
return `/logos/${fileName}`;
const fileName = logo.split("/").filter(Boolean).pop() ?? logo;
return `/logos/${fileName}`;
};
const getBrandLogo = (brand: string): string | null => {
const brandMap: Record<string, string> = {
Visa: "visa.png",
Mastercard: "mastercard.png",
Elo: "elo.png",
};
const brandMap: Record<string, string> = {
Visa: "visa.png",
Mastercard: "mastercard.png",
Elo: "elo.png",
};
return brandMap[brand] ?? null;
return brandMap[brand] ?? null;
};
export function BrandSelectContent({ label }: { label: string }) {
const brandLogo = getBrandLogo(label);
const logoSrc = brandLogo ? `/logos/${brandLogo}` : null;
const brandLogo = getBrandLogo(label);
const logoSrc = brandLogo ? `/logos/${brandLogo}` : null;
return (
<span className="flex items-center gap-2">
{logoSrc ? (
<Image
src={logoSrc}
alt={`Logo ${label}`}
width={24}
height={24}
className="rounded object-contain"
/>
) : (
<RiBankLine className="size-5 text-muted-foreground" aria-hidden />
)}
<span>{label}</span>
</span>
);
return (
<span className="flex items-center gap-2">
{logoSrc ? (
<Image
src={logoSrc}
alt={`Logo ${label}`}
width={24}
height={24}
className="rounded object-contain"
/>
) : (
<RiBankLine className="size-5 text-muted-foreground" aria-hidden />
)}
<span>{label}</span>
</span>
);
}
export function StatusSelectContent({ label }: { label: string }) {
const isActive = label === "Ativo";
const isActive = label === "Ativo";
return (
<span className="flex items-center gap-2">
<DotIcon
bg_dot={
isActive
? "bg-emerald-600 dark:bg-emerald-300"
: "bg-slate-400 dark:bg-slate-500"
}
/>
<span>{label}</span>
</span>
);
return (
<span className="flex items-center gap-2">
<DotIcon
bg_dot={
isActive
? "bg-emerald-600 dark:bg-emerald-300"
: "bg-slate-400 dark:bg-slate-500"
}
/>
<span>{label}</span>
</span>
);
}
export function AccountSelectContent({ label, logo }: SelectItemContentProps) {
const logoSrc = resolveLogoSrc(logo);
const logoSrc = resolveLogoSrc(logo);
return (
<span className="flex items-center gap-2">
{logoSrc ? (
<Image
src={logoSrc}
alt={`Logo de ${label}`}
width={20}
height={20}
className="rounded"
/>
) : (
<RiBankLine className="size-4 text-muted-foreground" aria-hidden />
)}
<span>{label}</span>
</span>
);
return (
<span className="flex items-center gap-2">
{logoSrc ? (
<Image
src={logoSrc}
alt={`Logo de ${label}`}
width={20}
height={20}
className="rounded"
/>
) : (
<RiBankLine className="size-4 text-muted-foreground" aria-hidden />
)}
<span>{label}</span>
</span>
);
}

View File

@@ -1,189 +1,189 @@
"use client";
import { RiAddCircleLine, RiBankCard2Line } from "@remixicon/react";
import { useRouter } from "next/navigation";
import { useCallback, useMemo, useState } from "react";
import { toast } from "sonner";
import { deleteCardAction } from "@/app/(dashboard)/cartoes/actions";
import { ConfirmActionDialog } from "@/components/confirm-action-dialog";
import { EmptyState } from "@/components/empty-state";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { RiAddCircleLine, RiBankCard2Line } from "@remixicon/react";
import { useRouter } from "next/navigation";
import { useCallback, useMemo, useState } from "react";
import { toast } from "sonner";
import { CardDialog } from "./card-dialog";
import { CardItem } from "./card-item";
type AccountOption = {
id: string;
name: string;
id: string;
name: string;
};
interface CardsPageProps {
cards: Card[];
accounts: AccountOption[];
logoOptions: string[];
isInativos?: boolean;
cards: Card[];
accounts: AccountOption[];
logoOptions: string[];
isInativos?: boolean;
}
export function CardsPage({
cards,
accounts,
logoOptions,
isInativos = false,
cards,
accounts,
logoOptions,
isInativos = false,
}: CardsPageProps) {
const router = useRouter();
const [editOpen, setEditOpen] = useState(false);
const [selectedCard, setSelectedCard] = useState<Card | null>(null);
const [removeOpen, setRemoveOpen] = useState(false);
const [cardToRemove, setCardToRemove] = useState<Card | null>(null);
const router = useRouter();
const [editOpen, setEditOpen] = useState(false);
const [selectedCard, setSelectedCard] = useState<Card | null>(null);
const [removeOpen, setRemoveOpen] = useState(false);
const [cardToRemove, setCardToRemove] = useState<Card | null>(null);
const hasCards = cards.length > 0;
const hasCards = cards.length > 0;
const orderedCards = useMemo(
() =>
[...cards].sort((a, b) => {
// Coloca inativos no final
const aIsInactive = a.status?.toLowerCase() === "inativo";
const bIsInactive = b.status?.toLowerCase() === "inativo";
const orderedCards = useMemo(
() =>
[...cards].sort((a, b) => {
// Coloca inativos no final
const aIsInactive = a.status?.toLowerCase() === "inativo";
const bIsInactive = b.status?.toLowerCase() === "inativo";
if (aIsInactive && !bIsInactive) return 1;
if (!aIsInactive && bIsInactive) return -1;
if (aIsInactive && !bIsInactive) return 1;
if (!aIsInactive && bIsInactive) return -1;
// Mesma ordem alfabética dentro de cada grupo
return a.name.localeCompare(b.name, "pt-BR", { sensitivity: "base" });
}),
[cards]
);
// Mesma ordem alfabética dentro de cada grupo
return a.name.localeCompare(b.name, "pt-BR", { sensitivity: "base" });
}),
[cards],
);
const handleEdit = useCallback((card: Card) => {
setSelectedCard(card);
setEditOpen(true);
}, []);
const handleEdit = useCallback((card: Card) => {
setSelectedCard(card);
setEditOpen(true);
}, []);
const handleEditOpenChange = useCallback((open: boolean) => {
setEditOpen(open);
if (!open) {
setSelectedCard(null);
}
}, []);
const handleEditOpenChange = useCallback((open: boolean) => {
setEditOpen(open);
if (!open) {
setSelectedCard(null);
}
}, []);
const handleRemoveRequest = useCallback((card: Card) => {
setCardToRemove(card);
setRemoveOpen(true);
}, []);
const handleRemoveRequest = useCallback((card: Card) => {
setCardToRemove(card);
setRemoveOpen(true);
}, []);
const handleInvoice = useCallback(
(card: Card) => {
router.push(`/cartoes/${card.id}/fatura`);
},
[router]
);
const handleInvoice = useCallback(
(card: Card) => {
router.push(`/cartoes/${card.id}/fatura`);
},
[router],
);
const handleRemoveOpenChange = useCallback((open: boolean) => {
setRemoveOpen(open);
if (!open) {
setCardToRemove(null);
}
}, []);
const handleRemoveOpenChange = useCallback((open: boolean) => {
setRemoveOpen(open);
if (!open) {
setCardToRemove(null);
}
}, []);
const handleRemoveConfirm = useCallback(async () => {
if (!cardToRemove) {
return;
}
const handleRemoveConfirm = useCallback(async () => {
if (!cardToRemove) {
return;
}
const result = await deleteCardAction({ id: cardToRemove.id });
const result = await deleteCardAction({ id: cardToRemove.id });
if (result.success) {
toast.success(result.message);
return;
}
if (result.success) {
toast.success(result.message);
return;
}
toast.error(result.error);
throw new Error(result.error);
}, [cardToRemove]);
toast.error(result.error);
throw new Error(result.error);
}, [cardToRemove]);
const removeTitle = cardToRemove
? `Remover cartão "${cardToRemove.name}"?`
: "Remover cartão?";
const removeTitle = cardToRemove
? `Remover cartão "${cardToRemove.name}"?`
: "Remover cartão?";
return (
<>
<div className="flex w-full flex-col gap-6">
{!isInativos && (
<div className="flex justify-start">
<CardDialog
mode="create"
accounts={accounts}
logoOptions={logoOptions}
trigger={
<Button>
<RiAddCircleLine className="size-4" />
Novo cartão
</Button>
}
/>
</div>
)}
return (
<>
<div className="flex w-full flex-col gap-6">
{!isInativos && (
<div className="flex justify-start">
<CardDialog
mode="create"
accounts={accounts}
logoOptions={logoOptions}
trigger={
<Button>
<RiAddCircleLine className="size-4" />
Novo cartão
</Button>
}
/>
</div>
)}
{hasCards ? (
<div className="flex flex-wrap gap-4">
{orderedCards.map((card) => (
<CardItem
key={card.id}
name={card.name}
brand={card.brand}
status={card.status}
closingDay={card.closingDay}
dueDay={card.dueDay}
limit={card.limit}
limitInUse={card.limitInUse ?? null}
limitAvailable={card.limitAvailable ?? card.limit ?? null}
contaName={card.contaName}
logo={card.logo}
note={card.note}
onEdit={() => handleEdit(card)}
onInvoice={() => handleInvoice(card)}
onRemove={() => handleRemoveRequest(card)}
/>
))}
</div>
) : (
<Card className="flex w-full items-center justify-center py-12">
<EmptyState
media={<RiBankCard2Line className="size-6 text-primary" />}
title={
isInativos
? "Nenhum cartão inativo"
: "Nenhum cartão cadastrado"
}
description={
isInativos
? "Os cartões inativos aparecerão aqui."
: "Adicione seu primeiro cartão para acompanhar limites e faturas com mais controle."
}
/>
</Card>
)}
</div>
{hasCards ? (
<div className="flex flex-wrap gap-4">
{orderedCards.map((card) => (
<CardItem
key={card.id}
name={card.name}
brand={card.brand}
status={card.status}
closingDay={card.closingDay}
dueDay={card.dueDay}
limit={card.limit}
limitInUse={card.limitInUse ?? null}
limitAvailable={card.limitAvailable ?? card.limit ?? null}
contaName={card.contaName}
logo={card.logo}
note={card.note}
onEdit={() => handleEdit(card)}
onInvoice={() => handleInvoice(card)}
onRemove={() => handleRemoveRequest(card)}
/>
))}
</div>
) : (
<Card className="flex w-full items-center justify-center py-12">
<EmptyState
media={<RiBankCard2Line className="size-6 text-primary" />}
title={
isInativos
? "Nenhum cartão inativo"
: "Nenhum cartão cadastrado"
}
description={
isInativos
? "Os cartões inativos aparecerão aqui."
: "Adicione seu primeiro cartão para acompanhar limites e faturas com mais controle."
}
/>
</Card>
)}
</div>
<CardDialog
mode="update"
accounts={accounts}
logoOptions={logoOptions}
card={selectedCard ?? undefined}
open={editOpen && !!selectedCard}
onOpenChange={handleEditOpenChange}
/>
<CardDialog
mode="update"
accounts={accounts}
logoOptions={logoOptions}
card={selectedCard ?? undefined}
open={editOpen && !!selectedCard}
onOpenChange={handleEditOpenChange}
/>
<ConfirmActionDialog
open={removeOpen && !!cardToRemove}
onOpenChange={handleRemoveOpenChange}
title={removeTitle}
description="Ao remover este cartão, os registros relacionados a ele serão excluídos permanentemente."
confirmLabel="Remover cartão"
pendingLabel="Removendo..."
confirmVariant="destructive"
onConfirm={handleRemoveConfirm}
/>
</>
);
<ConfirmActionDialog
open={removeOpen && !!cardToRemove}
onOpenChange={handleRemoveOpenChange}
title={removeTitle}
description="Ao remover este cartão, os registros relacionados a ele serão excluídos permanentemente."
confirmLabel="Remover cartão"
pendingLabel="Removendo..."
confirmVariant="destructive"
onConfirm={handleRemoveConfirm}
/>
</>
);
}

View File

@@ -3,5 +3,5 @@ export const DEFAULT_CARD_BRANDS = ["Visa", "Mastercard", "Elo"] as const;
export const DEFAULT_CARD_STATUS = ["Ativo", "Inativo"] as const;
export const DAYS_IN_MONTH = Array.from({ length: 31 }, (_, index) =>
String(index + 1).padStart(2, "0")
String(index + 1).padStart(2, "0"),
);

View File

@@ -1,27 +1,27 @@
export type Card = {
id: string;
name: string;
brand: string;
status: string;
closingDay: string;
dueDay: string;
note: string | null;
logo: string | null;
limit: number | null;
contaId: string;
contaName: string;
limitInUse?: number | null;
limitAvailable?: number | null;
id: string;
name: string;
brand: string;
status: string;
closingDay: string;
dueDay: string;
note: string | null;
logo: string | null;
limit: number | null;
contaId: string;
contaName: string;
limitInUse?: number | null;
limitAvailable?: number | null;
};
export type CardFormValues = {
name: string;
brand: string;
status: string;
closingDay: string;
dueDay: string;
limit: string;
note: string;
logo: string;
contaId: string;
name: string;
brand: string;
status: string;
closingDay: string;
dueDay: string;
limit: string;
note: string;
logo: string;
contaId: string;
};