feat(transactions): suporte a Ctrl+V em anexos e melhorias de UX no modal

This commit is contained in:
Felipe Coutinho
2026-05-14 19:13:26 +00:00
parent 86bcffec66
commit 246bb14a00
5 changed files with 181 additions and 50 deletions

View File

@@ -10,9 +10,9 @@ import {
import {
currencyFormatter,
formatCondition,
formatDate,
formatPeriod,
} from "@/features/transactions/lib/formatting-helpers";
import { EstablishmentLogo } from "@/shared/components/entity-avatar";
import { TransactionTypeBadge } from "@/shared/components/transaction-type-badge";
import {
Avatar,
@@ -34,7 +34,7 @@ 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 { parseLocalDateString } from "@/shared/utils/date";
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";
@@ -55,10 +55,9 @@ export function TransactionDetailsDialog({
}: TransactionDetailsDialogProps) {
const [attachmentCount, setAttachmentCount] = useState<number | null>(null);
// biome-ignore lint/correctness/useExhaustiveDependencies: transaction?.id é trigger intencional para reset do contador
useEffect(() => {
setAttachmentCount(null);
}, [transaction?.id]);
}, []);
if (!transaction) return null;
@@ -87,11 +86,16 @@ export function TransactionDetailsDialog({
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="min-w-0 overflow-x-hidden sm:max-w-xl">
<DialogHeader>
<DialogTitle>{transaction.name}</DialogTitle>
<DialogDescription>
{formatDate(transaction.purchaseDate)}
</DialogDescription>
<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">

View File

@@ -1,6 +1,6 @@
"use client";
import { RiArrowDropDownLine } from "@remixicon/react";
import { useEffect, useMemo, useState, useTransition } from "react";
import { useEffect, useMemo, useRef, useState, useTransition } from "react";
import { toast } from "sonner";
import {
createTransactionAction,
@@ -102,6 +102,8 @@ export function TransactionDialog({
const [pendingFiles, setPendingFiles] = useState<File[]>([]);
const [pendingDetachIds, setPendingDetachIds] = useState<string[]>([]);
const [pendingUploadFiles, setPendingUploadFiles] = useState<File[]>([]);
const [extrasOpen, setExtrasOpen] = useState(false);
const scrollContainerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (dialogOpen) {
@@ -142,6 +144,7 @@ export function TransactionDialog({
setPendingFiles([]);
setPendingDetachIds([]);
setPendingUploadFiles([]);
setExtrasOpen(initial.condition !== "À vista");
}
}, [
dialogOpen,
@@ -211,6 +214,22 @@ export function TransactionDialog({
});
}
function handleExtrasOpenChange(nextOpen: boolean) {
setExtrasOpen(nextOpen);
if (nextOpen) {
requestAnimationFrame(() => {
const scrollContainer = scrollContainerRef.current;
if (!scrollContainer) return;
scrollContainer.scrollTo({
top: scrollContainer.scrollHeight,
behavior: "smooth",
});
});
}
}
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setErrorMessage(null);
@@ -527,18 +546,21 @@ export function TransactionDialog({
return (
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
{trigger ? <DialogTrigger asChild>{trigger}</DialogTrigger> : null}
<DialogContent className="min-w-0 overflow-x-hidden">
<DialogContent className="flex max-h-[90vh] min-w-0 flex-col overflow-hidden p-4 sm:p-10">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<form
className="flex min-w-0 flex-col gap-0"
className="flex min-h-0 min-w-0 flex-1 flex-col gap-0"
onSubmit={handleSubmit}
noValidate
>
<div className="min-w-0 -mx-6 max-h-[90vh] overflow-x-hidden overflow-y-auto px-6 pb-1">
<div
ref={scrollContainerRef}
className="min-h-0 min-w-0 flex-1 overflow-x-hidden overflow-y-auto overscroll-contain pr-1 pb-1"
>
{/* Detalhes */}
<div className="space-y-3">
<BasicFieldsSection
@@ -634,7 +656,8 @@ export function TransactionDialog({
</>
) : (
<Collapsible
defaultOpen={formState.condition !== "À vista"}
open={extrasOpen}
onOpenChange={handleExtrasOpenChange}
className="min-w-0"
>
<CollapsibleTrigger className="flex w-full items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer [&[data-state=open]>svg]:rotate-180 mt-4">
@@ -680,7 +703,7 @@ export function TransactionDialog({
<p className="mt-3 text-sm text-destructive">{errorMessage}</p>
) : null}
<DialogFooter className="mt-4">
<DialogFooter className="mt-4 shrink-0">
<Button
type="button"
variant="outline"