mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-06-09 23:06:01 +00:00
feat(lancamentos): aprimora antecipacao de parcelas
This commit is contained in:
@@ -3,19 +3,13 @@
|
||||
import { RiCalendarCheckLine } 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 "@/features/transactions/actions/anticipation";
|
||||
import type { ReactNode } from "react";
|
||||
import type { InstallmentAnticipationListItem } from "@/features/transactions/hooks/use-installment-anticipations";
|
||||
import { ConfirmActionDialog } from "@/shared/components/confirm-action-dialog";
|
||||
import MoneyValues from "@/shared/components/money-values";
|
||||
import { Badge } from "@/shared/components/ui/badge";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/shared/components/ui/card";
|
||||
@@ -23,172 +17,129 @@ import { displayPeriod } from "@/shared/utils/period";
|
||||
|
||||
interface AnticipationCardProps {
|
||||
anticipation: InstallmentAnticipationListItem;
|
||||
onViewLancamento?: (transactionId: string) => void;
|
||||
onCanceled?: () => void;
|
||||
}
|
||||
|
||||
export function AnticipationCard({
|
||||
anticipation,
|
||||
onViewLancamento,
|
||||
onCanceled,
|
||||
}: AnticipationCardProps) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
export function AnticipationCard({ anticipation }: AnticipationCardProps) {
|
||||
const isSettled = anticipation.transaction?.isSettled === true;
|
||||
const canCancel = !isSettled;
|
||||
const totalAmount = Number(anticipation.totalAmount);
|
||||
const discount = Number(anticipation.discount);
|
||||
|
||||
const finalAmount =
|
||||
totalAmount < 0 ? totalAmount + discount : totalAmount - discount;
|
||||
|
||||
const hasDiscount = discount > 0;
|
||||
|
||||
const formatDate = (date: string) => {
|
||||
return format(new Date(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");
|
||||
}
|
||||
return format(new Date(date), "dd 'de' MMMM 'de' yyyy", {
|
||||
locale: ptBR,
|
||||
});
|
||||
};
|
||||
|
||||
const handleViewLancamento = () => {
|
||||
onViewLancamento?.(anticipation.transactionId);
|
||||
};
|
||||
|
||||
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>
|
||||
<Card className="shadow-none py-2">
|
||||
<CardHeader className="space-y-3 p-4 pb-1">
|
||||
<div className="flex min-w-0 items-start justify-between gap-3">
|
||||
<div className="min-w-0 space-y-1">
|
||||
<CardTitle className="text-base leading-none">
|
||||
{anticipation.installmentCount}{" "}
|
||||
{anticipation.installmentCount === 1
|
||||
? "parcela antecipada"
|
||||
: "parcelas antecipadas"}
|
||||
</CardTitle>
|
||||
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<RiCalendarCheckLine className="size-3 shrink-0" />
|
||||
<span>{formatDate(anticipation.anticipationDate)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Badge variant="secondary" className="shrink-0 rounded-full px-3">
|
||||
{displayPeriod(anticipation.anticipationPeriod)}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-3 rounded-lg bg-primary/10 p-3">
|
||||
<span className="text-xs font-medium text-foreground">
|
||||
{hasDiscount ? "Valor Final" : "Valor Total"}
|
||||
</span>
|
||||
|
||||
<span className="text-lg font-semibold leading-none text-primary">
|
||||
<MoneyValues amount={finalAmount} />
|
||||
</span>
|
||||
</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">
|
||||
<MoneyValues amount={Number(anticipation.totalAmount)} />
|
||||
</dd>
|
||||
</div>
|
||||
<CardContent className="px-4 pb-4 pt-0">
|
||||
<dl className="grid grid-cols-2 gap-x-6 gap-y-3 text-sm">
|
||||
<DetailItem label="Valor Original">
|
||||
<MoneyValues amount={totalAmount} />
|
||||
</DetailItem>
|
||||
|
||||
{Number(anticipation.discount) > 0 && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground">Desconto</dt>
|
||||
<dd className="mt-1 font-medium text-success">
|
||||
- <MoneyValues amount={Number(anticipation.discount)} />
|
||||
</dd>
|
||||
</div>
|
||||
{hasDiscount ? (
|
||||
<DetailItem label="Desconto" valueClassName="text-success">
|
||||
- <MoneyValues amount={discount} />
|
||||
</DetailItem>
|
||||
) : (
|
||||
<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 text-primary">
|
||||
<MoneyValues
|
||||
amount={
|
||||
Number(anticipation.totalAmount) < 0
|
||||
? Number(anticipation.totalAmount) +
|
||||
Number(anticipation.discount)
|
||||
: Number(anticipation.totalAmount) -
|
||||
Number(anticipation.discount)
|
||||
}
|
||||
/>
|
||||
</dd>
|
||||
</div>
|
||||
<DetailItem label="Status">
|
||||
<Badge
|
||||
variant={isSettled ? "success" : "outline"}
|
||||
className="h-5 rounded-full px-2 text-xs"
|
||||
>
|
||||
{isSettled ? "Pago" : "Pendente"}
|
||||
</Badge>
|
||||
</DetailItem>
|
||||
|
||||
<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.payer && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground">Pessoa</dt>
|
||||
<dd className="mt-1 font-medium">{anticipation.payer.name}</dd>
|
||||
</div>
|
||||
{anticipation.payer ? (
|
||||
<DetailItem label="Pessoa">{anticipation.payer.name}</DetailItem>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
|
||||
{anticipation.category && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground">Categoria</dt>
|
||||
<dd className="mt-1 font-medium">{anticipation.category.name}</dd>
|
||||
</div>
|
||||
)}
|
||||
{anticipation.category ? (
|
||||
<DetailItem label="Categoria">
|
||||
{anticipation.category.name}
|
||||
</DetailItem>
|
||||
) : null}
|
||||
</dl>
|
||||
|
||||
{anticipation.note && (
|
||||
<div className="rounded-lg border p-3">
|
||||
<dt className="text-xs font-medium text-muted-foreground">
|
||||
{anticipation.note ? (
|
||||
<div className="mt-3 border-t pt-3">
|
||||
<p className="text-xs font-medium text-muted-foreground">
|
||||
Observação
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm">{anticipation.note}</dd>
|
||||
</p>
|
||||
<p className="mt-1 text-sm leading-snug">{anticipation.note}</p>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</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}
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
|
||||
{canCancel && (
|
||||
<ConfirmActionDialog
|
||||
trigger={
|
||||
<Button variant="destructive" size="sm" disabled={isPending}>
|
||||
Desfazer 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>
|
||||
);
|
||||
}
|
||||
|
||||
function DetailItem({
|
||||
label,
|
||||
children,
|
||||
valueClassName,
|
||||
}: {
|
||||
label: string;
|
||||
children: ReactNode;
|
||||
valueClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="min-w-0 space-y-1">
|
||||
<dt className="text-xs font-medium leading-none text-muted-foreground">
|
||||
{label}
|
||||
</dt>
|
||||
|
||||
<dd
|
||||
className={`truncate text-sm font-medium leading-tight ${
|
||||
valueClassName ?? ""
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</dd>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user