feat: implementar sistema de preferências do usuário e refatorar changelog

Adiciona sistema completo de preferências de usuário:
  - Cria tabela userPreferences no schema com campos disableMagnetlines, periodMonthsBefore e periodMonthsAfter
  - Implementa página de Ajustes com abas (Preferências, Alterar nome, Senha, E-mail, Deletar conta)
  - Adiciona componente PreferencesForm para configuração de magnetlines e períodos de exibição
  - Propaga periodPreferences para todos os componentes de lançamentos e calendário

  Refatora sistema de changelog:
  - Remove implementação anterior baseada em JSON estático
  - Adiciona nova página de changelog dinâmica em app/(dashboard)/changelog
  - Adiciona componente changelog-list.tsx
  - Remove arquivos obsoletos (changelog-notification, actions, data, utils, scripts)

  Adiciona controle de saldo inicial em contas:
  - Novo campo excludeInitialBalanceFromIncome em contas
  - Permite excluir saldo inicial do cálculo de receitas
  - Atualiza queries de lançamentos para respeitar esta configuração

  Melhorias adicionais:
  - Adiciona componente ui/accordion.tsx do shadcn/ui
  - Refatora formatPeriodLabel para displayPeriod centralizado
  - Propaga estabelecimentos para componentes de lançamentos
  - Remove variável DB_PROVIDER obsoleta do .env.example e documentação
  - Adiciona 6 migrações de banco de dados (0003-0008)
This commit is contained in:
Felipe Coutinho
2026-01-03 14:18:03 +00:00
parent 3eca48c71a
commit fd817683ca
87 changed files with 13582 additions and 1445 deletions

View File

@@ -1,12 +1,12 @@
import { categorias, lancamentos, pagadores } from "@/db/schema";
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/accounts/constants";
import { categorias, lancamentos, pagadores, contas } from "@/db/schema";
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX, INITIAL_BALANCE_NOTE } from "@/lib/accounts/constants";
import type { CategoryType } from "@/lib/categorias/constants";
import { toNumber } from "@/lib/dashboard/common";
import { db } from "@/lib/db";
import { mapLancamentosData } from "@/lib/lancamentos/page-helpers";
import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants";
import { getPreviousPeriod } from "@/lib/utils/period";
import { and, desc, eq, isNull, or, sql } from "drizzle-orm";
import { and, desc, eq, isNull, or, sql, ne } from "drizzle-orm";
type MappedLancamentos = ReturnType<typeof mapLancamentosData>;
@@ -81,7 +81,17 @@ export async function fetchCategoryDetails(
});
const filteredRows = currentRows.filter(
(row) => row.pagador?.role === PAGADOR_ROLE_ADMIN
(row) => {
// Filtrar apenas pagadores admin
if (row.pagador?.role !== PAGADOR_ROLE_ADMIN) return false;
// Excluir saldos iniciais se a conta tiver o flag ativo
if (row.note === INITIAL_BALANCE_NOTE && row.conta?.excludeInitialBalanceFromIncome) {
return false;
}
return true;
}
);
const transactions = mapLancamentosData(filteredRows);
@@ -97,6 +107,7 @@ export async function fetchCategoryDetails(
})
.from(lancamentos)
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
.leftJoin(contas, eq(lancamentos.contaId, contas.id))
.where(
and(
eq(lancamentos.userId, userId),
@@ -104,7 +115,13 @@ export async function fetchCategoryDetails(
eq(lancamentos.transactionType, transactionType),
eq(pagadores.role, PAGADOR_ROLE_ADMIN),
sanitizedNote,
eq(lancamentos.period, previousPeriod)
eq(lancamentos.period, previousPeriod),
// Excluir saldos iniciais se a conta tiver o flag ativo
or(
ne(lancamentos.note, INITIAL_BALANCE_NOTE),
isNull(contas.excludeInitialBalanceFromIncome),
eq(contas.excludeInitialBalanceFromIncome, false)
)
)
);

View File

@@ -65,14 +65,15 @@ export async function fetchCategoryHistory(
userId: string,
currentPeriod: string
): Promise<CategoryHistoryData> {
// Generate last 6 months including current
// Generate last 8 months, current month, and next month (10 total)
const periods: string[] = [];
const monthLabels: string[] = [];
const [year, month] = currentPeriod.split("-").map(Number);
const currentDate = new Date(year, month - 1, 1);
for (let i = 8; i >= 0; i--) {
// Generate months from -8 to +1 (relative to current)
for (let i = 8; i >= -1; i--) {
const date = addMonths(currentDate, -i);
const period = format(date, "yyyy-MM");
const label = format(date, "MMM", { locale: ptBR }).toUpperCase();

View File

@@ -1,11 +1,11 @@
import { categorias, lancamentos, orcamentos, pagadores } from "@/db/schema";
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/accounts/constants";
import { categorias, lancamentos, orcamentos, pagadores, contas } from "@/db/schema";
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX, INITIAL_BALANCE_NOTE } from "@/lib/accounts/constants";
import { db } from "@/lib/db";
import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants";
import { getPreviousPeriod } from "@/lib/utils/period";
import { calculatePercentageChange } from "@/lib/utils/math";
import { safeToNumber } from "@/lib/utils/number";
import { and, eq, isNull, or, sql } from "drizzle-orm";
import { and, eq, isNull, or, sql, ne } from "drizzle-orm";
export type CategoryIncomeItem = {
categoryId: string;
@@ -43,6 +43,7 @@ export async function fetchIncomeByCategory(
.from(lancamentos)
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
.innerJoin(categorias, eq(lancamentos.categoriaId, categorias.id))
.leftJoin(contas, eq(lancamentos.contaId, contas.id))
.leftJoin(
orcamentos,
and(
@@ -61,6 +62,12 @@ export async function fetchIncomeByCategory(
or(
isNull(lancamentos.note),
sql`${lancamentos.note} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`
),
// Excluir saldos iniciais se a conta tiver o flag ativo
or(
ne(lancamentos.note, INITIAL_BALANCE_NOTE),
isNull(contas.excludeInitialBalanceFromIncome),
eq(contas.excludeInitialBalanceFromIncome, false)
)
)
)
@@ -75,6 +82,7 @@ export async function fetchIncomeByCategory(
.from(lancamentos)
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
.innerJoin(categorias, eq(lancamentos.categoriaId, categorias.id))
.leftJoin(contas, eq(lancamentos.contaId, contas.id))
.where(
and(
eq(lancamentos.userId, userId),
@@ -85,6 +93,12 @@ export async function fetchIncomeByCategory(
or(
isNull(lancamentos.note),
sql`${lancamentos.note} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`
),
// Excluir saldos iniciais se a conta tiver o flag ativo
or(
ne(lancamentos.note, INITIAL_BALANCE_NOTE),
isNull(contas.excludeInitialBalanceFromIncome),
eq(contas.excludeInitialBalanceFromIncome, false)
)
)
)