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,188 +1,198 @@
"use client";
import { cancelInstallmentAnticipationAction } from "@/app/(dashboard)/lancamentos/anticipation-actions";
import { ConfirmActionDialog } from "@/components/confirm-action-dialog";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import type { InstallmentAnticipationWithRelations } from "@/lib/installments/anticipation-types";
import { displayPeriod } from "@/lib/utils/period";
import { RiCalendarCheckLine, RiCloseLine, RiEyeLine } from "@remixicon/react";
import { format } from "date-fns";
import { ptBR } from "date-fns/locale";
import { useTransition } from "react";
import { toast } from "sonner";
import { cancelInstallmentAnticipationAction } from "@/app/(dashboard)/lancamentos/anticipation-actions";
import { ConfirmActionDialog } from "@/components/confirm-action-dialog";
import MoneyValues from "@/components/money-values";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import type { InstallmentAnticipationWithRelations } from "@/lib/installments/anticipation-types";
import { displayPeriod } from "@/lib/utils/period";
interface AnticipationCardProps {
anticipation: InstallmentAnticipationWithRelations;
onViewLancamento?: (lancamentoId: string) => void;
onCanceled?: () => void;
anticipation: InstallmentAnticipationWithRelations;
onViewLancamento?: (lancamentoId: string) => void;
onCanceled?: () => void;
}
export function AnticipationCard({
anticipation,
onViewLancamento,
onCanceled,
anticipation,
onViewLancamento,
onCanceled,
}: AnticipationCardProps) {
const [isPending, startTransition] = useTransition();
const [isPending, startTransition] = useTransition();
const isSettled = anticipation.lancamento.isSettled === true;
const canCancel = !isSettled;
const isSettled = anticipation.lancamento.isSettled === true;
const canCancel = !isSettled;
const formatDate = (date: Date) => {
return format(date, "dd 'de' MMMM 'de' yyyy", { locale: ptBR });
};
const formatDate = (date: Date) => {
return format(date, "dd 'de' MMMM 'de' yyyy", { locale: ptBR });
};
const handleCancel = async () => {
startTransition(async () => {
const result = await cancelInstallmentAnticipationAction({
anticipationId: anticipation.id,
});
const handleCancel = async () => {
startTransition(async () => {
const result = await cancelInstallmentAnticipationAction({
anticipationId: anticipation.id,
});
if (result.success) {
toast.success(result.message);
onCanceled?.();
} else {
toast.error(result.error || "Erro ao cancelar antecipação");
}
});
};
if (result.success) {
toast.success(result.message);
onCanceled?.();
} else {
toast.error(result.error || "Erro ao cancelar antecipação");
}
});
};
const handleViewLancamento = () => {
onViewLancamento?.(anticipation.lancamentoId);
};
const handleViewLancamento = () => {
onViewLancamento?.(anticipation.lancamentoId);
};
return (
<Card>
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-3">
<div className="space-y-1">
<CardTitle className="text-base">
{anticipation.installmentCount}{" "}
{anticipation.installmentCount === 1
? "parcela antecipada"
: "parcelas antecipadas"}
</CardTitle>
<CardDescription>
<RiCalendarCheckLine className="mr-1 inline size-3.5" />
{formatDate(anticipation.anticipationDate)}
</CardDescription>
</div>
<Badge variant="secondary">
{displayPeriod(anticipation.anticipationPeriod)}
</Badge>
</CardHeader>
return (
<Card>
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-3">
<div className="space-y-1">
<CardTitle className="text-base">
{anticipation.installmentCount}{" "}
{anticipation.installmentCount === 1
? "parcela antecipada"
: "parcelas antecipadas"}
</CardTitle>
<CardDescription>
<RiCalendarCheckLine className="mr-1 inline size-3.5" />
{formatDate(anticipation.anticipationDate)}
</CardDescription>
</div>
<Badge variant="secondary">
{displayPeriod(anticipation.anticipationPeriod)}
</Badge>
</CardHeader>
<CardContent className="space-y-3">
<dl className="grid grid-cols-2 gap-3 text-sm">
<div>
<dt className="text-muted-foreground">Valor Original</dt>
<dd className="mt-1 font-medium tabular-nums">
<MoneyValues amount={Number(anticipation.totalAmount)} />
</dd>
</div>
<CardContent className="space-y-3">
<dl className="grid grid-cols-2 gap-3 text-sm">
<div>
<dt className="text-muted-foreground">Valor Original</dt>
<dd className="mt-1 font-medium tabular-nums">
<MoneyValues amount={Number(anticipation.totalAmount)} />
</dd>
</div>
{Number(anticipation.discount) > 0 && (
<div>
<dt className="text-muted-foreground">Desconto</dt>
<dd className="mt-1 font-medium tabular-nums text-green-600">
- <MoneyValues amount={Number(anticipation.discount)} />
</dd>
</div>
)}
{Number(anticipation.discount) > 0 && (
<div>
<dt className="text-muted-foreground">Desconto</dt>
<dd className="mt-1 font-medium tabular-nums text-green-600">
- <MoneyValues amount={Number(anticipation.discount)} />
</dd>
</div>
)}
<div className={Number(anticipation.discount) > 0 ? "col-span-2 border-t pt-3" : ""}>
<dt className="text-muted-foreground">
{Number(anticipation.discount) > 0 ? "Valor Final" : "Valor Total"}
</dt>
<dd className="mt-1 text-lg font-semibold tabular-nums text-primary">
<MoneyValues
amount={
Number(anticipation.totalAmount) < 0
? Number(anticipation.totalAmount) + Number(anticipation.discount)
: Number(anticipation.totalAmount) - Number(anticipation.discount)
}
/>
</dd>
</div>
<div
className={
Number(anticipation.discount) > 0
? "col-span-2 border-t pt-3"
: ""
}
>
<dt className="text-muted-foreground">
{Number(anticipation.discount) > 0
? "Valor Final"
: "Valor Total"}
</dt>
<dd className="mt-1 text-lg font-semibold tabular-nums text-primary">
<MoneyValues
amount={
Number(anticipation.totalAmount) < 0
? Number(anticipation.totalAmount) +
Number(anticipation.discount)
: Number(anticipation.totalAmount) -
Number(anticipation.discount)
}
/>
</dd>
</div>
<div>
<dt className="text-muted-foreground">Status do Lançamento</dt>
<dd className="mt-1">
<Badge variant={isSettled ? "success" : "outline"}>
{isSettled ? "Pago" : "Pendente"}
</Badge>
</dd>
</div>
<div>
<dt className="text-muted-foreground">Status do Lançamento</dt>
<dd className="mt-1">
<Badge variant={isSettled ? "success" : "outline"}>
{isSettled ? "Pago" : "Pendente"}
</Badge>
</dd>
</div>
{anticipation.pagador && (
<div>
<dt className="text-muted-foreground">Pagador</dt>
<dd className="mt-1 font-medium">{anticipation.pagador.name}</dd>
</div>
)}
{anticipation.pagador && (
<div>
<dt className="text-muted-foreground">Pagador</dt>
<dd className="mt-1 font-medium">{anticipation.pagador.name}</dd>
</div>
)}
{anticipation.categoria && (
<div>
<dt className="text-muted-foreground">Categoria</dt>
<dd className="mt-1 font-medium">
{anticipation.categoria.name}
</dd>
</div>
)}
</dl>
{anticipation.categoria && (
<div>
<dt className="text-muted-foreground">Categoria</dt>
<dd className="mt-1 font-medium">
{anticipation.categoria.name}
</dd>
</div>
)}
</dl>
{anticipation.note && (
<div className="rounded-lg border bg-muted/20 p-3">
<dt className="text-xs font-medium text-muted-foreground">
Observação
</dt>
<dd className="mt-1 text-sm">{anticipation.note}</dd>
</div>
)}
</CardContent>
{anticipation.note && (
<div className="rounded-lg border bg-muted/20 p-3">
<dt className="text-xs font-medium text-muted-foreground">
Observação
</dt>
<dd className="mt-1 text-sm">{anticipation.note}</dd>
</div>
)}
</CardContent>
<CardFooter className="flex flex-wrap items-center justify-between gap-2 border-t pt-4">
<Button
variant="outline"
size="sm"
onClick={handleViewLancamento}
disabled={isPending}
>
<RiEyeLine className="mr-2 size-4" />
Ver Lançamento
</Button>
<CardFooter className="flex flex-wrap items-center justify-between gap-2 border-t pt-4">
<Button
variant="outline"
size="sm"
onClick={handleViewLancamento}
disabled={isPending}
>
<RiEyeLine className="mr-2 size-4" />
Ver Lançamento
</Button>
{canCancel && (
<ConfirmActionDialog
trigger={
<Button variant="destructive" size="sm" disabled={isPending}>
<RiCloseLine className="mr-2 size-4" />
Cancelar Antecipação
</Button>
}
title="Cancelar antecipação?"
description="Esta ação irá reverter a antecipação e restaurar as parcelas originais. O lançamento de antecipação será removido."
confirmLabel="Cancelar Antecipação"
confirmVariant="destructive"
pendingLabel="Cancelando..."
onConfirm={handleCancel}
/>
)}
{canCancel && (
<ConfirmActionDialog
trigger={
<Button variant="destructive" size="sm" disabled={isPending}>
<RiCloseLine className="mr-2 size-4" />
Cancelar Antecipação
</Button>
}
title="Cancelar antecipação?"
description="Esta ação irá reverter a antecipação e restaurar as parcelas originais. O lançamento de antecipação será removido."
confirmLabel="Cancelar Antecipação"
confirmVariant="destructive"
pendingLabel="Cancelando..."
onConfirm={handleCancel}
/>
)}
{isSettled && (
<div className="text-xs text-muted-foreground">
Não é possível cancelar uma antecipação paga
</div>
)}
</CardFooter>
</Card>
);
{isSettled && (
<div className="text-xs text-muted-foreground">
Não é possível cancelar uma antecipação paga
</div>
)}
</CardFooter>
</Card>
);
}

View File

@@ -4,126 +4,126 @@ import { RiCheckLine, RiSearchLine } from "@remixicon/react";
import * as React from "react";
import {
Command,
CommandEmpty,
CommandGroup,
CommandItem,
CommandList,
Command,
CommandEmpty,
CommandGroup,
CommandItem,
CommandList,
} from "@/components/ui/command";
import { Input } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils/ui";
export interface EstabelecimentoInputProps {
id?: string;
value: string;
onChange: (value: string) => void;
estabelecimentos: string[];
placeholder?: string;
required?: boolean;
maxLength?: number;
id?: string;
value: string;
onChange: (value: string) => void;
estabelecimentos: string[];
placeholder?: string;
required?: boolean;
maxLength?: number;
}
export function EstabelecimentoInput({
id,
value,
onChange,
estabelecimentos = [],
placeholder = "Ex.: Padaria",
required = false,
maxLength = 20,
id,
value,
onChange,
estabelecimentos = [],
placeholder = "Ex.: Padaria",
required = false,
maxLength = 20,
}: EstabelecimentoInputProps) {
const [open, setOpen] = React.useState(false);
const [searchValue, setSearchValue] = React.useState("");
const [open, setOpen] = React.useState(false);
const [searchValue, setSearchValue] = React.useState("");
const handleSelect = (selectedValue: string) => {
onChange(selectedValue);
setOpen(false);
setSearchValue("");
};
const handleSelect = (selectedValue: string) => {
onChange(selectedValue);
setOpen(false);
setSearchValue("");
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
onChange(newValue);
setSearchValue(newValue);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
onChange(newValue);
setSearchValue(newValue);
// Open popover when user types and there are suggestions
if (newValue.length > 0 && estabelecimentos.length > 0) {
setOpen(true);
}
};
// Open popover when user types and there are suggestions
if (newValue.length > 0 && estabelecimentos.length > 0) {
setOpen(true);
}
};
const filteredEstabelecimentos = React.useMemo(() => {
if (!searchValue) return estabelecimentos;
const filteredEstabelecimentos = React.useMemo(() => {
if (!searchValue) return estabelecimentos;
const lowerSearch = searchValue.toLowerCase();
return estabelecimentos.filter((item) =>
item.toLowerCase().includes(lowerSearch)
);
}, [estabelecimentos, searchValue]);
const lowerSearch = searchValue.toLowerCase();
return estabelecimentos.filter((item) =>
item.toLowerCase().includes(lowerSearch),
);
}, [estabelecimentos, searchValue]);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<div className="relative">
<Input
id={id}
value={value}
onChange={handleInputChange}
placeholder={placeholder}
required={required}
maxLength={maxLength}
autoComplete="off"
onFocus={() => {
if (estabelecimentos.length > 0) {
setOpen(true);
}
}}
/>
{estabelecimentos.length > 0 && (
<RiSearchLine className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground pointer-events-none" />
)}
</div>
</PopoverTrigger>
{estabelecimentos.length > 0 && (
<PopoverContent
className="p-0 w-[--radix-popover-trigger-width]"
align="start"
onOpenAutoFocus={(e) => e.preventDefault()}
>
<Command>
<CommandList className="max-h-[300px] overflow-y-auto">
<CommandEmpty className="p-6">
Nenhum estabelecimento encontrado.
</CommandEmpty>
<CommandGroup className="p-1">
{filteredEstabelecimentos.map((item) => (
<CommandItem
key={item}
value={item}
onSelect={() => handleSelect(item)}
className="cursor-pointer gap-1"
>
<RiCheckLine
className={cn(
"size-4 shrink-0",
value === item
? "opacity-100 text-green-500"
: "opacity-5"
)}
/>
<span className="truncate flex-1">{item}</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
)}
</Popover>
);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<div className="relative">
<Input
id={id}
value={value}
onChange={handleInputChange}
placeholder={placeholder}
required={required}
maxLength={maxLength}
autoComplete="off"
onFocus={() => {
if (estabelecimentos.length > 0) {
setOpen(true);
}
}}
/>
{estabelecimentos.length > 0 && (
<RiSearchLine className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground pointer-events-none" />
)}
</div>
</PopoverTrigger>
{estabelecimentos.length > 0 && (
<PopoverContent
className="p-0 w-[--radix-popover-trigger-width]"
align="start"
onOpenAutoFocus={(e) => e.preventDefault()}
>
<Command>
<CommandList className="max-h-[300px] overflow-y-auto">
<CommandEmpty className="p-6">
Nenhum estabelecimento encontrado.
</CommandEmpty>
<CommandGroup className="p-1">
{filteredEstabelecimentos.map((item) => (
<CommandItem
key={item}
value={item}
onSelect={() => handleSelect(item)}
className="cursor-pointer gap-1"
>
<RiCheckLine
className={cn(
"size-4 shrink-0",
value === item
? "opacity-100 text-green-500"
: "opacity-5",
)}
/>
<span className="truncate flex-1">{item}</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
)}
</Popover>
);
}

View File

@@ -3,70 +3,70 @@
import { cn } from "@/lib/utils/ui";
interface EstabelecimentoLogoProps {
name: string;
size?: number;
className?: string;
name: string;
size?: number;
className?: string;
}
const COLOR_PALETTE = [
"bg-purple-400 dark:bg-purple-600",
"bg-pink-400 dark:bg-pink-600",
"bg-red-400 dark:bg-red-600",
"bg-orange-400 dark:bg-orange-600",
"bg-indigo-400 dark:bg-indigo-600",
"bg-violet-400 dark:bg-violet-600",
"bg-fuchsia-400 dark:bg-fuchsia-600",
"bg-rose-400 dark:bg-rose-600",
"bg-amber-400 dark:bg-amber-600",
"bg-emerald-400 dark:bg-emerald-600",
"bg-purple-400 dark:bg-purple-600",
"bg-pink-400 dark:bg-pink-600",
"bg-red-400 dark:bg-red-600",
"bg-orange-400 dark:bg-orange-600",
"bg-indigo-400 dark:bg-indigo-600",
"bg-violet-400 dark:bg-violet-600",
"bg-fuchsia-400 dark:bg-fuchsia-600",
"bg-rose-400 dark:bg-rose-600",
"bg-amber-400 dark:bg-amber-600",
"bg-emerald-400 dark:bg-emerald-600",
];
function getInitials(name: string): string {
if (!name || !name.trim()) return "?";
if (!name || !name.trim()) return "?";
const words = name.trim().split(/\s+/);
const words = name.trim().split(/\s+/);
if (words.length === 1) {
return words[0]?.[0]?.toUpperCase() || "?";
}
if (words.length === 1) {
return words[0]?.[0]?.toUpperCase() || "?";
}
const firstInitial = words[0]?.[0]?.toUpperCase() || "";
const secondInitial = words[1]?.[0]?.toUpperCase() || "";
const firstInitial = words[0]?.[0]?.toUpperCase() || "";
const secondInitial = words[1]?.[0]?.toUpperCase() || "";
return `${firstInitial}${secondInitial}`;
return `${firstInitial}${secondInitial}`;
}
function generateColorFromName(name: string): string {
let hash = 0;
for (let i = 0; i < name.length; i++) {
hash = name.charCodeAt(i) + ((hash << 5) - hash);
}
const index = Math.abs(hash) % COLOR_PALETTE.length;
return COLOR_PALETTE[index] || "bg-gray-400";
let hash = 0;
for (let i = 0; i < name.length; i++) {
hash = name.charCodeAt(i) + ((hash << 5) - hash);
}
const index = Math.abs(hash) % COLOR_PALETTE.length;
return COLOR_PALETTE[index] || "bg-gray-400";
}
export function EstabelecimentoLogo({
name,
size = 32,
className,
name,
size = 32,
className,
}: EstabelecimentoLogoProps) {
const initials = getInitials(name);
const colorClass = generateColorFromName(name);
const initials = getInitials(name);
const colorClass = generateColorFromName(name);
return (
<div
className={cn(
"flex items-center justify-center rounded-md text-white font-medium shrink-0 ",
colorClass,
className
)}
style={{
width: size,
height: size,
fontSize: size * 0.4,
}}
>
{initials}
</div>
);
return (
<div
className={cn(
"flex items-center justify-center rounded-md text-white font-medium shrink-0 ",
colorClass,
className,
)}
style={{
width: size,
height: size,
fontSize: size * 0.4,
}}
>
{initials}
</div>
);
}

View File

@@ -1,4 +1,4 @@
export { AnticipationCard } from "./anticipation-card";
export { EstabelecimentoInput } from "./estabelecimento-input";
export { InstallmentTimeline } from "./installment-timeline";
export { EstabelecimentoLogo } from "./estabelecimento-logo";
export { InstallmentTimeline } from "./installment-timeline";

View File

@@ -1,92 +1,92 @@
import {
calculateLastInstallmentDate,
formatCurrentInstallment,
formatLastInstallmentDate,
formatPurchaseDate,
} from "@/lib/installments/utils";
import { RiArrowDownFill, RiCheckLine } from "@remixicon/react";
import {
calculateLastInstallmentDate,
formatCurrentInstallment,
formatLastInstallmentDate,
formatPurchaseDate,
} from "@/lib/installments/utils";
type InstallmentTimelineProps = {
purchaseDate: Date;
currentInstallment: number;
totalInstallments: number;
period: string;
purchaseDate: Date;
currentInstallment: number;
totalInstallments: number;
period: string;
};
export function InstallmentTimeline({
purchaseDate,
currentInstallment,
totalInstallments,
period,
purchaseDate,
currentInstallment,
totalInstallments,
period,
}: InstallmentTimelineProps) {
const lastInstallmentDate = calculateLastInstallmentDate(
period,
currentInstallment,
totalInstallments,
);
const lastInstallmentDate = calculateLastInstallmentDate(
period,
currentInstallment,
totalInstallments,
);
return (
<div className="relative flex items-center justify-between px-4 py-4">
{/* Linha de conexão */}
<div className="absolute left-0 right-0 top-6 h-0.5 bg-border">
<div
className="h-full bg-green-600 transition-all duration-300"
style={{
width: `${
((currentInstallment - 1) / (totalInstallments - 1)) * 100
}%`,
}}
/>
</div>
return (
<div className="relative flex items-center justify-between px-4 py-4">
{/* Linha de conexão */}
<div className="absolute left-0 right-0 top-6 h-0.5 bg-border">
<div
className="h-full bg-green-600 transition-all duration-300"
style={{
width: `${
((currentInstallment - 1) / (totalInstallments - 1)) * 100
}%`,
}}
/>
</div>
{/* Ponto 1: Data de Compra */}
<div className="relative z-10 flex flex-col items-center gap-2">
<div className="flex size-4 items-center justify-center rounded-full border-2 border-green-600 bg-green-600 shadow-sm">
<RiCheckLine className="size-5 text-white" />
</div>
<div className="flex flex-col items-center">
<span className="text-xs font-medium text-foreground">
Data de Compra
</span>
<span className="text-xs text-muted-foreground">
{formatPurchaseDate(purchaseDate)}
</span>
</div>
</div>
{/* Ponto 1: Data de Compra */}
<div className="relative z-10 flex flex-col items-center gap-2">
<div className="flex size-4 items-center justify-center rounded-full border-2 border-green-600 bg-green-600 shadow-sm">
<RiCheckLine className="size-5 text-white" />
</div>
<div className="flex flex-col items-center">
<span className="text-xs font-medium text-foreground">
Data de Compra
</span>
<span className="text-xs text-muted-foreground">
{formatPurchaseDate(purchaseDate)}
</span>
</div>
</div>
{/* Ponto 2: Parcela Atual */}
<div className="relative z-10 flex flex-col items-center gap-2">
<div
className={`flex size-4 items-center justify-center rounded-full border-2 shadow-sm border-orange-600 bg-orange-600`}
>
<RiArrowDownFill className="size-5 text-white" />
</div>
<div className="flex flex-col items-center">
<span className="text-xs font-medium text-foreground">
Parcela Atual
</span>
<span className="text-xs text-muted-foreground">
{formatCurrentInstallment(currentInstallment, totalInstallments)}
</span>
</div>
</div>
{/* Ponto 2: Parcela Atual */}
<div className="relative z-10 flex flex-col items-center gap-2">
<div
className={`flex size-4 items-center justify-center rounded-full border-2 shadow-sm border-orange-600 bg-orange-600`}
>
<RiArrowDownFill className="size-5 text-white" />
</div>
<div className="flex flex-col items-center">
<span className="text-xs font-medium text-foreground">
Parcela Atual
</span>
<span className="text-xs text-muted-foreground">
{formatCurrentInstallment(currentInstallment, totalInstallments)}
</span>
</div>
</div>
{/* Ponto 3: Última Parcela */}
<div className="relative z-10 flex flex-col items-center gap-2">
<div
className={`flex size-4 items-center justify-center rounded-full border-2 shadow-sm border-green-600 bg-green-600`}
>
<RiCheckLine className="size-5 text-white" />
</div>
<div className="flex flex-col items-center">
<span className="text-xs font-medium text-foreground">
Última Parcela
</span>
<span className="text-xs text-muted-foreground">
{formatLastInstallmentDate(lastInstallmentDate)}
</span>
</div>
</div>
</div>
);
{/* Ponto 3: Última Parcela */}
<div className="relative z-10 flex flex-col items-center gap-2">
<div
className={`flex size-4 items-center justify-center rounded-full border-2 shadow-sm border-green-600 bg-green-600`}
>
<RiCheckLine className="size-5 text-white" />
</div>
<div className="flex flex-col items-center">
<span className="text-xs font-medium text-foreground">
Última Parcela
</span>
<span className="text-xs text-muted-foreground">
{formatLastInstallmentDate(lastInstallmentDate)}
</span>
</div>
</div>
</div>
);
}