feat(lançamentos): escopo "period" na ação em lote e correção do fluxo de anexos em séries

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Felipe Coutinho
2026-03-30 18:46:33 +00:00
parent 59b4dea071
commit f418987f47
11 changed files with 477 additions and 141 deletions

View File

@@ -1,7 +1,9 @@
"use client";
import { RiFileAddLine } from "@remixicon/react";
import { useCallback, useEffect, useState } from "react";
import { fetchTransactionAttachmentsAction } from "@/features/transactions/actions/attachments";
import { Button } from "@/shared/components/ui/button";
import { AttachmentItem } from "./attachment-item";
import { AttachmentUpload } from "./attachment-upload";
@@ -16,16 +18,28 @@ type AttachmentRow = {
interface AttachmentSectionProps {
transactionId: string;
seriesId: string | null;
readonly?: boolean;
onLoaded?: (count: number) => void;
pendingDetachIds?: string[];
onPendingDetach?: (attachmentId: string) => void;
onUndoPendingDetach?: (attachmentId: string) => void;
pendingUploadFiles?: File[];
onPendingUpload?: (file: File) => void;
onCancelPendingUpload?: (file: File) => void;
maxSizeMb?: number;
}
export function AttachmentSection({
transactionId,
seriesId,
readonly = false,
onLoaded,
pendingDetachIds,
onPendingDetach,
onUndoPendingDetach,
pendingUploadFiles,
onPendingUpload,
onCancelPendingUpload,
maxSizeMb,
}: AttachmentSectionProps) {
const [items, setItems] = useState<AttachmentRow[]>([]);
const [isLoading, setIsLoading] = useState(true);
@@ -45,42 +59,70 @@ export function AttachmentSection({
load();
}, [load]);
if (isLoading) {
return <p className="text-xs text-muted-foreground">Carregando...</p>;
}
const hasPendingUploads = (pendingUploadFiles?.length ?? 0) > 0;
return (
<div className="min-w-0 space-y-2 overflow-hidden">
{isLoading ? (
<p className="text-xs text-muted-foreground">Carregando...</p>
) : (
<>
{items.length > 0 ? (
<div className="min-w-0 space-y-1.5">
{items.map((item) => (
<AttachmentItem
key={item.attachmentId}
attachmentId={item.attachmentId}
transactionId={transactionId}
fileName={item.fileName}
fileSize={item.fileSize}
mimeType={item.mimeType}
url={item.url}
onDeleted={load}
readonly={readonly}
/>
))}
</div>
) : (
readonly && (
<p className="text-xs text-muted-foreground">Nenhum anexo.</p>
)
)}
{items.length === 0 && !hasPendingUploads && readonly && (
<p className="text-xs text-muted-foreground">Nenhum anexo.</p>
)}
{!readonly && (
<AttachmentUpload
{(items.length > 0 || hasPendingUploads) && (
<div className="min-w-0 space-y-1.5">
{items.map((item) => (
<AttachmentItem
key={item.attachmentId}
attachmentId={item.attachmentId}
transactionId={transactionId}
seriesId={seriesId}
onUploaded={load}
fileName={item.fileName}
fileSize={item.fileSize}
mimeType={item.mimeType}
url={item.url}
onDeleted={load}
readonly={readonly}
isPendingDelete={pendingDetachIds?.includes(item.attachmentId)}
onPendingDelete={onPendingDetach}
onUndoPendingDelete={onUndoPendingDetach}
/>
)}
</>
))}
{pendingUploadFiles?.map((file) => (
<div
key={`${file.name}-${file.size}`}
className="flex min-w-0 items-center gap-2 overflow-hidden rounded-md border border-dashed px-3 py-2 text-sm opacity-60"
>
<RiFileAddLine className="size-4 shrink-0 text-muted-foreground" />
<div className="flex-1 min-w-0">
<p className="truncate font-medium">{file.name}</p>
<p className="text-xs text-muted-foreground">
Será adicionado ao salvar
</p>
</div>
<Button
type="button"
variant="ghost"
size="sm"
className="shrink-0 text-xs h-7 px-2"
onClick={() => onCancelPendingUpload?.(file)}
>
Cancelar
</Button>
</div>
))}
</div>
)}
{!readonly && (
<AttachmentUpload
transactionId={transactionId}
onUploaded={load}
onPendingUpload={onPendingUpload}
maxSizeMb={maxSizeMb}
/>
)}
</div>
);