mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-06-09 23:06:01 +00:00
368 lines
11 KiB
TypeScript
368 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import Image from "next/image";
|
|
import {
|
|
type ComponentType,
|
|
type CSSProperties,
|
|
useEffect,
|
|
useState,
|
|
} from "react";
|
|
import {
|
|
currencyFormatter,
|
|
formatCondition,
|
|
formatPeriod,
|
|
} from "@/features/transactions/lib/formatting-helpers";
|
|
import { EstablishmentLogo } from "@/shared/components/entity-avatar";
|
|
import { TransactionTypeBadge } from "@/shared/components/transaction-type-badge";
|
|
import {
|
|
Avatar,
|
|
AvatarFallback,
|
|
AvatarImage,
|
|
} from "@/shared/components/ui/avatar";
|
|
import { Badge } from "@/shared/components/ui/badge";
|
|
import { Button } from "@/shared/components/ui/button";
|
|
import {
|
|
Dialog,
|
|
DialogClose,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/shared/components/ui/dialog";
|
|
import { Separator } from "@/shared/components/ui/separator";
|
|
import { resolveLogoSrc } from "@/shared/lib/logo";
|
|
import { getAvatarSrc } from "@/shared/lib/payers/utils";
|
|
import { getCategoryColorFromName } from "@/shared/utils/category-colors";
|
|
import { formatDate, parseLocalDateString } from "@/shared/utils/date";
|
|
import { getIconComponent, getPaymentMethodIcon } from "@/shared/utils/icons";
|
|
import { AttachmentSection } from "../attachments/attachment-section";
|
|
import { InstallmentTimeline } from "../shared/installment-timeline";
|
|
import type { TransactionItem } from "../types";
|
|
|
|
interface TransactionDetailsDialogProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
transaction: TransactionItem | null;
|
|
onEdit?: (transaction: TransactionItem) => void;
|
|
}
|
|
|
|
export function TransactionDetailsDialog({
|
|
open,
|
|
onOpenChange,
|
|
transaction,
|
|
onEdit,
|
|
}: TransactionDetailsDialogProps) {
|
|
const [attachmentCount, setAttachmentCount] = useState<number | null>(null);
|
|
|
|
useEffect(() => {
|
|
setAttachmentCount(null);
|
|
}, []);
|
|
|
|
if (!transaction) return null;
|
|
|
|
const isInstallment =
|
|
transaction.condition?.toLowerCase() === "parcelado" &&
|
|
transaction.currentInstallment &&
|
|
transaction.installmentCount;
|
|
|
|
const valorParcela = Math.abs(transaction.amount);
|
|
const totalParcelas = transaction.installmentCount ?? 1;
|
|
const parcelaAtual = transaction.currentInstallment ?? 1;
|
|
const valorTotal = isInstallment
|
|
? valorParcela * totalParcelas
|
|
: valorParcela;
|
|
const valorRestante = isInstallment
|
|
? valorParcela * (totalParcelas - parcelaAtual)
|
|
: 0;
|
|
|
|
const isBoleto = transaction.paymentMethod === "Boleto";
|
|
|
|
const handleEdit = () => {
|
|
onOpenChange(false);
|
|
onEdit?.(transaction);
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="min-w-0 overflow-x-hidden sm:max-w-xl">
|
|
<DialogHeader className="text-left">
|
|
<div className="flex min-w-0 items-start gap-2">
|
|
<EstablishmentLogo size={40} name={transaction.name} />
|
|
<div className="min-w-0">
|
|
<DialogTitle className="truncate">{transaction.name}</DialogTitle>
|
|
<DialogDescription className="mt-1">
|
|
{formatDate(transaction.purchaseDate)}
|
|
</DialogDescription>
|
|
</div>
|
|
</div>
|
|
</DialogHeader>
|
|
|
|
<div className="min-w-0 max-h-[60vh] overflow-x-hidden overflow-y-auto text-sm">
|
|
<div className="min-w-0 space-y-4">
|
|
<section className="rounded-lg border p-3">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="min-w-0">
|
|
<p className="text-xs uppercase tracking-wide text-muted-foreground">
|
|
Total
|
|
</p>
|
|
<p className="mt-1 text-2xl font-semibold">
|
|
{currencyFormatter.format(valorTotal)}
|
|
</p>
|
|
</div>
|
|
<Badge
|
|
variant={transaction.isSettled ? "secondary" : "info"}
|
|
className={
|
|
transaction.isSettled
|
|
? "text-success bg-success/10"
|
|
: undefined
|
|
}
|
|
>
|
|
{transaction.isSettled ? "Pago" : "Em aberto"}
|
|
</Badge>
|
|
</div>
|
|
<div className="mt-3 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
|
<TransactionTypeBadge
|
|
kind={
|
|
transaction.categoriaName === "Saldo inicial"
|
|
? "Saldo inicial"
|
|
: transaction.transactionType
|
|
}
|
|
/>
|
|
<span>{formatCondition(transaction.condition)}</span>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="space-y-2">
|
|
<h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
Detalhes
|
|
</h3>
|
|
<ul className="min-w-0 grid gap-2 rounded-lg border p-3">
|
|
<DetailRow
|
|
label="ID"
|
|
value={transaction.id}
|
|
valueClassName="font-mono"
|
|
/>
|
|
|
|
<DetailRow
|
|
label="Período"
|
|
value={formatPeriod(transaction.period)}
|
|
/>
|
|
|
|
<li className="flex items-center justify-between">
|
|
<span className="text-muted-foreground">
|
|
Forma de Pagamento
|
|
</span>
|
|
<span className="flex items-center gap-1.5">
|
|
{getPaymentMethodIcon(transaction.paymentMethod)}
|
|
<span>{transaction.paymentMethod}</span>
|
|
</span>
|
|
</li>
|
|
|
|
<li className="min-w-0 flex items-center justify-between gap-3">
|
|
<span className="text-muted-foreground">
|
|
{transaction.cartaoName ? "Cartão" : "Conta"}
|
|
</span>
|
|
{(() => {
|
|
const accountLabel =
|
|
transaction.cartaoName ?? transaction.contaName;
|
|
if (!accountLabel) {
|
|
return <span className="min-w-0 truncate">—</span>;
|
|
}
|
|
const logoSrc = resolveLogoSrc(
|
|
transaction.cartaoLogo ?? transaction.contaLogo,
|
|
);
|
|
return (
|
|
<span className="inline-flex min-w-0 items-center gap-2">
|
|
{logoSrc && (
|
|
<Image
|
|
src={logoSrc}
|
|
alt={`Logo de ${accountLabel}`}
|
|
width={20}
|
|
height={20}
|
|
className="shrink-0 rounded-full"
|
|
/>
|
|
)}
|
|
<span className="min-w-0 truncate">{accountLabel}</span>
|
|
</span>
|
|
);
|
|
})()}
|
|
</li>
|
|
|
|
<li className="min-w-0 flex items-center justify-between gap-3">
|
|
<span className="text-muted-foreground">Categoria</span>
|
|
{(() => {
|
|
if (!transaction.categoriaName) {
|
|
return <span className="min-w-0 truncate">—</span>;
|
|
}
|
|
const IconComponent = transaction.categoriaIcon
|
|
? (getIconComponent(
|
|
transaction.categoriaIcon,
|
|
) as ComponentType<{
|
|
className?: string;
|
|
style?: CSSProperties;
|
|
}> | null)
|
|
: null;
|
|
const color = getCategoryColorFromName(
|
|
transaction.categoriaName,
|
|
);
|
|
return (
|
|
<span className="inline-flex min-w-0 items-center gap-1.5">
|
|
{IconComponent ? (
|
|
<IconComponent
|
|
className="size-3.5 shrink-0"
|
|
style={{ color }}
|
|
/>
|
|
) : null}
|
|
<span className="min-w-0 truncate">
|
|
{transaction.categoriaName}
|
|
</span>
|
|
</span>
|
|
);
|
|
})()}
|
|
</li>
|
|
|
|
<li className="min-w-0 flex items-center justify-between gap-3">
|
|
<span className="text-muted-foreground">Responsável</span>
|
|
{(() => {
|
|
const label = transaction.pagadorName?.trim() || "—";
|
|
if (label === "—") {
|
|
return <span className="min-w-0 truncate">—</span>;
|
|
}
|
|
const displayName = label.split(/\s+/)[0] ?? label;
|
|
const avatarSrc = getAvatarSrc(transaction.pagadorAvatar);
|
|
const initial = displayName.charAt(0).toUpperCase() || "?";
|
|
return (
|
|
<span className="inline-flex min-w-0 items-center gap-2">
|
|
<Avatar className="size-5">
|
|
<AvatarImage
|
|
src={avatarSrc}
|
|
alt={`Avatar de ${label}`}
|
|
/>
|
|
<AvatarFallback className="text-[10px] font-medium uppercase">
|
|
{initial}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<span className="min-w-0 truncate">{label}</span>
|
|
</span>
|
|
);
|
|
})()}
|
|
</li>
|
|
|
|
{isBoleto && transaction.dueDate && (
|
|
<DetailRow
|
|
label="Vencimento"
|
|
value={formatDate(transaction.dueDate)}
|
|
/>
|
|
)}
|
|
|
|
{transaction.isDivided && (
|
|
<li className="flex items-center justify-between">
|
|
<span className="text-muted-foreground">Divisão</span>
|
|
<Badge variant="outline">Dividido</Badge>
|
|
</li>
|
|
)}
|
|
</ul>
|
|
</section>
|
|
|
|
<section className="space-y-2">
|
|
<h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
Valores
|
|
</h3>
|
|
<ul className="min-w-0 grid gap-2 rounded-lg border p-3">
|
|
{isInstallment && (
|
|
<li className="mb-1">
|
|
<InstallmentTimeline
|
|
purchaseDate={parseLocalDateString(
|
|
transaction.purchaseDate,
|
|
)}
|
|
currentInstallment={parcelaAtual}
|
|
totalInstallments={totalParcelas}
|
|
period={transaction.period}
|
|
/>
|
|
</li>
|
|
)}
|
|
|
|
<DetailRow
|
|
label={isInstallment ? "Valor da Parcela" : "Valor"}
|
|
value={currencyFormatter.format(valorParcela)}
|
|
/>
|
|
|
|
{isInstallment && (
|
|
<DetailRow
|
|
label="Valor Restante"
|
|
value={currencyFormatter.format(valorRestante)}
|
|
/>
|
|
)}
|
|
|
|
{transaction.recurrenceCount && (
|
|
<DetailRow
|
|
label="Quantidade de Recorrências"
|
|
value={`${transaction.recurrenceCount} meses`}
|
|
/>
|
|
)}
|
|
</ul>
|
|
</section>
|
|
|
|
{transaction.note ? (
|
|
<section className="space-y-2">
|
|
<h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
Notas
|
|
</h3>
|
|
<div className="rounded-lg border p-3 text-foreground">
|
|
{transaction.note}
|
|
</div>
|
|
</section>
|
|
) : null}
|
|
|
|
{attachmentCount !== 0 && (
|
|
<section className="space-y-2">
|
|
<h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
Anexos
|
|
</h3>
|
|
<div className="min-w-0">
|
|
<AttachmentSection
|
|
transactionId={transaction.id}
|
|
readonly
|
|
onLoaded={setAttachmentCount}
|
|
/>
|
|
</div>
|
|
</section>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<DialogFooter>
|
|
<DialogClose asChild>
|
|
<Button type="button" variant="outline">
|
|
Fechar
|
|
</Button>
|
|
</DialogClose>
|
|
{onEdit && !transaction.readonly && (
|
|
<Button onClick={handleEdit}>Alterar</Button>
|
|
)}
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
interface DetailRowProps {
|
|
label: string;
|
|
value: string;
|
|
valueClassName?: string;
|
|
}
|
|
|
|
function DetailRow({ label, value, valueClassName }: DetailRowProps) {
|
|
return (
|
|
<li className="min-w-0 flex items-center justify-between gap-3">
|
|
<span className="text-muted-foreground">{label}</span>
|
|
<span className={`min-w-0 truncate ${valueClassName ?? ""}`}>
|
|
{value}
|
|
</span>
|
|
</li>
|
|
);
|
|
}
|