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}
|
||||
noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
|
||||
columnOrder={userPreferences?.transactionsColumnOrder ?? null}
|
||||
attachmentMaxSizeMb={userPreferences?.attachmentMaxSizeMb ?? 50}
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -202,6 +202,7 @@ export default async function Page({ params, searchParams }: PageProps) {
|
||||
allowCreate
|
||||
noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
|
||||
columnOrder={userPreferences?.transactionsColumnOrder ?? null}
|
||||
attachmentMaxSizeMb={userPreferences?.attachmentMaxSizeMb ?? 50}
|
||||
defaultCardId={card.id}
|
||||
defaultPaymentMethod="Cartão de crédito"
|
||||
lockCardSelection
|
||||
|
||||
@@ -99,6 +99,7 @@ export default async function Page({ params, searchParams }: PageProps) {
|
||||
allowCreate={true}
|
||||
noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
|
||||
columnOrder={userPreferences?.transactionsColumnOrder ?? null}
|
||||
attachmentMaxSizeMb={userPreferences?.attachmentMaxSizeMb ?? 50}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -390,6 +390,7 @@ export default async function Page({ params, searchParams }: PageProps) {
|
||||
allowCreate={canEdit}
|
||||
noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
|
||||
columnOrder={userPreferences?.transactionsColumnOrder ?? null}
|
||||
attachmentMaxSizeMb={userPreferences?.attachmentMaxSizeMb ?? 50}
|
||||
importPayerOptions={loggedUserOptionSets?.payerOptions}
|
||||
importSplitPayerOptions={loggedUserOptionSets?.splitPayerOptions}
|
||||
importDefaultPayerId={loggedUserOptionSets?.defaultPayerId}
|
||||
|
||||
@@ -102,6 +102,7 @@ export default async function Page({ searchParams }: PageProps) {
|
||||
}}
|
||||
noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
|
||||
columnOrder={userPreferences?.transactionsColumnOrder ?? null}
|
||||
attachmentMaxSizeMb={userPreferences?.attachmentMaxSizeMb ?? 50}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user