chore: remover seções vazias de mudanças de código

Este commit remove seções vazias de mudanças de código do arquivo de
mudanças. Isso ajuda a manter o histórico de mudanças mais limpo e
organizado, facilitando a leitura e a compreensão das alterações
realizadas no projeto.
This commit is contained in:
Felipe Coutinho
2025-12-16 23:20:47 +00:00
parent 0767636eed
commit e7cb9c9db1
37 changed files with 1350 additions and 920 deletions

View File

@@ -1,16 +1,19 @@
import { getRecentEstablishmentsAction } from "@/app/(dashboard)/lancamentos/actions";
import { cartoes, lancamentos } from "@/db/schema";
import { db } from "@/lib/db";
import {
buildOptionSets,
buildSluggedFilters,
fetchLancamentoFilterSources,
mapLancamentosData,
} from "@/lib/lancamentos/page-helpers";
import { db } from "@/lib/db";
import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants";
import { and, eq, gte, lte, ne, or } from "drizzle-orm";
import { getRecentEstablishmentsAction } from "@/app/(dashboard)/lancamentos/actions";
import type { CalendarData, CalendarEvent } from "@/components/calendario/types";
import type {
CalendarData,
CalendarEvent,
} from "@/components/calendario/types";
const PAYMENT_METHOD_BOLETO = "Boleto";
const TRANSACTION_TYPE_TRANSFERENCIA = "Transferência";
@@ -98,7 +101,11 @@ export const fetchCalendarData = async ({
const cardTotals = new Map<string, number>();
for (const item of lancamentosData) {
if (!item.cartaoId || item.period !== period || item.pagadorRole !== PAGADOR_ROLE_ADMIN) {
if (
!item.cartaoId ||
item.period !== period ||
item.pagadorRole !== PAGADOR_ROLE_ADMIN
) {
continue;
}
const amount = Math.abs(item.amount ?? 0);

View File

@@ -10,11 +10,7 @@ export default async function HistoricoCategoriasPage() {
const data = await fetchCategoryHistory(user.id, currentPeriod);
return (
<main className="flex flex-col gap-6">
<p className="text-muted-foreground">
Acompanhe o histórico de desempenho das suas categorias ao longo de 9
meses.
</p>
<main>
<CategoryHistoryWidget data={data} />
</main>
);

View File

@@ -15,7 +15,7 @@ export default function RootLayout({
<PageDescription
icon={<RiPriceTag3Line />}
title="Categorias"
subtitle="Gerencie suas categorias de despesas e receitas. Acompanhe o desempenho financeiro por categoria e faça ajustes conforme necessário."
subtitle="Gerencie suas categorias de despesas e receitas acompanhando o histórico de desempenho dos últimos 9 meses, permitindo ajustes financeiros precisos conforme necessário."
/>
{children}
</section>

View File

@@ -1,6 +1,6 @@
"use server";
import { contas, lancamentos } from "@/db/schema";
import { contas, lancamentos, pagadores, categorias, cartoes } from "@/db/schema";
import {
INITIAL_BALANCE_CONDITION,
INITIAL_BALANCE_NOTE,
@@ -31,6 +31,78 @@ import { and, asc, desc, eq, gte, inArray, sql } from "drizzle-orm";
import { randomUUID } from "node:crypto";
import { z } from "zod";
// ============================================================================
// Authorization Validation Functions
// ============================================================================
async function validatePagadorOwnership(
userId: string,
pagadorId: string | null | undefined
): Promise<boolean> {
if (!pagadorId) return true; // Se não tem pagadorId, não precisa validar
const pagador = await db.query.pagadores.findFirst({
where: and(
eq(pagadores.id, pagadorId),
eq(pagadores.userId, userId)
),
});
return !!pagador;
}
async function validateCategoriaOwnership(
userId: string,
categoriaId: string | null | undefined
): Promise<boolean> {
if (!categoriaId) return true;
const categoria = await db.query.categorias.findFirst({
where: and(
eq(categorias.id, categoriaId),
eq(categorias.userId, userId)
),
});
return !!categoria;
}
async function validateContaOwnership(
userId: string,
contaId: string | null | undefined
): Promise<boolean> {
if (!contaId) return true;
const conta = await db.query.contas.findFirst({
where: and(
eq(contas.id, contaId),
eq(contas.userId, userId)
),
});
return !!conta;
}
async function validateCartaoOwnership(
userId: string,
cartaoId: string | null | undefined
): Promise<boolean> {
if (!cartaoId) return true;
const cartao = await db.query.cartoes.findFirst({
where: and(
eq(cartoes.id, cartaoId),
eq(cartoes.userId, userId)
),
});
return !!cartao;
}
// ============================================================================
// Utility Functions
// ============================================================================
const resolvePeriod = (purchaseDate: string, period?: string | null) => {
if (period && /^\d{4}-\d{2}$/.test(period)) {
return period;
@@ -472,6 +544,42 @@ export async function createLancamentoAction(
const user = await getUser();
const data = createSchema.parse(input);
// Validar propriedade dos recursos referenciados
if (data.pagadorId) {
const isValid = await validatePagadorOwnership(user.id, data.pagadorId);
if (!isValid) {
return { success: false, error: "Pagador não encontrado ou sem permissão." };
}
}
if (data.secondaryPagadorId) {
const isValid = await validatePagadorOwnership(user.id, data.secondaryPagadorId);
if (!isValid) {
return { success: false, error: "Pagador secundário não encontrado ou sem permissão." };
}
}
if (data.categoriaId) {
const isValid = await validateCategoriaOwnership(user.id, data.categoriaId);
if (!isValid) {
return { success: false, error: "Categoria não encontrada." };
}
}
if (data.contaId) {
const isValid = await validateContaOwnership(user.id, data.contaId);
if (!isValid) {
return { success: false, error: "Conta não encontrada." };
}
}
if (data.cartaoId) {
const isValid = await validateCartaoOwnership(user.id, data.cartaoId);
if (!isValid) {
return { success: false, error: "Cartão não encontrado." };
}
}
const period = resolvePeriod(data.purchaseDate, data.period);
const purchaseDate = parseLocalDateString(data.purchaseDate);
const dueDate = data.dueDate ? parseLocalDateString(data.dueDate) : null;
@@ -556,6 +664,42 @@ export async function updateLancamentoAction(
const user = await getUser();
const data = updateSchema.parse(input);
// Validar propriedade dos recursos referenciados
if (data.pagadorId) {
const isValid = await validatePagadorOwnership(user.id, data.pagadorId);
if (!isValid) {
return { success: false, error: "Pagador não encontrado ou sem permissão." };
}
}
if (data.secondaryPagadorId) {
const isValid = await validatePagadorOwnership(user.id, data.secondaryPagadorId);
if (!isValid) {
return { success: false, error: "Pagador secundário não encontrado ou sem permissão." };
}
}
if (data.categoriaId) {
const isValid = await validateCategoriaOwnership(user.id, data.categoriaId);
if (!isValid) {
return { success: false, error: "Categoria não encontrada." };
}
}
if (data.contaId) {
const isValid = await validateContaOwnership(user.id, data.contaId);
if (!isValid) {
return { success: false, error: "Conta não encontrada." };
}
}
if (data.cartaoId) {
const isValid = await validateCartaoOwnership(user.id, data.cartaoId);
if (!isValid) {
return { success: false, error: "Cartão não encontrado." };
}
}
const existing = await db.query.lancamentos.findFirst({
columns: {
id: true,
@@ -1124,12 +1268,12 @@ const massAddTransactionSchema = z.object({
.number({ message: "Informe o valor da transação." })
.min(0, "Informe um valor maior ou igual a zero."),
categoriaId: uuidSchema("Categoria").nullable().optional(),
pagadorId: uuidSchema("Pagador").nullable().optional(),
});
const massAddSchema = z.object({
fixedFields: z.object({
transactionType: z.enum(LANCAMENTO_TRANSACTION_TYPES).optional(),
pagadorId: uuidSchema("Pagador").nullable().optional(),
paymentMethod: z.enum(LANCAMENTO_PAYMENT_METHODS).optional(),
condition: z.enum(LANCAMENTO_CONDITIONS).optional(),
period: z
@@ -1156,6 +1300,46 @@ export async function createMassLancamentosAction(
const user = await getUser();
const data = massAddSchema.parse(input);
// Validar campos fixos
if (data.fixedFields.contaId) {
const isValid = await validateContaOwnership(user.id, data.fixedFields.contaId);
if (!isValid) {
return { success: false, error: "Conta não encontrada." };
}
}
if (data.fixedFields.cartaoId) {
const isValid = await validateCartaoOwnership(user.id, data.fixedFields.cartaoId);
if (!isValid) {
return { success: false, error: "Cartão não encontrado." };
}
}
// Validar cada transação individual
for (let i = 0; i < data.transactions.length; i++) {
const transaction = data.transactions[i];
if (transaction.pagadorId) {
const isValid = await validatePagadorOwnership(user.id, transaction.pagadorId);
if (!isValid) {
return {
success: false,
error: `Pagador não encontrado na transação ${i + 1}.`
};
}
}
if (transaction.categoriaId) {
const isValid = await validateCategoriaOwnership(user.id, transaction.categoriaId);
if (!isValid) {
return {
success: false,
error: `Categoria não encontrada na transação ${i + 1}.`
};
}
}
}
// Default values for non-fixed fields
const defaultTransactionType = LANCAMENTO_TRANSACTION_TYPES[0];
const defaultCondition = LANCAMENTO_CONDITIONS[0];
@@ -1181,7 +1365,7 @@ export async function createMassLancamentosAction(
const condition = data.fixedFields.condition ?? defaultCondition;
const paymentMethod =
data.fixedFields.paymentMethod ?? defaultPaymentMethod;
const pagadorId = data.fixedFields.pagadorId ?? null;
const pagadorId = transaction.pagadorId ?? null;
const contaId =
paymentMethod === "Cartão de crédito"
? null

View File

@@ -159,11 +159,19 @@ export default async function Page() {
<div className="container">
<div className="mx-auto max-w-6xl">
<Image
src="/dashboard-preview.png"
src="/dashboard-preview-light.png"
alt="opensheets Dashboard Preview"
width={1920}
height={1080}
className="w-full h-auto"
className="w-full h-auto dark:hidden"
priority
/>
<Image
src="/dashboard-preview-dark.png"
alt="opensheets Dashboard Preview"
width={1920}
height={1080}
className="w-full h-auto hidden dark:block"
priority
/>
</div>

View File

@@ -90,11 +90,11 @@
.dark {
/* Base surfaces - true dark with minimal saturation */
--background: oklch(18.674% 0.00002 271.152);
--foreground: oklch(85.505% 0.02038 100.68);
--foreground: oklch(92.189% 0.0186 103.516);
--card: oklch(24.039% 0.00151 16.27);
--card-foreground: oklch(85.505% 0.02038 100.68);
--card-foreground: oklch(92.189% 0.0186 103.516);
--popover: oklch(24.039% 0.00151 16.27);
--popover-foreground: oklch(85.196% 0.0204 100.682);
--popover-foreground: oklch(92.189% 0.0186 103.516);
/* Primary - vibrant terracotta stands out on dark */
--primary: oklch(69.18% 0.18855 38.353);
@@ -102,7 +102,7 @@
/* Secondary - elevated surface */
--secondary: oklch(22% 0.004 285);
--secondary-foreground: oklch(85.196% 0.0204 100.682);
--secondary-foreground: oklch(92.189% 0.0186 103.516);
/* Muted - subtle surface variant */
--muted: oklch(33.674% 0.00531 91.552);
@@ -110,7 +110,7 @@
/* Accent - subtle highlight */
--accent: oklch(26.893% 0.00391 84.539);
--accent-foreground: oklch(85.196% 0.0204 100.682);
--accent-foreground: oklch(92.189% 0.0186 103.516);
/* Destructive - accessible red for dark */
--destructive: oklch(62% 0.2 28);
@@ -131,11 +131,11 @@
/* Sidebar - slight separation from main */
--sidebar: oklch(24.039% 0.00151 16.27);
--sidebar-foreground: oklch(85.196% 0.0204 100.682);
--sidebar-foreground: oklch(92.189% 0.0186 103.516);
--sidebar-primary: oklch(69.18% 0.18855 38.353);
--sidebar-primary-foreground: oklch(12.897% 0.00619 87.19);
--sidebar-accent: oklch(32.242% 0.00447 67.486);
--sidebar-accent-foreground: oklch(85.196% 0.0204 100.682);
--sidebar-accent-foreground: oklch(92.189% 0.0186 103.516);
--sidebar-border: oklch(26% 0.004 285);
--sidebar-ring: oklch(69.18% 0.18855 38.353);
@@ -162,11 +162,11 @@
/* Special components */
--month-picker: oklch(24.039% 0.00151 16.27);
--month-picker-foreground: oklch(85.196% 0.0204 100.682);
--dark: oklch(85.196% 0.0204 100.682);
--month-picker-foreground: oklch(92.189% 0.0186 103.516);
--dark: oklch(92.189% 0.0186 103.516);
--dark-foreground: oklch(18.711% 0.00427 84.566);
--welcome-banner: oklch(24.039% 0.00151 16.27);
--welcome-banner-foreground: oklch(85.196% 0.0204 100.682);
--welcome-banner-foreground: oklch(92.189% 0.0186 103.516);
}
@theme inline {

View File

@@ -1,10 +1,10 @@
import { ThemeProvider } from "@/components/theme-provider";
import { Toaster } from "@/components/ui/sonner";
import { main_font } from "@/public/fonts/font_index";
import type { Metadata } from "next";
import "./globals.css";
import { Analytics } from "@vercel/analytics/next";
import { SpeedInsights } from "@vercel/speed-insights/next";
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "Opensheets",
@@ -22,7 +22,7 @@ export default function RootLayout({
<meta name="apple-mobile-web-app-title" content="Opensheets" />
</head>
<body
className={`${main_font.className} antialiased`}
className={`${main_font.className} antialiased `}
suppressHydrationWarning
>
<ThemeProvider attribute="class" defaultTheme="light">