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

@@ -35,6 +35,8 @@ import { Textarea } from "@/components/ui/textarea";
import { useControlledState } from "@/hooks/use-controlled-state";
import { useFormState } from "@/hooks/use-form-state";
import type { EligibleInstallment } from "@/lib/installments/anticipation-types";
import type { PeriodPreferences } from "@/lib/user-preferences/period";
import { createMonthOptions } from "@/lib/utils/period";
import { RiLoader4Line } from "@remixicon/react";
import {
useCallback,
@@ -53,6 +55,7 @@ interface AnticipateInstallmentsDialogProps {
categorias: Array<{ id: string; name: string; icon: string | null }>;
pagadores: Array<{ id: string; name: string }>;
defaultPeriod: string;
periodPreferences: PeriodPreferences;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}
@@ -65,57 +68,6 @@ type AnticipationFormValues = {
note: string;
};
type SelectOption = {
value: string;
label: string;
};
const monthFormatter = new Intl.DateTimeFormat("pt-BR", {
month: "long",
year: "numeric",
});
const formatPeriodLabel = (period: string) => {
const [year, month] = period.split("-").map(Number);
if (!year || !month) {
return period;
}
const date = new Date(year, month - 1, 1);
if (Number.isNaN(date.getTime())) {
return period;
}
const label = monthFormatter.format(date);
return label.charAt(0).toUpperCase() + label.slice(1);
};
const buildPeriodOptions = (currentValue?: string): SelectOption[] => {
const now = new Date();
const options: SelectOption[] = [];
// Adiciona opções de 3 meses no passado até 6 meses no futuro
for (let offset = -3; offset <= 6; offset += 1) {
const date = new Date(now.getFullYear(), now.getMonth() + offset, 1);
const value = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(
2,
"0"
)}`;
options.push({ value, label: formatPeriodLabel(value) });
}
// Adiciona o valor atual se não estiver na lista
if (
currentValue &&
!options.some((option) => option.value === currentValue)
) {
options.push({
value: currentValue,
label: formatPeriodLabel(currentValue),
});
}
return options.sort((a, b) => a.value.localeCompare(b.value));
};
export function AnticipateInstallmentsDialog({
trigger,
seriesId,
@@ -123,6 +75,7 @@ export function AnticipateInstallmentsDialog({
categorias,
pagadores,
defaultPeriod,
periodPreferences,
open,
onOpenChange,
}: AnticipateInstallmentsDialogProps) {
@@ -152,8 +105,13 @@ export function AnticipateInstallmentsDialog({
});
const periodOptions = useMemo(
() => buildPeriodOptions(formState.anticipationPeriod),
[formState.anticipationPeriod]
() =>
createMonthOptions(
formState.anticipationPeriod,
periodPreferences.monthsBefore,
periodPreferences.monthsAfter
),
[formState.anticipationPeriod, periodPreferences.monthsBefore, periodPreferences.monthsAfter]
);
// Buscar parcelas elegíveis ao abrir o dialog

View File

@@ -110,10 +110,14 @@ export function LancamentoDetailsDialog({
<span className="capitalize">
<Badge
variant={getTransactionBadgeVariant(
lancamento.transactionType
lancamento.categoriaName === "Saldo inicial"
? "Saldo inicial"
: lancamento.transactionType
)}
>
{lancamento.transactionType}
{lancamento.categoriaName === "Saldo inicial"
? "Saldo Inicial"
: lancamento.transactionType}
</Badge>
</span>
</li>

View File

@@ -1,4 +1,5 @@
import type { LancamentoFormState } from "@/lib/lancamentos/form-helpers";
import type { PeriodPreferences } from "@/lib/user-preferences/period";
import type { LancamentoItem, SelectOption } from "../../types";
export type FormState = LancamentoFormState;
@@ -17,6 +18,7 @@ export interface LancamentoDialogProps {
estabelecimentos: string[];
lancamento?: LancamentoItem;
defaultPeriod?: string;
periodPreferences: PeriodPreferences;
defaultCartaoId?: string | null;
defaultPaymentMethod?: string | null;
defaultPurchaseDate?: string | null;

View File

@@ -58,6 +58,7 @@ export function LancamentoDialog({
estabelecimentos,
lancamento,
defaultPeriod,
periodPreferences,
defaultCartaoId,
defaultPaymentMethod,
defaultPurchaseDate,
@@ -125,8 +126,13 @@ export function LancamentoDialog({
}, [categoriaOptions, formState.transactionType]);
const monthOptions = useMemo(
() => createMonthOptions(formState.period),
[formState.period]
() =>
createMonthOptions(
formState.period,
periodPreferences.monthsBefore,
periodPreferences.monthsAfter
),
[formState.period, periodPreferences.monthsBefore, periodPreferences.monthsAfter]
);
const handleFieldChange = useCallback(

View File

@@ -31,8 +31,18 @@ export function PaymentMethodSection({
"Dinheiro",
"Boleto",
"Cartão de débito",
"Pré-Pago | VR/VA",
"Transferência bancária",
].includes(formState.paymentMethod);
// Filtrar contas apenas do tipo "Pré-Pago | VR/VA" quando forma de pagamento for "Pré-Pago | VR/VA"
const filteredContaOptions =
formState.paymentMethod === "Pré-Pago | VR/VA"
? contaOptions.filter(
(option) => option.accountType === "Pré-Pago | VR/VA"
)
: contaOptions;
return (
<>
{!isUpdateMode ? (
@@ -56,7 +66,9 @@ export function PaymentMethodSection({
>
<SelectValue placeholder="Selecione" className="w-full">
{formState.paymentMethod && (
<PaymentMethodSelectContent label={formState.paymentMethod} />
<PaymentMethodSelectContent
label={formState.paymentMethod}
/>
)}
</SelectValue>
</SelectTrigger>
@@ -138,7 +150,7 @@ export function PaymentMethodSection({
<SelectValue placeholder="Selecione">
{formState.contaId &&
(() => {
const selectedOption = contaOptions.find(
const selectedOption = filteredContaOptions.find(
(opt) => opt.value === formState.contaId
);
return selectedOption ? (
@@ -152,14 +164,14 @@ export function PaymentMethodSection({
</SelectValue>
</SelectTrigger>
<SelectContent>
{contaOptions.length === 0 ? (
{filteredContaOptions.length === 0 ? (
<div className="px-2 py-6 text-center">
<p className="text-sm text-muted-foreground">
Nenhuma conta cadastrada
</p>
</div>
) : (
contaOptions.map((option) => (
filteredContaOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<ContaCartaoSelectContent
label={option.label}
@@ -246,7 +258,7 @@ export function PaymentMethodSection({
<SelectValue placeholder="Selecione">
{formState.contaId &&
(() => {
const selectedOption = contaOptions.find(
const selectedOption = filteredContaOptions.find(
(opt) => opt.value === formState.contaId
);
return selectedOption ? (
@@ -260,14 +272,14 @@ export function PaymentMethodSection({
</SelectValue>
</SelectTrigger>
<SelectContent>
{contaOptions.length === 0 ? (
{filteredContaOptions.length === 0 ? (
<div className="px-2 py-6 text-center">
<p className="text-sm text-muted-foreground">
Nenhuma conta cadastrada
</p>
</div>
) : (
contaOptions.map((option) => (
filteredContaOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<ContaCartaoSelectContent
label={option.label}

View File

@@ -27,6 +27,7 @@ import { groupAndSortCategorias } from "@/lib/lancamentos/categoria-helpers";
import { LANCAMENTO_PAYMENT_METHODS } from "@/lib/lancamentos/constants";
import { getTodayDateString } from "@/lib/utils/date";
import { createMonthOptions } from "@/lib/utils/period";
import type { PeriodPreferences } from "@/lib/user-preferences/period";
import { RiAddLine, RiDeleteBinLine } from "@remixicon/react";
import { useMemo, useState } from "react";
import { toast } from "sonner";
@@ -51,6 +52,7 @@ interface MassAddDialogProps {
categoriaOptions: SelectOption[];
estabelecimentos: string[];
selectedPeriod: string;
periodPreferences: PeriodPreferences;
defaultPagadorId?: string | null;
}
@@ -91,6 +93,7 @@ export function MassAddDialog({
categoriaOptions,
estabelecimentos,
selectedPeriod,
periodPreferences,
defaultPagadorId,
}: MassAddDialogProps) {
const [loading, setLoading] = useState(false);
@@ -119,8 +122,13 @@ export function MassAddDialog({
// Period options
const periodOptions = useMemo(
() => createMonthOptions(selectedPeriod, 3),
[selectedPeriod]
() =>
createMonthOptions(
selectedPeriod,
periodPreferences.monthsBefore,
periodPreferences.monthsAfter
),
[selectedPeriod, periodPreferences.monthsBefore, periodPreferences.monthsAfter]
);
// Categorias agrupadas e filtradas por tipo de transação