mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-09 11:01:45 +00:00
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:
@@ -190,6 +190,7 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
allowCreate={false}
|
allowCreate={false}
|
||||||
noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
|
noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
|
||||||
columnOrder={userPreferences?.transactionsColumnOrder ?? null}
|
columnOrder={userPreferences?.transactionsColumnOrder ?? null}
|
||||||
|
attachmentMaxSizeMb={userPreferences?.attachmentMaxSizeMb ?? 50}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -202,6 +202,7 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
allowCreate
|
allowCreate
|
||||||
noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
|
noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
|
||||||
columnOrder={userPreferences?.transactionsColumnOrder ?? null}
|
columnOrder={userPreferences?.transactionsColumnOrder ?? null}
|
||||||
|
attachmentMaxSizeMb={userPreferences?.attachmentMaxSizeMb ?? 50}
|
||||||
defaultCardId={card.id}
|
defaultCardId={card.id}
|
||||||
defaultPaymentMethod="Cartão de crédito"
|
defaultPaymentMethod="Cartão de crédito"
|
||||||
lockCardSelection
|
lockCardSelection
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
allowCreate={true}
|
allowCreate={true}
|
||||||
noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
|
noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
|
||||||
columnOrder={userPreferences?.transactionsColumnOrder ?? null}
|
columnOrder={userPreferences?.transactionsColumnOrder ?? null}
|
||||||
|
attachmentMaxSizeMb={userPreferences?.attachmentMaxSizeMb ?? 50}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -390,6 +390,7 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
allowCreate={canEdit}
|
allowCreate={canEdit}
|
||||||
noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
|
noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
|
||||||
columnOrder={userPreferences?.transactionsColumnOrder ?? null}
|
columnOrder={userPreferences?.transactionsColumnOrder ?? null}
|
||||||
|
attachmentMaxSizeMb={userPreferences?.attachmentMaxSizeMb ?? 50}
|
||||||
importPayerOptions={loggedUserOptionSets?.payerOptions}
|
importPayerOptions={loggedUserOptionSets?.payerOptions}
|
||||||
importSplitPayerOptions={loggedUserOptionSets?.splitPayerOptions}
|
importSplitPayerOptions={loggedUserOptionSets?.splitPayerOptions}
|
||||||
importDefaultPayerId={loggedUserOptionSets?.defaultPayerId}
|
importDefaultPayerId={loggedUserOptionSets?.defaultPayerId}
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ export default async function Page({ searchParams }: PageProps) {
|
|||||||
}}
|
}}
|
||||||
noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
|
noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
|
||||||
columnOrder={userPreferences?.transactionsColumnOrder ?? null}
|
columnOrder={userPreferences?.transactionsColumnOrder ?? null}
|
||||||
|
attachmentMaxSizeMb={userPreferences?.attachmentMaxSizeMb ?? 50}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ const resetAccountSchema = z.object({
|
|||||||
const updatePreferencesSchema = z.object({
|
const updatePreferencesSchema = z.object({
|
||||||
statementNoteAsColumn: z.boolean(),
|
statementNoteAsColumn: z.boolean(),
|
||||||
transactionsColumnOrder: z.array(z.string()).nullable(),
|
transactionsColumnOrder: z.array(z.string()).nullable(),
|
||||||
|
attachmentMaxSizeMb: z.number().int().min(1).max(100),
|
||||||
});
|
});
|
||||||
|
|
||||||
type ResettableUser = {
|
type ResettableUser = {
|
||||||
@@ -561,6 +562,7 @@ export async function updatePreferencesAction(
|
|||||||
.set({
|
.set({
|
||||||
statementNoteAsColumn: validated.statementNoteAsColumn,
|
statementNoteAsColumn: validated.statementNoteAsColumn,
|
||||||
transactionsColumnOrder: validated.transactionsColumnOrder,
|
transactionsColumnOrder: validated.transactionsColumnOrder,
|
||||||
|
attachmentMaxSizeMb: validated.attachmentMaxSizeMb,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
})
|
})
|
||||||
.where(eq(schema.userPreferences.userId, session.user.id));
|
.where(eq(schema.userPreferences.userId, session.user.id));
|
||||||
@@ -570,6 +572,7 @@ export async function updatePreferencesAction(
|
|||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
statementNoteAsColumn: validated.statementNoteAsColumn,
|
statementNoteAsColumn: validated.statementNoteAsColumn,
|
||||||
transactionsColumnOrder: validated.transactionsColumnOrder,
|
transactionsColumnOrder: validated.transactionsColumnOrder,
|
||||||
|
attachmentMaxSizeMb: validated.attachmentMaxSizeMb,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,17 +21,27 @@ import { useRouter } from "next/navigation";
|
|||||||
import { useState, useTransition } from "react";
|
import { useState, useTransition } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { updatePreferencesAction } from "@/features/settings/actions";
|
import { updatePreferencesAction } from "@/features/settings/actions";
|
||||||
|
import {
|
||||||
|
ATTACHMENT_SIZE_OPTIONS,
|
||||||
|
type AttachmentSizeOption,
|
||||||
|
} from "@/features/transactions/attachments-config";
|
||||||
import {
|
import {
|
||||||
DEFAULT_LANCAMENTOS_COLUMN_ORDER,
|
DEFAULT_LANCAMENTOS_COLUMN_ORDER,
|
||||||
LANCAMENTOS_COLUMN_LABELS,
|
LANCAMENTOS_COLUMN_LABELS,
|
||||||
} from "@/features/transactions/column-order";
|
} from "@/features/transactions/column-order";
|
||||||
import { Button } from "@/shared/components/ui/button";
|
import { Button } from "@/shared/components/ui/button";
|
||||||
import { Label } from "@/shared/components/ui/label";
|
import { Label } from "@/shared/components/ui/label";
|
||||||
|
import { Separator } from "@/shared/components/ui/separator";
|
||||||
import { Switch } from "@/shared/components/ui/switch";
|
import { Switch } from "@/shared/components/ui/switch";
|
||||||
|
import {
|
||||||
|
ToggleGroup,
|
||||||
|
ToggleGroupItem,
|
||||||
|
} from "@/shared/components/ui/toggle-group";
|
||||||
|
|
||||||
interface PreferencesFormProps {
|
interface PreferencesFormProps {
|
||||||
statementNoteAsColumn: boolean;
|
statementNoteAsColumn: boolean;
|
||||||
transactionsColumnOrder: string[] | null;
|
transactionsColumnOrder: string[] | null;
|
||||||
|
attachmentMaxSizeMb: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SortableColumnItem({ id }: { id: string }) {
|
function SortableColumnItem({ id }: { id: string }) {
|
||||||
@@ -74,6 +84,7 @@ function SortableColumnItem({ id }: { id: string }) {
|
|||||||
export function PreferencesForm({
|
export function PreferencesForm({
|
||||||
statementNoteAsColumn: initialExtratoNoteAsColumn,
|
statementNoteAsColumn: initialExtratoNoteAsColumn,
|
||||||
transactionsColumnOrder: initialColumnOrder,
|
transactionsColumnOrder: initialColumnOrder,
|
||||||
|
attachmentMaxSizeMb: initialAttachmentMaxSizeMb,
|
||||||
}: PreferencesFormProps) {
|
}: PreferencesFormProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
@@ -85,6 +96,14 @@ export function PreferencesForm({
|
|||||||
? initialColumnOrder
|
? initialColumnOrder
|
||||||
: DEFAULT_LANCAMENTOS_COLUMN_ORDER,
|
: DEFAULT_LANCAMENTOS_COLUMN_ORDER,
|
||||||
);
|
);
|
||||||
|
const [attachmentMaxSizeMb, setAttachmentMaxSizeMb] =
|
||||||
|
useState<AttachmentSizeOption>(
|
||||||
|
(ATTACHMENT_SIZE_OPTIONS.includes(
|
||||||
|
initialAttachmentMaxSizeMb as AttachmentSizeOption,
|
||||||
|
)
|
||||||
|
? initialAttachmentMaxSizeMb
|
||||||
|
: 50) as AttachmentSizeOption,
|
||||||
|
);
|
||||||
|
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
|
useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
|
||||||
@@ -109,6 +128,7 @@ export function PreferencesForm({
|
|||||||
const result = await updatePreferencesAction({
|
const result = await updatePreferencesAction({
|
||||||
statementNoteAsColumn,
|
statementNoteAsColumn,
|
||||||
transactionsColumnOrder: columnOrder,
|
transactionsColumnOrder: columnOrder,
|
||||||
|
attachmentMaxSizeMb,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -122,19 +142,18 @@ export function PreferencesForm({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="flex flex-col gap-8">
|
<form onSubmit={handleSubmit} className="flex flex-col gap-8">
|
||||||
{/* Seção: Extrato / Lançamentos */}
|
{/* Seção: Lançamentos */}
|
||||||
<section className="space-y-4">
|
<section className="space-y-4">
|
||||||
<div>
|
<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">
|
<p className="text-sm text-muted-foreground">
|
||||||
Como exibir anotações e a ordem das colunas na tabela de
|
Configurações de exibição da tabela de movimentações.
|
||||||
movimentações.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between rounded-lg border p-4 max-w-md">
|
<section className="flex items-center justify-between max-w-md">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="extrato-note-column" className="text-base">
|
<Label htmlFor="extrato-note-column" className="text-sm">
|
||||||
Anotações em coluna
|
Anotações em coluna
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
@@ -149,10 +168,12 @@ export function PreferencesForm({
|
|||||||
onCheckedChange={setExtratoNoteAsColumn}
|
onCheckedChange={setExtratoNoteAsColumn}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
/>
|
/>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<div className="space-y-2 max-w-md">
|
<Separator />
|
||||||
<Label className="text-base">Ordem das colunas</Label>
|
|
||||||
|
<section className="space-y-2 max-w-md">
|
||||||
|
<Label className="text-sm">Ordem das colunas</Label>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Arraste os itens para definir a ordem em que as colunas aparecem na
|
Arraste os itens para definir a ordem em que as colunas aparecem na
|
||||||
tabela do extrato e dos lançamentos.
|
tabela do extrato e dos lançamentos.
|
||||||
@@ -173,7 +194,43 @@ export function PreferencesForm({
|
|||||||
</div>
|
</div>
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
</DndContext>
|
</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>
|
</section>
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { db, schema } from "@/shared/lib/db";
|
|||||||
export interface UserPreferences {
|
export interface UserPreferences {
|
||||||
statementNoteAsColumn: boolean;
|
statementNoteAsColumn: boolean;
|
||||||
transactionsColumnOrder: string[] | null;
|
transactionsColumnOrder: string[] | null;
|
||||||
|
attachmentMaxSizeMb: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiToken {
|
export interface ApiToken {
|
||||||
@@ -32,6 +33,7 @@ export async function fetchUserPreferences(
|
|||||||
.select({
|
.select({
|
||||||
statementNoteAsColumn: schema.userPreferences.statementNoteAsColumn,
|
statementNoteAsColumn: schema.userPreferences.statementNoteAsColumn,
|
||||||
transactionsColumnOrder: schema.userPreferences.transactionsColumnOrder,
|
transactionsColumnOrder: schema.userPreferences.transactionsColumnOrder,
|
||||||
|
attachmentMaxSizeMb: schema.userPreferences.attachmentMaxSizeMb,
|
||||||
})
|
})
|
||||||
.from(schema.userPreferences)
|
.from(schema.userPreferences)
|
||||||
.where(eq(schema.userPreferences.userId, userId))
|
.where(eq(schema.userPreferences.userId, userId))
|
||||||
|
|||||||
@@ -5,4 +5,9 @@ export const ALLOWED_MIME_TYPES = [
|
|||||||
"image/webp",
|
"image/webp",
|
||||||
] as const;
|
] 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];
|
||||||
|
|||||||
@@ -5,19 +5,22 @@ import { useRef } from "react";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
ALLOWED_MIME_TYPES,
|
ALLOWED_MIME_TYPES,
|
||||||
MAX_FILE_SIZE,
|
DEFAULT_MAX_FILE_SIZE_MB,
|
||||||
} from "@/features/transactions/attachments-config";
|
} from "@/features/transactions/attachments-config";
|
||||||
import { Button } from "@/shared/components/ui/button";
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
|
||||||
interface AttachmentFilePickerProps {
|
interface AttachmentFilePickerProps {
|
||||||
file: File | null;
|
file: File | null;
|
||||||
onChange: (file: File | null) => void;
|
onChange: (file: File | null) => void;
|
||||||
|
maxSizeMb?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AttachmentFilePicker({
|
export function AttachmentFilePicker({
|
||||||
file,
|
file,
|
||||||
onChange,
|
onChange,
|
||||||
|
maxSizeMb = DEFAULT_MAX_FILE_SIZE_MB,
|
||||||
}: AttachmentFilePickerProps) {
|
}: AttachmentFilePickerProps) {
|
||||||
|
const maxFileSizeBytes = maxSizeMb * 1024 * 1024;
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
|
function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
@@ -37,8 +40,8 @@ export function AttachmentFilePicker({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selected.size > MAX_FILE_SIZE) {
|
if (selected.size > maxFileSizeBytes) {
|
||||||
toast.error("O arquivo deve ter no máximo 50MB.");
|
toast.error(`O arquivo deve ter no máximo ${maxSizeMb}MB.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +86,7 @@ export function AttachmentFilePicker({
|
|||||||
Adicionar anexo
|
Adicionar anexo
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[11px]">
|
<span className="text-[11px]">
|
||||||
PDF, JPEG, PNG ou WebP · máx. 50 MB
|
PDF, JPEG, PNG ou WebP · máx. {maxSizeMb} MB
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user