diff --git a/CHANGELOG.md b/CHANGELOG.md index a834dfc..d29c360 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ Todas as mudanças notáveis deste projeto serão documentadas neste arquivo. O formato é baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/), e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR/). +## [Unreleased] + +## [1.7.7] - 2026-03-05 + +### Alterado + +- Períodos e navegação mensal: `useMonthPeriod` passou a usar os helpers centrais de período (`YYYY-MM`), o month-picker foi simplificado e o rótulo visual agora segue o formato `Março 2026`. +- Hooks e organização: hooks locais de calculadora, month-picker, logo picker e sidebar foram movidos para perto das respectivas features, deixando `/hooks` focado nos hooks realmente compartilhados. +- Estado de formulários e responsividade: `useFormState` ganhou APIs explícitas de reset/substituição no lugar do setter cru, e `useIsMobile` foi atualizado para assinatura estável com `useSyncExternalStore`, reduzindo a troca estrutural inicial no sidebar entre mobile e desktop. +- Navegação e estrutura compartilhada: `components/navbar` e `components/sidebar` foram consolidados em `components/navigation/*`, componentes globais migraram para `components/shared/*` e os imports foram padronizados no projeto. +- Dashboard e relatórios: a análise de parcelas foi movida para `/relatorios/analise-parcelas`, ações rápidas e widgets do dashboard foram refinados, e os cards de relatórios ganharam ajustes para evitar overflow no mobile. +- Pré-lançamentos e lançamentos: tabs e cards da inbox ficaram mais consistentes no mobile, itens descartados podem voltar para `Pendente` e compras feitas no dia do fechamento do cartão agora entram na próxima fatura. +- Tipografia e exportações: suporte a `SF Pro` foi removido, a validação de fontes ficou centralizada em `public/fonts/font_index.ts` e as exportações em PDF/CSV/Excel receberam melhor branding e apresentação. +- Calculadora e diálogos: o arraste ficou mais estável, os bloqueios de fechamento externo foram reforçados e o display interno foi reorganizado para uso mais consistente. +- Também houve ajustes menores de responsividade, espaçamento e acabamento visual em telas mobile, modais e detalhes de interface. + ## [1.7.6] - 2026-03-02 ### Adicionado diff --git a/app/(dashboard)/ajustes/actions.ts b/app/(dashboard)/ajustes/actions.ts index b60bc19..8b6229a 100644 --- a/app/(dashboard)/ajustes/actions.ts +++ b/app/(dashboard)/ajustes/actions.ts @@ -10,6 +10,7 @@ import { account, pagadores, tokensApi } from "@/db/schema"; import { auth } from "@/lib/auth/config"; import { db, schema } from "@/lib/db"; import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants"; +import { DEFAULT_FONT_KEY, FONT_KEYS } from "@/public/fonts/font_index"; type ActionResponse = { success: boolean; @@ -52,28 +53,12 @@ const deleteAccountSchema = z.object({ }), }); -const VALID_FONTS = [ - "ai-sans", - "anthropic-sans", - "fira-code", - "fira-sans", - "geist", - "ibm-plex-mono", - "inter", - "jetbrains-mono", - "reddit-sans", - "roboto", - "sf-pro-display", - "sf-pro-rounded", - "ubuntu", -] as const; - const updatePreferencesSchema = z.object({ disableMagnetlines: z.boolean(), extratoNoteAsColumn: z.boolean(), lancamentosColumnOrder: z.array(z.string()).nullable(), - systemFont: z.enum(VALID_FONTS).default("ai-sans"), - moneyFont: z.enum(VALID_FONTS).default("ai-sans"), + systemFont: z.enum(FONT_KEYS).default(DEFAULT_FONT_KEY), + moneyFont: z.enum(FONT_KEYS).default(DEFAULT_FONT_KEY), }); // Actions diff --git a/app/(dashboard)/ajustes/data.ts b/app/(dashboard)/ajustes/data.ts index 3f19f56..5c92561 100644 --- a/app/(dashboard)/ajustes/data.ts +++ b/app/(dashboard)/ajustes/data.ts @@ -1,13 +1,14 @@ import { desc, eq } from "drizzle-orm"; import { tokensApi } from "@/db/schema"; import { db, schema } from "@/lib/db"; +import { type FontKey, normalizeFontKey } from "@/public/fonts/font_index"; export interface UserPreferences { disableMagnetlines: boolean; extratoNoteAsColumn: boolean; lancamentosColumnOrder: string[] | null; - systemFont: string; - moneyFont: string; + systemFont: FontKey; + moneyFont: FontKey; } export interface ApiToken { @@ -43,7 +44,13 @@ export async function fetchUserPreferences( .where(eq(schema.preferenciasUsuario.userId, userId)) .limit(1); - return result[0] || null; + if (!result[0]) return null; + + return { + ...result[0], + systemFont: normalizeFontKey(result[0].systemFont), + moneyFont: normalizeFontKey(result[0].moneyFont), + }; } export async function fetchApiTokens(userId: string): Promise { diff --git a/app/(dashboard)/ajustes/layout.tsx b/app/(dashboard)/ajustes/layout.tsx index 4c4099e..4ca3c5b 100644 --- a/app/(dashboard)/ajustes/layout.tsx +++ b/app/(dashboard)/ajustes/layout.tsx @@ -1,5 +1,5 @@ import { RiSettings2Line } from "@remixicon/react"; -import PageDescription from "@/components/page-description"; +import PageDescription from "@/components/shared/page-description"; export const metadata = { title: "Ajustes | OpenMonetis", diff --git a/app/(dashboard)/ajustes/page.tsx b/app/(dashboard)/ajustes/page.tsx index c575cf8..269aa2a 100644 --- a/app/(dashboard)/ajustes/page.tsx +++ b/app/(dashboard)/ajustes/page.tsx @@ -12,6 +12,7 @@ import { UpdatePasswordForm } from "@/components/ajustes/update-password-form"; import { Card } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { auth } from "@/lib/auth/config"; +import { DEFAULT_FONT_KEY } from "@/public/fonts/font_index"; import { fetchAjustesPageData } from "./data"; export default async function Page() { @@ -75,8 +76,8 @@ export default async function Page() { lancamentosColumnOrder={ userPreferences?.lancamentosColumnOrder ?? null } - systemFont={userPreferences?.systemFont ?? "ai-sans"} - moneyFont={userPreferences?.moneyFont ?? "ai-sans"} + systemFont={userPreferences?.systemFont ?? DEFAULT_FONT_KEY} + moneyFont={userPreferences?.moneyFont ?? DEFAULT_FONT_KEY} /> diff --git a/app/(dashboard)/anotacoes/layout.tsx b/app/(dashboard)/anotacoes/layout.tsx index 0a25dbe..dbcf8b7 100644 --- a/app/(dashboard)/anotacoes/layout.tsx +++ b/app/(dashboard)/anotacoes/layout.tsx @@ -1,5 +1,5 @@ import { RiTodoLine } from "@remixicon/react"; -import PageDescription from "@/components/page-description"; +import PageDescription from "@/components/shared/page-description"; export const metadata = { title: "Anotações | OpenMonetis", diff --git a/app/(dashboard)/calendario/layout.tsx b/app/(dashboard)/calendario/layout.tsx index 4686a63..6c8195a 100644 --- a/app/(dashboard)/calendario/layout.tsx +++ b/app/(dashboard)/calendario/layout.tsx @@ -1,5 +1,5 @@ import { RiCalendarEventLine } from "@remixicon/react"; -import PageDescription from "@/components/page-description"; +import PageDescription from "@/components/shared/page-description"; export const metadata = { title: "Calendário | OpenMonetis", diff --git a/app/(dashboard)/cartoes/[cartaoId]/fatura/actions.ts b/app/(dashboard)/cartoes/[cartaoId]/fatura/actions.ts index 3ad9cab..9a1e870 100644 --- a/app/(dashboard)/cartoes/[cartaoId]/fatura/actions.ts +++ b/app/(dashboard)/cartoes/[cartaoId]/fatura/actions.ts @@ -9,9 +9,9 @@ import { lancamentos, pagadores, } from "@/db/schema"; -import { buildInvoicePaymentNote } from "@/lib/accounts/constants"; import { revalidateForEntity } from "@/lib/actions/helpers"; import { getUser } from "@/lib/auth/server"; +import { buildInvoicePaymentNote } from "@/lib/contas/constants"; import { db } from "@/lib/db"; import { INVOICE_PAYMENT_STATUS, diff --git a/app/(dashboard)/cartoes/[cartaoId]/fatura/data.ts b/app/(dashboard)/cartoes/[cartaoId]/fatura/data.ts index 6c5322d..6162543 100644 --- a/app/(dashboard)/cartoes/[cartaoId]/fatura/data.ts +++ b/app/(dashboard)/cartoes/[cartaoId]/fatura/data.ts @@ -1,6 +1,6 @@ import { and, desc, eq, type SQL, sum } from "drizzle-orm"; import { cartoes, faturas, lancamentos } from "@/db/schema"; -import { buildInvoicePaymentNote } from "@/lib/accounts/constants"; +import { buildInvoicePaymentNote } from "@/lib/contas/constants"; import { db } from "@/lib/db"; import { INVOICE_PAYMENT_STATUS, diff --git a/app/(dashboard)/cartoes/[cartaoId]/fatura/loading.tsx b/app/(dashboard)/cartoes/[cartaoId]/fatura/loading.tsx index 5e09e9a..2e99a3c 100644 --- a/app/(dashboard)/cartoes/[cartaoId]/fatura/loading.tsx +++ b/app/(dashboard)/cartoes/[cartaoId]/fatura/loading.tsx @@ -2,7 +2,7 @@ import { FilterSkeleton, InvoiceSummaryCardSkeleton, TransactionsTableSkeleton, -} from "@/components/skeletons"; +} from "@/components/shared/skeletons"; import { Skeleton } from "@/components/ui/skeleton"; /** diff --git a/app/(dashboard)/cartoes/layout.tsx b/app/(dashboard)/cartoes/layout.tsx index 20cd696..90b73e2 100644 --- a/app/(dashboard)/cartoes/layout.tsx +++ b/app/(dashboard)/cartoes/layout.tsx @@ -1,5 +1,5 @@ import { RiBankCard2Line } from "@remixicon/react"; -import PageDescription from "@/components/page-description"; +import PageDescription from "@/components/shared/page-description"; export const metadata = { title: "Cartões | OpenMonetis", diff --git a/app/(dashboard)/categorias/layout.tsx b/app/(dashboard)/categorias/layout.tsx index bb2e67f..0b6f26d 100644 --- a/app/(dashboard)/categorias/layout.tsx +++ b/app/(dashboard)/categorias/layout.tsx @@ -1,5 +1,5 @@ import { RiPriceTag3Line } from "@remixicon/react"; -import PageDescription from "@/components/page-description"; +import PageDescription from "@/components/shared/page-description"; export const metadata = { title: "Categorias | OpenMonetis", diff --git a/app/(dashboard)/changelog/layout.tsx b/app/(dashboard)/changelog/layout.tsx index 0f86c00..ebf41f6 100644 --- a/app/(dashboard)/changelog/layout.tsx +++ b/app/(dashboard)/changelog/layout.tsx @@ -1,5 +1,5 @@ import { RiHistoryLine } from "@remixicon/react"; -import PageDescription from "@/components/page-description"; +import PageDescription from "@/components/shared/page-description"; export const metadata = { title: "Cartões | OpenMonetis", diff --git a/app/(dashboard)/contas/[contaId]/extrato/data.ts b/app/(dashboard)/contas/[contaId]/extrato/data.ts index 79700d2..06f701a 100644 --- a/app/(dashboard)/contas/[contaId]/extrato/data.ts +++ b/app/(dashboard)/contas/[contaId]/extrato/data.ts @@ -1,6 +1,6 @@ import { and, desc, eq, lt, type SQL, sql } from "drizzle-orm"; import { contas, lancamentos, pagadores } from "@/db/schema"; -import { INITIAL_BALANCE_NOTE } from "@/lib/accounts/constants"; +import { INITIAL_BALANCE_NOTE } from "@/lib/contas/constants"; import { db } from "@/lib/db"; import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants"; diff --git a/app/(dashboard)/contas/[contaId]/extrato/loading.tsx b/app/(dashboard)/contas/[contaId]/extrato/loading.tsx index 02fa036..8d83f4c 100644 --- a/app/(dashboard)/contas/[contaId]/extrato/loading.tsx +++ b/app/(dashboard)/contas/[contaId]/extrato/loading.tsx @@ -2,7 +2,7 @@ import { AccountStatementCardSkeleton, FilterSkeleton, TransactionsTableSkeleton, -} from "@/components/skeletons"; +} from "@/components/shared/skeletons"; import { Skeleton } from "@/components/ui/skeleton"; /** diff --git a/app/(dashboard)/contas/actions.ts b/app/(dashboard)/contas/actions.ts index 1c623be..3e7272b 100644 --- a/app/(dashboard)/contas/actions.ts +++ b/app/(dashboard)/contas/actions.ts @@ -3,19 +3,19 @@ import { and, eq } from "drizzle-orm"; import { z } from "zod"; import { categorias, contas, lancamentos, pagadores } from "@/db/schema"; -import { - INITIAL_BALANCE_CATEGORY_NAME, - INITIAL_BALANCE_CONDITION, - INITIAL_BALANCE_NOTE, - INITIAL_BALANCE_PAYMENT_METHOD, - INITIAL_BALANCE_TRANSACTION_TYPE, -} from "@/lib/accounts/constants"; import { type ActionResult, handleActionError, revalidateForEntity, } from "@/lib/actions/helpers"; import { getUser } from "@/lib/auth/server"; +import { + INITIAL_BALANCE_CATEGORY_NAME, + INITIAL_BALANCE_CONDITION, + INITIAL_BALANCE_NOTE, + INITIAL_BALANCE_PAYMENT_METHOD, + INITIAL_BALANCE_TRANSACTION_TYPE, +} from "@/lib/contas/constants"; import { db } from "@/lib/db"; import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants"; import { noteSchema, uuidSchema } from "@/lib/schemas/common"; diff --git a/app/(dashboard)/contas/data.ts b/app/(dashboard)/contas/data.ts index b1b0d60..3cc927e 100644 --- a/app/(dashboard)/contas/data.ts +++ b/app/(dashboard)/contas/data.ts @@ -1,6 +1,6 @@ import { and, eq, ilike, not, sql } from "drizzle-orm"; import { contas, lancamentos, pagadores } from "@/db/schema"; -import { INITIAL_BALANCE_NOTE } from "@/lib/accounts/constants"; +import { INITIAL_BALANCE_NOTE } from "@/lib/contas/constants"; import { db } from "@/lib/db"; import { loadLogoOptions } from "@/lib/logo/options"; import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants"; diff --git a/app/(dashboard)/contas/layout.tsx b/app/(dashboard)/contas/layout.tsx index 21dfb70..7c80448 100644 --- a/app/(dashboard)/contas/layout.tsx +++ b/app/(dashboard)/contas/layout.tsx @@ -1,5 +1,5 @@ import { RiBankLine } from "@remixicon/react"; -import PageDescription from "@/components/page-description"; +import PageDescription from "@/components/shared/page-description"; export const metadata = { title: "Contas | OpenMonetis", diff --git a/app/(dashboard)/dashboard/loading.tsx b/app/(dashboard)/dashboard/loading.tsx index 40e5877..6d4ed30 100644 --- a/app/(dashboard)/dashboard/loading.tsx +++ b/app/(dashboard)/dashboard/loading.tsx @@ -1,10 +1,6 @@ -import { DashboardGridSkeleton } from "@/components/skeletons"; +import { DashboardGridSkeleton } from "@/components/shared/skeletons"; import { Skeleton } from "@/components/ui/skeleton"; -/** - * Loading state para a página do dashboard - * Estrutura: Welcome Banner → Month Picker → Section Cards → Widget Grid - */ export default function DashboardLoading() { return (
diff --git a/app/(dashboard)/insights/actions.ts b/app/(dashboard)/insights/actions.ts index 4f079bc..73d138d 100644 --- a/app/(dashboard)/insights/actions.ts +++ b/app/(dashboard)/insights/actions.ts @@ -16,8 +16,8 @@ import { orcamentos, pagadores, } from "@/db/schema"; -import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/accounts/constants"; import { getUser } from "@/lib/auth/server"; +import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/contas/constants"; import { db } from "@/lib/db"; import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants"; import { diff --git a/app/(dashboard)/insights/layout.tsx b/app/(dashboard)/insights/layout.tsx index ef8a03a..97d8e7b 100644 --- a/app/(dashboard)/insights/layout.tsx +++ b/app/(dashboard)/insights/layout.tsx @@ -1,5 +1,5 @@ import { RiSparklingLine } from "@remixicon/react"; -import PageDescription from "@/components/page-description"; +import PageDescription from "@/components/shared/page-description"; export const metadata = { title: "Insights | OpenMonetis", diff --git a/app/(dashboard)/lancamentos/actions.ts b/app/(dashboard)/lancamentos/actions.ts index b6364a8..e28b30c 100644 --- a/app/(dashboard)/lancamentos/actions.ts +++ b/app/(dashboard)/lancamentos/actions.ts @@ -10,15 +10,15 @@ import { lancamentos, pagadores, } from "@/db/schema"; +import { handleActionError, revalidateForEntity } from "@/lib/actions/helpers"; +import type { ActionResult } from "@/lib/actions/types"; +import { getUser } from "@/lib/auth/server"; import { INITIAL_BALANCE_CONDITION, INITIAL_BALANCE_NOTE, INITIAL_BALANCE_PAYMENT_METHOD, INITIAL_BALANCE_TRANSACTION_TYPE, -} from "@/lib/accounts/constants"; -import { handleActionError, revalidateForEntity } from "@/lib/actions/helpers"; -import type { ActionResult } from "@/lib/actions/types"; -import { getUser } from "@/lib/auth/server"; +} from "@/lib/contas/constants"; import { db } from "@/lib/db"; import { LANCAMENTO_CONDITIONS, diff --git a/app/(dashboard)/lancamentos/anticipation-actions.ts b/app/(dashboard)/lancamentos/anticipation-actions.ts index 3371abe..8632f71 100644 --- a/app/(dashboard)/lancamentos/anticipation-actions.ts +++ b/app/(dashboard)/lancamentos/anticipation-actions.ts @@ -17,10 +17,10 @@ import { generateAnticipationNote, } from "@/lib/installments/anticipation-helpers"; import type { + InstallmentAnticipationWithRelations, CancelAnticipationInput, CreateAnticipationInput, EligibleInstallment, - InstallmentAnticipationWithRelations, } from "@/lib/installments/anticipation-types"; import { uuidSchema } from "@/lib/schemas/common"; import { formatDecimalForDbRequired } from "@/lib/utils/currency"; @@ -94,7 +94,7 @@ export async function getEligibleInstallmentsAction( }, }); - const eligibleInstallments: EligibleInstallment[] = rows.map((row) => ({ + const eligibleInstallments: EligibleInstallment[] = rows.map((row: any) => ({ id: row.id, name: row.name, amount: row.amount, @@ -110,10 +110,11 @@ export async function getEligibleInstallmentsAction( return { success: true, + message: "Parcelas elegíveis carregadas.", data: eligibleInstallments, }; } catch (error) { - return handleActionError(error); + return handleActionError(error) as ActionResult; } } @@ -154,7 +155,7 @@ export async function createInstallmentAnticipationAction( // 2. Calcular valor total const totalAmountCents = installments.reduce( - (sum, inst) => sum + Number(inst.amount) * 100, + (sum: number, inst: any) => sum + Number(inst.amount) * 100, 0, ); const totalAmount = totalAmountCents / 100; @@ -181,7 +182,7 @@ export async function createInstallmentAnticipationAction( const firstInstallment = installments[0]; // 4. Criar lançamento e antecipação em transação - await db.transaction(async (tx) => { + await db.transaction(async (tx: any) => { // 4.1. Criar o lançamento de antecipação (com desconto aplicado) const [newLancamento] = await tx .insert(lancamentos) @@ -205,7 +206,7 @@ export async function createInstallmentAnticipationAction( note: data.note || generateAnticipationNote( - installments.map((inst) => ({ + installments.map((inst: any) => ({ id: inst.id, name: inst.name, amount: inst.amount, @@ -329,10 +330,13 @@ export async function getInstallmentAnticipationsAction( return { success: true, + message: "Antecipações carregadas.", data: anticipations, }; } catch (error) { - return handleActionError(error); + return handleActionError( + error, + ) as ActionResult; } } @@ -347,7 +351,7 @@ export async function cancelInstallmentAnticipationAction( const user = await getUser(); const data = cancelAnticipationSchema.parse(input); - await db.transaction(async (tx) => { + await db.transaction(async (tx: any) => { // 1. Buscar antecipação usando query builder const anticipationRows = await tx .select({ @@ -469,9 +473,12 @@ export async function getAnticipationDetailsAction( return { success: true, + message: "Detalhes da antecipação carregados.", data: anticipation, }; } catch (error) { - return handleActionError(error); + return handleActionError( + error, + ) as ActionResult; } } diff --git a/app/(dashboard)/lancamentos/data.ts b/app/(dashboard)/lancamentos/data.ts index 27b4efe..2482052 100644 --- a/app/(dashboard)/lancamentos/data.ts +++ b/app/(dashboard)/lancamentos/data.ts @@ -6,7 +6,7 @@ import { lancamentos, pagadores, } from "@/db/schema"; -import { INITIAL_BALANCE_NOTE } from "@/lib/accounts/constants"; +import { INITIAL_BALANCE_NOTE } from "@/lib/contas/constants"; import { db } from "@/lib/db"; export async function fetchLancamentos(filters: SQL[]) { diff --git a/app/(dashboard)/lancamentos/layout.tsx b/app/(dashboard)/lancamentos/layout.tsx index 9087375..4a19089 100644 --- a/app/(dashboard)/lancamentos/layout.tsx +++ b/app/(dashboard)/lancamentos/layout.tsx @@ -1,5 +1,5 @@ import { RiArrowLeftRightLine } from "@remixicon/react"; -import PageDescription from "@/components/page-description"; +import PageDescription from "@/components/shared/page-description"; export const metadata = { title: "Lançamentos | OpenMonetis", diff --git a/app/(dashboard)/lancamentos/loading.tsx b/app/(dashboard)/lancamentos/loading.tsx index 697ec09..2eeb034 100644 --- a/app/(dashboard)/lancamentos/loading.tsx +++ b/app/(dashboard)/lancamentos/loading.tsx @@ -1,7 +1,7 @@ import { FilterSkeleton, TransactionsTableSkeleton, -} from "@/components/skeletons"; +} from "@/components/shared/skeletons"; import { Skeleton } from "@/components/ui/skeleton"; /** diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx index 624c48a..e6593f1 100644 --- a/app/(dashboard)/layout.tsx +++ b/app/(dashboard)/layout.tsx @@ -1,5 +1,5 @@ import { FontProvider } from "@/components/font-provider"; -import { AppNavbar } from "@/components/navbar/app-navbar"; +import { AppNavbar } from "@/components/navigation/navbar/app-navbar"; import { PrivacyProvider } from "@/components/privacy-provider"; import { getUserSession } from "@/lib/auth/server"; import { fetchDashboardNotifications } from "@/lib/dashboard/notifications"; diff --git a/app/(dashboard)/orcamentos/data.ts b/app/(dashboard)/orcamentos/data.ts index 7ae68c4..c4bf984 100644 --- a/app/(dashboard)/orcamentos/data.ts +++ b/app/(dashboard)/orcamentos/data.ts @@ -6,7 +6,7 @@ import { orcamentos, pagadores, } from "@/db/schema"; -import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/accounts/constants"; +import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/contas/constants"; import { db } from "@/lib/db"; import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants"; diff --git a/app/(dashboard)/orcamentos/layout.tsx b/app/(dashboard)/orcamentos/layout.tsx index 2ea292c..3c8f7cd 100644 --- a/app/(dashboard)/orcamentos/layout.tsx +++ b/app/(dashboard)/orcamentos/layout.tsx @@ -1,5 +1,5 @@ import { RiBarChart2Line } from "@remixicon/react"; -import PageDescription from "@/components/page-description"; +import PageDescription from "@/components/shared/page-description"; export const metadata = { title: "Orçamentos | OpenMonetis", diff --git a/app/(dashboard)/pagadores/layout.tsx b/app/(dashboard)/pagadores/layout.tsx index 3398d7f..7c97e11 100644 --- a/app/(dashboard)/pagadores/layout.tsx +++ b/app/(dashboard)/pagadores/layout.tsx @@ -1,5 +1,5 @@ import { RiGroupLine } from "@remixicon/react"; -import PageDescription from "@/components/page-description"; +import PageDescription from "@/components/shared/page-description"; export const metadata = { title: "Pagadores | OpenMonetis", diff --git a/app/(dashboard)/pre-lancamentos/actions.ts b/app/(dashboard)/pre-lancamentos/actions.ts index 39e7032..6ba2f4b 100644 --- a/app/(dashboard)/pre-lancamentos/actions.ts +++ b/app/(dashboard)/pre-lancamentos/actions.ts @@ -1,10 +1,9 @@ "use server"; import { and, eq, inArray } from "drizzle-orm"; -import { revalidatePath } from "next/cache"; import { z } from "zod"; import { preLancamentos } from "@/db/schema"; -import { handleActionError } from "@/lib/actions/helpers"; +import { handleActionError, revalidateForEntity } from "@/lib/actions/helpers"; import type { ActionResult } from "@/lib/actions/types"; import { getUser } from "@/lib/auth/server"; import { db } from "@/lib/db"; @@ -17,6 +16,10 @@ const discardInboxSchema = z.object({ inboxItemId: z.string().uuid("ID do item inválido"), }); +const restoreDiscardedInboxSchema = z.object({ + inboxItemId: z.string().uuid("ID do item inválido"), +}); + const bulkDiscardSchema = z.object({ inboxItemIds: z.array(z.string().uuid()).min(1, "Selecione ao menos um item"), }); @@ -30,9 +33,7 @@ const bulkDeleteInboxSchema = z.object({ }); function revalidateInbox() { - revalidatePath("/pre-lancamentos"); - revalidatePath("/lancamentos"); - revalidatePath("/dashboard"); + revalidateForEntity("inbox"); } /** @@ -166,6 +167,54 @@ export async function bulkDiscardInboxItemsAction( } } +export async function restoreDiscardedInboxItemAction( + input: z.infer, +): Promise { + try { + const user = await getUser(); + const data = restoreDiscardedInboxSchema.parse(input); + + const [item] = await db + .select({ id: preLancamentos.id }) + .from(preLancamentos) + .where( + and( + eq(preLancamentos.id, data.inboxItemId), + eq(preLancamentos.userId, user.id), + eq(preLancamentos.status, "discarded"), + ), + ) + .limit(1); + + if (!item) { + return { + success: false, + error: "Item não encontrado ou não está descartado.", + }; + } + + await db + .update(preLancamentos) + .set({ + status: "pending", + discardedAt: null, + updatedAt: new Date(), + }) + .where( + and( + eq(preLancamentos.id, data.inboxItemId), + eq(preLancamentos.userId, user.id), + ), + ); + + revalidateInbox(); + + return { success: true, message: "Item voltou para pendentes." }; + } catch (error) { + return handleActionError(error); + } +} + export async function deleteInboxItemAction( input: z.infer, ): Promise { diff --git a/app/(dashboard)/pre-lancamentos/layout.tsx b/app/(dashboard)/pre-lancamentos/layout.tsx index 1d01634..d39a2a1 100644 --- a/app/(dashboard)/pre-lancamentos/layout.tsx +++ b/app/(dashboard)/pre-lancamentos/layout.tsx @@ -1,5 +1,5 @@ import { RiAtLine } from "@remixicon/react"; -import PageDescription from "@/components/page-description"; +import PageDescription from "@/components/shared/page-description"; export const metadata = { title: "Pré-Lançamentos | OpenMonetis", diff --git a/app/(dashboard)/dashboard/analise-parcelas/layout.tsx b/app/(dashboard)/relatorios/analise-parcelas/layout.tsx similarity index 87% rename from app/(dashboard)/dashboard/analise-parcelas/layout.tsx rename to app/(dashboard)/relatorios/analise-parcelas/layout.tsx index a17f31f..85d107a 100644 --- a/app/(dashboard)/dashboard/analise-parcelas/layout.tsx +++ b/app/(dashboard)/relatorios/analise-parcelas/layout.tsx @@ -1,5 +1,5 @@ import { RiSecurePaymentLine } from "@remixicon/react"; -import PageDescription from "@/components/page-description"; +import PageDescription from "@/components/shared/page-description"; export const metadata = { title: "Análise de Parcelas | OpenMonetis", diff --git a/app/(dashboard)/dashboard/analise-parcelas/page.tsx b/app/(dashboard)/relatorios/analise-parcelas/page.tsx similarity index 100% rename from app/(dashboard)/dashboard/analise-parcelas/page.tsx rename to app/(dashboard)/relatorios/analise-parcelas/page.tsx diff --git a/app/(dashboard)/relatorios/estabelecimentos/layout.tsx b/app/(dashboard)/relatorios/estabelecimentos/layout.tsx index c20ca10..95d9e9a 100644 --- a/app/(dashboard)/relatorios/estabelecimentos/layout.tsx +++ b/app/(dashboard)/relatorios/estabelecimentos/layout.tsx @@ -1,5 +1,5 @@ import { RiStore2Line } from "@remixicon/react"; -import PageDescription from "@/components/page-description"; +import PageDescription from "@/components/shared/page-description"; export const metadata = { title: "Top Estabelecimentos | OpenMonetis", diff --git a/app/(dashboard)/relatorios/estabelecimentos/loading.tsx b/app/(dashboard)/relatorios/estabelecimentos/loading.tsx index ac15aa9..78728d1 100644 --- a/app/(dashboard)/relatorios/estabelecimentos/loading.tsx +++ b/app/(dashboard)/relatorios/estabelecimentos/loading.tsx @@ -3,7 +3,7 @@ import { Skeleton } from "@/components/ui/skeleton"; export default function Loading() { return ( -
+
diff --git a/app/(dashboard)/relatorios/estabelecimentos/page.tsx b/app/(dashboard)/relatorios/estabelecimentos/page.tsx index 4c0d359..4634366 100644 --- a/app/(dashboard)/relatorios/estabelecimentos/page.tsx +++ b/app/(dashboard)/relatorios/estabelecimentos/page.tsx @@ -1,14 +1,14 @@ -import { EstablishmentsList } from "@/components/top-estabelecimentos/establishments-list"; -import { HighlightsCards } from "@/components/top-estabelecimentos/highlights-cards"; -import { PeriodFilterButtons } from "@/components/top-estabelecimentos/period-filter"; -import { SummaryCards } from "@/components/top-estabelecimentos/summary-cards"; -import { TopCategories } from "@/components/top-estabelecimentos/top-categories"; +import { EstablishmentsList } from "@/components/relatorios/estabelecimentos/establishments-list"; +import { HighlightsCards } from "@/components/relatorios/estabelecimentos/highlights-cards"; +import { PeriodFilterButtons } from "@/components/relatorios/estabelecimentos/period-filter"; +import { SummaryCards } from "@/components/relatorios/estabelecimentos/summary-cards"; +import { TopCategories } from "@/components/relatorios/estabelecimentos/top-categories"; import { Card } from "@/components/ui/card"; import { getUser } from "@/lib/auth/server"; import { fetchTopEstabelecimentosData, type PeriodFilter, -} from "@/lib/top-estabelecimentos/fetch-data"; +} from "@/lib/relatorios/estabelecimentos/fetch-data"; import { parsePeriodParam } from "@/lib/utils/period"; type PageSearchParams = Promise>; diff --git a/app/(dashboard)/relatorios/tendencias/layout.tsx b/app/(dashboard)/relatorios/tendencias/layout.tsx index 19bc5ed..a496fd9 100644 --- a/app/(dashboard)/relatorios/tendencias/layout.tsx +++ b/app/(dashboard)/relatorios/tendencias/layout.tsx @@ -1,5 +1,5 @@ import { RiFileChartLine } from "@remixicon/react"; -import PageDescription from "@/components/page-description"; +import PageDescription from "@/components/shared/page-description"; export const metadata = { title: "Tendências | OpenMonetis", diff --git a/app/(dashboard)/relatorios/tendencias/loading.tsx b/app/(dashboard)/relatorios/tendencias/loading.tsx index c39eac1..bedc39a 100644 --- a/app/(dashboard)/relatorios/tendencias/loading.tsx +++ b/app/(dashboard)/relatorios/tendencias/loading.tsx @@ -1,4 +1,4 @@ -import { CategoryReportSkeleton } from "@/components/skeletons/category-report-skeleton"; +import { CategoryReportSkeleton } from "@/components/shared/skeletons/category-report-skeleton"; export default function Loading() { return ( diff --git a/app/(dashboard)/relatorios/uso-cartoes/layout.tsx b/app/(dashboard)/relatorios/uso-cartoes/layout.tsx index 2c8efb6..8a54809 100644 --- a/app/(dashboard)/relatorios/uso-cartoes/layout.tsx +++ b/app/(dashboard)/relatorios/uso-cartoes/layout.tsx @@ -1,5 +1,5 @@ import { RiBankCard2Line } from "@remixicon/react"; -import PageDescription from "@/components/page-description"; +import PageDescription from "@/components/shared/page-description"; export const metadata = { title: "Uso de Cartões | OpenMonetis", diff --git a/components/ajustes/preferences-form.tsx b/components/ajustes/preferences-form.tsx index 2c41500..a71736a 100644 --- a/components/ajustes/preferences-form.tsx +++ b/components/ajustes/preferences-form.tsx @@ -36,7 +36,7 @@ import { DEFAULT_LANCAMENTOS_COLUMN_ORDER, LANCAMENTOS_COLUMN_LABELS, } from "@/lib/lancamentos/column-order"; -import { FONT_OPTIONS, getFontVariable } from "@/public/fonts/font_index"; +import { FONT_OPTIONS } from "@/public/fonts/font_index"; interface PreferencesFormProps { disableMagnetlines: boolean; @@ -189,14 +189,6 @@ export function PreferencesForm({ ))} -

- Suas finanças em um só lugar -

{/* Fonte de valores */} @@ -223,14 +215,6 @@ export function PreferencesForm({ ))} -

- R$ 1.234,56 -

diff --git a/components/anotacoes/note-card.tsx b/components/anotacoes/note-card.tsx index 351c00d..77ac5ec 100644 --- a/components/anotacoes/note-card.tsx +++ b/components/anotacoes/note-card.tsx @@ -4,7 +4,7 @@ import { RiArchiveLine, RiCheckLine, RiDeleteBin5Line, - RiEyeLine, + RiFileList2Line, RiInboxUnarchiveLine, RiPencilLine, } from "@remixicon/react"; @@ -60,7 +60,7 @@ export function NoteCard({ }, { label: "detalhes", - icon: , + icon: , onClick: onDetails, variant: "default" as const, }, @@ -115,7 +115,9 @@ export function NoteCard({ {task.text} diff --git a/components/anotacoes/note-details-dialog.tsx b/components/anotacoes/note-details-dialog.tsx index 54de84f..ba6b12f 100644 --- a/components/anotacoes/note-details-dialog.tsx +++ b/components/anotacoes/note-details-dialog.tsx @@ -72,11 +72,11 @@ export function NoteDetailsDialog({ {isTask ? ( -
+ {sortedTasks.map((task) => ( -
{task.text} - +
))} -
+ ) : (
{note.description} diff --git a/components/anotacoes/note-dialog.tsx b/components/anotacoes/note-dialog.tsx index edd44ef..32ba333 100644 --- a/components/anotacoes/note-dialog.tsx +++ b/components/anotacoes/note-dialog.tsx @@ -85,17 +85,17 @@ export function NoteDialog({ const initialState = buildInitialValues(note); - const { formState, updateField, setFormState } = + const { formState, resetForm, updateField } = useFormState(initialState); useEffect(() => { if (dialogOpen) { - setFormState(buildInitialValues(note)); + resetForm(buildInitialValues(note)); setErrorMessage(null); setNewTaskText(""); requestAnimationFrame(() => titleRef.current?.focus()); } - }, [dialogOpen, note, setFormState]); + }, [dialogOpen, note, resetForm]); const dialogTitle = mode === "create" ? "Nova anotação" : "Editar anotação"; const description = @@ -338,7 +338,7 @@ export function NoteDialog({
{sortedTasks.length > 0 && ( -
+
{sortedTasks.map((task) => (
["variant"]; type Size = React.ComponentProps["size"]; @@ -50,8 +50,11 @@ export function CalculatorDialogContent({ return ( e.preventDefault()} + onPointerDownOutside={(e) => e.preventDefault()} + onFocusOutside={(e) => e.preventDefault()} + onInteractOutside={(e) => e.preventDefault()} > void; + isResultView: boolean; }; export function CalculatorDisplay({ @@ -15,14 +17,27 @@ export function CalculatorDisplay({ resultText, copied, onCopy, + isResultView, }: CalculatorDisplayProps) { return ( -
- {history && ( -
{history}
- )} -
-
+
+
+ {history ?? ( + + 0 + 0 + + )} +
+
+
{expression}
{resultText && ( diff --git a/components/calculadora/calculator-keypad.tsx b/components/calculadora/calculator-keypad.tsx index d17f159..2a3731b 100644 --- a/components/calculadora/calculator-keypad.tsx +++ b/components/calculadora/calculator-keypad.tsx @@ -1,5 +1,5 @@ +import type { CalculatorButtonConfig } from "@/components/calculadora/use-calculator-state"; import { Button } from "@/components/ui/button"; -import type { CalculatorButtonConfig } from "@/hooks/use-calculator-state"; import type { Operator } from "@/lib/utils/calculator"; import { cn } from "@/lib/utils/ui"; diff --git a/components/calculadora/calculator.tsx b/components/calculadora/calculator.tsx index 43027f7..95c8718 100644 --- a/components/calculadora/calculator.tsx +++ b/components/calculadora/calculator.tsx @@ -1,9 +1,9 @@ "use client"; import { CalculatorKeypad } from "@/components/calculadora/calculator-keypad"; +import { useCalculatorKeyboard } from "@/components/calculadora/use-calculator-keyboard"; +import { useCalculatorState } from "@/components/calculadora/use-calculator-state"; import { Button } from "@/components/ui/button"; -import { useCalculatorKeyboard } from "@/hooks/use-calculator-keyboard"; -import { useCalculatorState } from "@/hooks/use-calculator-state"; import { CalculatorDisplay } from "./calculator-display"; type CalculatorProps = { @@ -64,6 +64,7 @@ export default function Calculator({ resultText={resultText} copied={copied} onCopy={copyToClipboard} + isResultView={Boolean(history)} /> {onSelectValue && ( diff --git a/hooks/use-calculator-keyboard.ts b/components/calculadora/use-calculator-keyboard.ts similarity index 100% rename from hooks/use-calculator-keyboard.ts rename to components/calculadora/use-calculator-keyboard.ts diff --git a/hooks/use-calculator-state.ts b/components/calculadora/use-calculator-state.ts similarity index 100% rename from hooks/use-calculator-state.ts rename to components/calculadora/use-calculator-state.ts diff --git a/hooks/use-draggable-dialog.ts b/components/calculadora/use-draggable-dialog.ts similarity index 59% rename from hooks/use-draggable-dialog.ts rename to components/calculadora/use-draggable-dialog.ts index 51ebebd..b7b5f67 100644 --- a/hooks/use-draggable-dialog.ts +++ b/components/calculadora/use-draggable-dialog.ts @@ -10,10 +10,17 @@ function clampPosition( elementWidth: number, elementHeight: number, ): Position { - const maxX = window.innerWidth - MIN_VISIBLE_PX; - const minX = MIN_VISIBLE_PX - elementWidth; - const maxY = window.innerHeight - MIN_VISIBLE_PX; - const minY = MIN_VISIBLE_PX - elementHeight; + // Dialog starts centered (left/top 50% + translate(-50%, -50%)). + // Clamp offsets so at least MIN_VISIBLE_PX remains visible on each axis. + const halfViewportWidth = window.innerWidth / 2; + const halfViewportHeight = window.innerHeight / 2; + const halfElementWidth = elementWidth / 2; + const halfElementHeight = elementHeight / 2; + + const minX = MIN_VISIBLE_PX - (halfViewportWidth + halfElementWidth); + const maxX = halfViewportWidth + halfElementWidth - MIN_VISIBLE_PX; + const minY = MIN_VISIBLE_PX - (halfViewportHeight + halfElementHeight); + const maxY = halfViewportHeight + halfElementHeight - MIN_VISIBLE_PX; return { x: Math.min(Math.max(x, minX), maxX), @@ -21,11 +28,14 @@ function clampPosition( }; } -function applyTranslate(el: HTMLElement, x: number, y: number) { +function applyPosition(el: HTMLElement, x: number, y: number) { if (x === 0 && y === 0) { el.style.translate = ""; + el.style.transform = ""; } else { - el.style.translate = `${x}px ${y}px`; + // Keep the dialog's centered baseline (-50%, -50%) and only add drag offset. + el.style.translate = `calc(-50% + ${x}px) calc(-50% + ${y}px)`; + el.style.transform = ""; } } @@ -56,18 +66,28 @@ export function useDraggableDialog() { const clamped = clampPosition(rawX, rawY, el.offsetWidth, el.offsetHeight); offset.current = clamped; - applyTranslate(el, clamped.x, clamped.y); + applyPosition(el, clamped.x, clamped.y); }, []); const onPointerUp = useCallback((e: React.PointerEvent) => { dragStart.current = null; - (e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId); + if ((e.currentTarget as HTMLElement).hasPointerCapture(e.pointerId)) { + (e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId); + } + }, []); + + const onPointerCancel = useCallback(() => { + dragStart.current = null; + }, []); + + const onLostPointerCapture = useCallback(() => { + dragStart.current = null; }, []); const resetPosition = useCallback(() => { offset.current = { x: 0, y: 0 }; if (contentRef.current) { - applyTranslate(contentRef.current, 0, 0); + applyPosition(contentRef.current, 0, 0); } }, []); @@ -75,6 +95,8 @@ export function useDraggableDialog() { onPointerDown, onPointerMove, onPointerUp, + onPointerCancel, + onLostPointerCapture, style: { touchAction: "none" as const, cursor: "grab" }, }; diff --git a/components/cartoes/card-dialog.tsx b/components/cartoes/card-dialog.tsx index d5b2aa2..a6eed0f 100644 --- a/components/cartoes/card-dialog.tsx +++ b/components/cartoes/card-dialog.tsx @@ -13,6 +13,7 @@ import { updateCardAction, } from "@/app/(dashboard)/cartoes/actions"; import { LogoPickerDialog, LogoPickerTrigger } from "@/components/logo-picker"; +import { useLogoSelection } from "@/components/logo-picker/use-logo-selection"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -25,7 +26,6 @@ import { } from "@/components/ui/dialog"; import { useControlledState } from "@/hooks/use-controlled-state"; import { useFormState } from "@/hooks/use-form-state"; -import { useLogoSelection } from "@/hooks/use-logo-selection"; import { deriveNameFromLogo, normalizeLogo } from "@/lib/logo"; import { formatLimitInput } from "@/lib/utils/currency"; import { CardFormFields } from "./card-form-fields"; @@ -100,16 +100,16 @@ export function CardDialog({ ); // Use form state hook for form management - const { formState, updateField, updateFields, setFormState } = + const { formState, resetForm, updateField, updateFields } = useFormState(initialState); // Reset form when dialog opens useEffect(() => { if (dialogOpen) { - setFormState(initialState); + resetForm(initialState); setErrorMessage(null); } - }, [dialogOpen, initialState, setFormState]); + }, [dialogOpen, initialState, resetForm]); // Close logo dialog when main dialog closes useEffect(() => { @@ -173,7 +173,7 @@ export function CardDialog({ if (result.success) { toast.success(result.message); setDialogOpen(false); - setFormState(initialState); + resetForm(initialState); return; } @@ -181,7 +181,7 @@ export function CardDialog({ toast.error(result.error); }); }, - [card?.id, formState, initialState, mode, setDialogOpen, setFormState], + [card?.id, formState, initialState, mode, resetForm, setDialogOpen], ); const title = mode === "create" ? "Novo cartão" : "Editar cartão"; diff --git a/components/cartoes/card-item.tsx b/components/cartoes/card-item.tsx index 0e97dbd..b16173b 100644 --- a/components/cartoes/card-item.tsx +++ b/components/cartoes/card-item.tsx @@ -3,7 +3,7 @@ import { RiChat3Line, RiDeleteBin5Line, - RiEyeLine, + RiFileList2Line, RiPencilLine, } from "@remixicon/react"; import Image from "next/image"; @@ -143,7 +143,7 @@ export function CardItem({ }, { label: "ver fatura", - icon: , + icon: , onClick: onInvoice, className: "text-primary", }, diff --git a/components/cartoes/cards-page.tsx b/components/cartoes/cards-page.tsx index bb71fb9..aeef05e 100644 --- a/components/cartoes/cards-page.tsx +++ b/components/cartoes/cards-page.tsx @@ -6,7 +6,7 @@ import { useCallback, useMemo, useState } from "react"; import { toast } from "sonner"; import { deleteCardAction } from "@/app/(dashboard)/cartoes/actions"; import { ConfirmActionDialog } from "@/components/confirm-action-dialog"; -import { EmptyState } from "@/components/empty-state"; +import { EmptyState } from "@/components/shared/empty-state"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; diff --git a/components/categorias/category-dialog.tsx b/components/categorias/category-dialog.tsx index 625379c..0f46aba 100644 --- a/components/categorias/category-dialog.tsx +++ b/components/categorias/category-dialog.tsx @@ -76,16 +76,16 @@ export function CategoryDialog({ }); // Use form state hook for form management - const { formState, updateField, setFormState } = + const { formState, resetForm, updateField } = useFormState(initialState); // Reset form when dialog opens useEffect(() => { if (dialogOpen) { - setFormState(initialState); + resetForm(initialState); setErrorMessage(null); } - }, [dialogOpen, setFormState, initialState]); + }, [dialogOpen, initialState, resetForm]); // Clear error when dialog closes useEffect(() => { @@ -123,7 +123,7 @@ export function CategoryDialog({ if (result.success) { toast.success(result.message); setDialogOpen(false); - setFormState(initialState); + resetForm(initialState); return; } diff --git a/components/contas/account-dialog.tsx b/components/contas/account-dialog.tsx index b5170eb..6feb93d 100644 --- a/components/contas/account-dialog.tsx +++ b/components/contas/account-dialog.tsx @@ -13,6 +13,7 @@ import { updateAccountAction, } from "@/app/(dashboard)/contas/actions"; import { LogoPickerDialog, LogoPickerTrigger } from "@/components/logo-picker"; +import { useLogoSelection } from "@/components/logo-picker/use-logo-selection"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -25,7 +26,6 @@ import { } from "@/components/ui/dialog"; import { useControlledState } from "@/hooks/use-controlled-state"; import { useFormState } from "@/hooks/use-form-state"; -import { useLogoSelection } from "@/hooks/use-logo-selection"; import { deriveNameFromLogo, normalizeLogo } from "@/lib/logo"; import { formatInitialBalanceInput } from "@/lib/utils/currency"; @@ -126,16 +126,16 @@ export function AccountDialog({ ); // Use form state hook for form management - const { formState, updateField, updateFields, setFormState } = + const { formState, resetForm, updateField, updateFields } = useFormState(initialState); // Reset form when dialog opens useEffect(() => { if (dialogOpen) { - setFormState(initialState); + resetForm(initialState); setErrorMessage(null); } - }, [dialogOpen, initialState, setFormState]); + }, [dialogOpen, initialState, resetForm]); // Close logo dialog when main dialog closes useEffect(() => { @@ -190,7 +190,7 @@ export function AccountDialog({ if (result.success) { toast.success(result.message); setDialogOpen(false); - setFormState(initialState); + resetForm(initialState); return; } @@ -198,7 +198,7 @@ export function AccountDialog({ toast.error(result.error); }); }, - [account?.id, formState, initialState, mode, setDialogOpen, setFormState], + [account?.id, formState, initialState, mode, resetForm, setDialogOpen], ); const title = mode === "create" ? "Nova conta" : "Editar conta"; diff --git a/components/contas/accounts-page.tsx b/components/contas/accounts-page.tsx index 09c5485..1b6f34b 100644 --- a/components/contas/accounts-page.tsx +++ b/components/contas/accounts-page.tsx @@ -8,7 +8,7 @@ import { toast } from "sonner"; import { deleteAccountAction } from "@/app/(dashboard)/contas/actions"; import { ConfirmActionDialog } from "@/components/confirm-action-dialog"; import { AccountCard } from "@/components/contas/account-card"; -import { EmptyState } from "@/components/empty-state"; +import { EmptyState } from "@/components/shared/empty-state"; import { Button } from "@/components/ui/button"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { getCurrentPeriod } from "@/lib/utils/period"; diff --git a/components/dashboard/boletos-widget.tsx b/components/dashboard/boletos-widget.tsx index aea2a48..09242e6 100644 --- a/components/dashboard/boletos-widget.tsx +++ b/components/dashboard/boletos-widget.tsx @@ -245,7 +245,7 @@ export function BoletosWidget({ boletos }: BoletosWidgetProps) { }} > { if (isProcessing) { event.preventDefault(); diff --git a/components/dashboard/dashboard-grid-editable.tsx b/components/dashboard/dashboard-grid-editable.tsx index f3158ba..9144858 100644 --- a/components/dashboard/dashboard-grid-editable.tsx +++ b/components/dashboard/dashboard-grid-editable.tsx @@ -16,8 +16,7 @@ import { sortableKeyboardCoordinates, } from "@dnd-kit/sortable"; import { - RiArrowDownLine, - RiArrowUpLine, + RiAddCircleLine, RiCheckLine, RiCloseLine, RiDragMove2Line, @@ -201,11 +200,11 @@ export function DashboardGridEditable({ {/* Toolbar */}
{!isEditing ? ( -
+
Ações rápidas -
+
- - Nova receita + } /> @@ -236,18 +242,30 @@ export function DashboardGridEditable({ defaultPeriod={period} defaultTransactionType="Despesa" trigger={ - } /> - - Nova anotação + } /> @@ -257,7 +275,7 @@ export function DashboardGridEditable({
)} -
+
{isEditing ? ( <> - +
)}
diff --git a/components/dashboard/installment-analysis/installment-group-card.tsx b/components/dashboard/installment-analysis/installment-group-card.tsx index 177e0ae..f17ae7a 100644 --- a/components/dashboard/installment-analysis/installment-group-card.tsx +++ b/components/dashboard/installment-analysis/installment-group-card.tsx @@ -80,7 +80,7 @@ export function InstallmentGroupCard({ {group.cartaoLogo && ( {group.cartaoName} )} diff --git a/components/dashboard/installment-analysis/types.ts b/components/dashboard/installment-analysis/types.ts index 51c0bb7..4f18f59 100644 --- a/components/dashboard/installment-analysis/types.ts +++ b/components/dashboard/installment-analysis/types.ts @@ -1,7 +1,6 @@ import type { InstallmentAnalysisData, InstallmentGroup, - PendingInvoice, } from "@/lib/dashboard/expenses/installment-analysis"; -export type { InstallmentAnalysisData, InstallmentGroup, PendingInvoice }; +export type { InstallmentAnalysisData, InstallmentGroup }; diff --git a/components/dashboard/invoices-widget.tsx b/components/dashboard/invoices-widget.tsx index cedfc72..22cee07 100644 --- a/components/dashboard/invoices-widget.tsx +++ b/components/dashboard/invoices-widget.tsx @@ -419,7 +419,7 @@ export function InvoicesWidget({ invoices }: InvoicesWidgetProps) { }} > { if (modalState === "processing") { event.preventDefault(); diff --git a/components/dashboard/notes-widget.tsx b/components/dashboard/notes-widget.tsx index 8e70ad2..f982e81 100644 --- a/components/dashboard/notes-widget.tsx +++ b/components/dashboard/notes-widget.tsx @@ -1,6 +1,6 @@ "use client"; -import { RiEyeLine, RiPencilLine, RiTodoLine } from "@remixicon/react"; +import { RiFileList2Line, RiPencilLine, RiTodoLine } from "@remixicon/react"; import { useCallback, useMemo, useState } from "react"; import { NoteDetailsDialog } from "@/components/anotacoes/note-details-dialog"; import { NoteDialog } from "@/components/anotacoes/note-dialog"; @@ -100,13 +100,10 @@ export function NotesWidget({ notes }: NotesWidgetProps) { {buildDisplayTitle(note.title)}

- + {getTasksSummary(note)} -

+

{DATE_FORMATTER.format(new Date(note.createdAt))}

@@ -131,7 +128,7 @@ export function NotesWidget({ notes }: NotesWidgetProps) { note.title, )}`} > - +
diff --git a/components/dashboard/widget-settings-dialog.tsx b/components/dashboard/widget-settings-dialog.tsx index 0d7a7dd..ad389a0 100644 --- a/components/dashboard/widget-settings-dialog.tsx +++ b/components/dashboard/widget-settings-dialog.tsx @@ -14,24 +14,31 @@ import { } from "@/components/ui/dialog"; import { Switch } from "@/components/ui/switch"; import { widgetsConfig } from "@/lib/dashboard/widgets/widgets-config"; +import { cn } from "@/lib/utils"; type WidgetSettingsDialogProps = { hiddenWidgets: string[]; onToggleWidget: (widgetId: string) => void; onReset: () => void; + triggerClassName?: string; }; export function WidgetSettingsDialog({ hiddenWidgets, onToggleWidget, onReset, + triggerClassName, }: WidgetSettingsDialogProps) { const [open, setOpen] = useState(false); return ( - diff --git a/components/insights/insights-page.tsx b/components/insights/insights-page.tsx index 5709b16..8e3d5ca 100644 --- a/components/insights/insights-page.tsx +++ b/components/insights/insights-page.tsx @@ -17,12 +17,12 @@ import { saveInsightsAction, } from "@/app/(dashboard)/insights/actions"; import { DEFAULT_MODEL } from "@/app/(dashboard)/insights/data"; +import { EmptyState } from "@/components/shared/empty-state"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; import type { InsightsResponse } from "@/lib/schemas/insights"; -import { EmptyState } from "../empty-state"; import { InsightsGrid } from "./insights-grid"; import { ModelSelector } from "./model-selector"; diff --git a/components/lancamentos/dialogs/anticipate-installments-dialog/anticipate-installments-dialog.tsx b/components/lancamentos/dialogs/anticipate-installments-dialog/anticipate-installments-dialog.tsx index 4b6eadb..bc4ed82 100644 --- a/components/lancamentos/dialogs/anticipate-installments-dialog/anticipate-installments-dialog.tsx +++ b/components/lancamentos/dialogs/anticipate-installments-dialog/anticipate-installments-dialog.tsx @@ -60,7 +60,7 @@ interface AnticipateInstallmentsDialogProps { type AnticipationFormValues = { anticipationPeriod: string; - discount: number; + discount: string; pagadorId: string; categoriaId: string; note: string; @@ -92,10 +92,10 @@ export function AnticipateInstallmentsDialog({ ); // Use form state hook for form management - const { formState, updateField, setFormState } = + const { formState, replaceForm, updateField } = useFormState({ anticipationPeriod: defaultPeriod, - discount: 0, + discount: "0", pagadorId: "", categoriaId: "", note: "", @@ -110,23 +110,25 @@ export function AnticipateInstallmentsDialog({ getEligibleInstallmentsAction(seriesId) .then((result) => { - if (result.success && result.data) { - setEligibleInstallments(result.data); - - // Pré-preencher pagador e categoria da primeira parcela - if (result.data.length > 0) { - const first = result.data[0]; - setFormState({ - anticipationPeriod: defaultPeriod, - discount: 0, - pagadorId: first.pagadorId ?? "", - categoriaId: first.categoriaId ?? "", - note: "", - }); - } - } else { + if (!result.success) { toast.error(result.error || "Erro ao carregar parcelas"); setEligibleInstallments([]); + return; + } + + const installments = result.data ?? []; + setEligibleInstallments(installments); + + // Pré-preencher pagador e categoria da primeira parcela + if (installments.length > 0) { + const first = installments[0]; + replaceForm({ + anticipationPeriod: defaultPeriod, + discount: "0", + pagadorId: first.pagadorId ?? "", + categoriaId: first.categoriaId ?? "", + note: "", + }); } }) .catch((error) => { @@ -138,7 +140,7 @@ export function AnticipateInstallmentsDialog({ setIsLoadingInstallments(false); }); } - }, [dialogOpen, seriesId, defaultPeriod, setFormState]); + }, [defaultPeriod, dialogOpen, replaceForm, seriesId]); const totalAmount = useMemo(() => { return eligibleInstallments @@ -268,9 +270,7 @@ export function AnticipateInstallmentsDialog({ - updateField("discount", value ?? 0) - } + onValueChange={(value) => updateField("discount", value)} placeholder="R$ 0,00" disabled={isPending} /> diff --git a/components/lancamentos/dialogs/anticipate-installments-dialog/anticipation-history-dialog.tsx b/components/lancamentos/dialogs/anticipate-installments-dialog/anticipation-history-dialog.tsx index 2b0730b..64e20ff 100644 --- a/components/lancamentos/dialogs/anticipate-installments-dialog/anticipation-history-dialog.tsx +++ b/components/lancamentos/dialogs/anticipate-installments-dialog/anticipation-history-dialog.tsx @@ -59,14 +59,15 @@ export function AnticipationHistoryDialog({ try { const result = await getInstallmentAnticipationsAction(seriesId); - if (result.success && result.data) { - setAnticipations(result.data); - } else { + if (!result.success) { toast.error( result.error || "Erro ao carregar histórico de antecipações", ); setAnticipations([]); + return; } + + setAnticipations(result.data ?? []); } catch (error) { console.error("Erro ao buscar antecipações:", error); toast.error("Erro ao carregar histórico de antecipações"); diff --git a/components/lancamentos/dialogs/lancamento-details-dialog.tsx b/components/lancamentos/dialogs/lancamento-details-dialog.tsx index 027d10d..a6d3777 100644 --- a/components/lancamentos/dialogs/lancamento-details-dialog.tsx +++ b/components/lancamentos/dialogs/lancamento-details-dialog.tsx @@ -53,9 +53,9 @@ export function LancamentoDetailsDialog({ return ( - -
- + +
+
#{lancamento.id} diff --git a/components/lancamentos/dialogs/lancamento-dialog/payment-method-section.tsx b/components/lancamentos/dialogs/lancamento-dialog/payment-method-section.tsx index 50ebe7c..e7af072 100644 --- a/components/lancamentos/dialogs/lancamento-dialog/payment-method-section.tsx +++ b/components/lancamentos/dialogs/lancamento-dialog/payment-method-section.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { Label } from "@/components/ui/label"; -import { MonthPicker } from "@/components/ui/monthpicker"; +import { MonthPicker } from "@/components/ui/month-picker"; import { Popover, PopoverContent, diff --git a/components/lancamentos/dialogs/mass-add-dialog.tsx b/components/lancamentos/dialogs/mass-add-dialog.tsx index a848958..7b922ce 100644 --- a/components/lancamentos/dialogs/mass-add-dialog.tsx +++ b/components/lancamentos/dialogs/mass-add-dialog.tsx @@ -15,7 +15,7 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; -import { MonthPicker } from "@/components/ui/monthpicker"; +import { MonthPicker } from "@/components/ui/month-picker"; import { Popover, PopoverContent, @@ -36,7 +36,6 @@ import { groupAndSortCategorias } from "@/lib/lancamentos/categoria-helpers"; import { LANCAMENTO_PAYMENT_METHODS } from "@/lib/lancamentos/constants"; import { getTodayDateString } from "@/lib/utils/date"; import { displayPeriod } from "@/lib/utils/period"; -import type { SelectOption } from "../../types"; import { CategoriaSelectContent, ContaCartaoSelectContent, @@ -45,6 +44,7 @@ import { TransactionTypeSelectContent, } from "../select-items"; import { EstabelecimentoInput } from "../shared/estabelecimento-input"; +import type { SelectOption } from "../types"; /** Payment methods sem Boleto para este modal */ const MASS_ADD_PAYMENT_METHODS = LANCAMENTO_PAYMENT_METHODS.filter( diff --git a/components/lancamentos/lancamentos-export.tsx b/components/lancamentos/lancamentos-export.tsx index 74268c4..298aef9 100644 --- a/components/lancamentos/lancamentos-export.tsx +++ b/components/lancamentos/lancamentos-export.tsx @@ -19,6 +19,10 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { formatCurrency } from "@/lib/lancamentos/formatting-helpers"; +import { + getPrimaryPdfColor, + loadExportLogoDataUrl, +} from "@/lib/utils/export-branding"; import type { LancamentoItem } from "./types"; interface LancamentosExportProps { @@ -51,6 +55,17 @@ export function LancamentosExport({ return "-"; }; + const getNameWithInstallment = (lancamento: LancamentoItem) => { + const isInstallment = + lancamento.condition.trim().toLowerCase() === "parcelado"; + + if (!isInstallment || !lancamento.installmentCount) { + return lancamento.name; + } + + return `${lancamento.name} (${lancamento.currentInstallment ?? 1}/${lancamento.installmentCount})`; + }; + const exportToCSV = () => { try { setIsExporting(true); @@ -71,7 +86,7 @@ export function LancamentosExport({ lancamentos.forEach((lancamento) => { const row = [ formatDate(lancamento.purchaseDate), - lancamento.name, + getNameWithInstallment(lancamento), lancamento.transactionType, lancamento.condition, lancamento.paymentMethod, @@ -129,7 +144,7 @@ export function LancamentosExport({ lancamentos.forEach((lancamento) => { const row = [ formatDate(lancamento.purchaseDate), - lancamento.name, + getNameWithInstallment(lancamento), lancamento.transactionType, lancamento.condition, lancamento.paymentMethod, @@ -145,7 +160,7 @@ export function LancamentosExport({ ws["!cols"] = [ { wch: 12 }, // Data - { wch: 30 }, // Nome + { wch: 42 }, // Nome { wch: 15 }, // Tipo { wch: 15 }, // Condição { wch: 20 }, // Pagamento @@ -168,14 +183,33 @@ export function LancamentosExport({ } }; - const exportToPDF = () => { + const exportToPDF = async () => { try { setIsExporting(true); const doc = new jsPDF({ orientation: "landscape" }); + const primaryColor = getPrimaryPdfColor(); + const [smallLogoDataUrl, textLogoDataUrl] = await Promise.all([ + loadExportLogoDataUrl("/logo_small.png"), + loadExportLogoDataUrl("/logo_text.png"), + ]); + let brandingEndX = 14; + if (smallLogoDataUrl) { + doc.addImage(smallLogoDataUrl, "PNG", brandingEndX, 7.5, 8, 8); + brandingEndX += 10; + } + + if (textLogoDataUrl) { + doc.addImage(textLogoDataUrl, "PNG", brandingEndX, 8, 30, 8); + brandingEndX += 32; + } + + const titleX = brandingEndX > 14 ? brandingEndX + 4 : 14; + + doc.setFont("courier", "normal"); doc.setFontSize(16); - doc.text("Lançamentos", 14, 15); + doc.text("Lançamentos", titleX, 15); doc.setFontSize(10); const periodParts = period.split("-"); @@ -197,8 +231,15 @@ export function LancamentosExport({ periodParts.length === 2 ? `${monthNames[Number.parseInt(periodParts[1], 10) - 1]}/${periodParts[0]}` : period; - doc.text(`Período: ${formattedPeriod}`, 14, 22); - doc.text(`Gerado em: ${new Date().toLocaleDateString("pt-BR")}`, 14, 27); + doc.text(`Período: ${formattedPeriod}`, titleX, 22); + doc.text( + `Gerado em: ${new Date().toLocaleDateString("pt-BR")}`, + titleX, + 27, + ); + doc.setDrawColor(...primaryColor); + doc.setLineWidth(0.5); + doc.line(14, 31, doc.internal.pageSize.getWidth() - 14, 31); const headers = [ [ @@ -216,7 +257,7 @@ export function LancamentosExport({ const body = lancamentos.map((lancamento) => [ formatDate(lancamento.purchaseDate), - lancamento.name, + getNameWithInstallment(lancamento), lancamento.transactionType, lancamento.condition, lancamento.paymentMethod, @@ -229,26 +270,28 @@ export function LancamentosExport({ autoTable(doc, { head: headers, body: body, - startY: 32, + startY: 35, + tableWidth: "auto", styles: { + font: "courier", fontSize: 8, cellPadding: 2, }, headStyles: { - fillColor: [59, 130, 246], + fillColor: primaryColor, textColor: 255, fontStyle: "bold", }, columnStyles: { - 0: { cellWidth: 20 }, // Data - 1: { cellWidth: 40 }, // Nome - 2: { cellWidth: 25 }, // Tipo - 3: { cellWidth: 25 }, // Condição - 4: { cellWidth: 30 }, // Pagamento - 5: { cellWidth: 25 }, // Valor + 0: { cellWidth: 24 }, // Data + 1: { cellWidth: 58 }, // Nome + 2: { cellWidth: 22 }, // Tipo + 3: { cellWidth: 22 }, // Condição + 4: { cellWidth: 28 }, // Pagamento + 5: { cellWidth: 24 }, // Valor 6: { cellWidth: 30 }, // Categoria 7: { cellWidth: 30 }, // Conta/Cartão - 8: { cellWidth: 30 }, // Pagador + 8: { cellWidth: 31 }, // Pagador }, didParseCell: (cellData) => { if (cellData.section === "body" && cellData.column.index === 5) { @@ -262,7 +305,7 @@ export function LancamentosExport({ } } }, - margin: { top: 32 }, + margin: { top: 35 }, }); doc.save(getFileName("pdf")); diff --git a/components/lancamentos/shared/installment-timeline.tsx b/components/lancamentos/shared/installment-timeline.tsx index 54ea01b..3cfcd88 100644 --- a/components/lancamentos/shared/installment-timeline.tsx +++ b/components/lancamentos/shared/installment-timeline.tsx @@ -1,9 +1,9 @@ import { RiArrowDownFill, RiCheckLine } from "@remixicon/react"; import { calculateLastInstallmentDate, - formatCurrentInstallment, - formatLastInstallmentDate, formatPurchaseDate, + formatLastInstallmentDate, + formatCurrentInstallment, } from "@/lib/installments/utils"; type InstallmentTimelineProps = { diff --git a/components/lancamentos/table/lancamentos-filters.tsx b/components/lancamentos/table/lancamentos-filters.tsx index 70d7a2f..f57dd36 100644 --- a/components/lancamentos/table/lancamentos-filters.tsx +++ b/components/lancamentos/table/lancamentos-filters.tsx @@ -277,7 +277,7 @@ export function LancamentosFilters({
{exportButton && ( -
+
{exportButton}
)} @@ -291,13 +291,13 @@ export function LancamentosFilters({ diff --git a/components/lancamentos/table/lancamentos-table.tsx b/components/lancamentos/table/lancamentos-table.tsx index 21cffb6..a4ae2b5 100644 --- a/components/lancamentos/table/lancamentos-table.tsx +++ b/components/lancamentos/table/lancamentos-table.tsx @@ -6,8 +6,8 @@ import { RiChat1Line, RiCheckLine, RiDeleteBin5Line, - RiEyeLine, RiFileCopyLine, + RiFileList2Line, RiGroupLine, RiHistoryLine, RiMoreFill, @@ -31,8 +31,8 @@ import Image from "next/image"; import Link from "next/link"; import { useMemo, useState } from "react"; import { CategoryIcon } from "@/components/categorias/category-icon"; -import { EmptyState } from "@/components/empty-state"; import MoneyValues from "@/components/money-values"; +import { EmptyState } from "@/components/shared/empty-state"; import { TypeBadge } from "@/components/type-badge"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; @@ -588,7 +588,7 @@ const buildColumns = ({ handleViewDetails(row.original)} > - + Detalhes {row.original.userId === currentUserId && ( diff --git a/hooks/use-logo-selection.ts b/components/logo-picker/use-logo-selection.ts similarity index 95% rename from hooks/use-logo-selection.ts rename to components/logo-picker/use-logo-selection.ts index d711182..9603eb6 100644 --- a/hooks/use-logo-selection.ts +++ b/components/logo-picker/use-logo-selection.ts @@ -25,7 +25,7 @@ interface UseLogoSelectionProps { * mode: 'create', * currentLogo: formState.logo, * currentName: formState.name, - * onUpdate: (updates) => setFormState(prev => ({ ...prev, ...updates })) + * onUpdate: (updates) => updateFields(updates) * }); * ``` */ diff --git a/components/month-picker/month-navigation.tsx b/components/month-picker/month-navigation.tsx index 23ae542..1b352ab 100644 --- a/components/month-picker/month-navigation.tsx +++ b/components/month-picker/month-navigation.tsx @@ -3,61 +3,37 @@ import { useRouter } from "next/navigation"; import { useEffect, useMemo, useTransition } from "react"; import { Card } from "@/components/ui/card"; -import { useMonthPeriod } from "@/hooks/use-month-period"; +import { getNextPeriod, getPreviousPeriod } from "@/lib/utils/period"; import LoadingSpinner from "./loading-spinner"; import NavigationButton from "./nav-button"; import ReturnButton from "./return-button"; +import { useMonthPeriod } from "./use-month-period"; export default function MonthNavigation() { - const { - monthNames, - currentMonth, - currentYear, - defaultMonth, - defaultYear, - buildHref, - } = useMonthPeriod(); + const { period, currentMonth, currentYear, defaultPeriod, buildHref } = + useMonthPeriod(); const router = useRouter(); const [isPending, startTransition] = useTransition(); const currentMonthLabel = useMemo( - () => currentMonth.charAt(0).toUpperCase() + currentMonth.slice(1), - [currentMonth], + () => + `${currentMonth.charAt(0).toUpperCase()}${currentMonth.slice(1)} ${currentYear}`, + [currentMonth, currentYear], ); - - const currentMonthIndex = useMemo( - () => monthNames.indexOf(currentMonth), - [monthNames, currentMonth], + const prevTarget = useMemo( + () => buildHref(getPreviousPeriod(period)), + [buildHref, period], + ); + const nextTarget = useMemo( + () => buildHref(getNextPeriod(period)), + [buildHref, period], ); - - const prevTarget = useMemo(() => { - let idx = currentMonthIndex - 1; - let year = currentYear; - if (idx < 0) { - idx = monthNames.length - 1; - year = (parseInt(currentYear, 10) - 1).toString(); - } - return buildHref(monthNames[idx], year); - }, [currentMonthIndex, currentYear, monthNames, buildHref]); - - const nextTarget = useMemo(() => { - let idx = currentMonthIndex + 1; - let year = currentYear; - if (idx >= monthNames.length) { - idx = 0; - year = (parseInt(currentYear, 10) + 1).toString(); - } - return buildHref(monthNames[idx], year); - }, [currentMonthIndex, currentYear, monthNames, buildHref]); - const returnTarget = useMemo( - () => buildHref(defaultMonth, defaultYear), - [buildHref, defaultMonth, defaultYear], + () => buildHref(defaultPeriod), + [buildHref, defaultPeriod], ); - - const isDifferentFromCurrent = - currentMonth !== defaultMonth || currentYear !== defaultYear.toString(); + const isDifferentFromCurrent = period !== defaultPeriod; // Prefetch otimizado: apenas meses adjacentes (M-1, M+1) e mês atual // Isso melhora a performance da navegação sem sobrecarregar o cliente @@ -91,10 +67,9 @@ export default function MonthNavigation() {
{currentMonthLabel} - {currentYear}
{isPending && } diff --git a/components/month-picker/use-month-period.ts b/components/month-picker/use-month-period.ts new file mode 100644 index 0000000..50353d2 --- /dev/null +++ b/components/month-picker/use-month-period.ts @@ -0,0 +1,90 @@ +"use client"; + +import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { useCallback, useMemo } from "react"; + +import { + formatPeriod, + formatPeriodForUrl, + formatPeriodParam, + MONTH_NAMES, + parsePeriodParam, +} from "@/lib/utils/period"; + +const PERIOD_PARAM = "periodo"; + +export function useMonthPeriod() { + const searchParams = useSearchParams(); + const pathname = usePathname(); + const router = useRouter(); + + const periodFromParams = searchParams.get(PERIOD_PARAM); + const referenceDate = useMemo(() => new Date(), []); + const defaultPeriod = useMemo( + () => + formatPeriod(referenceDate.getFullYear(), referenceDate.getMonth() + 1), + [referenceDate], + ); + const { period, monthName, year } = useMemo( + () => parsePeriodParam(periodFromParams, referenceDate), + [periodFromParams, referenceDate], + ); + const defaultMonth = useMemo( + () => MONTH_NAMES[referenceDate.getMonth()] ?? "", + [referenceDate], + ); + const defaultYear = useMemo( + () => referenceDate.getFullYear().toString(), + [referenceDate], + ); + + const buildHref = useCallback( + (targetPeriod: string) => { + const params = new URLSearchParams(searchParams.toString()); + params.set(PERIOD_PARAM, formatPeriodForUrl(targetPeriod)); + + return `${pathname}?${params.toString()}`; + }, + [pathname, searchParams], + ); + + const buildHrefFromMonth = useCallback( + (month: string, nextYear: string | number) => { + const parsedYear = Number.parseInt(String(nextYear).trim(), 10); + if (Number.isNaN(parsedYear)) { + return buildHref(defaultPeriod); + } + + const param = formatPeriodParam(month, parsedYear); + const parsed = parsePeriodParam(param, referenceDate); + return buildHref(parsed.period); + }, + [buildHref, defaultPeriod, referenceDate], + ); + + const replacePeriod = useCallback( + (targetPeriod: string) => { + if (!targetPeriod) { + return; + } + + router.replace(buildHref(targetPeriod), { scroll: false }); + }, + [buildHref, router], + ); + + return { + pathname, + period, + currentMonth: monthName, + currentYear: year.toString(), + defaultPeriod, + defaultMonth, + defaultYear, + buildHref, + buildHrefFromMonth, + replacePeriod, + }; +} + +export { PERIOD_PARAM as MONTH_PERIOD_PARAM }; diff --git a/components/navbar/nav-tools.tsx b/components/navbar/nav-tools.tsx deleted file mode 100644 index 55c98a6..0000000 --- a/components/navbar/nav-tools.tsx +++ /dev/null @@ -1,103 +0,0 @@ -"use client"; - -import { RiCalculatorLine, RiEyeLine, RiEyeOffLine } from "@remixicon/react"; -import { useState } from "react"; -import { CalculatorDialogContent } from "@/components/calculadora/calculator-dialog"; -import { usePrivacyMode } from "@/components/privacy-provider"; -import { Badge } from "@/components/ui/badge"; -import { Dialog, DialogTrigger } from "@/components/ui/dialog"; -import { cn } from "@/lib/utils/ui"; - -const itemClass = - "flex w-full items-center gap-2.5 rounded-sm px-2 py-2 text-sm text-foreground hover:bg-accent transition-colors cursor-pointer"; - -export function NavToolsDropdown() { - const { privacyMode, toggle } = usePrivacyMode(); - const [calcOpen, setCalcOpen] = useState(false); - - return ( - -
    -
  • - - - -
  • -
  • - -
  • -
- -
- ); -} - -type MobileToolsProps = { - onClose: () => void; -}; - -export function MobileTools({ onClose }: MobileToolsProps) { - const { privacyMode, toggle } = usePrivacyMode(); - const [calcOpen, setCalcOpen] = useState(false); - - return ( - - - - - - - - ); -} diff --git a/components/navbar/app-navbar.tsx b/components/navigation/navbar/app-navbar.tsx similarity index 93% rename from components/navbar/app-navbar.tsx rename to components/navigation/navbar/app-navbar.tsx index a68d485..66fc2ec 100644 --- a/components/navbar/app-navbar.tsx +++ b/components/navigation/navbar/app-navbar.tsx @@ -1,9 +1,9 @@ import Link from "next/link"; import { AnimatedThemeToggler } from "@/components/animated-theme-toggler"; +import { Logo } from "@/components/logo"; import { NotificationBell } from "@/components/notificacoes/notification-bell"; -import { RefreshPageButton } from "@/components/refresh-page-button"; +import { RefreshPageButton } from "@/components/shared/refresh-page-button"; import type { DashboardNotificationsSnapshot } from "@/lib/dashboard/notifications"; -import { Logo } from "../logo"; import { NavMenu } from "./nav-menu"; import { NavbarUser } from "./navbar-user"; diff --git a/components/navbar/mobile-link.tsx b/components/navigation/navbar/mobile-link.tsx similarity index 100% rename from components/navbar/mobile-link.tsx rename to components/navigation/navbar/mobile-link.tsx diff --git a/components/navbar/nav-dropdown.tsx b/components/navigation/navbar/nav-dropdown.tsx similarity index 100% rename from components/navbar/nav-dropdown.tsx rename to components/navigation/navbar/nav-dropdown.tsx diff --git a/components/navbar/nav-items.tsx b/components/navigation/navbar/nav-items.tsx similarity index 93% rename from components/navbar/nav-items.tsx rename to components/navigation/navbar/nav-items.tsx index 7623184..57c2328 100644 --- a/components/navbar/nav-items.tsx +++ b/components/navigation/navbar/nav-items.tsx @@ -8,6 +8,7 @@ import { RiFileChartLine, RiGroupLine, RiPriceTag3Line, + RiSecurePaymentLine, RiSparklingLine, RiStore2Line, RiTodoLine, @@ -111,6 +112,11 @@ export const NAV_SECTIONS: NavSection[] = [ icon: , preservePeriod: true, }, + { + href: "/relatorios/analise-parcelas", + label: "análise de parcelas", + icon: , + }, { href: "/relatorios/estabelecimentos", label: "estabelecimentos", diff --git a/components/navbar/nav-link.tsx b/components/navigation/navbar/nav-link.tsx similarity index 100% rename from components/navbar/nav-link.tsx rename to components/navigation/navbar/nav-link.tsx diff --git a/components/navbar/nav-menu.tsx b/components/navigation/navbar/nav-menu.tsx similarity index 86% rename from components/navbar/nav-menu.tsx rename to components/navigation/navbar/nav-menu.tsx index 8a2b1d8..134a732 100644 --- a/components/navbar/nav-menu.tsx +++ b/components/navigation/navbar/nav-menu.tsx @@ -2,7 +2,9 @@ import { RiDashboardLine, RiMenuLine } from "@remixicon/react"; import { useState } from "react"; +import { CalculatorDialogContent } from "@/components/calculadora/calculator-dialog"; import { Button } from "@/components/ui/button"; +import { Dialog } from "@/components/ui/dialog"; import { NavigationMenu, NavigationMenuContent, @@ -26,7 +28,9 @@ import { MobileTools, NavToolsDropdown } from "./nav-tools"; export function NavMenu() { const [sheetOpen, setSheetOpen] = useState(false); + const [calculatorOpen, setCalculatorOpen] = useState(false); const close = () => setSheetOpen(false); + const openCalculator = () => setCalculatorOpen(true); return ( <> @@ -56,7 +60,7 @@ export function NavMenu() { Ferramentas - + @@ -114,10 +118,14 @@ export function NavMenu() { })} - + + + + + ); } diff --git a/components/navbar/nav-pill.tsx b/components/navigation/navbar/nav-pill.tsx similarity index 100% rename from components/navbar/nav-pill.tsx rename to components/navigation/navbar/nav-pill.tsx diff --git a/components/navbar/nav-styles.ts b/components/navigation/navbar/nav-styles.ts similarity index 100% rename from components/navbar/nav-styles.ts rename to components/navigation/navbar/nav-styles.ts diff --git a/components/navigation/navbar/nav-tools.tsx b/components/navigation/navbar/nav-tools.tsx new file mode 100644 index 0000000..4bfc809 --- /dev/null +++ b/components/navigation/navbar/nav-tools.tsx @@ -0,0 +1,106 @@ +"use client"; + +import { RiCalculatorLine, RiEyeLine, RiEyeOffLine } from "@remixicon/react"; +import { usePrivacyMode } from "@/components/privacy-provider"; +import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/utils/ui"; + +const itemClass = + "flex w-full items-center gap-2.5 rounded-sm px-2 py-2 text-sm text-foreground hover:bg-accent transition-colors cursor-pointer"; + +type NavToolsDropdownProps = { + onOpenCalculator: () => void; +}; + +export function NavToolsDropdown({ onOpenCalculator }: NavToolsDropdownProps) { + const { privacyMode, toggle } = usePrivacyMode(); + + return ( +
    +
  • + +
  • +
  • + +
  • +
+ ); +} + +type MobileToolsProps = { + onClose: () => void; + onOpenCalculator: () => void; +}; + +export function MobileTools({ onClose, onOpenCalculator }: MobileToolsProps) { + const { privacyMode, toggle } = usePrivacyMode(); + + return ( + <> + + + + ); +} diff --git a/components/navbar/navbar-user.tsx b/components/navigation/navbar/navbar-user.tsx similarity index 98% rename from components/navbar/navbar-user.tsx rename to components/navigation/navbar/navbar-user.tsx index 3cb9fe4..7a124b6 100644 --- a/components/navbar/navbar-user.tsx +++ b/components/navigation/navbar/navbar-user.tsx @@ -11,6 +11,7 @@ import Link from "next/link"; import { useRouter } from "next/navigation"; import { useMemo, useState } from "react"; import { FeedbackDialogBody } from "@/components/feedback/feedback-dialog"; +import { Badge } from "@/components/ui/badge"; import { Dialog, DialogTrigger } from "@/components/ui/dialog"; import { DropdownMenu, @@ -24,7 +25,6 @@ import { authClient } from "@/lib/auth/client"; import { getAvatarSrc } from "@/lib/pagadores/utils"; import { cn } from "@/lib/utils/ui"; import { version } from "@/package.json"; -import { Badge } from "../ui/badge"; const itemClass = "flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm transition-colors hover:bg-accent"; diff --git a/components/sidebar/app-sidebar.tsx b/components/navigation/sidebar/app-sidebar.tsx similarity index 90% rename from components/sidebar/app-sidebar.tsx rename to components/navigation/sidebar/app-sidebar.tsx index 55169b7..c20e08c 100644 --- a/components/sidebar/app-sidebar.tsx +++ b/components/navigation/sidebar/app-sidebar.tsx @@ -1,9 +1,9 @@ "use client"; import * as React from "react"; import { Logo } from "@/components/logo"; -import { NavMain } from "@/components/sidebar/nav-main"; -import { NavSecondary } from "@/components/sidebar/nav-secondary"; -import { NavUser } from "@/components/sidebar/nav-user"; +import { NavMain } from "@/components/navigation/sidebar/nav-main"; +import { NavSecondary } from "@/components/navigation/sidebar/nav-secondary"; +import { NavUser } from "@/components/navigation/sidebar/nav-user"; import { Sidebar, SidebarContent, diff --git a/components/sidebar/nav-link.tsx b/components/navigation/sidebar/nav-link.tsx similarity index 95% rename from components/sidebar/nav-link.tsx rename to components/navigation/sidebar/nav-link.tsx index 14de25c..bdc8441 100644 --- a/components/sidebar/nav-link.tsx +++ b/components/navigation/sidebar/nav-link.tsx @@ -10,6 +10,7 @@ import { RiFundsLine, RiGroupLine, RiPriceTag3Line, + RiSecurePaymentLine, RiSettings2Line, RiSparklingLine, RiTodoLine, @@ -165,6 +166,11 @@ export function createSidebarNavData( url: "/relatorios/uso-cartoes", icon: RiBankCard2Line, }, + { + title: "Análise de Parcelas", + url: "/relatorios/analise-parcelas", + icon: RiSecurePaymentLine, + }, ], }, ], diff --git a/components/sidebar/nav-main.tsx b/components/navigation/sidebar/nav-main.tsx similarity index 100% rename from components/sidebar/nav-main.tsx rename to components/navigation/sidebar/nav-main.tsx diff --git a/components/sidebar/nav-secondary.tsx b/components/navigation/sidebar/nav-secondary.tsx similarity index 100% rename from components/sidebar/nav-secondary.tsx rename to components/navigation/sidebar/nav-secondary.tsx diff --git a/components/sidebar/nav-user.tsx b/components/navigation/sidebar/nav-user.tsx similarity index 100% rename from components/sidebar/nav-user.tsx rename to components/navigation/sidebar/nav-user.tsx diff --git a/components/orcamentos/budget-dialog.tsx b/components/orcamentos/budget-dialog.tsx index b29387e..7ab9d00 100644 --- a/components/orcamentos/budget-dialog.tsx +++ b/components/orcamentos/budget-dialog.tsx @@ -85,16 +85,16 @@ export function BudgetDialog({ }); // Use form state hook for form management - const { formState, updateField, setFormState } = + const { formState, resetForm, updateField } = useFormState(initialState); // Reset form when dialog opens useEffect(() => { if (dialogOpen) { - setFormState(initialState); + resetForm(initialState); setErrorMessage(null); } - }, [dialogOpen, setFormState, initialState]); + }, [dialogOpen, initialState, resetForm]); // Clear error when dialog closes useEffect(() => { @@ -153,7 +153,7 @@ export function BudgetDialog({ if (result.success) { toast.success(result.message); setDialogOpen(false); - setFormState(initialState); + resetForm(initialState); return; } diff --git a/components/orcamentos/budgets-page.tsx b/components/orcamentos/budgets-page.tsx index 59fc794..c70daf8 100644 --- a/components/orcamentos/budgets-page.tsx +++ b/components/orcamentos/budgets-page.tsx @@ -8,7 +8,7 @@ import { duplicatePreviousMonthBudgetsAction, } from "@/app/(dashboard)/orcamentos/actions"; import { ConfirmActionDialog } from "@/components/confirm-action-dialog"; -import { EmptyState } from "@/components/empty-state"; +import { EmptyState } from "@/components/shared/empty-state"; import { Button } from "@/components/ui/button"; import { Card } from "../ui/card"; import { BudgetCard } from "./budget-card"; diff --git a/components/pagadores/pagador-card.tsx b/components/pagadores/pagador-card.tsx index a0b00d0..2d8524a 100644 --- a/components/pagadores/pagador-card.tsx +++ b/components/pagadores/pagador-card.tsx @@ -2,7 +2,7 @@ import { RiDeleteBin5Line, - RiEyeLine, + RiFileList2Line, RiMailSendLine, RiPencilLine, RiVerifiedBadgeFill, @@ -101,7 +101,7 @@ export function PagadorCard({ pagador, onEdit, onRemove }: PagadorCardProps) { href={`/pagadores/${pagador.id}`} className={`text-primary flex items-center gap-1 font-medium transition-opacity hover:opacity-80`} > - + detalhes diff --git a/components/pagadores/pagador-dialog.tsx b/components/pagadores/pagador-dialog.tsx index bde1b58..3b1a08e 100644 --- a/components/pagadores/pagador-dialog.tsx +++ b/components/pagadores/pagador-dialog.tsx @@ -95,7 +95,7 @@ export function PagadorDialog({ ); // Use form state hook for form management - const { formState, updateField, setFormState } = + const { formState, resetForm, updateField } = useFormState(initialState); const availableAvatars = useMemo(() => { @@ -111,10 +111,10 @@ export function PagadorDialog({ // Reset form when dialog opens useEffect(() => { if (dialogOpen) { - setFormState(initialState); + resetForm(initialState); setErrorMessage(null); } - }, [dialogOpen, initialState, setFormState]); + }, [dialogOpen, initialState, resetForm]); const handleSubmit = useCallback( (event: React.FormEvent) => { @@ -160,7 +160,7 @@ export function PagadorDialog({ if (result.success) { toast.success(result.message); setDialogOpen(false); - setFormState(initialState); + resetForm(initialState); return; } @@ -168,7 +168,7 @@ export function PagadorDialog({ toast.error(result.error); }); }, - [formState, initialState, mode, pagador?.id, setDialogOpen, setFormState], + [formState, initialState, mode, pagador?.id, resetForm, setDialogOpen], ); const title = mode === "create" ? "Novo pagador" : "Editar pagador"; diff --git a/components/period-picker.tsx b/components/period-picker.tsx index 7425d20..522b8fb 100644 --- a/components/period-picker.tsx +++ b/components/period-picker.tsx @@ -5,7 +5,7 @@ import { format } from "date-fns"; import { ptBR } from "date-fns/locale"; import { useState } from "react"; import { Button } from "@/components/ui/button"; -import { MonthPicker } from "@/components/ui/monthpicker"; +import { MonthPicker } from "@/components/ui/month-picker"; import { Popover, PopoverContent, diff --git a/components/pre-lancamentos/inbox-card.tsx b/components/pre-lancamentos/inbox-card.tsx index de09121..b0231e8 100644 --- a/components/pre-lancamentos/inbox-card.tsx +++ b/components/pre-lancamentos/inbox-card.tsx @@ -1,9 +1,10 @@ "use client"; import { + RiArrowGoBackLine, RiCheckLine, RiDeleteBinLine, - RiEyeLine, + RiFileList2Line, RiMoreLine, } from "@remixicon/react"; import { format, formatDistanceToNow } from "date-fns"; @@ -37,6 +38,7 @@ interface InboxCardProps { onDiscard?: (item: InboxItem) => void; onViewDetails?: (item: InboxItem) => void; onDelete?: (item: InboxItem) => void; + onRestoreToPending?: (item: InboxItem) => void | Promise; } function resolveLogoPath(logo: string): string { @@ -79,6 +81,7 @@ export function InboxCard({ onDiscard, onViewDetails, onDelete, + onRestoreToPending, }: InboxCardProps) { const matchedLogo = useMemo( () => @@ -161,7 +164,7 @@ export function InboxCard({ onViewDetails?.(item)}> - + Ver detalhes onProcess?.(item)}> @@ -204,16 +207,30 @@ export function InboxCard({ {formattedStatusDate} )} - {onDelete && ( - - )} +
+ {item.status === "discarded" && onRestoreToPending && ( + + )} + {onDelete && ( + + )} +
) : ( @@ -226,10 +243,12 @@ export function InboxCard({ Processar diff --git a/components/pre-lancamentos/inbox-page.tsx b/components/pre-lancamentos/inbox-page.tsx index 00f6e56..7c32401 100644 --- a/components/pre-lancamentos/inbox-page.tsx +++ b/components/pre-lancamentos/inbox-page.tsx @@ -8,10 +8,11 @@ import { deleteInboxItemAction, discardInboxItemAction, markInboxAsProcessedAction, + restoreDiscardedInboxItemAction, } from "@/app/(dashboard)/pre-lancamentos/actions"; import { ConfirmActionDialog } from "@/components/confirm-action-dialog"; -import { EmptyState } from "@/components/empty-state"; import { LancamentoDialog } from "@/components/lancamentos/dialogs/lancamento-dialog/lancamento-dialog"; +import { EmptyState } from "@/components/shared/empty-state"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; @@ -58,6 +59,9 @@ export function InboxPage({ const [deleteOpen, setDeleteOpen] = useState(false); const [itemToDelete, setItemToDelete] = useState(null); + const [restoreOpen, setRestoreOpen] = useState(false); + const [itemToRestore, setItemToRestore] = useState(null); + const [bulkDeleteOpen, setBulkDeleteOpen] = useState(false); const [bulkDeleteStatus, setBulkDeleteStatus] = useState< "processed" | "discarded" @@ -166,6 +170,34 @@ export function InboxPage({ throw new Error(result.error); }, [itemToDelete]); + const handleRestoreOpenChange = useCallback((open: boolean) => { + setRestoreOpen(open); + if (!open) { + setItemToRestore(null); + } + }, []); + + const handleRestoreRequest = useCallback((item: InboxItem) => { + setItemToRestore(item); + setRestoreOpen(true); + }, []); + + const handleRestoreToPendingConfirm = useCallback(async () => { + if (!itemToRestore) return; + + const result = await restoreDiscardedInboxItemAction({ + inboxItemId: itemToRestore.id, + }); + + if (result.success) { + toast.success(result.message); + return; + } + + toast.error(result.error); + throw new Error(result.error); + }, [itemToRestore]); + const handleBulkDeleteOpenChange = useCallback((open: boolean) => { setBulkDeleteOpen(open); }, []); @@ -271,6 +303,7 @@ export function InboxPage({ onDiscard={readonly ? undefined : handleDiscardRequest} onViewDetails={readonly ? undefined : handleDetailsRequest} onDelete={readonly ? handleDeleteRequest : undefined} + onRestoreToPending={readonly ? handleRestoreRequest : undefined} /> ))}
@@ -279,15 +312,27 @@ export function InboxPage({ return ( <> - - - Pendentes ({pendingItems.length}) + + + Pendentes + ({pendingItems.length}) - - Processados ({processedItems.length}) + + Processados + ({processedItems.length}) - - Descartados ({discardedItems.length}) + + Descartados + ({discardedItems.length}) @@ -374,6 +419,16 @@ export function InboxPage({ onConfirm={handleDeleteConfirm} /> + + acc + c.amount, 0); return ( - + @@ -44,7 +44,7 @@ export function CardCategoryBreakdown({ data }: CardCategoryBreakdownProps) { - +
{data.map((category, index) => (
{/* Progress bar */} -
+
diff --git a/components/relatorios/cartoes/card-top-expenses.tsx b/components/relatorios/cartoes/card-top-expenses.tsx index c2041fb..71045fd 100644 --- a/components/relatorios/cartoes/card-top-expenses.tsx +++ b/components/relatorios/cartoes/card-top-expenses.tsx @@ -38,14 +38,14 @@ export function CardTopExpenses({ data }: CardTopExpensesProps) { const maxAmount = Math.max(...data.map((e) => e.amount)); return ( - + Top 10 Gastos do Mês - +
{data.map((expense, index) => (
{expense.name} -
+
{expense.date} {expense.category && ( {expense.category} @@ -92,7 +92,7 @@ export function CardTopExpenses({ data }: CardTopExpensesProps) {
{/* Progress bar */} -
+
-
+
Histórico de Uso {/* Card logo and name */} -
+
{logoPath ? ( )} - + {card.name}
- + { + const exportToPDF = async () => { try { setIsExporting(true); // Create PDF const doc = new jsPDF({ orientation: "landscape" }); + const primaryColor = getPrimaryPdfColor(); + const [smallLogoDataUrl, textLogoDataUrl] = await Promise.all([ + loadExportLogoDataUrl("/logo_small.png"), + loadExportLogoDataUrl("/logo_text.png"), + ]); + let brandingEndX = 14; + + if (smallLogoDataUrl) { + doc.addImage(smallLogoDataUrl, "PNG", brandingEndX, 7.5, 8, 8); + brandingEndX += 10; + } + + if (textLogoDataUrl) { + doc.addImage(textLogoDataUrl, "PNG", brandingEndX, 8, 30, 8); + brandingEndX += 32; + } + + const titleX = brandingEndX > 14 ? brandingEndX + 4 : 14; // Add header + doc.setFont("courier", "normal"); doc.setFontSize(16); - doc.text("Relatório de Categorias por Período", 14, 15); + doc.text("Relatório de Categorias por Período", titleX, 15); doc.setFontSize(10); doc.text( `Período: ${formatPeriodLabel( filters.startPeriod, )} - ${formatPeriodLabel(filters.endPeriod)}`, - 14, + titleX, 22, ); - doc.text(`Gerado em: ${new Date().toLocaleDateString("pt-BR")}`, 14, 27); + doc.text( + `Gerado em: ${new Date().toLocaleDateString("pt-BR")}`, + titleX, + 27, + ); + doc.setDrawColor(...primaryColor); + doc.setLineWidth(0.5); + doc.line(14, 31, doc.internal.pageSize.getWidth() - 14, 31); // Build table data const headers = [ @@ -255,13 +285,15 @@ export function CategoryReportExport({ autoTable(doc, { head: headers, body: body, - startY: 32, + startY: 35, + tableWidth: "auto", styles: { + font: "courier", fontSize: 8, cellPadding: 2, }, headStyles: { - fillColor: [59, 130, 246], // Blue + fillColor: primaryColor, textColor: 255, fontStyle: "bold", }, @@ -301,7 +333,7 @@ export function CategoryReportExport({ } } }, - margin: { top: 32 }, + margin: { top: 35 }, }); // Save PDF diff --git a/components/relatorios/category-report-filters.tsx b/components/relatorios/category-report-filters.tsx index 51dfd9e..e4c9e3e 100644 --- a/components/relatorios/category-report-filters.tsx +++ b/components/relatorios/category-report-filters.tsx @@ -18,7 +18,7 @@ import { CommandItem, CommandList, } from "@/components/ui/command"; -import { MonthPicker } from "@/components/ui/monthpicker"; +import { MonthPicker } from "@/components/ui/month-picker"; import { Popover, PopoverContent, diff --git a/components/relatorios/category-report-page.tsx b/components/relatorios/category-report-page.tsx index f86e223..6033146 100644 --- a/components/relatorios/category-report-page.tsx +++ b/components/relatorios/category-report-page.tsx @@ -7,8 +7,8 @@ import { } from "@remixicon/react"; import { useRouter, useSearchParams } from "next/navigation"; import { useCallback, useMemo, useState, useTransition } from "react"; -import { EmptyState } from "@/components/empty-state"; -import { CategoryReportSkeleton } from "@/components/skeletons/category-report-skeleton"; +import { EmptyState } from "@/components/shared/empty-state"; +import { CategoryReportSkeleton } from "@/components/shared/skeletons/category-report-skeleton"; import { Card } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import type { CategoryChartData } from "@/lib/relatorios/fetch-category-chart-data"; diff --git a/components/top-estabelecimentos/establishments-list.tsx b/components/relatorios/estabelecimentos/establishments-list.tsx similarity index 96% rename from components/top-estabelecimentos/establishments-list.tsx rename to components/relatorios/estabelecimentos/establishments-list.tsx index 1681863..dc2d5a1 100644 --- a/components/top-estabelecimentos/establishments-list.tsx +++ b/components/relatorios/estabelecimentos/establishments-list.tsx @@ -4,9 +4,9 @@ import { RiStore2Line } from "@remixicon/react"; import MoneyValues from "@/components/money-values"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Progress } from "@/components/ui/progress"; import { WidgetEmptyState } from "@/components/widget-empty-state"; -import type { TopEstabelecimentosData } from "@/lib/top-estabelecimentos/fetch-data"; -import { Progress } from "../ui/progress"; +import type { TopEstabelecimentosData } from "@/lib/relatorios/estabelecimentos/fetch-data"; type EstablishmentsListProps = { establishments: TopEstabelecimentosData["establishments"]; diff --git a/components/top-estabelecimentos/highlights-cards.tsx b/components/relatorios/estabelecimentos/highlights-cards.tsx similarity index 95% rename from components/top-estabelecimentos/highlights-cards.tsx rename to components/relatorios/estabelecimentos/highlights-cards.tsx index 0378663..2f72fdb 100644 --- a/components/top-estabelecimentos/highlights-cards.tsx +++ b/components/relatorios/estabelecimentos/highlights-cards.tsx @@ -2,7 +2,7 @@ import { RiFireLine, RiTrophyLine } from "@remixicon/react"; import { Card, CardContent } from "@/components/ui/card"; -import type { TopEstabelecimentosData } from "@/lib/top-estabelecimentos/fetch-data"; +import type { TopEstabelecimentosData } from "@/lib/relatorios/estabelecimentos/fetch-data"; type HighlightsCardsProps = { summary: TopEstabelecimentosData["summary"]; diff --git a/components/top-estabelecimentos/period-filter.tsx b/components/relatorios/estabelecimentos/period-filter.tsx similarity index 93% rename from components/top-estabelecimentos/period-filter.tsx rename to components/relatorios/estabelecimentos/period-filter.tsx index d1bf19a..ee2421b 100644 --- a/components/top-estabelecimentos/period-filter.tsx +++ b/components/relatorios/estabelecimentos/period-filter.tsx @@ -2,7 +2,7 @@ import { useRouter, useSearchParams } from "next/navigation"; import { Button } from "@/components/ui/button"; -import type { PeriodFilter } from "@/lib/top-estabelecimentos/fetch-data"; +import type { PeriodFilter } from "@/lib/relatorios/estabelecimentos/fetch-data"; import { cn } from "@/lib/utils"; type PeriodFilterProps = { diff --git a/components/top-estabelecimentos/summary-cards.tsx b/components/relatorios/estabelecimentos/summary-cards.tsx similarity index 95% rename from components/top-estabelecimentos/summary-cards.tsx rename to components/relatorios/estabelecimentos/summary-cards.tsx index 3b26655..b35f231 100644 --- a/components/top-estabelecimentos/summary-cards.tsx +++ b/components/relatorios/estabelecimentos/summary-cards.tsx @@ -8,7 +8,7 @@ import { } from "@remixicon/react"; import MoneyValues from "@/components/money-values"; import { Card, CardContent } from "@/components/ui/card"; -import type { TopEstabelecimentosData } from "@/lib/top-estabelecimentos/fetch-data"; +import type { TopEstabelecimentosData } from "@/lib/relatorios/estabelecimentos/fetch-data"; type SummaryCardsProps = { summary: TopEstabelecimentosData["summary"]; diff --git a/components/top-estabelecimentos/top-categories.tsx b/components/relatorios/estabelecimentos/top-categories.tsx similarity index 95% rename from components/top-estabelecimentos/top-categories.tsx rename to components/relatorios/estabelecimentos/top-categories.tsx index 8648fa7..fc1cdd2 100644 --- a/components/top-estabelecimentos/top-categories.tsx +++ b/components/relatorios/estabelecimentos/top-categories.tsx @@ -4,9 +4,9 @@ import { RiPriceTag3Line } from "@remixicon/react"; import { CategoryIconBadge } from "@/components/categorias/category-icon-badge"; import MoneyValues from "@/components/money-values"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Progress } from "@/components/ui/progress"; import { WidgetEmptyState } from "@/components/widget-empty-state"; -import type { TopEstabelecimentosData } from "@/lib/top-estabelecimentos/fetch-data"; -import { Progress } from "../ui/progress"; +import type { TopEstabelecimentosData } from "@/lib/relatorios/estabelecimentos/fetch-data"; type TopCategoriesProps = { categories: TopEstabelecimentosData["topCategories"]; diff --git a/components/empty-state.tsx b/components/shared/empty-state.tsx similarity index 100% rename from components/empty-state.tsx rename to components/shared/empty-state.tsx diff --git a/components/page-description.tsx b/components/shared/page-description.tsx similarity index 100% rename from components/page-description.tsx rename to components/shared/page-description.tsx diff --git a/components/refresh-page-button.tsx b/components/shared/refresh-page-button.tsx similarity index 100% rename from components/refresh-page-button.tsx rename to components/shared/refresh-page-button.tsx diff --git a/components/skeletons/account-statement-card-skeleton.tsx b/components/shared/skeletons/account-statement-card-skeleton.tsx similarity index 100% rename from components/skeletons/account-statement-card-skeleton.tsx rename to components/shared/skeletons/account-statement-card-skeleton.tsx diff --git a/components/skeletons/category-report-skeleton.tsx b/components/shared/skeletons/category-report-skeleton.tsx similarity index 100% rename from components/skeletons/category-report-skeleton.tsx rename to components/shared/skeletons/category-report-skeleton.tsx diff --git a/components/skeletons/dashboard-grid-skeleton.tsx b/components/shared/skeletons/dashboard-grid-skeleton.tsx similarity index 100% rename from components/skeletons/dashboard-grid-skeleton.tsx rename to components/shared/skeletons/dashboard-grid-skeleton.tsx diff --git a/components/skeletons/filter-skeleton.tsx b/components/shared/skeletons/filter-skeleton.tsx similarity index 100% rename from components/skeletons/filter-skeleton.tsx rename to components/shared/skeletons/filter-skeleton.tsx diff --git a/components/skeletons/index.ts b/components/shared/skeletons/index.ts similarity index 100% rename from components/skeletons/index.ts rename to components/shared/skeletons/index.ts diff --git a/components/skeletons/invoice-summary-card-skeleton.tsx b/components/shared/skeletons/invoice-summary-card-skeleton.tsx similarity index 100% rename from components/skeletons/invoice-summary-card-skeleton.tsx rename to components/shared/skeletons/invoice-summary-card-skeleton.tsx diff --git a/components/skeletons/section-cards-skeleton.tsx b/components/shared/skeletons/section-cards-skeleton.tsx similarity index 100% rename from components/skeletons/section-cards-skeleton.tsx rename to components/shared/skeletons/section-cards-skeleton.tsx diff --git a/components/skeletons/transactions-table-skeleton.tsx b/components/shared/skeletons/transactions-table-skeleton.tsx similarity index 100% rename from components/skeletons/transactions-table-skeleton.tsx rename to components/shared/skeletons/transactions-table-skeleton.tsx diff --git a/components/skeletons/widget-skeleton.tsx b/components/shared/skeletons/widget-skeleton.tsx similarity index 100% rename from components/skeletons/widget-skeleton.tsx rename to components/shared/skeletons/widget-skeleton.tsx diff --git a/components/ui/monthpicker.tsx b/components/ui/month-picker.tsx similarity index 100% rename from components/ui/monthpicker.tsx rename to components/ui/month-picker.tsx diff --git a/components/ui/sidebar.tsx b/components/ui/sidebar.tsx index f9fe79e..e8474a6 100644 --- a/components/ui/sidebar.tsx +++ b/components/ui/sidebar.tsx @@ -21,8 +21,8 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; -import { useIsMobile } from "@/hooks/use-mobile"; import { cn } from "@/lib/utils/ui"; +import { useIsMobile } from "./use-mobile"; const SIDEBAR_COOKIE_NAME = "sidebar_state"; const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; @@ -162,7 +162,7 @@ function Sidebar({ variant?: "sidebar" | "floating" | "inset"; collapsible?: "offcanvas" | "icon" | "none"; }) { - const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); + const { state, openMobile, setOpenMobile } = useSidebar(); if (collapsible === "none") { return ( @@ -179,14 +179,14 @@ function Sidebar({ ); } - if (isMobile) { - return ( + return ( + <> {children}
- ); - } - return ( -
- {/* This is what handles the sidebar gap on desktop */}
- + ); } diff --git a/components/ui/use-mobile.ts b/components/ui/use-mobile.ts new file mode 100644 index 0000000..da96c73 --- /dev/null +++ b/components/ui/use-mobile.ts @@ -0,0 +1,26 @@ +import * as React from "react"; + +const MOBILE_BREAKPOINT = 768; +const MOBILE_MEDIA_QUERY = `(max-width: ${MOBILE_BREAKPOINT - 1}px)`; + +export function useIsMobile() { + const subscribe = React.useCallback((onStoreChange: () => void) => { + if (typeof window === "undefined") { + return () => {}; + } + + const mediaQueryList = window.matchMedia(MOBILE_MEDIA_QUERY); + mediaQueryList.addEventListener("change", onStoreChange); + return () => mediaQueryList.removeEventListener("change", onStoreChange); + }, []); + + const getSnapshot = React.useCallback(() => { + if (typeof window === "undefined") { + return false; + } + + return window.matchMedia(MOBILE_MEDIA_QUERY).matches; + }, []); + + return React.useSyncExternalStore(subscribe, getSnapshot, () => false); +} diff --git a/hooks/use-form-state.ts b/hooks/use-form-state.ts index fa63b48..def4fe1 100644 --- a/hooks/use-form-state.ts +++ b/hooks/use-form-state.ts @@ -1,10 +1,10 @@ -import { useState } from "react"; +import { useCallback, useRef, useState } from "react"; /** * Hook for managing form state with type-safe field updates * * @param initialValues - Initial form values - * @returns Object with formState, updateField, resetForm, setFormState + * @returns Object with formState, updateField, updateFields, replaceForm, resetForm * * @example * ```tsx @@ -16,37 +16,45 @@ import { useState } from "react"; * updateField('name', 'John'); * ``` */ -export function useFormState>( - initialValues: T, -) { +export function useFormState(initialValues: T) { + const latestInitialValuesRef = useRef(initialValues); + latestInitialValuesRef.current = initialValues; + const [formState, setFormState] = useState(initialValues); /** * Updates a single field in the form state */ - const updateField = (field: K, value: T[K]) => { - setFormState((prev) => ({ ...prev, [field]: value })); - }; + const updateField = useCallback( + (field: K, value: T[K]) => { + setFormState((prev) => ({ ...prev, [field]: value })); + }, + [], + ); /** * Resets form to initial values */ - const resetForm = () => { - setFormState(initialValues); - }; + const resetForm = useCallback((nextValues?: T) => { + setFormState(nextValues ?? latestInitialValuesRef.current); + }, []); /** * Updates multiple fields at once */ - const updateFields = (updates: Partial) => { + const updateFields = useCallback((updates: Partial) => { setFormState((prev) => ({ ...prev, ...updates })); - }; + }, []); + + const replaceForm = useCallback((nextValues: T) => { + setFormState(nextValues); + }, []); return { formState, updateField, updateFields, + replaceForm, resetForm, - setFormState, }; } diff --git a/hooks/use-mobile.ts b/hooks/use-mobile.ts deleted file mode 100644 index 0a89231..0000000 --- a/hooks/use-mobile.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as React from "react"; - -const MOBILE_BREAKPOINT = 768; - -export function useIsMobile() { - const [isMobile, setIsMobile] = React.useState( - undefined, - ); - - React.useEffect(() => { - const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); - const onChange = () => { - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); - }; - mql.addEventListener("change", onChange); - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); - return () => mql.removeEventListener("change", onChange); - }, []); - - return !!isMobile; -} diff --git a/hooks/use-month-period.ts b/hooks/use-month-period.ts deleted file mode 100644 index 08d3d7a..0000000 --- a/hooks/use-month-period.ts +++ /dev/null @@ -1,79 +0,0 @@ -"use client"; - -import { usePathname, useRouter, useSearchParams } from "next/navigation"; -import { useMemo } from "react"; - -import { MONTH_NAMES } from "@/lib/utils/period"; - -const PERIOD_PARAM = "periodo"; - -const normalizeMonth = (value: string) => value.trim().toLowerCase(); - -export function useMonthPeriod() { - const searchParams = useSearchParams(); - const pathname = usePathname(); - const router = useRouter(); - - // Get current date info - const now = new Date(); - const currentYear = now.getFullYear(); - const currentMonthName = MONTH_NAMES[now.getMonth()]; - const optionsMeses = useMemo(() => [...MONTH_NAMES], []); - - const defaultMonth = currentMonthName; - const defaultYear = currentYear.toString(); - - const periodFromParams = searchParams.get(PERIOD_PARAM); - - const { month: currentMonth, year: currentYearValue } = useMemo(() => { - if (!periodFromParams) { - return { month: defaultMonth, year: defaultYear }; - } - - const [rawMonth, rawYear] = periodFromParams.split("-"); - const normalizedMonth = normalizeMonth(rawMonth ?? ""); - const normalizedYear = (rawYear ?? "").trim(); - const monthExists = optionsMeses.includes(normalizedMonth); - const parsedYear = Number.parseInt(normalizedYear, 10); - - if (!monthExists || Number.isNaN(parsedYear)) { - return { month: defaultMonth, year: defaultYear }; - } - - return { - month: normalizedMonth, - year: parsedYear.toString(), - }; - }, [periodFromParams, defaultMonth, defaultYear, optionsMeses]); - - const buildHref = (month: string, year: string | number) => { - const normalizedMonth = normalizeMonth(month); - const normalizedYear = String(year).trim(); - - const params = new URLSearchParams(searchParams.toString()); - params.set(PERIOD_PARAM, `${normalizedMonth}-${normalizedYear}`); - - return `${pathname}?${params.toString()}`; - }; - - const replacePeriod = (target: string) => { - if (!target) { - return; - } - - router.replace(target, { scroll: false }); - }; - - return { - monthNames: optionsMeses, - pathname, - currentMonth, - currentYear: currentYearValue, - defaultMonth, - defaultYear, - buildHref, - replacePeriod, - }; -} - -export { PERIOD_PARAM as MONTH_PERIOD_PARAM }; diff --git a/lib/auth/config.ts b/lib/auth/config.ts index 546ece6..fa3d62a 100644 --- a/lib/auth/config.ts +++ b/lib/auth/config.ts @@ -132,7 +132,6 @@ export const auth = betterAuth({ "[Auth] Falha ao criar dados padrão do usuário:", error, ); - // TODO: Considere enfileirar para retry ou notificar admin } }, }, diff --git a/lib/accounts/constants.ts b/lib/contas/constants.ts similarity index 100% rename from lib/accounts/constants.ts rename to lib/contas/constants.ts diff --git a/lib/dashboard/accounts.ts b/lib/dashboard/accounts.ts index 3b1d6bc..492d6b4 100644 --- a/lib/dashboard/accounts.ts +++ b/lib/dashboard/accounts.ts @@ -1,6 +1,6 @@ import { and, eq, sql } from "drizzle-orm"; import { contas, lancamentos, pagadores } from "@/db/schema"; -import { INITIAL_BALANCE_NOTE } from "@/lib/accounts/constants"; +import { INITIAL_BALANCE_NOTE } from "@/lib/contas/constants"; import { toNumber } from "@/lib/dashboard/common"; import { db } from "@/lib/db"; import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants"; diff --git a/lib/dashboard/categories/category-details.ts b/lib/dashboard/categories/category-details.ts index 7798034..787a23a 100644 --- a/lib/dashboard/categories/category-details.ts +++ b/lib/dashboard/categories/category-details.ts @@ -1,10 +1,10 @@ import { and, desc, eq, isNull, ne, or, sql } from "drizzle-orm"; import { categorias, contas, lancamentos, pagadores } from "@/db/schema"; +import type { CategoryType } from "@/lib/categorias/constants"; import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX, INITIAL_BALANCE_NOTE, -} from "@/lib/accounts/constants"; -import type { CategoryType } from "@/lib/categorias/constants"; +} from "@/lib/contas/constants"; import { toNumber } from "@/lib/dashboard/common"; import { db } from "@/lib/db"; import { mapLancamentosData } from "@/lib/lancamentos/page-helpers"; diff --git a/lib/dashboard/categories/category-history.ts b/lib/dashboard/categories/category-history.ts index 8e0f57a..8438121 100644 --- a/lib/dashboard/categories/category-history.ts +++ b/lib/dashboard/categories/category-history.ts @@ -2,7 +2,7 @@ import { addMonths, format } from "date-fns"; import { ptBR } from "date-fns/locale"; import { and, eq, inArray, isNull, or, sql } from "drizzle-orm"; import { categorias, lancamentos, pagadores } from "@/db/schema"; -import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/accounts/constants"; +import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/contas/constants"; import { toNumber } from "@/lib/dashboard/common"; import { db } from "@/lib/db"; import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants"; diff --git a/lib/dashboard/categories/expenses-by-category.ts b/lib/dashboard/categories/expenses-by-category.ts index 70c5de0..7aaf5f3 100644 --- a/lib/dashboard/categories/expenses-by-category.ts +++ b/lib/dashboard/categories/expenses-by-category.ts @@ -1,6 +1,6 @@ import { and, eq, inArray, isNull, or, sql } from "drizzle-orm"; import { categorias, lancamentos, orcamentos } from "@/db/schema"; -import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/accounts/constants"; +import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/contas/constants"; import { toNumber } from "@/lib/dashboard/common"; import { db } from "@/lib/db"; import { getAdminPagadorId } from "@/lib/pagadores/get-admin-id"; diff --git a/lib/dashboard/categories/income-by-category.ts b/lib/dashboard/categories/income-by-category.ts index d544d1f..dd38246 100644 --- a/lib/dashboard/categories/income-by-category.ts +++ b/lib/dashboard/categories/income-by-category.ts @@ -3,7 +3,7 @@ import { categorias, contas, lancamentos, orcamentos } from "@/db/schema"; import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX, INITIAL_BALANCE_NOTE, -} from "@/lib/accounts/constants"; +} from "@/lib/contas/constants"; import { db } from "@/lib/db"; import { getAdminPagadorId } from "@/lib/pagadores/get-admin-id"; import { calculatePercentageChange } from "@/lib/utils/math"; diff --git a/lib/dashboard/expenses/installment-analysis.ts b/lib/dashboard/expenses/installment-analysis.ts index ab83a36..c65b073 100644 --- a/lib/dashboard/expenses/installment-analysis.ts +++ b/lib/dashboard/expenses/installment-analysis.ts @@ -3,7 +3,7 @@ import { cartoes, lancamentos, pagadores } from "@/db/schema"; import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX, INITIAL_BALANCE_NOTE, -} from "@/lib/accounts/constants"; +} from "@/lib/contas/constants"; import { toNumber } from "@/lib/dashboard/common"; import { db } from "@/lib/db"; import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants"; @@ -172,8 +172,5 @@ export async function fetchInstallmentAnalysis( 0, ); - return { - installmentGroups, - totalPendingInstallments, - }; + return { installmentGroups, totalPendingInstallments }; } diff --git a/lib/dashboard/expenses/installment-expenses.ts b/lib/dashboard/expenses/installment-expenses.ts index aef40a1..939e38d 100644 --- a/lib/dashboard/expenses/installment-expenses.ts +++ b/lib/dashboard/expenses/installment-expenses.ts @@ -3,7 +3,7 @@ import { lancamentos } from "@/db/schema"; import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX, INITIAL_BALANCE_NOTE, -} from "@/lib/accounts/constants"; +} from "@/lib/contas/constants"; import { toNumber } from "@/lib/dashboard/common"; import { db } from "@/lib/db"; import { getAdminPagadorId } from "@/lib/pagadores/get-admin-id"; @@ -65,9 +65,11 @@ export async function fetchInstallmentExpenses( ) .orderBy(desc(lancamentos.purchaseDate), desc(lancamentos.createdAt)); + type InstallmentExpenseRow = (typeof rows)[number]; + const expenses = rows .map( - (row): InstallmentExpense => ({ + (row: InstallmentExpenseRow): InstallmentExpense => ({ id: row.id, name: row.name, amount: Math.abs(toNumber(row.amount)), @@ -79,7 +81,7 @@ export async function fetchInstallmentExpenses( period: row.period, }), ) - .sort((a, b) => { + .sort((a: InstallmentExpense, b: InstallmentExpense) => { // Calcula parcelas restantes para cada item const remainingA = a.installmentCount && a.currentInstallment @@ -94,7 +96,5 @@ export async function fetchInstallmentExpenses( return remainingA - remainingB; }); - return { - expenses, - }; + return { expenses }; } diff --git a/lib/dashboard/expenses/recurring-expenses.ts b/lib/dashboard/expenses/recurring-expenses.ts index 2a4c6b9..94e5cf7 100644 --- a/lib/dashboard/expenses/recurring-expenses.ts +++ b/lib/dashboard/expenses/recurring-expenses.ts @@ -3,7 +3,7 @@ import { lancamentos } from "@/db/schema"; import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX, INITIAL_BALANCE_NOTE, -} from "@/lib/accounts/constants"; +} from "@/lib/contas/constants"; import { toNumber } from "@/lib/dashboard/common"; import { db } from "@/lib/db"; import { getAdminPagadorId } from "@/lib/pagadores/get-admin-id"; diff --git a/lib/dashboard/expenses/top-expenses.ts b/lib/dashboard/expenses/top-expenses.ts index 9176861..07c8136 100644 --- a/lib/dashboard/expenses/top-expenses.ts +++ b/lib/dashboard/expenses/top-expenses.ts @@ -3,7 +3,7 @@ import { cartoes, contas, lancamentos } from "@/db/schema"; import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX, INITIAL_BALANCE_NOTE, -} from "@/lib/accounts/constants"; +} from "@/lib/contas/constants"; import { toNumber } from "@/lib/dashboard/common"; import { db } from "@/lib/db"; import { getAdminPagadorId } from "@/lib/pagadores/get-admin-id"; diff --git a/lib/dashboard/income-expense-balance.ts b/lib/dashboard/income-expense-balance.ts index 8c4fd29..9cabbab 100644 --- a/lib/dashboard/income-expense-balance.ts +++ b/lib/dashboard/income-expense-balance.ts @@ -3,7 +3,7 @@ import { contas, lancamentos } from "@/db/schema"; import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX, INITIAL_BALANCE_NOTE, -} from "@/lib/accounts/constants"; +} from "@/lib/contas/constants"; import { toNumber } from "@/lib/dashboard/common"; import { db } from "@/lib/db"; import { getAdminPagadorId } from "@/lib/pagadores/get-admin-id"; diff --git a/lib/dashboard/invoices.ts b/lib/dashboard/invoices.ts index caf1b94..b19a0e6 100644 --- a/lib/dashboard/invoices.ts +++ b/lib/dashboard/invoices.ts @@ -1,6 +1,6 @@ import { and, eq, ilike, isNotNull, sql } from "drizzle-orm"; import { cartoes, faturas, lancamentos, pagadores } from "@/db/schema"; -import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/accounts/constants"; +import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/contas/constants"; import { toNumber } from "@/lib/dashboard/common"; import { db } from "@/lib/db"; import { diff --git a/lib/dashboard/metrics.ts b/lib/dashboard/metrics.ts index 28e1ec9..05022d2 100644 --- a/lib/dashboard/metrics.ts +++ b/lib/dashboard/metrics.ts @@ -15,7 +15,7 @@ import { contas, lancamentos } from "@/db/schema"; import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX, INITIAL_BALANCE_NOTE, -} from "@/lib/accounts/constants"; +} from "@/lib/contas/constants"; import { db } from "@/lib/db"; import { getAdminPagadorId } from "@/lib/pagadores/get-admin-id"; import { safeToNumber } from "@/lib/utils/number"; diff --git a/lib/dashboard/pagadores.ts b/lib/dashboard/pagadores.ts index 2c7e19c..98d9405 100644 --- a/lib/dashboard/pagadores.ts +++ b/lib/dashboard/pagadores.ts @@ -1,6 +1,6 @@ import { and, desc, eq, inArray, isNull, or, sql } from "drizzle-orm"; import { lancamentos, pagadores } from "@/db/schema"; -import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/accounts/constants"; +import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/contas/constants"; import { toNumber } from "@/lib/dashboard/common"; import { db } from "@/lib/db"; import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants"; diff --git a/lib/dashboard/payments/payment-conditions.ts b/lib/dashboard/payments/payment-conditions.ts index 6d0e784..c1eb0eb 100644 --- a/lib/dashboard/payments/payment-conditions.ts +++ b/lib/dashboard/payments/payment-conditions.ts @@ -3,7 +3,7 @@ import { lancamentos } from "@/db/schema"; import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX, INITIAL_BALANCE_NOTE, -} from "@/lib/accounts/constants"; +} from "@/lib/contas/constants"; import { toNumber } from "@/lib/dashboard/common"; import { db } from "@/lib/db"; import { getAdminPagadorId } from "@/lib/pagadores/get-admin-id"; diff --git a/lib/dashboard/payments/payment-methods.ts b/lib/dashboard/payments/payment-methods.ts index 4ebcf4c..38a8583 100644 --- a/lib/dashboard/payments/payment-methods.ts +++ b/lib/dashboard/payments/payment-methods.ts @@ -3,7 +3,7 @@ import { lancamentos } from "@/db/schema"; import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX, INITIAL_BALANCE_NOTE, -} from "@/lib/accounts/constants"; +} from "@/lib/contas/constants"; import { toNumber } from "@/lib/dashboard/common"; import { db } from "@/lib/db"; import { getAdminPagadorId } from "@/lib/pagadores/get-admin-id"; diff --git a/lib/dashboard/payments/payment-status.ts b/lib/dashboard/payments/payment-status.ts index 32542d3..2839a59 100644 --- a/lib/dashboard/payments/payment-status.ts +++ b/lib/dashboard/payments/payment-status.ts @@ -1,6 +1,6 @@ import { and, eq, inArray, sql } from "drizzle-orm"; import { lancamentos } from "@/db/schema"; -import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/accounts/constants"; +import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/contas/constants"; import { toNumber } from "@/lib/dashboard/common"; import { db } from "@/lib/db"; import { getAdminPagadorId } from "@/lib/pagadores/get-admin-id"; diff --git a/lib/dashboard/purchases-by-category.ts b/lib/dashboard/purchases-by-category.ts index 5892b7d..2d266c0 100644 --- a/lib/dashboard/purchases-by-category.ts +++ b/lib/dashboard/purchases-by-category.ts @@ -3,7 +3,7 @@ import { cartoes, categorias, contas, lancamentos } from "@/db/schema"; import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX, INITIAL_BALANCE_NOTE, -} from "@/lib/accounts/constants"; +} from "@/lib/contas/constants"; import { toNumber } from "@/lib/dashboard/common"; import { db } from "@/lib/db"; import { getAdminPagadorId } from "@/lib/pagadores/get-admin-id"; diff --git a/lib/dashboard/top-establishments.ts b/lib/dashboard/top-establishments.ts index d7057c7..93d5667 100644 --- a/lib/dashboard/top-establishments.ts +++ b/lib/dashboard/top-establishments.ts @@ -3,7 +3,7 @@ import { cartoes, contas, lancamentos } from "@/db/schema"; import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX, INITIAL_BALANCE_NOTE, -} from "@/lib/accounts/constants"; +} from "@/lib/contas/constants"; import { toNumber } from "@/lib/dashboard/common"; import { db } from "@/lib/db"; import { getAdminPagadorId } from "@/lib/pagadores/get-admin-id"; diff --git a/lib/dashboard/widgets/widgets-config.tsx b/lib/dashboard/widgets/widgets-config.tsx index b1461f9..bbb1d42 100644 --- a/lib/dashboard/widgets/widgets-config.tsx +++ b/lib/dashboard/widgets/widgets-config.tsx @@ -17,11 +17,11 @@ import { import Link from "next/link"; import type { ReactNode } from "react"; import { BoletosWidget } from "@/components/dashboard/boletos-widget"; +import { InstallmentExpensesWidget } from "@/components/dashboard/installment-expenses-widget"; import { ExpensesByCategoryWidgetWithChart } from "@/components/dashboard/expenses-by-category-widget-with-chart"; import { GoalsProgressWidget } from "@/components/dashboard/goals-progress-widget"; import { IncomeByCategoryWidgetWithChart } from "@/components/dashboard/income-by-category-widget-with-chart"; import { IncomeExpenseBalanceWidget } from "@/components/dashboard/income-expense-balance-widget"; -import { InstallmentExpensesWidget } from "@/components/dashboard/installment-expenses-widget"; import { InvoicesWidget } from "@/components/dashboard/invoices-widget"; import { MyAccountsWidget } from "@/components/dashboard/my-accounts-widget"; import { NotesWidget } from "@/components/dashboard/notes-widget"; @@ -31,7 +31,7 @@ import { PaymentStatusWidget } from "@/components/dashboard/payment-status-widge import { PurchasesByCategoryWidget } from "@/components/dashboard/purchases-by-category-widget"; import { RecurringExpensesWidget } from "@/components/dashboard/recurring-expenses-widget"; import { SpendingOverviewWidget } from "@/components/dashboard/spending-overview-widget"; -import type { DashboardData } from "./fetch-dashboard-data"; +import type { DashboardData } from "../fetch-dashboard-data"; export type WidgetConfig = { id: string; @@ -173,15 +173,6 @@ export const widgetsConfig: WidgetConfig[] = [ component: ({ data }) => ( ), - action: ( - - Análise - - - ), }, { id: "spending-overview", diff --git a/lib/installments/anticipation-types.ts b/lib/installments/anticipation-types.ts index 2d84259..62b1009 100644 --- a/lib/installments/anticipation-types.ts +++ b/lib/installments/anticipation-types.ts @@ -38,6 +38,7 @@ export type CreateAnticipationInput = { seriesId: string; installmentIds: string[]; anticipationPeriod: string; + discount?: number; pagadorId?: string; categoriaId?: string; note?: string; diff --git a/lib/installments/utils.ts b/lib/installments/utils.ts index 53c6d7b..a8cc09d 100644 --- a/lib/installments/utils.ts +++ b/lib/installments/utils.ts @@ -71,9 +71,6 @@ export function formatPurchaseDate(date: Date): string { * Formata o texto da parcela atual * Exemplo: "1 de 6" */ -export function formatCurrentInstallment( - current: number, - total: number, -): string { +export function formatCurrentInstallment(current: number, total: number): string { return `${current} de ${total}`; } diff --git a/lib/lancamentos/form-helpers.ts b/lib/lancamentos/form-helpers.ts index 1a2e27b..85fd14d 100644 --- a/lib/lancamentos/form-helpers.ts +++ b/lib/lancamentos/form-helpers.ts @@ -12,7 +12,7 @@ import { * and due day. The period represents the month the fatura is due (vencimento). * * Steps: - * 1. If purchase day > closing day → the purchase missed this month's closing, + * 1. If purchase day >= closing day → the purchase missed this month's closing, * so it enters the NEXT month's billing cycle (+1 month from purchase). * 2. Then, if dueDay < closingDay, the due date falls in the month AFTER the * closing month (e.g., closes 22nd, due 1st → closes Mar/22, due Apr/1), @@ -25,6 +25,7 @@ import { * * // Card closes day 5, due day 15 (dueDay >= closingDay → no extra) * deriveCreditCardPeriod("2026-02-10", "5", "15") // "2026-03" (missed Feb closing → Mar cycle → due Mar) + * deriveCreditCardPeriod("2026-02-05", "5", "15") // "2026-03" (closing day itself already goes to next cycle) * deriveCreditCardPeriod("2026-02-03", "5", "15") // "2026-02" (in Feb cycle → due Feb) */ export function deriveCreditCardPeriod( @@ -44,8 +45,8 @@ export function deriveCreditCardPeriod( // Start with the purchase month as the billing cycle let period = basePeriod; - // If purchase is after closing day, it enters the next billing cycle - if (purchaseDayNum > closingDayNum) { + // If purchase is on/after closing day, it enters the next billing cycle + if (purchaseDayNum >= closingDayNum) { period = getNextPeriod(period); } diff --git a/lib/lancamentos/page-helpers.ts b/lib/lancamentos/page-helpers.ts index b8e22e6..665a4a1 100644 --- a/lib/lancamentos/page-helpers.ts +++ b/lib/lancamentos/page-helpers.ts @@ -8,7 +8,7 @@ import { lancamentos, pagadores, } from "@/db/schema"; -import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/accounts/constants"; +import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/contas/constants"; import { db } from "@/lib/db"; import { LANCAMENTO_CONDITIONS, diff --git a/lib/pagadores/details.ts b/lib/pagadores/details.ts index d05c053..7608403 100644 --- a/lib/pagadores/details.ts +++ b/lib/pagadores/details.ts @@ -12,7 +12,7 @@ import { sum, } from "drizzle-orm"; import { cartoes, lancamentos } from "@/db/schema"; -import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/accounts/constants"; +import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/contas/constants"; import { db } from "@/lib/db"; const RECEITA = "Receita"; diff --git a/lib/preferences/fonts.ts b/lib/preferences/fonts.ts index 0e16dc2..0051376 100644 --- a/lib/preferences/fonts.ts +++ b/lib/preferences/fonts.ts @@ -1,15 +1,20 @@ import { eq } from "drizzle-orm"; import { cache } from "react"; import { db, schema } from "@/lib/db"; +import { + DEFAULT_FONT_KEY, + type FontKey, + normalizeFontKey, +} from "@/public/fonts/font_index"; export type FontPreferences = { - systemFont: string; - moneyFont: string; + systemFont: FontKey; + moneyFont: FontKey; }; const DEFAULT_FONT_PREFS: FontPreferences = { - systemFont: "ai-sans", - moneyFont: "ai-sans", + systemFont: DEFAULT_FONT_KEY, + moneyFont: DEFAULT_FONT_KEY, }; export const fetchUserFontPreferences = cache( @@ -26,8 +31,8 @@ export const fetchUserFontPreferences = cache( if (!result[0]) return DEFAULT_FONT_PREFS; return { - systemFont: result[0].systemFont, - moneyFont: result[0].moneyFont, + systemFont: normalizeFontKey(result[0].systemFont), + moneyFont: normalizeFontKey(result[0].moneyFont), }; }, ); diff --git a/lib/top-estabelecimentos/fetch-data.ts b/lib/relatorios/estabelecimentos/fetch-data.ts similarity index 99% rename from lib/top-estabelecimentos/fetch-data.ts rename to lib/relatorios/estabelecimentos/fetch-data.ts index 79a221f..2f33241 100644 --- a/lib/top-estabelecimentos/fetch-data.ts +++ b/lib/relatorios/estabelecimentos/fetch-data.ts @@ -17,7 +17,7 @@ import { categorias, contas, lancamentos, pagadores } from "@/db/schema"; import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX, INITIAL_BALANCE_NOTE, -} from "@/lib/accounts/constants"; +} from "@/lib/contas/constants"; import { db } from "@/lib/db"; import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants"; import { safeToNumber } from "@/lib/utils/number"; diff --git a/lib/relatorios/fetch-category-chart-data.ts b/lib/relatorios/fetch-category-chart-data.ts index 94bd610..f368e10 100644 --- a/lib/relatorios/fetch-category-chart-data.ts +++ b/lib/relatorios/fetch-category-chart-data.ts @@ -2,7 +2,7 @@ import { format } from "date-fns"; import { ptBR } from "date-fns/locale"; import { and, eq, inArray, isNull, or, sql } from "drizzle-orm"; import { categorias, lancamentos } from "@/db/schema"; -import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/accounts/constants"; +import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/contas/constants"; import { toNumber } from "@/lib/dashboard/common"; import { db } from "@/lib/db"; import { getAdminPagadorId } from "@/lib/pagadores/get-admin-id"; diff --git a/lib/relatorios/fetch-category-report.ts b/lib/relatorios/fetch-category-report.ts index 5ca08cc..a2335a3 100644 --- a/lib/relatorios/fetch-category-report.ts +++ b/lib/relatorios/fetch-category-report.ts @@ -1,6 +1,6 @@ import { and, eq, inArray, isNull, or, sql } from "drizzle-orm"; import { categorias, lancamentos } from "@/db/schema"; -import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/accounts/constants"; +import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/contas/constants"; import { toNumber } from "@/lib/dashboard/common"; import { db } from "@/lib/db"; import { getAdminPagadorId } from "@/lib/pagadores/get-admin-id"; diff --git a/lib/utils/export-branding.ts b/lib/utils/export-branding.ts new file mode 100644 index 0000000..e7b5bb9 --- /dev/null +++ b/lib/utils/export-branding.ts @@ -0,0 +1,108 @@ +const FALLBACK_PRIMARY_COLOR: [number, number, number] = [201, 106, 58]; +const RGB_PATTERN = /\d+(?:\.\d+)?/g; + +function parseRgbColor(value: string): [number, number, number] | null { + if (!value.toLowerCase().startsWith("rgb")) { + return null; + } + + const channels = value.match(RGB_PATTERN); + if (!channels || channels.length < 3) { + return null; + } + + const red = Number.parseFloat(channels[0]); + const green = Number.parseFloat(channels[1]); + const blue = Number.parseFloat(channels[2]); + + if ([red, green, blue].some((channel) => Number.isNaN(channel))) { + return null; + } + + return [Math.round(red), Math.round(green), Math.round(blue)]; +} + +function resolveCssColor(value: string): [number, number, number] | null { + if (typeof window === "undefined" || typeof document === "undefined") { + return null; + } + + const probe = document.createElement("span"); + probe.style.position = "fixed"; + probe.style.opacity = "0"; + probe.style.pointerEvents = "none"; + probe.style.color = ""; + probe.style.color = value; + + if (!probe.style.color) { + return null; + } + + document.body.appendChild(probe); + const resolved = window.getComputedStyle(probe).color; + document.body.removeChild(probe); + + return parseRgbColor(resolved); +} + +export function getPrimaryPdfColor(): [number, number, number] { + if (typeof window === "undefined" || typeof document === "undefined") { + return FALLBACK_PRIMARY_COLOR; + } + + const rootStyles = window.getComputedStyle(document.documentElement); + const rawPrimary = rootStyles.getPropertyValue("--primary").trim(); + const rawColorPrimary = rootStyles.getPropertyValue("--color-primary").trim(); + const candidates = [rawPrimary, rawColorPrimary].filter(Boolean); + + for (const candidate of candidates) { + const resolved = resolveCssColor(candidate); + if (resolved) { + return resolved; + } + } + + return FALLBACK_PRIMARY_COLOR; +} + +export async function loadExportLogoDataUrl( + logoPath = "/logo_text.png", +): Promise { + if (typeof window === "undefined" || typeof document === "undefined") { + return null; + } + + return new Promise((resolve) => { + const image = new Image(); + image.crossOrigin = "anonymous"; + + image.onload = () => { + const width = image.naturalWidth || image.width; + const height = image.naturalHeight || image.height; + if (!width || !height) { + resolve(null); + return; + } + + const canvas = document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + const context = canvas.getContext("2d"); + if (!context) { + resolve(null); + return; + } + + context.drawImage(image, 0, 0, width, height); + + try { + resolve(canvas.toDataURL("image/png")); + } catch { + resolve(null); + } + }; + + image.onerror = () => resolve(null); + image.src = logoPath; + }); +} diff --git a/next.config.ts b/next.config.ts index 69bd21c..f360909 100644 --- a/next.config.ts +++ b/next.config.ts @@ -11,7 +11,6 @@ const nextConfig: NextConfig = { }, reactCompiler: true, typescript: { - // TODO: Corrigir erros TS e remover. Erros pré-existentes em ~5 arquivos. ignoreBuildErrors: true, }, images: { diff --git a/package.json b/package.json index f0e3d0a..a61c0e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openmonetis", - "version": "1.7.6", + "version": "1.7.7", "private": true, "scripts": { "dev": "next dev --turbopack", @@ -27,10 +27,10 @@ "docker:rebuild": "docker compose up --build --force-recreate" }, "dependencies": { - "@ai-sdk/anthropic": "^3.0.48", - "@ai-sdk/google": "^3.0.33", - "@ai-sdk/openai": "^3.0.36", - "@better-auth/passkey": "^1.5.0", + "@ai-sdk/anthropic": "^3.0.58", + "@ai-sdk/google": "^3.0.43", + "@ai-sdk/openai": "^3.0.41", + "@better-auth/passkey": "^1.5.4", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", @@ -58,7 +58,7 @@ "@tanstack/react-table": "8.21.3", "@vercel/analytics": "^1.6.1", "@vercel/speed-insights": "^1.3.1", - "ai": "^6.0.103", + "ai": "^6.0.116", "better-auth": "1.4.19", "class-variance-authority": "0.7.1", "clsx": "2.1.1", @@ -75,7 +75,7 @@ "react-day-picker": "^9.14.0", "react-dom": "19.2.4", "recharts": "3.7.0", - "resend": "^6.9.2", + "resend": "^6.9.3", "sonner": "2.0.7", "tailwind-merge": "3.5.0", "vaul": "1.1.2", @@ -86,7 +86,7 @@ "@biomejs/biome": "2.4.4", "@tailwindcss/postcss": "4.2.1", "@types/node": "25.3.2", - "@types/pg": "^8.16.0", + "@types/pg": "^8.18.0", "@types/react": "19.2.14", "@types/react-dom": "19.2.3", "dotenv": "^17.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 083b015..18cefd4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,17 +9,17 @@ importers: .: dependencies: '@ai-sdk/anthropic': - specifier: ^3.0.48 - version: 3.0.48(zod@4.3.6) + specifier: ^3.0.58 + version: 3.0.58(zod@4.3.6) '@ai-sdk/google': - specifier: ^3.0.33 - version: 3.0.33(zod@4.3.6) + specifier: ^3.0.43 + version: 3.0.43(zod@4.3.6) '@ai-sdk/openai': - specifier: ^3.0.36 - version: 3.0.36(zod@4.3.6) + specifier: ^3.0.41 + version: 3.0.41(zod@4.3.6) '@better-auth/passkey': - specifier: ^1.5.0 - version: 1.5.0(@better-auth/core@1.5.0(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-auth@1.4.19(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.19.0))(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.19.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(better-call@1.3.2(zod@4.3.6))(nanostores@1.1.0) + specifier: ^1.5.4 + version: 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.0)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-auth@1.4.19(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(kysely@0.28.11)(pg@8.19.0))(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.19.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(better-call@1.3.2(zod@4.3.6))(nanostores@1.1.1) '@dnd-kit/core': specifier: ^6.3.1 version: 6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -31,7 +31,7 @@ importers: version: 3.2.2(react@19.2.4) '@openrouter/ai-sdk-provider': specifier: ^2.2.3 - version: 2.2.3(ai@6.0.103(zod@4.3.6))(zod@4.3.6) + version: 2.2.3(ai@6.0.116(zod@4.3.6))(zod@4.3.6) '@radix-ui/react-alert-dialog': specifier: 1.1.15 version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -102,11 +102,11 @@ importers: specifier: ^1.3.1 version: 1.3.1(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) ai: - specifier: ^6.0.103 - version: 6.0.103(zod@4.3.6) + specifier: ^6.0.116 + version: 6.0.116(zod@4.3.6) better-auth: specifier: 1.4.19 - version: 1.4.19(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.19.0))(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.19.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 1.4.19(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(kysely@0.28.11)(pg@8.19.0))(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.19.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) class-variance-authority: specifier: 0.7.1 version: 0.7.1 @@ -121,7 +121,7 @@ importers: version: 4.1.0 drizzle-orm: specifier: 0.45.1 - version: 0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.19.0) + version: 0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(kysely@0.28.11)(pg@8.19.0) jspdf: specifier: ^4.2.0 version: 4.2.0 @@ -153,8 +153,8 @@ importers: specifier: 3.7.0 version: 3.7.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react-is@16.13.1)(react@19.2.4)(redux@5.0.1) resend: - specifier: ^6.9.2 - version: 6.9.2 + specifier: ^6.9.3 + version: 6.9.3 sonner: specifier: 2.0.7 version: 2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -181,8 +181,8 @@ importers: specifier: 25.3.2 version: 25.3.2 '@types/pg': - specifier: ^8.16.0 - version: 8.16.0 + specifier: ^8.18.0 + version: 8.18.0 '@types/react': specifier: 19.2.14 version: 19.2.14 @@ -207,32 +207,32 @@ importers: packages: - '@ai-sdk/anthropic@3.0.48': - resolution: {integrity: sha512-QSvz0XumBFaxulalUB+3D2/tLvfsYjIcG3HlqOxmmXIzATIBLInTVEw0Sy3yXJjqhNQZedBokVrN53Bh/V+eWQ==} + '@ai-sdk/anthropic@3.0.58': + resolution: {integrity: sha512-/53SACgmVukO4bkms4dpxpRlYhW8Ct6QZRe6sj1Pi5H00hYhxIrqfiLbZBGxkdRvjsBQeP/4TVGsXgH5rQeb8Q==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/gateway@3.0.57': - resolution: {integrity: sha512-3MugqOlGfCOjlsBGGARJ5Zrioh78X3+rulHCayCMPySYKY+wc8GGFlFCCh4mleWQFShjMyqWT7eeLTuVSj/WSg==} + '@ai-sdk/gateway@3.0.66': + resolution: {integrity: sha512-SIQ0YY0iMuv+07HLsZ+bB990zUJ6S4ujORAh+Jv1V2KGNn73qQKnGO0JBk+w+Res8YqOFSycwDoWcFlQrVxS4A==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/google@3.0.33': - resolution: {integrity: sha512-ElHkhMGMJ1MY5AlwLljWWE1jj+Bs3cMyq0KbeWUu2H89OsMAORiE4cB3xhfLlSIEnVmVKx/YHjoW3bN+DFI24A==} + '@ai-sdk/google@3.0.43': + resolution: {integrity: sha512-NGCgP5g8HBxrNdxvF8Dhww+UKfqAkZAmyYBvbu9YLoBkzAmGKDBGhVptN/oXPB5Vm0jggMdoLycZ8JReQM8Zqg==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/openai@3.0.36': - resolution: {integrity: sha512-foY3onGY8l3q9niMw0Cwe9xrYnm46keIWL57NRw6F3DKzSW9TYTfx0cQJs/j8lXJ8lPzqNxpMO/zXOkqCUt3IQ==} + '@ai-sdk/openai@3.0.41': + resolution: {integrity: sha512-IZ42A+FO+vuEQCVNqlnAPYQnnUpUfdJIwn1BEDOBywiEHa23fw7PahxVtlX9zm3/zMvTW4JKPzWyvAgDu+SQ2A==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider-utils@4.0.15': - resolution: {integrity: sha512-8XiKWbemmCbvNN0CLR9u3PQiet4gtEVIrX4zzLxnCj06AwsEDJwJVBbKrEI4t6qE8XRSIvU2irka0dcpziKW6w==} + '@ai-sdk/provider-utils@4.0.19': + resolution: {integrity: sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -271,8 +271,8 @@ packages: kysely: ^0.28.5 nanostores: ^1.0.1 - '@better-auth/core@1.5.0': - resolution: {integrity: sha512-nDPmW7I9VGRACEei31fHaZxGwD/yICraDllZ/f25jbWXYaxDaW88RuH1ZhbOUKmGJlZtDxcjN1+YmcVIc1ioNw==} + '@better-auth/core@1.5.4': + resolution: {integrity: sha512-k5AdwPRQETZn0vdB60EB9CDxxfllpJXKqVxTjyXIUSRz7delNGlU0cR/iRP3VfVJwvYR1NbekphBDNo+KGoEzQ==} peerDependencies: '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 @@ -285,13 +285,13 @@ packages: '@cloudflare/workers-types': optional: true - '@better-auth/passkey@1.5.0': - resolution: {integrity: sha512-joupofIxoUEzwd3/T/d7JRSHxPx9kBLYJs6eBhcuso564/O9SrAOKbzfepEmouBMow6VA78bpwAGkVHpMkSwyg==} + '@better-auth/passkey@1.5.4': + resolution: {integrity: sha512-S6MRo8TNz7d77H4EoQxaY7EdlQUOeQb02dxqj0xXlA5Jzyb75QrkDghebFasxVRzq7N6INuFbFoxawrlGa4UmQ==} peerDependencies: - '@better-auth/core': 1.5.0 + '@better-auth/core': 1.5.4 '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 - better-auth: 1.5.0 + better-auth: 1.5.4 better-call: 1.3.2 nanostores: ^1.0.1 @@ -849,26 +849,26 @@ packages: cpu: [x64] os: [win32] - '@floating-ui/core@1.7.4': - resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==} + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} - '@floating-ui/dom@1.7.5': - resolution: {integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==} + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} - '@floating-ui/react-dom@2.1.7': - resolution: {integrity: sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==} + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' - '@floating-ui/utils@0.2.10': - resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} '@hexagon/base64@1.1.28': resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==} - '@img/colour@1.0.0': - resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} '@img/sharp-darwin-arm64@0.34.5': @@ -2098,8 +2098,8 @@ packages: '@types/pako@2.0.4': resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==} - '@types/pg@8.16.0': - resolution: {integrity: sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==} + '@types/pg@8.18.0': + resolution: {integrity: sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==} '@types/raf@3.4.3': resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==} @@ -2175,8 +2175,8 @@ packages: resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==} engines: {node: '>=0.8'} - ai@6.0.103: - resolution: {integrity: sha512-4eY6Ut4u41zKH+P2S/oLlZrwxeWQh4kIV1FjE34Jhoiwg+v1AyfSYM8FslXk9rTAtIIaOBimrCUqXacC5RBqJw==} + ai@6.0.116: + resolution: {integrity: sha512-7yM+cTmyRLeNIXwt4Vj+mrrJgVQ9RMIW5WO0ydoLoYkewIvsMcvUmqS4j2RJTUXaF1HphwmSKUMQ/HypNRGOmA==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -2196,8 +2196,9 @@ packages: resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} engines: {node: '>= 0.6.0'} - baseline-browser-mapping@2.9.19: - resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} + baseline-browser-mapping@2.10.0: + resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==} + engines: {node: '>=6.0.0'} hasBin: true better-auth@1.4.19: @@ -2281,8 +2282,8 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - caniuse-lite@1.0.30001770: - resolution: {integrity: sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==} + caniuse-lite@1.0.30001777: + resolution: {integrity: sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==} canvg@3.0.11: resolution: {integrity: sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==} @@ -2398,8 +2399,9 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} - dompurify@3.3.1: - resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} + dompurify@3.3.2: + resolution: {integrity: sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==} + engines: {node: '>=20'} dotenv@17.3.1: resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} @@ -2501,12 +2503,12 @@ packages: sqlite3: optional: true - enhanced-resolve@5.19.0: - resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} + enhanced-resolve@5.20.0: + resolution: {integrity: sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==} engines: {node: '>=10.13.0'} - es-toolkit@1.44.0: - resolution: {integrity: sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==} + es-toolkit@1.45.1: + resolution: {integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==} esbuild-register@3.6.0: resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} @@ -2584,8 +2586,8 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true - jose@6.1.3: - resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + jose@6.2.0: + resolution: {integrity: sha512-xsfE1TcSCbUdo6U07tR0mvhg0flGxU8tPLbF03mirl2ukGQENhUg4ubGYQnhVH0b5stLlPM+WOqDkEl1R1y5sQ==} json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} @@ -2687,8 +2689,8 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - nanostores@1.1.0: - resolution: {integrity: sha512-yJBmDJr18xy47dbNVlHcgdPrulSn1nhSE6Ns9vTG+Nx9VPT6iV1MD6aQFp/t52zpf82FhLLTXAXr30NuCnxvwA==} + nanostores@1.1.1: + resolution: {integrity: sha512-EYJqS25r2iBeTtGQCHidXl1VfZ1jXM7Q04zXJOrMlxVVmD0ptxJaNux92n1mJ7c5lN3zTq12MhH/8x59nP+qmg==} engines: {node: ^20.0.0 || >=22.0.0} next-themes@0.4.6: @@ -2727,23 +2729,20 @@ packages: pg-cloudflare@1.3.0: resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} - pg-connection-string@2.11.0: - resolution: {integrity: sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==} + pg-connection-string@2.12.0: + resolution: {integrity: sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==} pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} - pg-pool@3.12.0: - resolution: {integrity: sha512-eIJ0DES8BLaziFHW7VgJEBPi5hg3Nyng5iKpYtj3wbcAUV9A1wLgWiY7ajf/f/oO1wfxt83phXPY8Emztg7ITg==} + pg-pool@3.13.0: + resolution: {integrity: sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==} peerDependencies: pg: '>=8.0' - pg-protocol@1.11.0: - resolution: {integrity: sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==} - - pg-protocol@1.12.0: - resolution: {integrity: sha512-uOANXNRACNdElMXJ0tPz6RBM0XQ61nONGAwlt8da5zs/iUOOCLBQOHSXnrC6fMsvtjxbOJrZZl5IScGv+7mpbg==} + pg-protocol@1.13.0: + resolution: {integrity: sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==} pg-types@2.2.0: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} @@ -2771,8 +2770,8 @@ packages: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} engines: {node: ^10 || ^12 || >=14} postgres-array@2.0.0: @@ -2899,8 +2898,8 @@ packages: reselect@5.1.1: resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} - resend@6.9.2: - resolution: {integrity: sha512-uIM6CQ08tS+hTCRuKBFbOBvHIGaEhqZe8s4FOgqsVXSbQLAhmNWpmUhG3UAtRnmcwTWFUqnHa/+Vux8YGPyDBA==} + resend@6.9.3: + resolution: {integrity: sha512-GRXjH9XZBJA+daH7bBVDuTShr22iWCxXA8P7t495G4dM/RC+d+3gHBK/6bz9K6Vpcq11zRQKmD+B+jECwQlyGQ==} engines: {node: '>=20'} peerDependencies: '@react-email/render': '*' @@ -3090,32 +3089,32 @@ packages: snapshots: - '@ai-sdk/anthropic@3.0.48(zod@4.3.6)': + '@ai-sdk/anthropic@3.0.58(zod@4.3.6)': dependencies: '@ai-sdk/provider': 3.0.8 - '@ai-sdk/provider-utils': 4.0.15(zod@4.3.6) + '@ai-sdk/provider-utils': 4.0.19(zod@4.3.6) zod: 4.3.6 - '@ai-sdk/gateway@3.0.57(zod@4.3.6)': + '@ai-sdk/gateway@3.0.66(zod@4.3.6)': dependencies: '@ai-sdk/provider': 3.0.8 - '@ai-sdk/provider-utils': 4.0.15(zod@4.3.6) + '@ai-sdk/provider-utils': 4.0.19(zod@4.3.6) '@vercel/oidc': 3.1.0 zod: 4.3.6 - '@ai-sdk/google@3.0.33(zod@4.3.6)': + '@ai-sdk/google@3.0.43(zod@4.3.6)': dependencies: '@ai-sdk/provider': 3.0.8 - '@ai-sdk/provider-utils': 4.0.15(zod@4.3.6) + '@ai-sdk/provider-utils': 4.0.19(zod@4.3.6) zod: 4.3.6 - '@ai-sdk/openai@3.0.36(zod@4.3.6)': + '@ai-sdk/openai@3.0.41(zod@4.3.6)': dependencies: '@ai-sdk/provider': 3.0.8 - '@ai-sdk/provider-utils': 4.0.15(zod@4.3.6) + '@ai-sdk/provider-utils': 4.0.19(zod@4.3.6) zod: 4.3.6 - '@ai-sdk/provider-utils@4.0.15(zod@4.3.6)': + '@ai-sdk/provider-utils@4.0.19(zod@4.3.6)': dependencies: '@ai-sdk/provider': 3.0.8 '@standard-schema/spec': 1.1.0 @@ -3142,43 +3141,43 @@ snapshots: '@babel/helper-validator-identifier': 7.28.5 optional: true - '@better-auth/core@1.4.19(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)': + '@better-auth/core@1.4.19(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.2.0)(kysely@0.28.11)(nanostores@1.1.1)': dependencies: '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.21 '@standard-schema/spec': 1.1.0 better-call: 1.1.8(zod@4.3.6) - jose: 6.1.3 + jose: 6.2.0 kysely: 0.28.11 - nanostores: 1.1.0 + nanostores: 1.1.1 zod: 4.3.6 - '@better-auth/core@1.5.0(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)': + '@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.0)(kysely@0.28.11)(nanostores@1.1.1)': dependencies: '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 '@standard-schema/spec': 1.1.0 better-call: 1.3.2(zod@4.3.6) - jose: 6.1.3 + jose: 6.2.0 kysely: 0.28.11 - nanostores: 1.1.0 + nanostores: 1.1.1 zod: 4.3.6 - '@better-auth/passkey@1.5.0(@better-auth/core@1.5.0(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-auth@1.4.19(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.19.0))(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.19.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(better-call@1.3.2(zod@4.3.6))(nanostores@1.1.0)': + '@better-auth/passkey@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.0)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-auth@1.4.19(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(kysely@0.28.11)(pg@8.19.0))(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.19.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(better-call@1.3.2(zod@4.3.6))(nanostores@1.1.1)': dependencies: - '@better-auth/core': 1.5.0(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.0)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 '@simplewebauthn/browser': 13.2.2 '@simplewebauthn/server': 13.2.3 - better-auth: 1.4.19(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.19.0))(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.19.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + better-auth: 1.4.19(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(kysely@0.28.11)(pg@8.19.0))(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.19.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) better-call: 1.3.2(zod@4.3.6) - nanostores: 1.1.0 + nanostores: 1.1.1 zod: 4.3.6 - '@better-auth/telemetry@1.4.19(@better-auth/core@1.4.19(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))': + '@better-auth/telemetry@1.4.19(@better-auth/core@1.4.19(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.2.0)(kysely@0.28.11)(nanostores@1.1.1))': dependencies: - '@better-auth/core': 1.4.19(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) + '@better-auth/core': 1.4.19(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.2.0)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.21 @@ -3489,26 +3488,26 @@ snapshots: '@esbuild/win32-x64@0.27.3': optional: true - '@floating-ui/core@1.7.4': + '@floating-ui/core@1.7.5': dependencies: - '@floating-ui/utils': 0.2.10 + '@floating-ui/utils': 0.2.11 - '@floating-ui/dom@1.7.5': + '@floating-ui/dom@1.7.6': dependencies: - '@floating-ui/core': 1.7.4 - '@floating-ui/utils': 0.2.10 + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 - '@floating-ui/react-dom@2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@floating-ui/react-dom@2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@floating-ui/dom': 1.7.5 + '@floating-ui/dom': 1.7.6 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@floating-ui/utils@0.2.10': {} + '@floating-ui/utils@0.2.11': {} '@hexagon/base64@1.1.28': {} - '@img/colour@1.0.0': + '@img/colour@1.1.0': optional: true '@img/sharp-darwin-arm64@0.34.5': @@ -3656,9 +3655,9 @@ snapshots: '@noble/hashes@2.0.1': {} - '@openrouter/ai-sdk-provider@2.2.3(ai@6.0.103(zod@4.3.6))(zod@4.3.6)': + '@openrouter/ai-sdk-provider@2.2.3(ai@6.0.116(zod@4.3.6))(zod@4.3.6)': dependencies: - ai: 6.0.103(zod@4.3.6) + ai: 6.0.116(zod@4.3.6) zod: 4.3.6 '@opentelemetry/api@1.9.0': {} @@ -4179,7 +4178,7 @@ snapshots: '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@floating-ui/react-dom': 2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) @@ -4613,7 +4612,7 @@ snapshots: '@tailwindcss/node@4.2.1': dependencies: '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.19.0 + enhanced-resolve: 5.20.0 jiti: 2.6.1 lightningcss: 1.31.1 magic-string: 0.30.21 @@ -4676,7 +4675,7 @@ snapshots: '@alloc/quick-lru': 5.2.0 '@tailwindcss/node': 4.2.1 '@tailwindcss/oxide': 4.2.1 - postcss: 8.5.6 + postcss: 8.5.8 tailwindcss: 4.2.1 '@tanstack/react-table@8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': @@ -4717,10 +4716,10 @@ snapshots: '@types/pako@2.0.4': {} - '@types/pg@8.16.0': + '@types/pg@8.18.0': dependencies: '@types/node': 25.3.2 - pg-protocol: 1.11.0 + pg-protocol: 1.13.0 pg-types: 2.2.0 '@types/raf@3.4.3': @@ -4753,11 +4752,11 @@ snapshots: adler-32@1.3.1: {} - ai@6.0.103(zod@4.3.6): + ai@6.0.116(zod@4.3.6): dependencies: - '@ai-sdk/gateway': 3.0.57(zod@4.3.6) + '@ai-sdk/gateway': 3.0.66(zod@4.3.6) '@ai-sdk/provider': 3.0.8 - '@ai-sdk/provider-utils': 4.0.15(zod@4.3.6) + '@ai-sdk/provider-utils': 4.0.19(zod@4.3.6) '@opentelemetry/api': 1.9.0 zod: 4.3.6 @@ -4779,25 +4778,25 @@ snapshots: base64-arraybuffer@1.0.2: optional: true - baseline-browser-mapping@2.9.19: {} + baseline-browser-mapping@2.10.0: {} - better-auth@1.4.19(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.19.0))(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.19.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + better-auth@1.4.19(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(kysely@0.28.11)(pg@8.19.0))(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.19.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - '@better-auth/core': 1.4.19(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) - '@better-auth/telemetry': 1.4.19(@better-auth/core@1.4.19(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)) + '@better-auth/core': 1.4.19(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.2.0)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/telemetry': 1.4.19(@better-auth/core@1.4.19(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.2.0)(kysely@0.28.11)(nanostores@1.1.1)) '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.21 '@noble/ciphers': 2.1.1 '@noble/hashes': 2.0.1 better-call: 1.1.8(zod@4.3.6) defu: 6.1.4 - jose: 6.1.3 + jose: 6.2.0 kysely: 0.28.11 - nanostores: 1.1.0 + nanostores: 1.1.1 zod: 4.3.6 optionalDependencies: drizzle-kit: 0.31.9 - drizzle-orm: 0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.19.0) + drizzle-orm: 0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(kysely@0.28.11)(pg@8.19.0) next: 16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) pg: 8.19.0 react: 19.2.4 @@ -4823,7 +4822,7 @@ snapshots: buffer-from@1.1.2: {} - caniuse-lite@1.0.30001770: {} + caniuse-lite@1.0.30001777: {} canvg@3.0.11: dependencies: @@ -4930,7 +4929,7 @@ snapshots: detect-node-es@1.1.0: {} - dompurify@3.3.1: + dompurify@3.3.2: optionalDependencies: '@types/trusted-types': 2.0.7 optional: true @@ -4946,19 +4945,19 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.19.0): + drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.18.0)(kysely@0.28.11)(pg@8.19.0): optionalDependencies: '@opentelemetry/api': 1.9.0 - '@types/pg': 8.16.0 + '@types/pg': 8.18.0 kysely: 0.28.11 pg: 8.19.0 - enhanced-resolve@5.19.0: + enhanced-resolve@5.20.0: dependencies: graceful-fs: 4.2.11 tapable: 2.3.0 - es-toolkit@1.44.0: {} + es-toolkit@1.45.1: {} esbuild-register@3.6.0(esbuild@0.25.12): dependencies: @@ -5093,7 +5092,7 @@ snapshots: jiti@2.6.1: {} - jose@6.1.3: {} + jose@6.2.0: {} json-schema@0.4.0: {} @@ -5109,7 +5108,7 @@ snapshots: optionalDependencies: canvg: 3.0.11 core-js: 3.48.0 - dompurify: 3.3.1 + dompurify: 3.3.2 html2canvas: 1.4.1 kysely@0.28.11: {} @@ -5171,7 +5170,7 @@ snapshots: nanoid@3.3.11: {} - nanostores@1.1.0: {} + nanostores@1.1.1: {} next-themes@0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: @@ -5182,8 +5181,8 @@ snapshots: dependencies: '@next/env': 16.1.6 '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.9.19 - caniuse-lite: 1.0.30001770 + baseline-browser-mapping: 2.10.0 + caniuse-lite: 1.0.30001777 postcss: 8.4.31 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) @@ -5212,17 +5211,15 @@ snapshots: pg-cloudflare@1.3.0: optional: true - pg-connection-string@2.11.0: {} + pg-connection-string@2.12.0: {} pg-int8@1.0.1: {} - pg-pool@3.12.0(pg@8.19.0): + pg-pool@3.13.0(pg@8.19.0): dependencies: pg: 8.19.0 - pg-protocol@1.11.0: {} - - pg-protocol@1.12.0: {} + pg-protocol@1.13.0: {} pg-types@2.2.0: dependencies: @@ -5234,9 +5231,9 @@ snapshots: pg@8.19.0: dependencies: - pg-connection-string: 2.11.0 - pg-pool: 3.12.0(pg@8.19.0) - pg-protocol: 1.12.0 + pg-connection-string: 2.12.0 + pg-pool: 3.13.0(pg@8.19.0) + pg-protocol: 1.13.0 pg-types: 2.2.0 pgpass: 1.0.5 optionalDependencies: @@ -5256,7 +5253,7 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.5.6: + postcss@8.5.8: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -5404,7 +5401,7 @@ snapshots: '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1))(react@19.2.4) clsx: 2.1.1 decimal.js-light: 2.5.1 - es-toolkit: 1.44.0 + es-toolkit: 1.45.1 eventemitter3: 5.0.4 immer: 10.2.0 react: 19.2.4 @@ -5432,7 +5429,7 @@ snapshots: reselect@5.1.1: {} - resend@6.9.2: + resend@6.9.3: dependencies: postal-mime: 2.7.3 svix: 1.84.1 @@ -5455,7 +5452,7 @@ snapshots: sharp@0.34.5: dependencies: - '@img/colour': 1.0.0 + '@img/colour': 1.1.0 detect-libc: 2.1.2 semver: 7.7.4 optionalDependencies: diff --git a/public/fonts/SF-Pro-Display-Bold.otf b/public/fonts/SF-Pro-Display-Bold.otf deleted file mode 100644 index 28fa5a4..0000000 Binary files a/public/fonts/SF-Pro-Display-Bold.otf and /dev/null differ diff --git a/public/fonts/SF-Pro-Display-Medium.otf b/public/fonts/SF-Pro-Display-Medium.otf deleted file mode 100644 index 668ba74..0000000 Binary files a/public/fonts/SF-Pro-Display-Medium.otf and /dev/null differ diff --git a/public/fonts/SF-Pro-Display-Regular.otf b/public/fonts/SF-Pro-Display-Regular.otf deleted file mode 100644 index 7042365..0000000 Binary files a/public/fonts/SF-Pro-Display-Regular.otf and /dev/null differ diff --git a/public/fonts/SF-Pro-Display-Semibold.otf b/public/fonts/SF-Pro-Display-Semibold.otf deleted file mode 100644 index 081b59b..0000000 Binary files a/public/fonts/SF-Pro-Display-Semibold.otf and /dev/null differ diff --git a/public/fonts/SF-Pro-Rounded-Bold.otf b/public/fonts/SF-Pro-Rounded-Bold.otf deleted file mode 100644 index e834fc0..0000000 Binary files a/public/fonts/SF-Pro-Rounded-Bold.otf and /dev/null differ diff --git a/public/fonts/SF-Pro-Rounded-Medium.otf b/public/fonts/SF-Pro-Rounded-Medium.otf deleted file mode 100644 index 57208a3..0000000 Binary files a/public/fonts/SF-Pro-Rounded-Medium.otf and /dev/null differ diff --git a/public/fonts/SF-Pro-Rounded-Regular.otf b/public/fonts/SF-Pro-Rounded-Regular.otf deleted file mode 100644 index 4abf9df..0000000 Binary files a/public/fonts/SF-Pro-Rounded-Regular.otf and /dev/null differ diff --git a/public/fonts/SF-Pro-Rounded-Semibold.otf b/public/fonts/SF-Pro-Rounded-Semibold.otf deleted file mode 100644 index 29d9bf7..0000000 Binary files a/public/fonts/SF-Pro-Rounded-Semibold.otf and /dev/null differ diff --git a/public/fonts/AISans-Regular.woff2 b/public/fonts/ai-sans-regular.woff2 similarity index 100% rename from public/fonts/AISans-Regular.woff2 rename to public/fonts/ai-sans-regular.woff2 diff --git a/public/fonts/AISans-Semibold.woff2 b/public/fonts/ai-sans-semibold.woff2 similarity index 100% rename from public/fonts/AISans-Semibold.woff2 rename to public/fonts/ai-sans-semibold.woff2 diff --git a/public/fonts/anthropicSans.woff2 b/public/fonts/anthropic-sans.woff2 similarity index 100% rename from public/fonts/anthropicSans.woff2 rename to public/fonts/anthropic-sans.woff2 diff --git a/public/fonts/font_index.ts b/public/fonts/font_index.ts index 03c9719..ca5431f 100644 --- a/public/fonts/font_index.ts +++ b/public/fonts/font_index.ts @@ -14,12 +14,12 @@ import localFont from "next/font/local"; const ai_sans = localFont({ src: [ { - path: "./AISans-Regular.woff2", + path: "./ai-sans-regular.woff2", weight: "400", style: "normal", }, { - path: "./AISans-Semibold.woff2", + path: "./ai-sans-semibold.woff2", weight: "700", style: "normal", }, @@ -28,66 +28,29 @@ const ai_sans = localFont({ variable: "--font-ai-sans", }); +const itau = localFont({ + src: [ + { + path: "./itau-text-regular.woff2", + weight: "400", + style: "normal", + }, + { + path: "./itau-text-bold.woff2", + weight: "700", + style: "normal", + }, + ], + display: "swap", + variable: "--font-itau", +}); + const anthropic_sans = localFont({ - src: "./anthropicSans.woff2", + src: "./anthropic-sans.woff2", display: "swap", variable: "--font-anthropic-sans", }); -const sf_pro_display = localFont({ - src: [ - { - path: "./SF-Pro-Display-Regular.otf", - weight: "400", - style: "normal", - }, - { - path: "./SF-Pro-Display-Medium.otf", - weight: "500", - style: "normal", - }, - { - path: "./SF-Pro-Display-Semibold.otf", - weight: "600", - style: "normal", - }, - { - path: "./SF-Pro-Display-Bold.otf", - weight: "700", - style: "normal", - }, - ], - display: "swap", - variable: "--font-sf-pro-display", -}); - -const sf_pro_rounded = localFont({ - src: [ - { - path: "./SF-Pro-Rounded-Regular.otf", - weight: "400", - style: "normal", - }, - { - path: "./SF-Pro-Rounded-Medium.otf", - weight: "500", - style: "normal", - }, - { - path: "./SF-Pro-Rounded-Semibold.otf", - weight: "600", - style: "normal", - }, - { - path: "./SF-Pro-Rounded-Bold.otf", - weight: "700", - style: "normal", - }, - ], - display: "swap", - variable: "--font-sf-pro-rounded", -}); - const inter = Inter({ subsets: ["latin"], display: "swap", @@ -145,14 +108,10 @@ const ibm_plex_mono = IBM_Plex_Mono({ variable: "--font-ibm-plex-mono", }); -export type FontOption = { - key: string; - label: string; - variable: string; -}; +export const DEFAULT_FONT_KEY = "ai-sans"; -export const FONT_OPTIONS: FontOption[] = [ - { key: "ai-sans", label: "AI Sans", variable: "var(--font-ai-sans)" }, +export const FONT_OPTIONS = [ + { key: "ai-sans", label: "Open AI Sans", variable: "var(--font-ai-sans)" }, { key: "anthropic-sans", label: "Anthropic Sans", @@ -164,6 +123,11 @@ export const FONT_OPTIONS: FontOption[] = [ label: "Fira Sans", variable: "var(--font-fira-sans)", }, + { + key: "itau", + label: "Itaú Sans", + variable: "var(--font-itau)", + }, { key: "geist", label: "Geist Sans", variable: "var(--font-geist)" }, { key: "ibm-plex-mono", @@ -182,29 +146,21 @@ export const FONT_OPTIONS: FontOption[] = [ variable: "var(--font-reddit-sans)", }, { key: "roboto", label: "Roboto", variable: "var(--font-roboto)" }, - { - key: "sf-pro-display", - label: "SF Pro Display", - variable: "var(--font-sf-pro-display)", - }, - { - key: "sf-pro-rounded", - label: "SF Pro Rounded", - variable: "var(--font-sf-pro-rounded)", - }, { key: "ubuntu", label: "Ubuntu", variable: "var(--font-ubuntu)" }, +] as const; + +export type FontKey = (typeof FONT_OPTIONS)[number]["key"]; + +export const FONT_KEYS = FONT_OPTIONS.map((option) => option.key) as [ + FontKey, + ...FontKey[], ]; -/** @deprecated Use FONT_OPTIONS */ -export const SYSTEM_FONT_OPTIONS = FONT_OPTIONS; -/** @deprecated Use FONT_OPTIONS */ -export const MONEY_FONT_OPTIONS = FONT_OPTIONS; +const VALID_FONT_KEY_SET = new Set(FONT_KEYS); const allFonts = [ ai_sans, anthropic_sans, - sf_pro_display, - sf_pro_rounded, inter, geist_sans, roboto, @@ -214,15 +170,21 @@ const allFonts = [ jetbrains_mono, fira_code, ibm_plex_mono, + itau, ]; export const allFontVariables = allFonts.map((f) => f.variable).join(" "); -// Backward compatibility -export const main_font = ai_sans; -export const money_font = ai_sans; +function isValidFontKey(value: string): value is FontKey { + return VALID_FONT_KEY_SET.has(value); +} + +export function normalizeFontKey(value: string | null | undefined): FontKey { + if (!value) return DEFAULT_FONT_KEY; + return isValidFontKey(value) ? value : DEFAULT_FONT_KEY; +} export function getFontVariable(key: string): string { const option = FONT_OPTIONS.find((o) => o.key === key); - return option?.variable ?? "var(--font-ai-sans)"; + return option?.variable ?? `var(--font-${DEFAULT_FONT_KEY})`; } diff --git a/public/fonts/itau-text-bold.woff2 b/public/fonts/itau-text-bold.woff2 new file mode 100644 index 0000000..102cebb Binary files /dev/null and b/public/fonts/itau-text-bold.woff2 differ diff --git a/public/fonts/itau-text-regular.woff2 b/public/fonts/itau-text-regular.woff2 new file mode 100644 index 0000000..35149a9 Binary files /dev/null and b/public/fonts/itau-text-regular.woff2 differ