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,5 +1,6 @@
"use client";
import { RiErrorWarningLine } from "@remixicon/react";
import { useState } from "react";
import { Button } from "@/shared/components/ui/button";
import {
@@ -13,7 +14,7 @@ import {
import { Label } from "@/shared/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/shared/components/ui/radio-group";
export type BulkActionScope = "current" | "future" | "all";
export type BulkActionScope = "current" | "period" | "future" | "all";
type BulkActionDialogProps = {
open: boolean;
@@ -108,6 +109,30 @@ export function BulkActionDialog({
</div>
</div>
<div className="flex items-start space-x-3">
<RadioGroupItem value="period" id="period" className="mt-0.5" />
<div className="flex-1">
<Label
htmlFor="period"
className="text-sm cursor-pointer font-medium"
>
Todos os pagadores deste período
</Label>
<p className="text-xs text-muted-foreground">
Aplica a todos os lançamentos deste mesmo mês na série
</p>
{scope === "period" && actionType === "edit" && (
<div className="mt-1.5 flex items-start gap-1.5 rounded-md bg-amber-50 px-2 py-1.5 text-amber-800 dark:bg-amber-950/40 dark:text-amber-300">
<RiErrorWarningLine className="mt-0.5 size-3.5 shrink-0" />
<p className="text-xs">
Atenção: os valores individuais de cada pagador serão
substituídos pelos valores deste lançamento.
</p>
</div>
)}
</div>
</div>
<div className="flex items-start space-x-3">
<RadioGroupItem value="future" id="future" className="mt-0.5" />
<div className="flex-1">

View File

@@ -223,7 +223,6 @@ export function TransactionDetailsDialog({
<div className="min-w-0">
<AttachmentSection
transactionId={transaction.id}
seriesId={transaction.seriesId}
readonly
onLoaded={setAttachmentCount}
/>

View File

@@ -30,6 +30,8 @@ export interface TransactionDialogProps {
forceShowTransactionType?: boolean;
/** Called after successful create/update. Receives the action result. */
onSuccess?: () => void;
/** Max attachment file size in MB for this user */
maxSizeMb?: number;
onBulkEditRequest?: (data: {
id: string;
name: string;
@@ -42,6 +44,8 @@ export interface TransactionDialogProps {
dueDate: string | null;
boletoPaymentDate: string | null;
isSettled: boolean | null;
pendingDetachIds: string[];
pendingUploadFiles: File[];
}) => void;
}

View File

@@ -8,6 +8,7 @@ import {
} from "@/features/transactions/actions";
import {
confirmAttachmentUploadAction,
detachTransactionAttachmentAction,
getPresignedUploadUrlAction,
} from "@/features/transactions/actions/attachments";
import {
@@ -72,10 +73,11 @@ export function TransactionDialog({
defaultAmount,
lockCardSelection,
lockPaymentMethod,
isImporting = false,
isImporting,
defaultTransactionType,
forceShowTransactionType = false,
forceShowTransactionType,
onSuccess,
maxSizeMb,
onBulkEditRequest,
}: TransactionDialogProps) {
const [dialogOpen, setDialogOpen] = useControlledState(
@@ -98,6 +100,8 @@ export function TransactionDialog({
const [isPending, startTransition] = useTransition();
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [pendingFile, setPendingFile] = useState<File | null>(null);
const [pendingDetachIds, setPendingDetachIds] = useState<string[]>([]);
const [pendingUploadFiles, setPendingUploadFiles] = useState<File[]>([]);
useEffect(() => {
if (dialogOpen) {
@@ -136,6 +140,8 @@ export function TransactionDialog({
setFormState(initial);
setErrorMessage(null);
setPendingFile(null);
setPendingDetachIds([]);
setPendingUploadFiles([]);
}
}, [
dialogOpen,
@@ -342,7 +348,7 @@ export function TransactionDialog({
});
await confirmAttachmentUploadAction({
uploadToken: presign.uploadToken,
applyToSeries: isNewSeries,
scope: isNewSeries ? "all" : "current",
});
}
}
@@ -357,11 +363,11 @@ export function TransactionDialog({
return;
}
// Update mode
const hasSeriesId = Boolean(transaction?.seriesId);
if (hasSeriesId && onBulkEditRequest) {
// Para lançamentos em série, abre o diálogo de bulk action
// Para lançamentos em série, passa os arquivos para a página confirmar
// o upload após o escopo ser escolhido (sem upload antecipado ao S3)
onBulkEditRequest({
id: transaction?.id ?? "",
name: formState.name.trim(),
@@ -383,11 +389,13 @@ export function TransactionDialog({
formState.paymentMethod === "Cartão de crédito"
? null
: Boolean(formState.isSettled),
pendingDetachIds,
pendingUploadFiles,
});
return;
}
// Atualização normal para lançamentos únicos ou todos os campos
// Atualização normal para lançamentos únicos
const updatePayload: UpdateTransactionInput = {
id: transaction?.id ?? "",
...payload,
@@ -396,6 +404,31 @@ export function TransactionDialog({
const result = await updateTransactionAction(updatePayload);
if (result.success) {
for (const attachmentId of pendingDetachIds) {
await detachTransactionAttachmentAction({
attachmentId,
transactionId: transaction?.id ?? "",
});
}
for (const file of pendingUploadFiles) {
const presign = await getPresignedUploadUrlAction({
fileName: file.name,
mimeType: file.type,
fileSize: file.size,
transactionId: transaction?.id ?? "",
});
if (presign.success) {
await fetch(presign.presignedUrl, {
method: "PUT",
body: file,
headers: { "Content-Type": file.type },
});
await confirmAttachmentUploadAction({
uploadToken: presign.uploadToken,
scope: "current",
});
}
}
toast.success(result.message);
onSuccess?.();
setDialogOpen(false);
@@ -521,7 +554,40 @@ export function TransactionDialog({
</Label>
<AttachmentSection
transactionId={transaction?.id ?? ""}
seriesId={transaction?.seriesId ?? null}
maxSizeMb={maxSizeMb}
pendingDetachIds={
transaction?.seriesId ? pendingDetachIds : undefined
}
onPendingDetach={
transaction?.seriesId
? (id) => setPendingDetachIds((prev) => [...prev, id])
: undefined
}
onUndoPendingDetach={
transaction?.seriesId
? (id) =>
setPendingDetachIds((prev) =>
prev.filter((x) => x !== id),
)
: undefined
}
pendingUploadFiles={
transaction?.seriesId ? pendingUploadFiles : undefined
}
onPendingUpload={
transaction?.seriesId
? (file) =>
setPendingUploadFiles((prev) => [...prev, file])
: undefined
}
onCancelPendingUpload={
transaction?.seriesId
? (file) =>
setPendingUploadFiles((prev) =>
prev.filter((f) => f !== file),
)
: undefined
}
/>
</div>
</>
@@ -548,6 +614,7 @@ export function TransactionDialog({
<AttachmentFilePicker
file={pendingFile}
onChange={setPendingFile}
maxSizeMb={maxSizeMb}
/>
</CollapsibleContent>
</Collapsible>