Files
openmonetis/components/lancamentos/shared/anticipation-card.tsx
Felipe Coutinho ea0b8618e0 feat: adição de novos ícones SVG e configuração do ambiente
- Adicionados ícones SVG para ChatGPT, Claude, Gemini e OpenRouter
- Implementados ícones para modos claro e escuro do ChatGPT
- Criado script de inicialização para PostgreSQL com extensão pgcrypto
- Adicionado script de configuração de ambiente que faz backup do .env
- Configurado tsconfig.json para TypeScript com opções de compilação
2025-11-15 15:49:36 -03:00

206 lines
6.6 KiB
TypeScript

"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 { 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 MoneyValues from "@/components/money-values";
interface AnticipationCardProps {
anticipation: InstallmentAnticipationWithRelations;
onViewLancamento?: (lancamentoId: string) => void;
onCanceled?: () => void;
}
const monthFormatter = new Intl.DateTimeFormat("pt-BR", {
month: "long",
year: "numeric",
});
const formatPeriodLabel = (period: string) => {
const [year, month] = period.split("-").map(Number);
if (!year || !month) {
return period;
}
const date = new Date(year, month - 1, 1);
if (Number.isNaN(date.getTime())) {
return period;
}
const label = monthFormatter.format(date);
return label.charAt(0).toUpperCase() + label.slice(1);
};
export function AnticipationCard({
anticipation,
onViewLancamento,
onCanceled,
}: AnticipationCardProps) {
const [isPending, startTransition] = useTransition();
const isSettled = anticipation.lancamento.isSettled === true;
const canCancel = !isSettled;
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,
});
if (result.success) {
toast.success(result.message);
onCanceled?.();
} else {
toast.error(result.error || "Erro ao cancelar antecipação");
}
});
};
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">
{formatPeriodLabel(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>
{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>
<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.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>
<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}
/>
)}
{isSettled && (
<div className="text-xs text-muted-foreground">
Não é possível cancelar uma antecipação paga
</div>
)}
</CardFooter>
</Card>
);
}