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,76 +0,0 @@
"use server";
import { userUpdateLog } from "@/db/schema";
import { successResult, type ActionResult } from "@/lib/actions/types";
import { getUser } from "@/lib/auth/server";
import { db } from "@/lib/db";
import { and, eq } from "drizzle-orm";
import { handleActionError } from "../actions/helpers";
export async function markUpdateAsRead(
updateId: string
): Promise<ActionResult> {
try {
const user = await getUser();
// Check if already marked as read
const existing = await db
.select()
.from(userUpdateLog)
.where(
and(
eq(userUpdateLog.userId, user.id),
eq(userUpdateLog.updateId, updateId)
)
)
.limit(1);
if (existing.length > 0) {
return successResult("Já marcado como lido");
}
await db.insert(userUpdateLog).values({
userId: user.id,
updateId,
});
return successResult("Marcado como lido");
} catch (error) {
return handleActionError(error);
}
}
export async function markAllUpdatesAsRead(
updateIds: string[]
): Promise<ActionResult> {
try {
const user = await getUser();
// Get existing read updates
const existing = await db
.select()
.from(userUpdateLog)
.where(eq(userUpdateLog.userId, user.id));
const existingIds = new Set(existing.map((log) => log.updateId));
// Filter out already read updates
const newUpdateIds = updateIds.filter((id) => !existingIds.has(id));
if (newUpdateIds.length === 0) {
return successResult("Todos já marcados como lidos");
}
// Insert new read logs
await db.insert(userUpdateLog).values(
newUpdateIds.map((updateId) => ({
userId: user.id,
updateId,
}))
);
return successResult("Todas as atualizações marcadas como lidas");
} catch (error) {
return handleActionError(error);
}
}

View File

@@ -1,75 +0,0 @@
import { db } from "@/lib/db";
import { userUpdateLog } from "@/db/schema";
import { eq } from "drizzle-orm";
import fs from "fs";
import path from "path";
export interface ChangelogEntry {
id: string;
type: string;
title: string;
date: string;
icon: string;
category: string;
}
export interface Changelog {
version: string;
generatedAt: string;
entries: ChangelogEntry[];
}
export function getChangelog(): Changelog {
try {
const changelogPath = path.join(process.cwd(), "public", "changelog.json");
if (!fs.existsSync(changelogPath)) {
return {
version: "1.0.0",
generatedAt: new Date().toISOString(),
entries: [],
};
}
const content = fs.readFileSync(changelogPath, "utf-8");
return JSON.parse(content);
} catch (error) {
console.error("Error reading changelog:", error);
return {
version: "1.0.0",
generatedAt: new Date().toISOString(),
entries: [],
};
}
}
export async function getUnreadUpdates(userId: string) {
const changelog = getChangelog();
if (changelog.entries.length === 0) {
return {
unreadCount: 0,
unreadEntries: [],
allEntries: [],
};
}
// Get read updates from database
const readLogs = await db
.select()
.from(userUpdateLog)
.where(eq(userUpdateLog.userId, userId));
const readUpdateIds = new Set(readLogs.map((log) => log.updateId));
// Filter unread entries
const unreadEntries = changelog.entries.filter(
(entry) => !readUpdateIds.has(entry.id)
);
return {
unreadCount: unreadEntries.length,
unreadEntries,
allEntries: changelog.entries,
};
}

View File

@@ -1,43 +0,0 @@
import type { ChangelogEntry } from "./data";
/**
* Converte uma string de data para um formato compatível com Safari.
* Safari não aceita "YYYY-MM-DD HH:mm:ss ±HHMM", requer "YYYY-MM-DDTHH:mm:ss±HHMM"
*
* @param dateString - String de data no formato "YYYY-MM-DD HH:mm:ss ±HHMM"
* @returns Date object válido
*/
export function parseSafariCompatibleDate(dateString: string): Date {
// Substitui o espaço entre data e hora por "T" (formato ISO 8601)
// Exemplo: "2025-12-09 17:26:08 +0000" → "2025-12-09T17:26:08+0000"
const isoString = dateString.replace(/(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})\s+/, "$1T$2");
return new Date(isoString);
}
export function getCategoryLabel(category: string): string {
const labels: Record<string, string> = {
feature: "Novidades",
bugfix: "Correções",
performance: "Performance",
documentation: "Documentação",
style: "Interface",
refactor: "Melhorias",
test: "Testes",
chore: "Manutenção",
other: "Outros",
};
return labels[category] || "Outros";
}
export function groupEntriesByCategory(entries: ChangelogEntry[]) {
return entries.reduce(
(acc, entry) => {
if (!acc[entry.category]) {
acc[entry.category] = [];
}
acc[entry.category].push(entry);
return acc;
},
{} as Record<string, ChangelogEntry[]>
);
}