Files
openmonetis/lib/dashboard/income-expense-balance.ts
Felipe Coutinho fd817683ca 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)
2026-01-03 14:18:03 +00:00

146 lines
3.8 KiB
TypeScript

import { lancamentos, pagadores, contas } from "@/db/schema";
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX, INITIAL_BALANCE_NOTE } from "@/lib/accounts/constants";
import { db } from "@/lib/db";
import { toNumber } from "@/lib/dashboard/common";
import { and, eq, sql, or, isNull, ne } from "drizzle-orm";
export type MonthData = {
month: string;
monthLabel: string;
income: number;
expense: number;
balance: number;
};
export type IncomeExpenseBalanceData = {
months: MonthData[];
};
const MONTH_LABELS: Record<string, string> = {
"01": "jan",
"02": "fev",
"03": "mar",
"04": "abr",
"05": "mai",
"06": "jun",
"07": "jul",
"08": "ago",
"09": "set",
"10": "out",
"11": "nov",
"12": "dez",
};
const generateLast6Months = (currentPeriod: string): string[] => {
const [yearStr, monthStr] = currentPeriod.split("-");
let year = Number.parseInt(yearStr ?? "", 10);
let month = Number.parseInt(monthStr ?? "", 10);
if (Number.isNaN(year) || Number.isNaN(month)) {
const now = new Date();
year = now.getFullYear();
month = now.getMonth() + 1;
}
const periods: string[] = [];
for (let i = 5; i >= 0; i--) {
let targetMonth = month - i;
let targetYear = year;
while (targetMonth <= 0) {
targetMonth += 12;
targetYear -= 1;
}
periods.push(`${targetYear}-${String(targetMonth).padStart(2, "0")}`);
}
return periods;
};
export async function fetchIncomeExpenseBalance(
userId: string,
currentPeriod: string
): Promise<IncomeExpenseBalanceData> {
const periods = generateLast6Months(currentPeriod);
const results = await Promise.all(
periods.map(async (period) => {
// Busca receitas do período
const [incomeRow] = await db
.select({
total: sql<number>`
coalesce(
sum(${lancamentos.amount}),
0
)
`,
})
.from(lancamentos)
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
.leftJoin(contas, eq(lancamentos.contaId, contas.id))
.where(
and(
eq(lancamentos.userId, userId),
eq(lancamentos.period, period),
eq(lancamentos.transactionType, "Receita"),
eq(pagadores.role, "admin"),
sql`(${lancamentos.note} IS NULL OR ${
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)
)
)
);
// Busca despesas do período
const [expenseRow] = await db
.select({
total: sql<number>`
coalesce(
sum(${lancamentos.amount}),
0
)
`,
})
.from(lancamentos)
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
.where(
and(
eq(lancamentos.userId, userId),
eq(lancamentos.period, period),
eq(lancamentos.transactionType, "Despesa"),
eq(pagadores.role, "admin"),
sql`(${lancamentos.note} IS NULL OR ${
lancamentos.note
} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`})`
)
);
const income = Math.abs(toNumber(incomeRow?.total));
const expense = Math.abs(toNumber(expenseRow?.total));
const balance = income - expense;
const [, monthPart] = period.split("-");
const monthLabel = MONTH_LABELS[monthPart ?? "01"] ?? monthPart;
return {
month: period,
monthLabel: monthLabel ?? "",
income,
expense,
balance,
};
})
);
return {
months: results,
};
}