feat(preferências): configuração de tamanho máximo de anexo por arquivo

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Felipe Coutinho
2026-03-30 18:46:28 +00:00
parent 6ce132fe0c
commit 59b4dea071
10 changed files with 91 additions and 16 deletions

View File

@@ -65,6 +65,7 @@ const resetAccountSchema = z.object({
const updatePreferencesSchema = z.object({
statementNoteAsColumn: z.boolean(),
transactionsColumnOrder: z.array(z.string()).nullable(),
attachmentMaxSizeMb: z.number().int().min(1).max(100),
});
type ResettableUser = {
@@ -561,6 +562,7 @@ export async function updatePreferencesAction(
.set({
statementNoteAsColumn: validated.statementNoteAsColumn,
transactionsColumnOrder: validated.transactionsColumnOrder,
attachmentMaxSizeMb: validated.attachmentMaxSizeMb,
updatedAt: new Date(),
})
.where(eq(schema.userPreferences.userId, session.user.id));
@@ -570,6 +572,7 @@ export async function updatePreferencesAction(
userId: session.user.id,
statementNoteAsColumn: validated.statementNoteAsColumn,
transactionsColumnOrder: validated.transactionsColumnOrder,
attachmentMaxSizeMb: validated.attachmentMaxSizeMb,
});
}

View File

@@ -21,17 +21,27 @@ import { useRouter } from "next/navigation";
import { useState, useTransition } from "react";
import { toast } from "sonner";
import { updatePreferencesAction } from "@/features/settings/actions";
import {
ATTACHMENT_SIZE_OPTIONS,
type AttachmentSizeOption,
} from "@/features/transactions/attachments-config";
import {
DEFAULT_LANCAMENTOS_COLUMN_ORDER,
LANCAMENTOS_COLUMN_LABELS,
} from "@/features/transactions/column-order";
import { Button } from "@/shared/components/ui/button";
import { Label } from "@/shared/components/ui/label";
import { Separator } from "@/shared/components/ui/separator";
import { Switch } from "@/shared/components/ui/switch";
import {
ToggleGroup,
ToggleGroupItem,
} from "@/shared/components/ui/toggle-group";
interface PreferencesFormProps {
statementNoteAsColumn: boolean;
transactionsColumnOrder: string[] | null;
attachmentMaxSizeMb: number;
}
function SortableColumnItem({ id }: { id: string }) {
@@ -74,6 +84,7 @@ function SortableColumnItem({ id }: { id: string }) {
export function PreferencesForm({
statementNoteAsColumn: initialExtratoNoteAsColumn,
transactionsColumnOrder: initialColumnOrder,
attachmentMaxSizeMb: initialAttachmentMaxSizeMb,
}: PreferencesFormProps) {
const router = useRouter();
const [isPending, startTransition] = useTransition();
@@ -85,6 +96,14 @@ export function PreferencesForm({
? initialColumnOrder
: DEFAULT_LANCAMENTOS_COLUMN_ORDER,
);
const [attachmentMaxSizeMb, setAttachmentMaxSizeMb] =
useState<AttachmentSizeOption>(
(ATTACHMENT_SIZE_OPTIONS.includes(
initialAttachmentMaxSizeMb as AttachmentSizeOption,
)
? initialAttachmentMaxSizeMb
: 50) as AttachmentSizeOption,
);
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
@@ -109,6 +128,7 @@ export function PreferencesForm({
const result = await updatePreferencesAction({
statementNoteAsColumn,
transactionsColumnOrder: columnOrder,
attachmentMaxSizeMb,
});
if (result.success) {
@@ -122,19 +142,18 @@ export function PreferencesForm({
return (
<form onSubmit={handleSubmit} className="flex flex-col gap-8">
{/* Seção: Extrato / Lançamentos */}
{/* Seção: Lançamentos */}
<section className="space-y-4">
<div>
<h3 className="text-base font-semibold">Extrato e lançamentos</h3>
<h3 className="text-base font-semibold">Lançamentos</h3>
<p className="text-sm text-muted-foreground">
Como exibir anotações e a ordem das colunas na tabela de
movimentações.
Configurações de exibição da tabela de movimentações.
</p>
</div>
<div className="flex items-center justify-between rounded-lg border p-4 max-w-md">
<div className="space-y-0.5">
<Label htmlFor="extrato-note-column" className="text-base">
<section className="flex items-center justify-between max-w-md">
<div className="space-y-2">
<Label htmlFor="extrato-note-column" className="text-sm">
Anotações em coluna
</Label>
<p className="text-sm text-muted-foreground">
@@ -149,10 +168,12 @@ export function PreferencesForm({
onCheckedChange={setExtratoNoteAsColumn}
disabled={isPending}
/>
</div>
</section>
<div className="space-y-2 max-w-md">
<Label className="text-base">Ordem das colunas</Label>
<Separator />
<section className="space-y-2 max-w-md">
<Label className="text-sm">Ordem das colunas</Label>
<p className="text-sm text-muted-foreground">
Arraste os itens para definir a ordem em que as colunas aparecem na
tabela do extrato e dos lançamentos.
@@ -173,7 +194,43 @@ export function PreferencesForm({
</div>
</SortableContext>
</DndContext>
</div>
</section>
<Separator />
<section className="space-y-2">
<Label className="text-sm">Anexos</Label>
<p className="text-sm text-muted-foreground">
Configurações de upload de arquivos nos lançamentos.
</p>
<div className="space-y-2 max-w-md mt-4">
<Label>Tamanho máximo por arquivo</Label>
<p className="text-sm text-muted-foreground">
Limite aplicado ao upload de PDFs e imagens.
</p>
<ToggleGroup
type="single"
value={String(attachmentMaxSizeMb)}
onValueChange={(val) => {
if (val)
setAttachmentMaxSizeMb(Number(val) as AttachmentSizeOption);
}}
className="flex flex-wrap gap-2 justify-start"
>
{ATTACHMENT_SIZE_OPTIONS.map((size) => (
<ToggleGroupItem
key={size}
value={String(size)}
aria-label={`${size} MB`}
className="min-w-14"
>
{size} MB
</ToggleGroupItem>
))}
</ToggleGroup>
</div>
</section>
</section>
<div className="flex justify-end">

View File

@@ -5,6 +5,7 @@ import { db, schema } from "@/shared/lib/db";
export interface UserPreferences {
statementNoteAsColumn: boolean;
transactionsColumnOrder: string[] | null;
attachmentMaxSizeMb: number;
}
export interface ApiToken {
@@ -32,6 +33,7 @@ export async function fetchUserPreferences(
.select({
statementNoteAsColumn: schema.userPreferences.statementNoteAsColumn,
transactionsColumnOrder: schema.userPreferences.transactionsColumnOrder,
attachmentMaxSizeMb: schema.userPreferences.attachmentMaxSizeMb,
})
.from(schema.userPreferences)
.where(eq(schema.userPreferences.userId, userId))

View File

@@ -5,4 +5,9 @@ export const ALLOWED_MIME_TYPES = [
"image/webp",
] as const;
export const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
export const DEFAULT_MAX_FILE_SIZE_MB = 50;
export const MAX_FILE_SIZE = DEFAULT_MAX_FILE_SIZE_MB * 1024 * 1024; // 50MB (fallback)
export const ATTACHMENT_SIZE_OPTIONS = [5, 10, 25, 50, 100] as const;
export type AttachmentSizeOption = (typeof ATTACHMENT_SIZE_OPTIONS)[number];

View File

@@ -5,19 +5,22 @@ import { useRef } from "react";
import { toast } from "sonner";
import {
ALLOWED_MIME_TYPES,
MAX_FILE_SIZE,
DEFAULT_MAX_FILE_SIZE_MB,
} from "@/features/transactions/attachments-config";
import { Button } from "@/shared/components/ui/button";
interface AttachmentFilePickerProps {
file: File | null;
onChange: (file: File | null) => void;
maxSizeMb?: number;
}
export function AttachmentFilePicker({
file,
onChange,
maxSizeMb = DEFAULT_MAX_FILE_SIZE_MB,
}: AttachmentFilePickerProps) {
const maxFileSizeBytes = maxSizeMb * 1024 * 1024;
const inputRef = useRef<HTMLInputElement>(null);
function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
@@ -37,8 +40,8 @@ export function AttachmentFilePicker({
return;
}
if (selected.size > MAX_FILE_SIZE) {
toast.error("O arquivo deve ter no máximo 50MB.");
if (selected.size > maxFileSizeBytes) {
toast.error(`O arquivo deve ter no máximo ${maxSizeMb}MB.`);
return;
}
@@ -83,7 +86,7 @@ export function AttachmentFilePicker({
Adicionar anexo
</span>
<span className="text-[11px]">
PDF, JPEG, PNG ou WebP · máx. 50 MB
PDF, JPEG, PNG ou WebP · máx. {maxSizeMb} MB
</span>
</button>
)}