chore: preparar versão 2.7.13

This commit is contained in:
Felipe Coutinho
2026-08-09 19:32:12 -03:00
parent ed9196797b
commit bf4138db82
31 changed files with 4299 additions and 744 deletions

View File

@@ -33,6 +33,14 @@ jobs:
node-version: 22 node-version: 22
cache: pnpm cache: pnpm
- name: Cache Next.js build
uses: actions/cache@v4
with:
path: ${{ github.workspace }}/.next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.ts', 'src/**/*.tsx', 'src/**/*.css', 'next.config.ts') }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('pnpm-lock.yaml') }}-
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile

View File

@@ -364,3 +364,13 @@ Erros nao devem expor stack traces, paths ou nomes de bibliotecas ao cliente. Us
Verificar pacotes novos sugeridos pela IA em npmjs.com antes de instalar. Red flags: menos de 1.000 downloads/semana, publicado nos ultimos 30 dias, nome muito parecido com pacote popular. Rodar `pnpm audit` periodicamente. Verificar pacotes novos sugeridos pela IA em npmjs.com antes de instalar. Red flags: menos de 1.000 downloads/semana, publicado nos ultimos 30 dias, nome muito parecido com pacote popular. Rodar `pnpm audit` periodicamente.
--- ---
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
<!-- END:nextjs-agent-rules -->

View File

@@ -5,6 +5,27 @@ 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/), 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/). e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR/).
## [2.7.13] - 2026-08-09
Esta versão atualiza a base técnica do OpenMonetis com as correções de segurança e desempenho do Next.js 16.3, acelera as verificações de tipos, melhora a recuperação de falhas nas áreas mais complexas do aplicativo e corrige a importação de extratos OFX que reutilizam identificadores bancários.
### Adicionado
- Interface: dashboard, Insights, anexos e relatórios agora possuem limites de erro próprios, preservando a navegação e oferecendo uma nova tentativa sem derrubar toda a área autenticada.
- Desenvolvimento: agentes de código passam a consultar a documentação do Next.js correspondente à versão instalada no projeto.
### Alterado
- ATENÇÃO — mudança no banco de dados: esta versão adiciona a coluna ofx_import_fingerprint e substitui o índice único baseado apenas no FITID por um índice de fingerprint. Aplique a migração 0034_superb_blonde_phantom.sql antes de iniciar a aplicação atualizada (pnpm run db:migrate em instalações manuais). A imagem Docker tenta aplicar as migrações automaticamente durante a inicialização.
- Banco de dados: a deduplicação de importações OFX agora considera os dados completos e a ocorrência de cada transação, em vez de depender somente do FITID fornecido pela instituição financeira.
- Dependências: Next.js atualizado para 16.3.0 e TypeScript para 7.0.2.
- Desenvolvimento: Turbopack e seus caches passam a usar os padrões nativos do Next.js 16.3, removendo flags redundantes da configuração.
- CI: o cache de build em `.next/cache` agora é preservado entre execuções para acelerar compilações sucessivas.
### Corrigido
- Importação: transações OFX legítimas que compartilham o mesmo FITID agora permanecem independentes na revisão e podem ser importadas sem sobrescrever linhas, travar seletores ou serem descartadas como duplicatas (issue #88 - @cunhanai).
- Interface: a identidade estável de cada linha evita duplicações visuais e mantém funcionando a seleção, a escolha de categoria e a rolagem da tabela de revisão.
- Segurança: incorporadas as correções do Next.js 16.3 para Server Actions, Proxy, cache de respostas e otimização de imagens.
- Segurança: o otimizador de imagens agora aceita somente avatares do Google e logos do Logo.dev, removendo os curingas globais de HTTP e HTTPS; anexos do S3 continuam servidos diretamente, sem passar pelo otimizador.
## [2.7.12] - 2026-06-30 ## [2.7.12] - 2026-06-30
Esta versão corrige a seleção de faturas e períodos em popovers usados dentro de diálogos, alinhando esses componentes ao mesmo comportamento seguro aplicado recentemente aos seletores de data. Esta versão corrige a seleção de faturas e períodos em popovers usados dentro de diálogos, alinhando esses componentes ao mesmo comportamento seguro aplicado recentemente aos seletores de data.

366
CLAUDE.md
View File

@@ -1,366 +0,0 @@
# CLAUDE.md - OpenMonetis
> Self-hosted personal finance app (Next.js 16, React 19, PostgreSQL, Drizzle ORM, Better Auth, Tailwind 4, shadcn/ui).
> Portuguese UI, English folders/imports. Linter: Biome 2.x. Package manager: pnpm.
## Related Projects
- **OpenMonetis Companion** (`~/github/openmonetis-companion`): Android app que captura notificacoes de apps bancarios e envia para o OpenMonetis via API. Os itens chegam na feature `inbox` para revisao.
---
## Critical Rules
1. **Sempre filtrar por `userId`** em queries.
2. **Usar `getAdminPayerId(userId)`** de `src/shared/lib/payers/get-admin-id.ts` ao inves de JOIN com `payers` para descobrir o admin.
3. **Periods** usam formato `YYYY-MM` (ex: `"2025-11"`). Utils em `src/shared/utils/period/`.
4. **Moeda**: R$ com 2 decimais. DB: `numeric(12, 2)`. Utils em `src/shared/utils/currency.ts`.
5. **Revalidation**: usar `revalidateForEntity("entity")` de `src/shared/lib/actions/helpers.ts` apos mutations.
6. **Versionamento e publicação**: registrar mudancas no `CHANGELOG.md` seguindo Keep a Changelog, também alterar o `package.json` e o badge de versão do `README.md`. Cada versão deve ter um parágrafo introdutório em linguagem humana logo abaixo do cabeçalho `## [x.y.z]`, antes das seções `### Adicionado/Alterado/Removido` — descrevendo em prosa o que a versão representa (ex: "Esta versão foca em polimento visual e reorganização interna..."). A `main` executa somente a CI; imagens Docker e GitHub Releases são publicadas exclusivamente por tags SemVer no formato `vX.Y.Z`. Antes de criar a tag, confirmar que a CI da `main` passou e que tag, `package.json`, `CHANGELOG.md` e badge do `README.md` usam a mesma versão. A tag deve apontar para o commit validado. Criar ou enviar uma tag dispara publicação externa (`X.Y.Z`, `X.Y`, `X` e `latest` no Docker Hub, seguida da GitHub Release), portanto agentes nunca devem criar ou fazer push de tags sem autorização explícita do usuário. Quando o usuário pedir **"commit e push"** em uma mudança que prepara uma nova versão, isso conta como autorização explícita para executar o fluxo completo: commitar, enviar a `main`, aguardar a CI da `main` passar, criar a tag SemVer correspondente à versão preparada e enviar essa tag. Se não houver versão preparada ou se houver divergência entre `package.json`, `CHANGELOG.md`, badge do `README.md` e tag pretendida, parar e pedir confirmação. Não voltar a publicar `latest` diretamente de pushes na `main`.
7. **Comunicacao**: responder em portugues clara e direta com o time.
8. **Commit messages**: agrupar por natureza. em pt-br. seguindo o padrao do sistema.
9. **README.md**: sempre que fizer alteracoes significativas, atualize o README.md.
---
## Architecture
### Feature-First
- `src/app/`: roteamento, layouts, loading states e paginas finas
- `src/features/`: codigo de dominio por feature
- `src/shared/`: tudo que e genuinamente reutilizado entre features
- `src/db/`: schema do banco
### Regra Feature vs Shared
Use esta pergunta:
> Se eu deletar esta feature, este arquivo deveria sumir junto?
- Sim: vai para `src/features/<feature>/`
- Nao: vai para `src/shared/`
### Features nao importam outras features
Se um contrato cruza dominios, ele deve morar em `src/shared/`.
**Excecao intencional: `attachments` depende de `transactions`**
`src/features/attachments` importa `TransactionDialog`, `TransactionDetailsDialog` e `TransactionItem` diretamente de `src/features/transactions`. Isso e uma dependencia explicita e aceita: anexos sao semanticamente uma extensao de lancamentos — existem por causa deles e nao fazem sentido sem esse contexto. Mover esses componentes para `shared/` seria errado (eles pertencem a transactions). Nao tratar isso como bug a corrigir.
Exemplos comuns:
- auth: `src/shared/lib/auth/*`
- db: `src/shared/lib/db.ts`
- revalidation helpers: `src/shared/lib/actions/*`
- payers cross-domain helpers: `src/shared/lib/payers/*`
- period/currency/date: `src/shared/utils/*`
- shadcn/ui: `src/shared/components/ui/*`
---
## Directory Structure
```text
src/
├── app/
│ ├── (auth)/
│ │ ├── login/page.tsx
│ │ └── signup/page.tsx
│ ├── (dashboard)/
│ │ ├── dashboard/
│ │ ├── transactions/
│ │ ├── cards/
│ │ │ └── [cardId]/invoice/
│ │ ├── accounts/
│ │ │ └── [accountId]/statement/
│ │ ├── categories/
│ │ │ ├── [categoryId]/
│ │ │ └── history/
│ │ ├── budgets/
│ │ ├── payers/
│ │ │ └── [payerId]/
│ │ ├── notes/
│ │ ├── insights/
│ │ ├── calendar/
│ │ ├── inbox/
│ │ ├── attachments/
│ │ ├── changelog/
│ │ ├── reports/
│ │ │ ├── category-trends/
│ │ │ ├── card-usage/
│ │ │ ├── installment-analysis/
│ │ │ └── establishments/
│ │ └── settings/
│ ├── (landing-page)/
│ ├── api/
│ ├── globals.css
│ └── layout.tsx
├── features/ # cada feature segue: actions.ts, queries.ts, actions/, components/, hooks/, lib/
│ ├── auth/
│ ├── landing/
│ ├── dashboard/
│ ├── transactions/
│ ├── cards/
│ ├── invoices/
│ ├── accounts/
│ ├── categories/
│ ├── budgets/
│ ├── payers/
│ ├── notes/
│ ├── insights/
│ ├── calendar/
│ ├── inbox/
│ ├── attachments/
│ ├── reports/
│ └── settings/
├── shared/
│ ├── components/
│ │ ├── ui/ # shadcn/ui primitives
│ │ ├── navigation/ # navbar, sidebar, breadcrumbs
│ │ ├── providers/ # React context providers
│ │ ├── brand/ # logos do app (logo, logo-icon, logo-text)
│ │ ├── widgets/ # widget-card, widget-empty-state, expandable-widget-card
│ │ ├── feedback/ # empty-state, status-dot, payment-success
│ │ ├── month-picker/
│ │ ├── logo-picker/
│ │ ├── calculator/
│ │ ├── entity-avatar/
│ │ └── skeletons/
│ ├── hooks/
│ ├── lib/
│ │ ├── actions/
│ │ ├── auth/
│ │ ├── accounts/
│ │ ├── cards/
│ │ ├── calculator/
│ │ ├── categories/
│ │ ├── email/
│ │ ├── import/
│ │ ├── installments/
│ │ ├── invoices/
│ │ ├── logo/
│ │ ├── notifications/
│ │ ├── payers/
│ │ ├── schemas/
│ │ ├── storage/
│ │ ├── transfers/
│ │ ├── types/
│ │ ├── version/
│ │ └── db.ts
│ └── utils/
│ ├── period/
│ ├── calculator.ts
│ ├── calendar.ts
│ ├── category-colors.ts
│ ├── currency.ts
│ ├── date.ts
│ ├── export-branding.ts
│ ├── fetch-json.ts
│ ├── financial-dates.ts
│ ├── icons.tsx
│ ├── id.ts
│ ├── initials.ts
│ ├── math.ts
│ ├── number.ts
│ ├── percentage.ts
│ ├── string.ts
│ └── ui.ts
└── db/
└── schema.ts
```
### Estrutura interna padrão de uma feature
Toda feature em `src/features/<nome>/` segue:
```text
<feature>/
├── actions.ts # entry point de Server Actions (barrel quando há actions/)
├── queries.ts # entry point de leitura do banco
├── actions/ # (opcional) Server Actions divididas por domínio quando o volume cresce
├── components/ # componentes de UI da feature
├── hooks/ # React hooks específicos da feature
└── lib/ # helpers, types, sub-queries e constantes internas
```
`actions.ts` e `queries.ts` são as portas de entrada da feature. Tudo que é helper interno fica em `lib/`. Componentes e hooks ficam nas pastas com nome óbvio.
---
## Import Patterns
### Preferidos
```ts
import { getUser } from "@/shared/lib/auth/server";
import { revalidateForEntity } from "@/shared/lib/actions/helpers";
import { parsePeriodParam } from "@/shared/utils/period";
import { TransactionsPage } from "@/features/transactions/components/page/transactions-page";
import { fetchLancamentos } from "@/features/transactions/queries";
```
### Evitar
```ts
import { Something } from "@/components/...";
import { Something } from "@/lib/...";
import { something } from "@/app/(dashboard)/...";
```
---
## App Router Pattern
Paginas em `src/app/` devem ser finas:
```ts
import { getUser } from "@/shared/lib/auth/server";
import { TransactionsPage } from "@/features/transactions/components/page/transactions-page";
import { fetchLancamentos } from "@/features/transactions/queries";
export default async function Page() {
const user = await getUser();
const data = await fetchLancamentos([/* filters */]);
return <TransactionsPage {...data} />;
}
```
Layouts, `loading.tsx` e metadata continuam em `src/app/`.
---
## Naming
### Routes / folders
| Portugues | English |
|---|---|
| `lancamentos` | `transactions` |
| `cartoes` | `cards` |
| `contas` | `accounts` |
| `categorias` | `categories` |
| `orcamentos` | `budgets` |
| `pessoas` | `payers` |
> **Nota:** o conceito de "pagador" foi renomeado para **"pessoa"** na UI (labels, toasts, textos visíveis ao usuário). O código, rotas e schema continuam usando o termo original em inglês (`payer`, `payerId`, `adminPayerId`) e em português interno (`pagador` como variável). Não renomear esses identificadores — a divergência entre UI e código é intencional e documentada.
| `anotacoes` | `notes` |
| `calendario` | `calendar` |
| `ajustes` | `settings` |
| `pre-lancamentos` | `inbox` |
| `relatorios/tendencias` | `reports/category-trends` |
| `relatorios/uso-cartoes` | `reports/card-usage` |
| `relatorios/analise-parcelas` | `reports/installment-analysis` |
| `relatorios/estabelecimentos` | `reports/establishments` |
| `contas/[contaId]/extrato` | `accounts/[accountId]/statement` |
| `cartoes/[cartaoId]/fatura` | `cards/[cardId]/invoice` |
| `categorias/historico` | `categories/history` |
| `changelog` | `settings/changelog` |
### Files
- preferir `kebab-case`
- preferir nomes em ingles
- manter nomes internos de tipos/funcoes somente quando a troca aumentar risco sem ganho real
---
## Commands
```bash
pnpm run dev
pnpm run build
pnpm run lint
pnpm run lint:fix
pnpm exec next typegen
pnpm exec tsc --noEmit
pnpm run db:generate
pnpm run db:push
pnpm run db:studio
pnpm run docker:up:db
```
---
## Revalidation
Arquivo: `src/shared/lib/actions/helpers.ts`
- atualizar sempre os paths em ingles
- lembrar de manter a tag `"dashboard"` para invalidacoes financeiras
---
## Auth
- `getUser()` / `getUserId()` em `src/shared/lib/auth/server.ts`
- sessao deduplicada por request com `React.cache()`
---
## Dashboard Fetcher
Padrao recomendado:
```ts
import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
export async function fetchData(userId: string, period: string) {
const adminPayerId = await getAdminPayerId(userId);
if (!adminPayerId) return [];
return db.query.transactions.findMany({
where: /* sempre com userId + adminPayerId + period */,
});
}
```
---
## New Feature Checklist
1. Criar a rota fina em `src/app/(dashboard)/<feature>/page.tsx`
2. Criar a feature em `src/features/<feature>/`
3. Separar:
- `components/`
- `queries.ts` (entry point de leitura)
- `actions.ts` (entry point de Server Actions; vira barrel quando crescer e migrar para `actions/`)
- `lib/` para helpers internos, sub-queries por tópico, types e constantes da feature
- `types.ts` ou `schemas.ts` quando fizer sentido (alternativa a `lib/`)
- `hooks/` quando houver hooks específicos da feature
4. Extrair para `src/shared/` tudo que for reutilizavel
5. Atualizar navegacao e `revalidateForEntity()` se a feature tiver CRUD
6. Rodar:
- `pnpm exec next typegen`
- `pnpm exec tsc --noEmit`
- `pnpm run lint`
---
## Security Rules
Regras aplicadas automaticamente ao gerar codigo.
### Secrets
Nunca colocar API keys, credenciais de banco ou tokens em codigo frontend. Evitar variaveis prefixadas com `NEXT_PUBLIC_` para dados sensiveis — estas sao bundladas no cliente. Usar variaveis server-side apenas. `.env` deve estar no `.gitignore` antes do primeiro commit. `.env.example` deve ter apenas placeholders.
### Autenticacao & Autorizacao
Toda rota protegida em `src/app/api/` requer `getUser()` ou `getOptionalUserSession()` antes de qualquer logica, retornando 401 para nao autenticados. Rotas com IDs de recursos devem verificar ownership: `eq(table.userId, userId)`. Rotas admin devem checar role e retornar 403 para nao-admins. Session cookies em Better Auth ja tem `httpOnly`, `secure` e `sameSite` configurados — nao alterar.
### Input & Output
Usar Drizzle ORM (parametrizado por padrao) — nunca concatenar input de usuario em SQL. Validar todo input com Zod antes de usar. Upload de arquivos: usar whitelist de MIME types (`ALLOWED_MIME_TYPES`), presigned URLs para S3, token de upload assinado com verificacao pos-upload. Nunca usar `dangerouslySetInnerHTML` com conteudo de usuario.
### Headers & CSP
CSP definida em `src/proxy.ts` via middleware — alterar la, nao em `next.config.ts`. Headers de seguranca (HSTS, X-Frame-Options, etc.) definidos em `next.config.ts`. Nao remover nem enfraquecer essas configuracoes.
### Rate Limiting
Login: 5 tentativas/min. Signup: 3 tentativas/min. API tokens: 100 req/min (inbox), 20 req/min (batch). Configurado em `src/shared/lib/auth/config.ts` e nas rotas de inbox. Nao remover.
### Tratamento de Erros
Erros nao devem expor stack traces, paths ou nomes de bibliotecas ao cliente. Usar mensagens genericas: `"Algo deu errado"`. Logar detalhes apenas no servidor com `console.error()`.
### Dependencias
Verificar pacotes novos sugeridos pela IA em npmjs.com antes de instalar. Red flags: menos de 1.000 downloads/semana, publicado nos ultimos 30 dias, nome muito parecido com pacote popular. Rodar `pnpm audit` periodicamente.
---

View File

@@ -10,7 +10,7 @@
> **Não há versão online hospedada.** Você precisa clonar o repositório e rodar localmente ou no seu próprio servidor. > **Não há versão online hospedada.** Você precisa clonar o repositório e rodar localmente ou no seu próprio servidor.
[![Version](https://img.shields.io/badge/version-2.7.12-blue?style=flat-square)](CHANGELOG.md) [![Version](https://img.shields.io/badge/version-2.7.13-blue?style=flat-square)](CHANGELOG.md)
[![Next.js](https://img.shields.io/badge/Next.js-black?style=flat-square&logo=next.js)](https://nextjs.org/) [![Next.js](https://img.shields.io/badge/Next.js-black?style=flat-square&logo=next.js)](https://nextjs.org/)
[![TypeScript](https://img.shields.io/badge/TypeScript-blue?style=flat-square&logo=typescript)](https://www.typescriptlang.org/) [![TypeScript](https://img.shields.io/badge/TypeScript-blue?style=flat-square&logo=typescript)](https://www.typescriptlang.org/)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-blue?style=flat-square&logo=postgresql)](https://www.postgresql.org/) [![PostgreSQL](https://img.shields.io/badge/PostgreSQL-blue?style=flat-square&logo=postgresql)](https://www.postgresql.org/)
@@ -65,7 +65,7 @@ A ideia é simples: ter um lugar onde consigo ver todas as minhas contas, cartõ
### Funcionalidades ### Funcionalidades
💰 **Contas e transações** — Contas bancárias, cartões, dinheiro. Receitas, despesas, rendimentos e transferências. Categorização, divisão de lançamentos entre várias pessoas, filtros combináveis com intervalo de datas, extratos detalhados com identificação visual clara da conta e importação de extratos OFX e XLS/XLSX com detecção automática de categoria. 💰 **Contas e transações** — Contas bancárias, cartões, dinheiro. Receitas, despesas, rendimentos e transferências. Categorização, divisão de lançamentos entre várias pessoas, filtros combináveis com intervalo de datas, extratos detalhados com identificação visual clara da conta e importação de extratos OFX e XLS/XLSX com detecção automática de categoria e deduplicação resiliente a identificadores bancários repetidos.
📊 **Dashboard e relatórios** — Widgets personalizáveis com listas consistentes, métricas com atalhos para lançamentos, gráficos de evolução, comparativos por categoria, tendências, uso de cartões, top estabelecimentos e navegação direta entre meses pelo seletor de período. Exportação em PDF e Excel. 📊 **Dashboard e relatórios** — Widgets personalizáveis com listas consistentes, métricas com atalhos para lançamentos, gráficos de evolução, comparativos por categoria, tendências, uso de cartões, top estabelecimentos e navegação direta entre meses pelo seletor de período. Exportação em PDF e Excel.
@@ -628,8 +628,8 @@ Antes de começar, leia o [`CLAUDE.md`](CLAUDE.md) — ele documenta a arquitetu
As validações rodam em pull requests e em cada push na `main`. A publicação só começa quando uma tag SemVer aponta para um commit validado e a versão da tag corresponde ao `package.json` e ao `CHANGELOG.md`. As validações rodam em pull requests e em cada push na `main`. A publicação só começa quando uma tag SemVer aponta para um commit validado e a versão da tag corresponde ao `package.json` e ao `CHANGELOG.md`.
```bash ```bash
git tag -a v2.7.12 -m "v2.7.12" git tag -a v2.7.13 -m "v2.7.13"
git push origin v2.7.12 git push origin v2.7.13
``` ```
O workflow da tag valida o código, publica as imagens Docker versionadas e `latest` e, somente depois, cria a GitHub Release com as notas do changelog. O workflow da tag valida o código, publica as imagens Docker versionadas e `latest` e, somente depois, cria a GitHub Release com as notas do changelog.

View File

@@ -0,0 +1,3 @@
DROP INDEX "lancamentos_ofx_fit_id_user_id_idx";--> statement-breakpoint
ALTER TABLE "lancamentos" ADD COLUMN "ofx_import_fingerprint" text;--> statement-breakpoint
CREATE UNIQUE INDEX "lancamentos_ofx_import_fingerprint_user_id_idx" ON "lancamentos" USING btree ("user_id","ofx_import_fingerprint") WHERE ofx_import_fingerprint IS NOT NULL;

File diff suppressed because it is too large Load Diff

View File

@@ -232,6 +232,13 @@
"when": 1782685465530, "when": 1782685465530,
"tag": "0033_demonic_supreme_intelligence", "tag": "0033_demonic_supreme_intelligence",
"breakpoints": true "breakpoints": true
},
{
"idx": 34,
"version": "7",
"when": 1786299746933,
"tag": "0034_superb_blonde_phantom",
"breakpoints": true
} }
] ]
} }

View File

@@ -4,23 +4,34 @@ import type { NextConfig } from "next";
// Carregar variáveis de ambiente explicitamente // Carregar variáveis de ambiente explicitamente
dotenv.config(); dotenv.config();
type RemotePattern = NonNullable<
NonNullable<NextConfig["images"]>["remotePatterns"]
>[number];
const imageRemotePatterns: RemotePattern[] = [
{
protocol: "https",
hostname: "lh3.googleusercontent.com",
pathname: "/**",
},
{
protocol: "https",
hostname: "img.logo.dev",
pathname: "/**",
},
];
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
output: "standalone", output: "standalone",
cacheComponents: true, cacheComponents: true,
reactCompiler: true, reactCompiler: true,
images: { images: {
remotePatterns: [ remotePatterns: imageRemotePatterns,
new URL("https://lh3.googleusercontent.com/**"),
{ protocol: "https", hostname: "**" },
{ protocol: "http", hostname: "**" },
],
}, },
devIndicators: { devIndicators: {
position: "bottom-right", position: "bottom-right",
}, },
experimental: { experimental: {
prefetchInlining: true,
turbopackFileSystemCacheForDev: true,
optimizePackageImports: ["@remixicon/react"], optimizePackageImports: ["@remixicon/react"],
}, },

View File

@@ -1,10 +1,10 @@
{ {
"name": "openmonetis", "name": "openmonetis",
"version": "2.7.12", "version": "2.7.13",
"private": true, "private": true,
"packageManager": "pnpm@11.1.3", "packageManager": "pnpm@11.1.3",
"scripts": { "scripts": {
"dev": "next dev --turbopack", "dev": "next dev",
"db:seed": "tsx scripts/mock-data.ts", "db:seed": "tsx scripts/mock-data.ts",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
@@ -78,7 +78,7 @@
"exceljs": "^4.4.0", "exceljs": "^4.4.0",
"jspdf": "^4.2.1", "jspdf": "^4.2.1",
"jspdf-autotable": "^5.0.8", "jspdf-autotable": "^5.0.8",
"next": "16.2.7", "next": "16.3.0",
"next-themes": "0.4.6", "next-themes": "0.4.6",
"pdfjs-dist": "^6.0.227", "pdfjs-dist": "^6.0.227",
"pg": "8.21.0", "pg": "8.21.0",
@@ -108,6 +108,6 @@
"knip": "^6.22.0", "knip": "^6.22.0",
"tailwindcss": "4.3.0", "tailwindcss": "4.3.0",
"tsx": "4.22.4", "tsx": "4.22.4",
"typescript": "6.0.3" "typescript": "7.0.2"
} }
} }

607
pnpm-lock.yaml generated
View File

@@ -31,7 +31,7 @@ importers:
version: 3.1075.0 version: 3.1075.0
'@better-auth/passkey': '@better-auth/passkey':
specifier: ^1.6.22 specifier: ^1.6.22
version: 1.6.22(@better-auth/core@1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.4.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.22(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.29.2)(pg@8.21.0))(next@16.2.7(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(better-call@1.3.7(zod@4.4.3))(nanostores@1.4.0) version: 1.6.22(@better-auth/core@1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.4.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.22(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.29.2)(pg@8.21.0))(next@16.3.0(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(better-call@1.3.7(zod@4.4.3))(nanostores@1.4.0)
'@dnd-kit/core': '@dnd-kit/core':
specifier: ^6.3.1 specifier: ^6.3.1
version: 6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) version: 6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
@@ -124,7 +124,7 @@ importers:
version: 6.0.213(zod@4.4.3) version: 6.0.213(zod@4.4.3)
better-auth: better-auth:
specifier: 1.6.22 specifier: 1.6.22
version: 1.6.22(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.29.2)(pg@8.21.0))(next@16.2.7(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) version: 1.6.22(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.29.2)(pg@8.21.0))(next@16.3.0(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
canvas-confetti: canvas-confetti:
specifier: ^1.9.4 specifier: ^1.9.4
version: 1.9.4 version: 1.9.4
@@ -153,8 +153,8 @@ importers:
specifier: ^5.0.8 specifier: ^5.0.8
version: 5.0.8(jspdf@4.2.1) version: 5.0.8(jspdf@4.2.1)
next: next:
specifier: 16.2.7 specifier: 16.3.0
version: 16.2.7(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) version: 16.3.0(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
next-themes: next-themes:
specifier: 0.4.6 specifier: 0.4.6
version: 0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) version: 0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
@@ -238,8 +238,8 @@ importers:
specifier: 4.22.4 specifier: 4.22.4
version: 4.22.4 version: 4.22.4
typescript: typescript:
specifier: 6.0.3 specifier: 7.0.2
version: 6.0.3 version: 7.0.2
packages: packages:
@@ -1105,152 +1105,161 @@ packages:
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
'@img/sharp-darwin-arm64@0.34.5': '@img/sharp-darwin-arm64@0.35.3':
resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} resolution: {integrity: sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: '>=20.9.0'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@img/sharp-darwin-x64@0.34.5': '@img/sharp-darwin-x64@0.35.3':
resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} resolution: {integrity: sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: '>=20.9.0'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@img/sharp-libvips-darwin-arm64@1.2.4': '@img/sharp-freebsd-wasm32@0.35.3':
resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} resolution: {integrity: sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==}
engines: {node: '>=20.9.0'}
os: [freebsd]
'@img/sharp-libvips-darwin-arm64@1.3.2':
resolution: {integrity: sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@img/sharp-libvips-darwin-x64@1.2.4': '@img/sharp-libvips-darwin-x64@1.3.2':
resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} resolution: {integrity: sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@img/sharp-libvips-linux-arm64@1.2.4': '@img/sharp-libvips-linux-arm64@1.3.2':
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} resolution: {integrity: sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@img/sharp-libvips-linux-arm@1.2.4': '@img/sharp-libvips-linux-arm@1.3.2':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} resolution: {integrity: sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.2.4': '@img/sharp-libvips-linux-ppc64@1.3.2':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} resolution: {integrity: sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.2.4': '@img/sharp-libvips-linux-riscv64@1.3.2':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} resolution: {integrity: sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.2.4': '@img/sharp-libvips-linux-s390x@1.3.2':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} resolution: {integrity: sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@img/sharp-libvips-linux-x64@1.2.4': '@img/sharp-libvips-linux-x64@1.3.2':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} resolution: {integrity: sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.2.4': '@img/sharp-libvips-linuxmusl-arm64@1.3.2':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} resolution: {integrity: sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.2.4': '@img/sharp-libvips-linuxmusl-x64@1.3.2':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} resolution: {integrity: sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@img/sharp-linux-arm64@0.34.5': '@img/sharp-linux-arm64@0.35.3':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} resolution: {integrity: sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: '>=20.9.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@img/sharp-linux-arm@0.34.5': '@img/sharp-linux-arm@0.35.3':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} resolution: {integrity: sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: '>=20.9.0'}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@img/sharp-linux-ppc64@0.34.5': '@img/sharp-linux-ppc64@0.35.3':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} resolution: {integrity: sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: '>=20.9.0'}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@img/sharp-linux-riscv64@0.34.5': '@img/sharp-linux-riscv64@0.35.3':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} resolution: {integrity: sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: '>=20.9.0'}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@img/sharp-linux-s390x@0.34.5': '@img/sharp-linux-s390x@0.35.3':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} resolution: {integrity: sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: '>=20.9.0'}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@img/sharp-linux-x64@0.34.5': '@img/sharp-linux-x64@0.35.3':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} resolution: {integrity: sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: '>=20.9.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.34.5': '@img/sharp-linuxmusl-arm64@0.35.3':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} resolution: {integrity: sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: '>=20.9.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@img/sharp-linuxmusl-x64@0.34.5': '@img/sharp-linuxmusl-x64@0.35.3':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} resolution: {integrity: sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: '>=20.9.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@img/sharp-wasm32@0.34.5': '@img/sharp-wasm32@0.35.3':
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} resolution: {integrity: sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: '>=20.9.0'}
'@img/sharp-webcontainers-wasm32@0.35.3':
resolution: {integrity: sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==}
engines: {node: '>=20.9.0'}
cpu: [wasm32] cpu: [wasm32]
'@img/sharp-win32-arm64@0.34.5': '@img/sharp-win32-arm64@0.35.3':
resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} resolution: {integrity: sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: '>=20.9.0'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@img/sharp-win32-ia32@0.34.5': '@img/sharp-win32-ia32@0.35.3':
resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} resolution: {integrity: sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^20.9.0}
cpu: [ia32] cpu: [ia32]
os: [win32] os: [win32]
'@img/sharp-win32-x64@0.34.5': '@img/sharp-win32-x64@0.35.3':
resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} resolution: {integrity: sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: '>=20.9.0'}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
@@ -1354,57 +1363,57 @@ packages:
'@emnapi/core': ^1.7.1 '@emnapi/core': ^1.7.1
'@emnapi/runtime': ^1.7.1 '@emnapi/runtime': ^1.7.1
'@next/env@16.2.7': '@next/env@16.3.0':
resolution: {integrity: sha512-tMJizPlj6ZYpBMMdK8S0LJufrP4QTdR6pcv9KQ/bVETPAmg0j1mlHE9G2c38UyGHxoBapgwuj7XjbGJ2RcDFOg==} resolution: {integrity: sha512-o9r1S0BNiNreHP9Vs+Qnqd9kviDkJh8xIACY7UFZSmiGbbQRzPBBosvHzAU4TULHOIuOj/18RSsyz2qrREmIFw==}
'@next/swc-darwin-arm64@16.2.7': '@next/swc-darwin-arm64@16.3.0':
resolution: {integrity: sha512-vm1EDI/pVaBNNiychmxk3fft+OhQPVD9cIM/tReLZIQ3TfQ4kqI9DwKk00dzuS1ulC7icbrzCFrmRRlk9PfNdw==} resolution: {integrity: sha512-55hpqq18bEVAlxedlTt3tFqZmKg2nUXT1kn1G/BGEy0R13h3LwtwHPVzzjG6P4LLeOHE32PFDQUVaJEWvBEZBw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@next/swc-darwin-x64@16.2.7': '@next/swc-darwin-x64@16.3.0':
resolution: {integrity: sha512-O3IRSv1ZBL1zs0WrIgefTEcTKFVn+ryxBNe54erJ6KsD+2f/Mmt7g2jOYh8PSBdUwPtKQJuCsTMlZ7tIu2AcsQ==} resolution: {integrity: sha512-SOi96kSaF5T+0wW4koiM1bWzSPwjzTesC1p3df+FjdOi5LIQkBK/blxh7HdoKnNuI4PURF1OO7TZqtfnbWDSgw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@next/swc-linux-arm64-gnu@16.2.7': '@next/swc-linux-arm64-gnu@16.3.0':
resolution: {integrity: sha512-Re6PZtjBDd0aMU+VcZcC/PrIvj4WhrjDYtMhhCVQamWN4L90EVP0pcEOBQD25prSlw7OzNw5QpHLWMilRLsRNw==} resolution: {integrity: sha512-P0gZAoPMF4dyTRzhmkV4PrqVzSOB6t4mC1oI3c4dqijJ+OVEVx5clIXAKR4/uQpsqw2KKM/0D5tVumcR2r5blg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@next/swc-linux-arm64-musl@16.2.7': '@next/swc-linux-arm64-musl@16.3.0':
resolution: {integrity: sha512-qyogG9QtBzWxgJfeGBvOEHI3851gTfCF3wLZ5RDLTBJGAmE9p1qDwKCOdrBrvBzRvYDT+gUDp72pzlSEfAXgNA==} resolution: {integrity: sha512-tXXGKJw0m37O0eKJARVTX/TheKPhz0QFVtVVZXmOig+9YKLQOSP6hvf2pxv5DO7CLEJyTHx3Pg043CDQkv1G4Q==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@next/swc-linux-x64-gnu@16.2.7': '@next/swc-linux-x64-gnu@16.3.0':
resolution: {integrity: sha512-Vhe4ZDuBpmMogrGi5D4R2Kq4JAQlj6+wvgaFYy31zfES0zPmt6TLA+cuYpM/OLrPZjo2MYQTHVqNUSCR6+fDZQ==} resolution: {integrity: sha512-pjGxK5EY7yWml78ALejFkWmgHsU7wbFQrISiugpH6FbUJhgEvw3xFZ/EBAtLl7QtL0WdQKiG9eWJ3mOKGTukHw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@next/swc-linux-x64-musl@16.2.7': '@next/swc-linux-x64-musl@16.3.0':
resolution: {integrity: sha512-srvian89JahFLw1YLBEuhvPJ0DO5lpUeJQMXy4xYo7g628ZlNgXdNkqoxSAv9OYrBfByh6vxISMwW/mRbzCY+g==} resolution: {integrity: sha512-sjo++Xx+lomlPs3HRsHWhVDyGG6ms1kGW5EtHLERdII8AyG1i+f6aq68xHREO6AEMlhjTNEWBSmfJfqm9orf7g==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@next/swc-win32-arm64-msvc@16.2.7': '@next/swc-win32-arm64-msvc@16.3.0':
resolution: {integrity: sha512-GX3wvLpULFuRFJzwHaKfm7QZJ18F4ZSuxlPJ96BoBglCzBmdSjyeBKF+ZhWhvL/ckxNfLnNa7bsObO2ipYpszw==} resolution: {integrity: sha512-C5JSgiO54wURdaxdEUIXqkz04uMqC9UmPX1gtDrV/5Tf1UowdWYI8uA5hfFbPolTlp0q4KZ60xlHePNibf0VIw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@next/swc-win32-x64-msvc@16.2.7': '@next/swc-win32-x64-msvc@16.3.0':
resolution: {integrity: sha512-J4WlM72NMk076Qsg0jTdK3SNXatlSdnjW7L7oNGLst1tAGjHrJh/FYi+pw9wyIjEtGRKDNzD0zuiY16oWYWVaw==} resolution: {integrity: sha512-fDOggsweNb5SSw0ZKVk6U+gxSyGFFlIBY/LBc1r8GUj4u/6t6oArL+Pmkg0MBnsgR+KkdsURilVH4F3GXUGepA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
@@ -2801,6 +2810,126 @@ packages:
'@types/use-sync-external-store@0.0.6': '@types/use-sync-external-store@0.0.6':
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
'@typescript/typescript-aix-ppc64@7.0.2':
resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==}
engines: {node: '>=16.20.0'}
cpu: [ppc64]
os: [aix]
'@typescript/typescript-darwin-arm64@7.0.2':
resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==}
engines: {node: '>=16.20.0'}
cpu: [arm64]
os: [darwin]
'@typescript/typescript-darwin-x64@7.0.2':
resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==}
engines: {node: '>=16.20.0'}
cpu: [x64]
os: [darwin]
'@typescript/typescript-freebsd-arm64@7.0.2':
resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==}
engines: {node: '>=16.20.0'}
cpu: [arm64]
os: [freebsd]
'@typescript/typescript-freebsd-x64@7.0.2':
resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==}
engines: {node: '>=16.20.0'}
cpu: [x64]
os: [freebsd]
'@typescript/typescript-linux-arm64@7.0.2':
resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==}
engines: {node: '>=16.20.0'}
cpu: [arm64]
os: [linux]
'@typescript/typescript-linux-arm@7.0.2':
resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==}
engines: {node: '>=16.20.0'}
cpu: [arm]
os: [linux]
'@typescript/typescript-linux-loong64@7.0.2':
resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==}
engines: {node: '>=16.20.0'}
cpu: [loong64]
os: [linux]
'@typescript/typescript-linux-mips64el@7.0.2':
resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==}
engines: {node: '>=16.20.0'}
cpu: [mips64el]
os: [linux]
'@typescript/typescript-linux-ppc64@7.0.2':
resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==}
engines: {node: '>=16.20.0'}
cpu: [ppc64]
os: [linux]
'@typescript/typescript-linux-riscv64@7.0.2':
resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==}
engines: {node: '>=16.20.0'}
cpu: [riscv64]
os: [linux]
'@typescript/typescript-linux-s390x@7.0.2':
resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==}
engines: {node: '>=16.20.0'}
cpu: [s390x]
os: [linux]
'@typescript/typescript-linux-x64@7.0.2':
resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==}
engines: {node: '>=16.20.0'}
cpu: [x64]
os: [linux]
'@typescript/typescript-netbsd-arm64@7.0.2':
resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==}
engines: {node: '>=16.20.0'}
cpu: [arm64]
os: [netbsd]
'@typescript/typescript-netbsd-x64@7.0.2':
resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==}
engines: {node: '>=16.20.0'}
cpu: [x64]
os: [netbsd]
'@typescript/typescript-openbsd-arm64@7.0.2':
resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==}
engines: {node: '>=16.20.0'}
cpu: [arm64]
os: [openbsd]
'@typescript/typescript-openbsd-x64@7.0.2':
resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==}
engines: {node: '>=16.20.0'}
cpu: [x64]
os: [openbsd]
'@typescript/typescript-sunos-x64@7.0.2':
resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==}
engines: {node: '>=16.20.0'}
cpu: [x64]
os: [sunos]
'@typescript/typescript-win32-arm64@7.0.2':
resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==}
engines: {node: '>=16.20.0'}
cpu: [arm64]
os: [win32]
'@typescript/typescript-win32-x64@7.0.2':
resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==}
engines: {node: '>=16.20.0'}
cpu: [x64]
os: [win32]
'@vercel/oidc@3.2.0': '@vercel/oidc@3.2.0':
resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==} resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==}
engines: {node: '>= 20'} engines: {node: '>= 20'}
@@ -3492,6 +3621,11 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true hasBin: true
nanoid@3.3.17:
resolution: {integrity: sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
nanostores@1.4.0: nanostores@1.4.0:
resolution: {integrity: sha512-i0tloweeudshAEuddpDxcg9Ik6pkPfVsHIgKyf143JrgG7/MOh0+q7BypdLXZPoOP7fOYt1eTcwGkyiVmhJFkA==} resolution: {integrity: sha512-i0tloweeudshAEuddpDxcg9Ik6pkPfVsHIgKyf143JrgG7/MOh0+q7BypdLXZPoOP7fOYt1eTcwGkyiVmhJFkA==}
engines: {node: ^20.0.0 || >=22.0.0} engines: {node: ^20.0.0 || >=22.0.0}
@@ -3502,8 +3636,8 @@ packages:
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
next@16.2.7: next@16.3.0:
resolution: {integrity: sha512-eMJxgjRzBaj3olkP4cBamHDXL79A8FC6u1GcsO1D1Tsx8bw/LLXUJCaoajVxtnhD3A1IJqIT8IcRJjgBIPJq4w==} resolution: {integrity: sha512-NEdGOzH+08eTXMUp9UYkA99Nhi5N6Thrhc1jgFOQgfgnGK/dA2hRwBpXep+exdFQrnwlRf/3Wixyp8lLBUpE2A==}
engines: {node: '>=20.9.0'} engines: {node: '>=20.9.0'}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@@ -3598,14 +3732,14 @@ packages:
postal-mime@2.7.4: postal-mime@2.7.4:
resolution: {integrity: sha512-0WdnFQYUrPGGTFu1uOqD2s7omwua8xaeYGdO6rb88oD5yJ/4pPHDA4sdWqfD8wQVfCny563n/HQS7zTFft+f/g==} resolution: {integrity: sha512-0WdnFQYUrPGGTFu1uOqD2s7omwua8xaeYGdO6rb88oD5yJ/4pPHDA4sdWqfD8wQVfCny563n/HQS7zTFft+f/g==}
postcss@8.4.31:
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
engines: {node: ^10 || ^12 || >=14}
postcss@8.5.15: postcss@8.5.15:
resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
postcss@8.5.23:
resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==}
engines: {node: ^10 || ^12 || >=14}
postgres-array@2.0.0: postgres-array@2.0.0:
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -3782,9 +3916,14 @@ packages:
setimmediate@1.0.5: setimmediate@1.0.5:
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
sharp@0.34.5: sharp@0.35.3:
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} resolution: {integrity: sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: '>=20.9.0'}
peerDependencies:
'@types/node': '*'
peerDependenciesMeta:
'@types/node':
optional: true
smol-toml@1.7.0: smol-toml@1.7.0:
resolution: {integrity: sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ==} resolution: {integrity: sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ==}
@@ -3894,9 +4033,9 @@ packages:
tw-animate-css@1.4.0: tw-animate-css@1.4.0:
resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==}
typescript@6.0.3: typescript@7.0.2:
resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==}
engines: {node: '>=14.17'} engines: {node: '>=16.20.0'}
hasBin: true hasBin: true
unbash@4.0.1: unbash@4.0.1:
@@ -4347,14 +4486,14 @@ snapshots:
'@better-auth/core': 1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.4.0) '@better-auth/core': 1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.4.0)
'@better-auth/utils': 0.4.2 '@better-auth/utils': 0.4.2
'@better-auth/passkey@1.6.22(@better-auth/core@1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.4.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.22(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.29.2)(pg@8.21.0))(next@16.2.7(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(better-call@1.3.7(zod@4.4.3))(nanostores@1.4.0)': '@better-auth/passkey@1.6.22(@better-auth/core@1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.4.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.22(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.29.2)(pg@8.21.0))(next@16.3.0(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(better-call@1.3.7(zod@4.4.3))(nanostores@1.4.0)':
dependencies: dependencies:
'@better-auth/core': 1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.4.0) '@better-auth/core': 1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.4.0)
'@better-auth/utils': 0.4.2 '@better-auth/utils': 0.4.2
'@better-fetch/fetch': 1.3.1 '@better-fetch/fetch': 1.3.1
'@simplewebauthn/browser': 13.3.0 '@simplewebauthn/browser': 13.3.0
'@simplewebauthn/server': 13.3.2 '@simplewebauthn/server': 13.3.2
better-auth: 1.6.22(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.29.2)(pg@8.21.0))(next@16.2.7(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) better-auth: 1.6.22(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.29.2)(pg@8.21.0))(next@16.3.0(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
better-call: 1.3.7(zod@4.4.3) better-call: 1.3.7(zod@4.4.3)
nanostores: 1.4.0 nanostores: 1.4.0
zod: 4.4.3 zod: 4.4.3
@@ -4740,98 +4879,108 @@ snapshots:
'@img/colour@1.1.0': '@img/colour@1.1.0':
optional: true optional: true
'@img/sharp-darwin-arm64@0.34.5': '@img/sharp-darwin-arm64@0.35.3':
optionalDependencies: optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.2.4 '@img/sharp-libvips-darwin-arm64': 1.3.2
optional: true optional: true
'@img/sharp-darwin-x64@0.34.5': '@img/sharp-darwin-x64@0.35.3':
optionalDependencies: optionalDependencies:
'@img/sharp-libvips-darwin-x64': 1.2.4 '@img/sharp-libvips-darwin-x64': 1.3.2
optional: true optional: true
'@img/sharp-libvips-darwin-arm64@1.2.4': '@img/sharp-freebsd-wasm32@0.35.3':
dependencies:
'@img/sharp-wasm32': 0.35.3
optional: true optional: true
'@img/sharp-libvips-darwin-x64@1.2.4': '@img/sharp-libvips-darwin-arm64@1.3.2':
optional: true optional: true
'@img/sharp-libvips-linux-arm64@1.2.4': '@img/sharp-libvips-darwin-x64@1.3.2':
optional: true optional: true
'@img/sharp-libvips-linux-arm@1.2.4': '@img/sharp-libvips-linux-arm64@1.3.2':
optional: true optional: true
'@img/sharp-libvips-linux-ppc64@1.2.4': '@img/sharp-libvips-linux-arm@1.3.2':
optional: true optional: true
'@img/sharp-libvips-linux-riscv64@1.2.4': '@img/sharp-libvips-linux-ppc64@1.3.2':
optional: true optional: true
'@img/sharp-libvips-linux-s390x@1.2.4': '@img/sharp-libvips-linux-riscv64@1.3.2':
optional: true optional: true
'@img/sharp-libvips-linux-x64@1.2.4': '@img/sharp-libvips-linux-s390x@1.3.2':
optional: true optional: true
'@img/sharp-libvips-linuxmusl-arm64@1.2.4': '@img/sharp-libvips-linux-x64@1.3.2':
optional: true optional: true
'@img/sharp-libvips-linuxmusl-x64@1.2.4': '@img/sharp-libvips-linuxmusl-arm64@1.3.2':
optional: true optional: true
'@img/sharp-linux-arm64@0.34.5': '@img/sharp-libvips-linuxmusl-x64@1.3.2':
optional: true
'@img/sharp-linux-arm64@0.35.3':
optionalDependencies: optionalDependencies:
'@img/sharp-libvips-linux-arm64': 1.2.4 '@img/sharp-libvips-linux-arm64': 1.3.2
optional: true optional: true
'@img/sharp-linux-arm@0.34.5': '@img/sharp-linux-arm@0.35.3':
optionalDependencies: optionalDependencies:
'@img/sharp-libvips-linux-arm': 1.2.4 '@img/sharp-libvips-linux-arm': 1.3.2
optional: true optional: true
'@img/sharp-linux-ppc64@0.34.5': '@img/sharp-linux-ppc64@0.35.3':
optionalDependencies: optionalDependencies:
'@img/sharp-libvips-linux-ppc64': 1.2.4 '@img/sharp-libvips-linux-ppc64': 1.3.2
optional: true optional: true
'@img/sharp-linux-riscv64@0.34.5': '@img/sharp-linux-riscv64@0.35.3':
optionalDependencies: optionalDependencies:
'@img/sharp-libvips-linux-riscv64': 1.2.4 '@img/sharp-libvips-linux-riscv64': 1.3.2
optional: true optional: true
'@img/sharp-linux-s390x@0.34.5': '@img/sharp-linux-s390x@0.35.3':
optionalDependencies: optionalDependencies:
'@img/sharp-libvips-linux-s390x': 1.2.4 '@img/sharp-libvips-linux-s390x': 1.3.2
optional: true optional: true
'@img/sharp-linux-x64@0.34.5': '@img/sharp-linux-x64@0.35.3':
optionalDependencies: optionalDependencies:
'@img/sharp-libvips-linux-x64': 1.2.4 '@img/sharp-libvips-linux-x64': 1.3.2
optional: true optional: true
'@img/sharp-linuxmusl-arm64@0.34.5': '@img/sharp-linuxmusl-arm64@0.35.3':
optionalDependencies: optionalDependencies:
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4 '@img/sharp-libvips-linuxmusl-arm64': 1.3.2
optional: true optional: true
'@img/sharp-linuxmusl-x64@0.34.5': '@img/sharp-linuxmusl-x64@0.35.3':
optionalDependencies: optionalDependencies:
'@img/sharp-libvips-linuxmusl-x64': 1.2.4 '@img/sharp-libvips-linuxmusl-x64': 1.3.2
optional: true optional: true
'@img/sharp-wasm32@0.34.5': '@img/sharp-wasm32@0.35.3':
dependencies: dependencies:
'@emnapi/runtime': 1.11.1 '@emnapi/runtime': 1.11.1
optional: true optional: true
'@img/sharp-win32-arm64@0.34.5': '@img/sharp-webcontainers-wasm32@0.35.3':
dependencies:
'@img/sharp-wasm32': 0.35.3
optional: true optional: true
'@img/sharp-win32-ia32@0.34.5': '@img/sharp-win32-arm64@0.35.3':
optional: true optional: true
'@img/sharp-win32-x64@0.34.5': '@img/sharp-win32-ia32@0.35.3':
optional: true
'@img/sharp-win32-x64@0.35.3':
optional: true optional: true
'@jridgewell/gen-mapping@0.3.13': '@jridgewell/gen-mapping@0.3.13':
@@ -4917,30 +5066,30 @@ snapshots:
'@tybys/wasm-util': 0.10.3 '@tybys/wasm-util': 0.10.3
optional: true optional: true
'@next/env@16.2.7': {} '@next/env@16.3.0': {}
'@next/swc-darwin-arm64@16.2.7': '@next/swc-darwin-arm64@16.3.0':
optional: true optional: true
'@next/swc-darwin-x64@16.2.7': '@next/swc-darwin-x64@16.3.0':
optional: true optional: true
'@next/swc-linux-arm64-gnu@16.2.7': '@next/swc-linux-arm64-gnu@16.3.0':
optional: true optional: true
'@next/swc-linux-arm64-musl@16.2.7': '@next/swc-linux-arm64-musl@16.3.0':
optional: true optional: true
'@next/swc-linux-x64-gnu@16.2.7': '@next/swc-linux-x64-gnu@16.3.0':
optional: true optional: true
'@next/swc-linux-x64-musl@16.2.7': '@next/swc-linux-x64-musl@16.3.0':
optional: true optional: true
'@next/swc-win32-arm64-msvc@16.2.7': '@next/swc-win32-arm64-msvc@16.3.0':
optional: true optional: true
'@next/swc-win32-x64-msvc@16.2.7': '@next/swc-win32-x64-msvc@16.3.0':
optional: true optional: true
'@noble/ciphers@2.2.0': {} '@noble/ciphers@2.2.0': {}
@@ -6238,6 +6387,66 @@ snapshots:
'@types/use-sync-external-store@0.0.6': {} '@types/use-sync-external-store@0.0.6': {}
'@typescript/typescript-aix-ppc64@7.0.2':
optional: true
'@typescript/typescript-darwin-arm64@7.0.2':
optional: true
'@typescript/typescript-darwin-x64@7.0.2':
optional: true
'@typescript/typescript-freebsd-arm64@7.0.2':
optional: true
'@typescript/typescript-freebsd-x64@7.0.2':
optional: true
'@typescript/typescript-linux-arm64@7.0.2':
optional: true
'@typescript/typescript-linux-arm@7.0.2':
optional: true
'@typescript/typescript-linux-loong64@7.0.2':
optional: true
'@typescript/typescript-linux-mips64el@7.0.2':
optional: true
'@typescript/typescript-linux-ppc64@7.0.2':
optional: true
'@typescript/typescript-linux-riscv64@7.0.2':
optional: true
'@typescript/typescript-linux-s390x@7.0.2':
optional: true
'@typescript/typescript-linux-x64@7.0.2':
optional: true
'@typescript/typescript-netbsd-arm64@7.0.2':
optional: true
'@typescript/typescript-netbsd-x64@7.0.2':
optional: true
'@typescript/typescript-openbsd-arm64@7.0.2':
optional: true
'@typescript/typescript-openbsd-x64@7.0.2':
optional: true
'@typescript/typescript-sunos-x64@7.0.2':
optional: true
'@typescript/typescript-win32-arm64@7.0.2':
optional: true
'@typescript/typescript-win32-x64@7.0.2':
optional: true
'@vercel/oidc@3.2.0': {} '@vercel/oidc@3.2.0': {}
ai@6.0.213(zod@4.4.3): ai@6.0.213(zod@4.4.3):
@@ -6309,7 +6518,7 @@ snapshots:
baseline-browser-mapping@2.10.40: {} baseline-browser-mapping@2.10.40: {}
better-auth@1.6.22(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.29.2)(pg@8.21.0))(next@16.2.7(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): better-auth@1.6.22(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.29.2)(pg@8.21.0))(next@16.3.0(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
dependencies: dependencies:
'@better-auth/core': 1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.4.0) '@better-auth/core': 1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.4.0)
'@better-auth/drizzle-adapter': 1.6.22(@better-auth/core@1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.4.0))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.29.2)(pg@8.21.0)) '@better-auth/drizzle-adapter': 1.6.22(@better-auth/core@1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.4.0))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.29.2)(pg@8.21.0))
@@ -6331,7 +6540,7 @@ snapshots:
optionalDependencies: optionalDependencies:
drizzle-kit: 0.31.10 drizzle-kit: 0.31.10
drizzle-orm: 0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.29.2)(pg@8.21.0) drizzle-orm: 0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.29.2)(pg@8.21.0)
next: 16.2.7(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) next: 16.3.0(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
pg: 8.21.0 pg: 8.21.0
react: 19.2.7 react: 19.2.7
react-dom: 19.2.7(react@19.2.7) react-dom: 19.2.7(react@19.2.7)
@@ -6876,6 +7085,8 @@ snapshots:
nanoid@3.3.15: {} nanoid@3.3.15: {}
nanoid@3.3.17: {}
nanostores@1.4.0: {} nanostores@1.4.0: {}
next-themes@0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7): next-themes@0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
@@ -6883,30 +7094,31 @@ snapshots:
react: 19.2.7 react: 19.2.7
react-dom: 19.2.7(react@19.2.7) react-dom: 19.2.7(react@19.2.7)
next@16.2.7(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): next@16.3.0(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
dependencies: dependencies:
'@next/env': 16.2.7 '@next/env': 16.3.0
'@swc/helpers': 0.5.15 '@swc/helpers': 0.5.15
baseline-browser-mapping: 2.10.40 baseline-browser-mapping: 2.10.40
caniuse-lite: 1.0.30001799 caniuse-lite: 1.0.30001799
postcss: 8.4.31 postcss: 8.5.23
react: 19.2.7 react: 19.2.7
react-dom: 19.2.7(react@19.2.7) react-dom: 19.2.7(react@19.2.7)
styled-jsx: 5.1.6(react@19.2.7) styled-jsx: 5.1.6(react@19.2.7)
optionalDependencies: optionalDependencies:
'@next/swc-darwin-arm64': 16.2.7 '@next/swc-darwin-arm64': 16.3.0
'@next/swc-darwin-x64': 16.2.7 '@next/swc-darwin-x64': 16.3.0
'@next/swc-linux-arm64-gnu': 16.2.7 '@next/swc-linux-arm64-gnu': 16.3.0
'@next/swc-linux-arm64-musl': 16.2.7 '@next/swc-linux-arm64-musl': 16.3.0
'@next/swc-linux-x64-gnu': 16.2.7 '@next/swc-linux-x64-gnu': 16.3.0
'@next/swc-linux-x64-musl': 16.2.7 '@next/swc-linux-x64-musl': 16.3.0
'@next/swc-win32-arm64-msvc': 16.2.7 '@next/swc-win32-arm64-msvc': 16.3.0
'@next/swc-win32-x64-msvc': 16.2.7 '@next/swc-win32-x64-msvc': 16.3.0
'@opentelemetry/api': 1.9.1 '@opentelemetry/api': 1.9.1
babel-plugin-react-compiler: 1.0.0 babel-plugin-react-compiler: 1.0.0
sharp: 0.34.5 sharp: 0.35.3(@types/node@25.9.1)
transitivePeerDependencies: transitivePeerDependencies:
- '@babel/core' - '@babel/core'
- '@types/node'
- babel-plugin-macros - babel-plugin-macros
normalize-path@3.0.0: {} normalize-path@3.0.0: {}
@@ -7016,15 +7228,15 @@ snapshots:
postal-mime@2.7.4: {} postal-mime@2.7.4: {}
postcss@8.4.31: postcss@8.5.15:
dependencies: dependencies:
nanoid: 3.3.15 nanoid: 3.3.15
picocolors: 1.1.1 picocolors: 1.1.1
source-map-js: 1.2.1 source-map-js: 1.2.1
postcss@8.5.15: postcss@8.5.23:
dependencies: dependencies:
nanoid: 3.3.15 nanoid: 3.3.17
picocolors: 1.1.1 picocolors: 1.1.1
source-map-js: 1.2.1 source-map-js: 1.2.1
@@ -7190,36 +7402,38 @@ snapshots:
setimmediate@1.0.5: {} setimmediate@1.0.5: {}
sharp@0.34.5: sharp@0.35.3(@types/node@25.9.1):
dependencies: dependencies:
'@img/colour': 1.1.0 '@img/colour': 1.1.0
detect-libc: 2.1.2 detect-libc: 2.1.2
semver: 7.8.5 semver: 7.8.5
optionalDependencies: optionalDependencies:
'@img/sharp-darwin-arm64': 0.34.5 '@img/sharp-darwin-arm64': 0.35.3
'@img/sharp-darwin-x64': 0.34.5 '@img/sharp-darwin-x64': 0.35.3
'@img/sharp-libvips-darwin-arm64': 1.2.4 '@img/sharp-freebsd-wasm32': 0.35.3
'@img/sharp-libvips-darwin-x64': 1.2.4 '@img/sharp-libvips-darwin-arm64': 1.3.2
'@img/sharp-libvips-linux-arm': 1.2.4 '@img/sharp-libvips-darwin-x64': 1.3.2
'@img/sharp-libvips-linux-arm64': 1.2.4 '@img/sharp-libvips-linux-arm': 1.3.2
'@img/sharp-libvips-linux-ppc64': 1.2.4 '@img/sharp-libvips-linux-arm64': 1.3.2
'@img/sharp-libvips-linux-riscv64': 1.2.4 '@img/sharp-libvips-linux-ppc64': 1.3.2
'@img/sharp-libvips-linux-s390x': 1.2.4 '@img/sharp-libvips-linux-riscv64': 1.3.2
'@img/sharp-libvips-linux-x64': 1.2.4 '@img/sharp-libvips-linux-s390x': 1.3.2
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4 '@img/sharp-libvips-linux-x64': 1.3.2
'@img/sharp-libvips-linuxmusl-x64': 1.2.4 '@img/sharp-libvips-linuxmusl-arm64': 1.3.2
'@img/sharp-linux-arm': 0.34.5 '@img/sharp-libvips-linuxmusl-x64': 1.3.2
'@img/sharp-linux-arm64': 0.34.5 '@img/sharp-linux-arm': 0.35.3
'@img/sharp-linux-ppc64': 0.34.5 '@img/sharp-linux-arm64': 0.35.3
'@img/sharp-linux-riscv64': 0.34.5 '@img/sharp-linux-ppc64': 0.35.3
'@img/sharp-linux-s390x': 0.34.5 '@img/sharp-linux-riscv64': 0.35.3
'@img/sharp-linux-x64': 0.34.5 '@img/sharp-linux-s390x': 0.35.3
'@img/sharp-linuxmusl-arm64': 0.34.5 '@img/sharp-linux-x64': 0.35.3
'@img/sharp-linuxmusl-x64': 0.34.5 '@img/sharp-linuxmusl-arm64': 0.35.3
'@img/sharp-wasm32': 0.34.5 '@img/sharp-linuxmusl-x64': 0.35.3
'@img/sharp-win32-arm64': 0.34.5 '@img/sharp-webcontainers-wasm32': 0.35.3
'@img/sharp-win32-ia32': 0.34.5 '@img/sharp-win32-arm64': 0.35.3
'@img/sharp-win32-x64': 0.34.5 '@img/sharp-win32-ia32': 0.35.3
'@img/sharp-win32-x64': 0.35.3
'@types/node': 25.9.1
optional: true optional: true
smol-toml@1.7.0: {} smol-toml@1.7.0: {}
@@ -7312,7 +7526,28 @@ snapshots:
tw-animate-css@1.4.0: {} tw-animate-css@1.4.0: {}
typescript@6.0.3: {} typescript@7.0.2:
optionalDependencies:
'@typescript/typescript-aix-ppc64': 7.0.2
'@typescript/typescript-darwin-arm64': 7.0.2
'@typescript/typescript-darwin-x64': 7.0.2
'@typescript/typescript-freebsd-arm64': 7.0.2
'@typescript/typescript-freebsd-x64': 7.0.2
'@typescript/typescript-linux-arm': 7.0.2
'@typescript/typescript-linux-arm64': 7.0.2
'@typescript/typescript-linux-loong64': 7.0.2
'@typescript/typescript-linux-mips64el': 7.0.2
'@typescript/typescript-linux-ppc64': 7.0.2
'@typescript/typescript-linux-riscv64': 7.0.2
'@typescript/typescript-linux-s390x': 7.0.2
'@typescript/typescript-linux-x64': 7.0.2
'@typescript/typescript-netbsd-arm64': 7.0.2
'@typescript/typescript-netbsd-x64': 7.0.2
'@typescript/typescript-openbsd-arm64': 7.0.2
'@typescript/typescript-openbsd-x64': 7.0.2
'@typescript/typescript-sunos-x64': 7.0.2
'@typescript/typescript-win32-arm64': 7.0.2
'@typescript/typescript-win32-x64': 7.0.2
unbash@4.0.1: {} unbash@4.0.1: {}

View File

@@ -1,6 +1,7 @@
import { connection } from "next/server"; import { connection } from "next/server";
import { AttachmentsPage } from "@/features/attachments/components/attachments-page"; import { AttachmentsPage } from "@/features/attachments/components/attachments-page";
import { fetchAttachmentsPageData } from "@/features/attachments/queries"; import { fetchAttachmentsPageData } from "@/features/attachments/queries";
import { ContentErrorBoundary } from "@/shared/components/feedback/content-error-boundary";
import { getUserId } from "@/shared/lib/auth/server"; import { getUserId } from "@/shared/lib/auth/server";
import { parsePeriodParam } from "@/shared/utils/period"; import { parsePeriodParam } from "@/shared/utils/period";
@@ -19,7 +20,18 @@ const getSingleParam = (
return Array.isArray(value) ? (value[0] ?? null) : value; return Array.isArray(value) ? (value[0] ?? null) : value;
}; };
export default async function Page({ searchParams }: PageProps) { export default function Page({ searchParams }: PageProps) {
return (
<ContentErrorBoundary
title="Não foi possível carregar os anexos"
description="Os documentos e comprovantes não puderam ser carregados agora."
>
<AttachmentsContent searchParams={searchParams} />
</ContentErrorBoundary>
);
}
async function AttachmentsContent({ searchParams }: PageProps) {
await connection(); await connection();
const userId = await getUserId(); const userId = await getUserId();
const resolvedSearchParams = searchParams ? await searchParams : undefined; const resolvedSearchParams = searchParams ? await searchParams : undefined;

View File

@@ -6,6 +6,7 @@ import { extractDashboardLogoNames } from "@/features/dashboard/lib/extract-logo
import { fetchDashboardPageData } from "@/features/dashboard/page-data-queries"; import { fetchDashboardPageData } from "@/features/dashboard/page-data-queries";
import { getSingleParam } from "@/features/transactions/lib/page-helpers"; import { getSingleParam } from "@/features/transactions/lib/page-helpers";
import { LogoPrefetchProvider } from "@/shared/components/entity-avatar"; import { LogoPrefetchProvider } from "@/shared/components/entity-avatar";
import { ContentErrorBoundary } from "@/shared/components/feedback/content-error-boundary";
import MonthNavigation from "@/shared/components/month-picker/month-navigation"; import MonthNavigation from "@/shared/components/month-picker/month-navigation";
import { getUser } from "@/shared/lib/auth/server"; import { getUser } from "@/shared/lib/auth/server";
import { prefetchLogoMappings } from "@/shared/lib/logo/prefetch-server"; import { prefetchLogoMappings } from "@/shared/lib/logo/prefetch-server";
@@ -17,7 +18,18 @@ type PageProps = {
searchParams?: PageSearchParams; searchParams?: PageSearchParams;
}; };
export default async function Page({ searchParams }: PageProps) { export default function Page({ searchParams }: PageProps) {
return (
<ContentErrorBoundary
title="Não foi possível carregar o dashboard"
description="Seus dados financeiros não puderam ser carregados agora."
>
<DashboardContent searchParams={searchParams} />
</ContentErrorBoundary>
);
}
async function DashboardContent({ searchParams }: PageProps) {
await connection(); await connection();
const user = await getUser(); const user = await getUser();
const resolvedSearchParams = searchParams ? await searchParams : undefined; const resolvedSearchParams = searchParams ? await searchParams : undefined;
@@ -41,11 +53,20 @@ export default async function Page({ searchParams }: PageProps) {
<main className="flex flex-col gap-4"> <main className="flex flex-col gap-4">
<DashboardWelcome name={user.name} /> <DashboardWelcome name={user.name} />
<MonthNavigation /> <MonthNavigation />
<ContentErrorBoundary
title="Não foi possível exibir o resumo"
description="Os indicadores do período não puderam ser exibidos agora."
>
<DashboardMetricsCards <DashboardMetricsCards
metrics={dashboardData.metrics} metrics={dashboardData.metrics}
period={selectedPeriod} period={selectedPeriod}
adminPayerSlug={adminPayerSlug} adminPayerSlug={adminPayerSlug}
/> />
</ContentErrorBoundary>
<ContentErrorBoundary
title="Não foi possível exibir os widgets"
description="Os detalhes do dashboard não puderam ser exibidos agora."
>
<LogoPrefetchProvider mappings={logoMappings}> <LogoPrefetchProvider mappings={logoMappings}>
<DashboardGridEditable <DashboardGridEditable
data={dashboardData} data={dashboardData}
@@ -54,6 +75,7 @@ export default async function Page({ searchParams }: PageProps) {
quickActionOptions={quickActionOptions} quickActionOptions={quickActionOptions}
/> />
</LogoPrefetchProvider> </LogoPrefetchProvider>
</ContentErrorBoundary>
</main> </main>
); );
} }

View File

@@ -1,5 +1,6 @@
import { connection } from "next/server"; import { connection } from "next/server";
import { InsightsPage } from "@/features/insights/components/insights-page"; import { InsightsPage } from "@/features/insights/components/insights-page";
import { ContentErrorBoundary } from "@/shared/components/feedback/content-error-boundary";
import MonthNavigation from "@/shared/components/month-picker/month-navigation"; import MonthNavigation from "@/shared/components/month-picker/month-navigation";
import { parsePeriodParam } from "@/shared/utils/period"; import { parsePeriodParam } from "@/shared/utils/period";
@@ -18,7 +19,18 @@ const getSingleParam = (
return Array.isArray(value) ? (value[0] ?? null) : value; return Array.isArray(value) ? (value[0] ?? null) : value;
}; };
export default async function Page({ searchParams }: PageProps) { export default function Page({ searchParams }: PageProps) {
return (
<ContentErrorBoundary
title="Não foi possível carregar os Insights"
description="As análises financeiras não puderam ser carregadas agora."
>
<InsightsContent searchParams={searchParams} />
</ContentErrorBoundary>
);
}
async function InsightsContent({ searchParams }: PageProps) {
await connection(); await connection();
const resolvedSearchParams = searchParams ? await searchParams : undefined; const resolvedSearchParams = searchParams ? await searchParams : undefined;
const periodoParam = getSingleParam(resolvedSearchParams, "periodo"); const periodoParam = getSingleParam(resolvedSearchParams, "periodo");

View File

@@ -6,6 +6,7 @@ import { CardTopExpenses } from "@/features/reports/components/cards/card-top-ex
import { CardUsageChart } from "@/features/reports/components/cards/card-usage-chart"; import { CardUsageChart } from "@/features/reports/components/cards/card-usage-chart";
import { CardsOverview } from "@/features/reports/components/cards/cards-overview"; import { CardsOverview } from "@/features/reports/components/cards/cards-overview";
import { fetchCartoesReportData } from "@/features/reports/lib/cards-report-queries"; import { fetchCartoesReportData } from "@/features/reports/lib/cards-report-queries";
import { ContentErrorBoundary } from "@/shared/components/feedback/content-error-boundary";
import MonthNavigation from "@/shared/components/month-picker/month-navigation"; import MonthNavigation from "@/shared/components/month-picker/month-navigation";
import { Card } from "@/shared/components/ui/card"; import { Card } from "@/shared/components/ui/card";
import { getUser } from "@/shared/lib/auth/server"; import { getUser } from "@/shared/lib/auth/server";
@@ -26,9 +27,18 @@ const getSingleParam = (
return Array.isArray(value) ? (value[0] ?? null) : value; return Array.isArray(value) ? (value[0] ?? null) : value;
}; };
export default async function RelatorioCartoesPage({ export default function CardUsagePage({ searchParams }: PageProps) {
searchParams, return (
}: PageProps) { <ContentErrorBoundary
title="Não foi possível carregar o uso dos cartões"
description="Os dados deste relatório não puderam ser carregados agora."
>
<CardUsageContent searchParams={searchParams} />
</ContentErrorBoundary>
);
}
async function CardUsageContent({ searchParams }: PageProps) {
await connection(); await connection();
const user = await getUser(); const user = await getUser();
const resolvedSearchParams = searchParams ? await searchParams : undefined; const resolvedSearchParams = searchParams ? await searchParams : undefined;

View File

@@ -10,6 +10,7 @@ import { fetchCategoryChartData } from "@/features/reports/lib/category-chart-qu
import { fetchCategoryReport } from "@/features/reports/lib/category-report-queries"; import { fetchCategoryReport } from "@/features/reports/lib/category-report-queries";
import { fetchUserCategories } from "@/features/reports/lib/category-trends-queries"; import { fetchUserCategories } from "@/features/reports/lib/category-trends-queries";
import { validateDateRange } from "@/features/reports/lib/utils"; import { validateDateRange } from "@/features/reports/lib/utils";
import { ContentErrorBoundary } from "@/shared/components/feedback/content-error-boundary";
import { getUserId } from "@/shared/lib/auth/server"; import { getUserId } from "@/shared/lib/auth/server";
import type { CategoryReportFilters } from "@/shared/lib/types/reports"; import type { CategoryReportFilters } from "@/shared/lib/types/reports";
import { addMonthsToPeriod, getCurrentPeriod } from "@/shared/utils/period"; import { addMonthsToPeriod, getCurrentPeriod } from "@/shared/utils/period";
@@ -29,7 +30,18 @@ const getSingleParam = (
return Array.isArray(value) ? (value[0] ?? null) : value; return Array.isArray(value) ? (value[0] ?? null) : value;
}; };
export default async function Page({ searchParams }: PageProps) { export default function Page({ searchParams }: PageProps) {
return (
<ContentErrorBoundary
title="Não foi possível carregar as tendências"
description="A evolução das categorias não pôde ser carregada agora."
>
<CategoryTrendsContent searchParams={searchParams} />
</ContentErrorBoundary>
);
}
async function CategoryTrendsContent({ searchParams }: PageProps) {
await connection(); await connection();
// Get authenticated user // Get authenticated user
const userId = await getUserId(); const userId = await getUserId();

View File

@@ -8,6 +8,7 @@ import {
fetchTopEstablishmentsData, fetchTopEstablishmentsData,
type PeriodFilter, type PeriodFilter,
} from "@/features/reports/establishments/queries"; } from "@/features/reports/establishments/queries";
import { ContentErrorBoundary } from "@/shared/components/feedback/content-error-boundary";
import { Card } from "@/shared/components/ui/card"; import { Card } from "@/shared/components/ui/card";
import { getUser } from "@/shared/lib/auth/server"; import { getUser } from "@/shared/lib/auth/server";
import { parsePeriodParam } from "@/shared/utils/period"; import { parsePeriodParam } from "@/shared/utils/period";
@@ -34,9 +35,18 @@ const validatePeriodFilter = (value: string | null): PeriodFilter => {
return "6"; return "6";
}; };
export default async function TopEstablishmentsPage({ export default function EstablishmentsPage({ searchParams }: PageProps) {
searchParams, return (
}: PageProps) { <ContentErrorBoundary
title="Não foi possível carregar os estabelecimentos"
description="Os dados deste relatório não puderam ser carregados agora."
>
<EstablishmentsContent searchParams={searchParams} />
</ContentErrorBoundary>
);
}
async function EstablishmentsContent({ searchParams }: PageProps) {
await connection(); await connection();
const user = await getUser(); const user = await getUser();
const resolvedSearchParams = searchParams ? await searchParams : undefined; const resolvedSearchParams = searchParams ? await searchParams : undefined;

View File

@@ -2,10 +2,22 @@ import { connection } from "next/server";
import { InstallmentAnalysisPage } from "@/features/dashboard/components/installment-analysis/installment-analysis-page"; import { InstallmentAnalysisPage } from "@/features/dashboard/components/installment-analysis/installment-analysis-page";
import { fetchInstallmentAnalysis } from "@/features/dashboard/expenses/installment-analysis-queries"; import { fetchInstallmentAnalysis } from "@/features/dashboard/expenses/installment-analysis-queries";
import { LogoPrefetchProvider } from "@/shared/components/entity-avatar"; import { LogoPrefetchProvider } from "@/shared/components/entity-avatar";
import { ContentErrorBoundary } from "@/shared/components/feedback/content-error-boundary";
import { getUser } from "@/shared/lib/auth/server"; import { getUser } from "@/shared/lib/auth/server";
import { prefetchLogoMappings } from "@/shared/lib/logo/prefetch-server"; import { prefetchLogoMappings } from "@/shared/lib/logo/prefetch-server";
export default async function Page() { export default function Page() {
return (
<ContentErrorBoundary
title="Não foi possível carregar a análise de parcelas"
description="Os parcelamentos não puderam ser analisados agora."
>
<InstallmentAnalysisContent />
</ContentErrorBoundary>
);
}
async function InstallmentAnalysisContent() {
await connection(); await connection();
const user = await getUser(); const user = await getUser();
const data = await fetchInstallmentAnalysis(user.id); const data = await fetchInstallmentAnalysis(user.id);

View File

@@ -4,11 +4,10 @@ import {
RiShieldCheckLine, RiShieldCheckLine,
RiSmartphoneLine, RiSmartphoneLine,
} from "@remixicon/react"; } from "@remixicon/react";
import { headers } from "next/headers";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { AnimateOnScroll } from "@/features/landing/components/animate-on-scroll"; import { AnimateOnScroll } from "@/features/landing/components/animate-on-scroll";
import { MobileNav } from "@/features/landing/components/mobile-nav"; import { LandingNavbar } from "@/features/landing/components/landing-navbar";
import { SetupTabs } from "@/features/landing/components/setup-tabs"; import { SetupTabs } from "@/features/landing/components/setup-tabs";
import { import {
companionBanks, companionBanks,
@@ -16,98 +15,31 @@ import {
extraFeatures, extraFeatures,
getMetricsItems, getMetricsItems,
mainFeatures, mainFeatures,
navLinks,
pwaHighlights, pwaHighlights,
stackItems, stackItems,
whoIsItForItems, whoIsItForItems,
} from "@/features/landing/constants"; } from "@/features/landing/constants";
import { landingImages } from "@/features/landing/images"; import { landingImages } from "@/features/landing/images";
import { fetchGitHubStats } from "@/features/landing/queries"; import {
import { AnimatedThemeToggler } from "@/shared/components/animated-theme-toggler"; fetchGitHubStats,
getLandingCopyrightYear,
} from "@/features/landing/queries";
import { Logo } from "@/shared/components/brand/logo"; import { Logo } from "@/shared/components/brand/logo";
import { NavbarShell } from "@/shared/components/navigation/navbar/navbar-shell";
import { Badge } from "@/shared/components/ui/badge"; import { Badge } from "@/shared/components/ui/badge";
import { Button } from "@/shared/components/ui/button"; import { Button } from "@/shared/components/ui/button";
import { Card, CardContent } from "@/shared/components/ui/card"; import { Card, CardContent } from "@/shared/components/ui/card";
import { getOptionalUserSession } from "@/shared/lib/auth/server";
import { isSignupDisabled } from "@/shared/lib/auth/signup";
export default async function Page() { export default async function Page() {
const [session, headersList, githubStats] = await Promise.all([ const [githubStats, copyrightYear] = await Promise.all([
getOptionalUserSession(),
headers(),
fetchGitHubStats(), fetchGitHubStats(),
getLandingCopyrightYear(),
]); ]);
const hostname = headersList.get("host")?.replace(/:\d+$/, "");
const publicDomain = process.env.PUBLIC_DOMAIN?.replace(
/^https?:\/\//,
"",
).replace(/:\d+$/, "");
const isPublicDomain = !!(publicDomain && hostname === publicDomain);
const signupDisabled = isSignupDisabled();
const metricsItems = getMetricsItems(githubStats.stars, githubStats.forks); const metricsItems = getMetricsItems(githubStats.stars, githubStats.forks);
return ( return (
<div className="flex min-h-screen flex-col"> <div className="flex min-h-screen flex-col">
{/* Navigation */} {/* Navigation */}
<NavbarShell> <LandingNavbar />
{/* Center Navigation Links */}
<nav className="hidden md:flex items-center gap-1 absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
{navLinks.map(({ href, label }) => (
<Link
key={href}
href={href}
className="inline-flex h-9 items-center justify-center rounded-md px-2 text-sm font-medium leading-none text-primary-foreground/75 transition-colors hover:bg-primary-foreground/10 hover:text-primary-foreground dark:text-white/75 dark:hover:bg-white/10 dark:hover:text-white"
>
{label}
</Link>
))}
</nav>
<nav className="ml-auto flex items-center gap-1">
<AnimatedThemeToggler variant="navbar" />
{!isPublicDomain &&
(session?.user ? (
<Link prefetch href="/dashboard" className="hidden md:block">
<Button
variant="navbar"
size="sm"
className="h-9 text-primary-foreground/75 hover:bg-primary-foreground/10 hover:text-primary-foreground shadow-none dark:text-white/75 dark:hover:bg-white/10 dark:hover:text-white"
>
Dashboard
</Button>
</Link>
) : (
<div className="hidden md:flex items-center gap-1">
<Link href="/login">
<Button
variant="ghost"
size="sm"
className="h-9 text-primary-foreground/75 hover:bg-primary-foreground/10 hover:text-primary-foreground shadow-none dark:text-white/75 dark:hover:bg-white/10 dark:hover:text-white"
>
Entrar
</Button>
</Link>
{!signupDisabled && (
<Link href="/signup">
<Button
variant="ghost"
size="sm"
className="h-9 text-primary-foreground/75 hover:bg-primary-foreground/10 hover:text-primary-foreground shadow-none dark:text-white/75 dark:hover:bg-white/10 dark:hover:text-white"
>
Começar
</Button>
</Link>
)}
</div>
))}
<MobileNav
isPublicDomain={isPublicDomain}
isLoggedIn={!!session?.user}
signupDisabled={signupDisabled}
/>
</nav>
</NavbarShell>
{/* Hero Section */} {/* Hero Section */}
<section className="relative overflow-hidden pt-14 md:pt-20 lg:pt-24 pb-0"> <section className="relative overflow-hidden pt-14 md:pt-20 lg:pt-24 pb-0">
@@ -698,8 +630,7 @@ export default async function Page() {
<div className="border-t mt-8 md:mt-12 pt-6 md:pt-8 flex flex-col md:flex-row justify-between items-center gap-3 md:gap-4 text-sm text-muted-foreground"> <div className="border-t mt-8 md:mt-12 pt-6 md:pt-8 flex flex-col md:flex-row justify-between items-center gap-3 md:gap-4 text-sm text-muted-foreground">
<p> <p>
© {new Date().getFullYear()} openmonetis. Projeto open source © {copyrightYear} openmonetis. Projeto open source sob licença.
sob licença.
</p> </p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<RiShieldCheckLine className="size-4 text-primary" /> <RiShieldCheckLine className="size-4 text-primary" />

View File

@@ -682,6 +682,7 @@ export const transactions = pgTable(
splitGroupId: uuid("split_group_id"), splitGroupId: uuid("split_group_id"),
transferId: uuid("transfer_id"), transferId: uuid("transfer_id"),
ofxFitId: text("ofx_fit_id"), ofxFitId: text("ofx_fit_id"),
ofxImportFingerprint: text("ofx_import_fingerprint"),
importBatchId: text("import_batch_id"), importBatchId: text("import_batch_id"),
}, },
(table) => ({ (table) => ({
@@ -735,10 +736,12 @@ export const transactions = pgTable(
anticipationIdIdx: index("lancamentos_antecipacao_id_idx").on( anticipationIdIdx: index("lancamentos_antecipacao_id_idx").on(
table.anticipationId, table.anticipationId,
), ),
// Dedup OFX: garante FITID único por usuário // Dedup OFX: identifica a transação completa sem assumir FITID único
ofxFitIdUserIdIdx: uniqueIndex("lancamentos_ofx_fit_id_user_id_idx") ofxImportFingerprintUserIdIdx: uniqueIndex(
.on(table.userId, table.ofxFitId) "lancamentos_ofx_import_fingerprint_user_id_idx",
.where(sql`ofx_fit_id IS NOT NULL`), )
.on(table.userId, table.ofxImportFingerprint)
.where(sql`ofx_import_fingerprint IS NOT NULL`),
}), }),
); );

View File

@@ -0,0 +1,103 @@
import { headers } from "next/headers";
import Link from "next/link";
import { Suspense } from "react";
import { AnimatedThemeToggler } from "@/shared/components/animated-theme-toggler";
import { NavbarShell } from "@/shared/components/navigation/navbar/navbar-shell";
import { Button } from "@/shared/components/ui/button";
import { getOptionalUserSession } from "@/shared/lib/auth/server";
import { isSignupDisabled } from "@/shared/lib/auth/signup";
import { navLinks } from "../constants";
import { MobileNav } from "./mobile-nav";
async function LandingNavbarControls() {
const [session, headersList] = await Promise.all([
getOptionalUserSession(),
headers(),
]);
const hostname = headersList.get("host")?.replace(/:\d+$/, "");
const publicDomain = process.env.PUBLIC_DOMAIN?.replace(
/^https?:\/\//,
"",
).replace(/:\d+$/, "");
const isPublicDomain = !!(publicDomain && hostname === publicDomain);
const signupDisabled = isSignupDisabled();
return (
<>
{!isPublicDomain &&
(session?.user ? (
<Link prefetch href="/dashboard" className="hidden md:block">
<Button
variant="navbar"
size="sm"
className="h-9 text-primary-foreground/75 hover:bg-primary-foreground/10 hover:text-primary-foreground shadow-none dark:text-white/75 dark:hover:bg-white/10 dark:hover:text-white"
>
Dashboard
</Button>
</Link>
) : (
<div className="hidden md:flex items-center gap-1">
<Link href="/login">
<Button
variant="ghost"
size="sm"
className="h-9 text-primary-foreground/75 hover:bg-primary-foreground/10 hover:text-primary-foreground shadow-none dark:text-white/75 dark:hover:bg-white/10 dark:hover:text-white"
>
Entrar
</Button>
</Link>
{!signupDisabled && (
<Link href="/signup">
<Button
variant="ghost"
size="sm"
className="h-9 text-primary-foreground/75 hover:bg-primary-foreground/10 hover:text-primary-foreground shadow-none dark:text-white/75 dark:hover:bg-white/10 dark:hover:text-white"
>
Começar
</Button>
</Link>
)}
</div>
))}
<MobileNav
isPublicDomain={isPublicDomain}
isLoggedIn={!!session?.user}
signupDisabled={signupDisabled}
/>
</>
);
}
function LandingNavbarControlsFallback() {
return (
<>
<div className="hidden h-9 w-36 md:block" aria-hidden="true" />
<MobileNav isPublicDomain isLoggedIn={false} signupDisabled />
</>
);
}
export function LandingNavbar() {
return (
<NavbarShell>
<nav className="hidden md:flex items-center gap-1 absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
{navLinks.map(({ href, label }) => (
<Link
key={href}
href={href}
className="inline-flex h-9 items-center justify-center rounded-md px-2 text-sm font-medium leading-none text-primary-foreground/75 transition-colors hover:bg-primary-foreground/10 hover:text-primary-foreground dark:text-white/75 dark:hover:bg-white/10 dark:hover:text-white"
>
{label}
</Link>
))}
</nav>
<nav className="ml-auto flex items-center gap-1">
<AnimatedThemeToggler variant="navbar" />
<Suspense fallback={<LandingNavbarControlsFallback />}>
<LandingNavbarControls />
</Suspense>
</nav>
</NavbarShell>
);
}

View File

@@ -1,11 +1,22 @@
import { cacheLife } from "next/cache";
export async function getLandingCopyrightYear(): Promise<number> {
"use cache";
cacheLife({ revalidate: 86_400 });
return new Date().getFullYear();
}
export async function fetchGitHubStats(): Promise<{ export async function fetchGitHubStats(): Promise<{
stars: number; stars: number;
forks: number; forks: number;
}> { }> {
"use cache";
cacheLife({ revalidate: 3600 });
try { try {
const res = await fetch( const res = await fetch(
"https://api.github.com/repos/felipegcoutinho/openmonetis", "https://api.github.com/repos/felipegcoutinho/openmonetis",
{ next: { revalidate: 3600 } },
); );
if (!res.ok) return { stars: 200, forks: 60 }; if (!res.ok) return { stars: 200, forks: 60 };
const data = await res.json(); const data = await res.json();

View File

@@ -1,6 +1,6 @@
"use server"; "use server";
import { and, eq, inArray } from "drizzle-orm"; import { and, eq, inArray, isNull, sql } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { transactions } from "@/db/schema"; import { transactions } from "@/db/schema";
import { import {
@@ -9,23 +9,49 @@ import {
validateCartaoOwnership, validateCartaoOwnership,
validateContaOwnership, validateContaOwnership,
} from "@/features/transactions/actions/core"; } from "@/features/transactions/actions/core";
import { createOfxImportFingerprint } from "@/features/transactions/lib/ofx-import-fingerprint";
import { revalidateForEntity } from "@/shared/lib/actions/helpers"; import { revalidateForEntity } from "@/shared/lib/actions/helpers";
import { getUserId } from "@/shared/lib/auth/server"; import { getUserId } from "@/shared/lib/auth/server";
import { db } from "@/shared/lib/db"; import { db } from "@/shared/lib/db";
import {
normalizeOfxIdentityText,
type OfxIdentityRow,
type OfxImportDestination,
} from "@/shared/lib/import/ofx-identity";
import { uuidSchema } from "@/shared/lib/schemas/common"; import { uuidSchema } from "@/shared/lib/schemas/common";
import { parseLocalDateString } from "@/shared/utils/date"; import { formatDecimalForDbRequired } from "@/shared/utils/currency";
import { parseLocalDateString, toDateOnlyString } from "@/shared/utils/date";
const importRowSchema = z.object({ const ofxIdentityRowSchema = z.object({
externalId: z.string().nullable(), externalId: z.string().nullable(),
externalIdOccurrence: z.number().int().nonnegative(),
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Data inválida."), date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Data inválida."),
amount: z.number().positive(), amount: z.number().positive(),
description: z.string().min(1, "Descrição obrigatória."),
transactionType: z.enum(["income", "expense"]), transactionType: z.enum(["income", "expense"]),
sourceDescription: z.string(),
});
const ofxImportDestinationSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal("account"), id: uuidSchema("Conta") }),
z.object({ type: z.literal("card"), id: uuidSchema("Cartão") }),
]);
const duplicateCheckSchema = z.object({
source: z.string().min(1),
accountNumber: z.string().nullable(),
destination: ofxImportDestinationSchema,
rows: z.array(ofxIdentityRowSchema),
});
const importRowSchema = ofxIdentityRowSchema.extend({
description: z.string().min(1, "Descrição obrigatória."),
categoryId: uuidSchema("Category").nullable().optional(), categoryId: uuidSchema("Category").nullable().optional(),
payerId: uuidSchema("Payer").nullable().optional(), payerId: uuidSchema("Payer").nullable().optional(),
}); });
const importSchema = z.object({ const importSchema = z.object({
source: z.string().min(1),
accountNumber: z.string().nullable(),
rows: z.array(importRowSchema).min(1, "Selecione ao menos uma transação."), rows: z.array(importRowSchema).min(1, "Selecione ao menos uma transação."),
payerId: uuidSchema("Payer").nullable().optional(), payerId: uuidSchema("Payer").nullable().optional(),
accountId: uuidSchema("FinancialAccount").nullable().optional(), accountId: uuidSchema("FinancialAccount").nullable().optional(),
@@ -44,22 +70,197 @@ type ImportResult =
| { success: true; imported: number; skipped: number; importBatchId: string } | { success: true; imported: number; skipped: number; importBatchId: string }
| { success: false; error: string }; | { success: false; error: string };
// Retorna os externalIds que já existem para o usuário (para marcar duplicatas) type ImportMatch = {
export async function checkDuplicateFitIds( fingerprint: string | null;
fitIds: string[], existingTransactionId: string | null;
): Promise<string[]> { };
const userId = await getUserId();
const ids = fitIds.filter(Boolean);
if (ids.length === 0) return [];
const rows = await db type LegacyImportCandidate = {
.select({ ofxFitId: transactions.ofxFitId }) id: string;
name: string;
amount: string;
purchaseDate: Date;
transactionType: string;
ofxFitId: string | null;
accountId: string | null;
cardId: string | null;
};
function isDestinationMatch(
candidate: LegacyImportCandidate,
destination: OfxImportDestination,
): boolean {
return destination.type === "card"
? candidate.cardId === destination.id
: candidate.accountId === destination.id;
}
function isLegacyImportMatch(
candidate: LegacyImportCandidate,
row: OfxIdentityRow,
destination: OfxImportDestination,
): boolean {
if (
!row.externalId ||
row.externalIdOccurrence !== 0 ||
candidate.ofxFitId !== row.externalId ||
!isDestinationMatch(candidate, destination)
) {
return false;
}
const expectedType = row.transactionType === "income" ? "Receita" : "Despesa";
const signedAmount =
row.transactionType === "expense" ? -row.amount : row.amount;
return (
toDateOnlyString(candidate.purchaseDate) === row.date &&
formatDecimalForDbRequired(Number(candidate.amount)) ===
formatDecimalForDbRequired(signedAmount) &&
candidate.transactionType === expectedType &&
normalizeOfxIdentityText(candidate.name) ===
normalizeOfxIdentityText(row.sourceDescription)
);
}
async function findExistingImports(
userId: string,
source: string,
accountNumber: string | null,
destination: OfxImportDestination,
rows: OfxIdentityRow[],
): Promise<ImportMatch[]> {
const fingerprints = rows.map((row) =>
createOfxImportFingerprint({
source,
accountNumber,
destination,
row,
}),
);
const fingerprintValues = [
...new Set(
fingerprints.filter(
(fingerprint): fingerprint is string => fingerprint !== null,
),
),
];
const fitIds = [
...new Set(
rows
.map((row) => row.externalId)
.filter((fitId): fitId is string => fitId !== null),
),
];
const [fingerprintMatches, legacyCandidates] = await Promise.all([
fingerprintValues.length > 0
? db
.select({
id: transactions.id,
fingerprint: transactions.ofxImportFingerprint,
})
.from(transactions) .from(transactions)
.where( .where(
and(eq(transactions.userId, userId), inArray(transactions.ofxFitId, ids)), and(
eq(transactions.userId, userId),
inArray(transactions.ofxImportFingerprint, fingerprintValues),
),
)
: Promise.resolve([]),
fitIds.length > 0
? db
.select({
id: transactions.id,
name: transactions.name,
amount: transactions.amount,
purchaseDate: transactions.purchaseDate,
transactionType: transactions.transactionType,
ofxFitId: transactions.ofxFitId,
accountId: transactions.accountId,
cardId: transactions.cardId,
})
.from(transactions)
.where(
and(
eq(transactions.userId, userId),
isNull(transactions.ofxImportFingerprint),
inArray(transactions.ofxFitId, fitIds),
),
)
: Promise.resolve([]),
]);
const transactionIdByFingerprint = new Map(
fingerprintMatches.flatMap((match) =>
match.fingerprint ? [[match.fingerprint, match.id] as const] : [],
),
);
const consumedLegacyIds = new Set<string>();
return rows.map((row, index) => {
const fingerprint = fingerprints[index] ?? null;
const currentTransactionId = fingerprint
? (transactionIdByFingerprint.get(fingerprint) ?? null)
: null;
if (currentTransactionId) {
return { fingerprint, existingTransactionId: currentTransactionId };
}
const legacyMatch = legacyCandidates.find(
(candidate) =>
!consumedLegacyIds.has(candidate.id) &&
isLegacyImportMatch(candidate, row, destination),
); );
return rows.map((r) => r.ofxFitId).filter((id): id is string => id !== null); if (legacyMatch) {
consumedLegacyIds.add(legacyMatch.id);
}
return {
fingerprint,
existingTransactionId: legacyMatch?.id ?? null,
};
});
}
async function validateDestinationOwnership(
userId: string,
destination: OfxImportDestination,
): Promise<boolean> {
return destination.type === "card"
? validateCartaoOwnership(userId, destination.id)
: validateContaOwnership(userId, destination.id);
}
export async function checkDuplicateOfxTransactions(
input: unknown,
): Promise<
{ success: true; rows: ImportMatch[] } | { success: false; error: string }
> {
const userId = await getUserId();
const parsed = duplicateCheckSchema.safeParse(input);
if (!parsed.success) {
return { success: false, error: "Dados do arquivo inválidos." };
}
const { source, accountNumber, destination, rows } = parsed.data;
if (!(await validateDestinationOwnership(userId, destination))) {
return { success: false, error: "Conta ou cartão não encontrado." };
}
return {
success: true,
rows: await findExistingImports(
userId,
source,
accountNumber,
destination,
rows,
),
};
} }
export async function importTransactionsAction( export async function importTransactionsAction(
@@ -75,8 +276,25 @@ export async function importTransactionsAction(
}; };
} }
const { rows, payerId, accountId, cardId, paymentMethod, invoicePeriod } = const {
parsed.data; source,
accountNumber,
rows,
payerId,
accountId,
cardId,
paymentMethod,
invoicePeriod,
} = parsed.data;
const destination: OfxImportDestination | null = cardId
? { type: "card", id: cardId }
: accountId
? { type: "account", id: accountId }
: null;
if (!destination || (accountId && cardId)) {
return { success: false, error: "Selecione uma conta ou cartão." };
}
const payerIdsByRow = rows.map((row) => row.payerId ?? payerId ?? null); const payerIdsByRow = rows.map((row) => row.payerId ?? payerId ?? null);
@@ -109,8 +327,33 @@ export async function importTransactionsAction(
if (!accountOk) return { success: false, error: "Conta não encontrada." }; if (!accountOk) return { success: false, error: "Conta não encontrada." };
if (!cardOk) return { success: false, error: "Cartão não encontrado." }; if (!cardOk) return { success: false, error: "Cartão não encontrado." };
if (rows.length === 0) { const importMatches = await findExistingImports(
return { success: true, imported: 0, skipped: 0, importBatchId: "" }; userId,
source,
accountNumber,
destination,
rows,
);
const rowsToImport = rows.flatMap((row, index) => {
const match = importMatches[index];
return match?.existingTransactionId
? []
: [
{
row,
payerId: payerIdsByRow[index],
fingerprint: match?.fingerprint ?? null,
},
];
});
if (rowsToImport.length === 0) {
return {
success: true,
imported: 0,
skipped: rows.length,
importBatchId: "",
};
} }
const importBatchId = crypto.randomUUID(); const importBatchId = crypto.randomUUID();
@@ -118,7 +361,7 @@ export async function importTransactionsAction(
// Cartão de crédito: fatura pode ainda não ter sido paga // Cartão de crédito: fatura pode ainda não ter sido paga
const isSettled = paymentMethod !== "Cartão de crédito"; const isSettled = paymentMethod !== "Cartão de crédito";
const records = rows.map((row, index) => { const records = rowsToImport.map(({ row, payerId, fingerprint }) => {
const purchaseDate = parseLocalDateString(row.date); const purchaseDate = parseLocalDateString(row.date);
const period = const period =
invoicePeriod ?? invoicePeriod ??
@@ -137,21 +380,24 @@ export async function importTransactionsAction(
period, period,
isSettled, isSettled,
userId, userId,
payerId: payerIdsByRow[index], payerId,
accountId: accountId ?? null, accountId: accountId ?? null,
cardId: cardId ?? null, cardId: cardId ?? null,
categoryId: row.categoryId ?? null, categoryId: row.categoryId ?? null,
ofxFitId: row.externalId, ofxFitId: row.externalId,
ofxImportFingerprint: fingerprint,
importBatchId, importBatchId,
}; };
}); });
// onConflictDoNothing usa o uniqueIndex (userId, ofxFitId) WHERE ofxFitId IS NOT NULL // O índice de fingerprint protege contra importações concorrentes do mesmo OFX.
// eliminando o SELECT prévio de checkDuplicateFitIds
const inserted = await db const inserted = await db
.insert(transactions) .insert(transactions)
.values(records) .values(records)
.onConflictDoNothing() .onConflictDoNothing({
target: [transactions.userId, transactions.ofxImportFingerprint],
where: sql`ofx_import_fingerprint IS NOT NULL`,
})
.returning({ id: transactions.id }); .returning({ id: transactions.id });
await revalidateForEntity("transactions", userId); await revalidateForEntity("transactions", userId);
@@ -159,23 +405,31 @@ export async function importTransactionsAction(
return { return {
success: true, success: true,
imported: inserted.length, imported: inserted.length,
skipped: records.length - inserted.length, skipped: rows.length - inserted.length,
importBatchId, importBatchId,
}; };
} }
export async function deleteTransactionByFitId( export async function deleteImportedTransaction(
fitId: string, transactionId: string,
): Promise<{ success: boolean; error?: string }> { ): Promise<{ success: boolean; error?: string }> {
if (!fitId) return { success: false, error: "FITID inválido." }; const parsedId = uuidSchema("Lançamento").safeParse(transactionId);
if (!parsedId.success) {
return { success: false, error: "Lançamento inválido." };
}
const userId = await getUserId(); const userId = await getUserId();
await db const deleted = await db
.delete(transactions) .delete(transactions)
.where( .where(
and(eq(transactions.userId, userId), eq(transactions.ofxFitId, fitId)), and(eq(transactions.userId, userId), eq(transactions.id, parsedId.data)),
); )
.returning({ id: transactions.id });
if (deleted.length === 0) {
return { success: false, error: "Lançamento não encontrado." };
}
await revalidateForEntity("transactions", userId); await revalidateForEntity("transactions", userId);

View File

@@ -1,21 +1,15 @@
"use client"; "use client";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { import { useCallback, useMemo, useRef, useState, useTransition } from "react";
useCallback,
useEffect,
useMemo,
useState,
useTransition,
} from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { import {
fetchCategoryMappings, fetchCategoryMappings,
saveCategoryMappings, saveCategoryMappings,
} from "@/features/transactions/actions/category-memory-action"; } from "@/features/transactions/actions/category-memory-action";
import { import {
checkDuplicateFitIds, checkDuplicateOfxTransactions,
deleteTransactionByFitId, deleteImportedTransaction,
importTransactionsAction, importTransactionsAction,
undoImportAction, undoImportAction,
} from "@/features/transactions/actions/import-action"; } from "@/features/transactions/actions/import-action";
@@ -43,6 +37,7 @@ import {
} from "@/shared/components/ui/card"; } from "@/shared/components/ui/card";
import { Skeleton } from "@/shared/components/ui/skeleton"; import { Skeleton } from "@/shared/components/ui/skeleton";
import type { ImportStatement } from "@/shared/lib/import/types"; import type { ImportStatement } from "@/shared/lib/import/types";
import { createClientSafeId } from "@/shared/utils/id";
const categoryGroupByTransactionType = { const categoryGroupByTransactionType = {
expense: "despesa", expense: "despesa",
@@ -69,6 +64,7 @@ export function ImportPage({
const router = useRouter(); const router = useRouter();
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const [isChecking, setIsChecking] = useState(false); const [isChecking, setIsChecking] = useState(false);
const duplicateCheckRequestId = useRef(0);
const [statement, setStatement] = useState<ImportStatement | null>(null); const [statement, setStatement] = useState<ImportStatement | null>(null);
const [rows, setRows] = useState<ReviewRow[]>([]); const [rows, setRows] = useState<ReviewRow[]>([]);
@@ -95,23 +91,45 @@ export function ImportPage({
const handleParsed = useCallback( const handleParsed = useCallback(
async (stmt: ImportStatement) => { async (stmt: ImportStatement) => {
const requestId = ++duplicateCheckRequestId.current;
setStatement(stmt); setStatement(stmt);
setIsChecking(true); setIsChecking(true);
const defaultAccountCardValue = stmt.isCreditCard
? cardOptions[0]
? encodeAccountCard("card", cardOptions[0].value)
: null
: accountOptions[0]
? encodeAccountCard("account", accountOptions[0].value)
: null;
const destination = defaultAccountCardValue
? decodeAccountCard(defaultAccountCardValue)
: null;
setAccountCardValue(defaultAccountCardValue);
try { try {
const fitIds = stmt.transactions const [duplicateResult, categoryMappings] = await Promise.all([
.map((t) => t.externalId) destination
.filter((id): id is string => id !== null); ? checkDuplicateOfxTransactions({
source: stmt.source,
const [duplicates, categoryMappings] = await Promise.all([ accountNumber: stmt.accountNumber,
checkDuplicateFitIds(fitIds).then((ids) => new Set(ids)), destination,
rows: stmt.transactions,
})
: Promise.resolve({ success: true as const, rows: [] }),
fetchCategoryMappings(stmt.transactions.map((t) => t.description)), fetchCategoryMappings(stmt.transactions.map((t) => t.description)),
]); ]);
if (requestId !== duplicateCheckRequestId.current) return;
if (!duplicateResult.success) {
toast.error(duplicateResult.error);
}
setRows( setRows(
stmt.transactions.map((t) => { stmt.transactions.map((t, index) => {
let mappedCategoryId = let mappedCategoryId =
categoryMappings[normalizeDescriptionKey(t.description)] ?? null; categoryMappings[normalizeDescriptionKey(t.description)] ?? null;
const existingTransactionId = duplicateResult.success
? (duplicateResult.rows[index]?.existingTransactionId ?? null)
: null;
if (t.categoryRaw) { if (t.categoryRaw) {
const categoryRaw = normalizeCategoryName(t.categoryRaw); const categoryRaw = normalizeCategoryName(t.categoryRaw);
@@ -125,8 +143,10 @@ export function ImportPage({
return { return {
...t, ...t,
isDuplicate: t.externalId ? duplicates.has(t.externalId) : false, reviewId: createClientSafeId(),
selected: t.externalId ? !duplicates.has(t.externalId) : true, existingTransactionId,
isDuplicate: existingTransactionId !== null,
selected: existingTransactionId === null,
payerId, payerId,
categoryId: isCategoryCompatible( categoryId: isCategoryCompatible(
mappedCategoryId, mappedCategoryId,
@@ -138,23 +158,64 @@ export function ImportPage({
}), }),
); );
} finally { } finally {
if (requestId === duplicateCheckRequestId.current) {
setIsChecking(false); setIsChecking(false);
} }
}
}, },
[isCategoryCompatible, payerId, categoryOptions], [
accountOptions,
cardOptions,
categoryOptions,
isCategoryCompatible,
payerId,
],
); );
// Pré-seleciona cartão ou conta com base no tipo detectado no OFX const handleAccountCardChange = async (value: string | null) => {
useEffect(() => { const requestId = ++duplicateCheckRequestId.current;
if (!statement || accountCardValue) return; setAccountCardValue(value);
if (statement.isCreditCard && cardOptions[0]) { const destination = value ? decodeAccountCard(value) : null;
setAccountCardValue(encodeAccountCard("card", cardOptions[0].value)); if (!statement || !destination) {
} else if (!statement.isCreditCard && accountOptions[0]) { setIsChecking(false);
setAccountCardValue( return;
encodeAccountCard("account", accountOptions[0].value),
);
} }
}, [statement, cardOptions, accountOptions, accountCardValue]);
setIsChecking(true);
try {
const result = await checkDuplicateOfxTransactions({
source: statement.source,
accountNumber: statement.accountNumber,
destination,
rows,
});
if (requestId !== duplicateCheckRequestId.current) return;
if (!result.success) {
toast.error(result.error);
return;
}
setRows((previousRows) =>
previousRows.map((row, index) => {
const existingTransactionId =
result.rows[index]?.existingTransactionId ?? null;
const isDuplicate = existingTransactionId !== null;
return {
...row,
existingTransactionId,
selected:
row.isDuplicate === isDuplicate ? row.selected : !isDuplicate,
isDuplicate,
};
}),
);
} finally {
if (requestId === duplicateCheckRequestId.current) {
setIsChecking(false);
}
}
};
const toggleRow = (index: number) => { const toggleRow = (index: number) => {
setRows((prev) => setRows((prev) =>
@@ -184,9 +245,9 @@ export function ImportPage({
const handleUndoDuplicate = async (index: number) => { const handleUndoDuplicate = async (index: number) => {
const row = rows[index]; const row = rows[index];
if (!row?.externalId) return; if (!row?.existingTransactionId) return;
const result = await deleteTransactionByFitId(row.externalId); const result = await deleteImportedTransaction(row.existingTransactionId);
if (!result.success) { if (!result.success) {
toast.error("Não foi possível desfazer a importação anterior."); toast.error("Não foi possível desfazer a importação anterior.");
return; return;
@@ -194,7 +255,14 @@ export function ImportPage({
setRows((prev) => setRows((prev) =>
prev.map((r, i) => prev.map((r, i) =>
i === index ? { ...r, isDuplicate: false, selected: true } : r, i === index
? {
...r,
existingTransactionId: null,
isDuplicate: false,
selected: true,
}
: r,
), ),
); );
toast.success("Importação anterior removida."); toast.success("Importação anterior removida.");
@@ -261,11 +329,15 @@ export function ImportPage({
startTransition(async () => { startTransition(async () => {
const result = await importTransactionsAction({ const result = await importTransactionsAction({
source: statement.source,
accountNumber: statement.accountNumber,
rows: selectedRows.map((r) => ({ rows: selectedRows.map((r) => ({
externalId: r.externalId, externalId: r.externalId,
externalIdOccurrence: r.externalIdOccurrence,
date: r.date, date: r.date,
amount: r.amount, amount: r.amount,
description: r.description, description: r.description,
sourceDescription: r.sourceDescription,
transactionType: r.transactionType, transactionType: r.transactionType,
categoryId: r.categoryId, categoryId: r.categoryId,
payerId: r.payerId, payerId: r.payerId,
@@ -369,7 +441,7 @@ export function ImportPage({
accountCardValue={accountCardValue} accountCardValue={accountCardValue}
payerId={payerId} payerId={payerId}
invoicePeriod={invoicePeriod} invoicePeriod={invoicePeriod}
onAccountCardChange={setAccountCardValue} onAccountCardChange={handleAccountCardChange}
onPayerChange={handleBulkPayerChange} onPayerChange={handleBulkPayerChange}
onInvoicePeriodChange={setInvoicePeriod} onInvoicePeriodChange={setInvoicePeriod}
onBulkCategoryChange={handleBulkCategoryChange} onBulkCategoryChange={handleBulkCategoryChange}

View File

@@ -43,8 +43,10 @@ const categoryGroupByTransactionType: Record<
}; };
export type ReviewRow = ImportedTransaction & { export type ReviewRow = ImportedTransaction & {
reviewId: string;
selected: boolean; selected: boolean;
isDuplicate: boolean; isDuplicate: boolean;
existingTransactionId: string | null;
categoryId: string | null; categoryId: string | null;
payerId: string | null; payerId: string | null;
}; };
@@ -80,6 +82,7 @@ export function ReviewTable({
const virtualizer = useVirtualizer({ const virtualizer = useVirtualizer({
count: rows.length, count: rows.length,
getScrollElement: () => parentRef.current, getScrollElement: () => parentRef.current,
getItemKey: (index) => rows[index]?.reviewId ?? index,
estimateSize: () => 44, estimateSize: () => 44,
overscan: 8, overscan: 8,
}); });
@@ -141,7 +144,7 @@ export function ReviewTable({
); );
return ( return (
<TableRow <TableRow
key={row.externalId ?? `${row.date}-${index}`} key={row.reviewId}
className={ className={
row.isDuplicate && !row.selected ? "opacity-50" : "" row.isDuplicate && !row.selected ? "opacity-50" : ""
} }

View File

@@ -0,0 +1,24 @@
import "server-only";
import { createHash } from "node:crypto";
import {
buildOfxFingerprintPayload,
type OfxIdentityRow,
type OfxImportDestination,
} from "@/shared/lib/import/ofx-identity";
type CreateOfxImportFingerprintInput = {
source: string;
accountNumber: string | null;
destination: OfxImportDestination;
row: OfxIdentityRow;
};
export function createOfxImportFingerprint(
input: CreateOfxImportFingerprintInput,
): string | null {
const payload = buildOfxFingerprintPayload(input);
if (!payload) return null;
return createHash("sha256").update(payload).digest("hex");
}

View File

@@ -0,0 +1,38 @@
"use client";
import { RiErrorWarningLine } from "@remixicon/react";
import { catchError, type ErrorInfo } from "next/error";
import { EmptyState } from "@/shared/components/feedback/empty-state";
import { Button } from "@/shared/components/ui/button";
type ContentErrorBoundaryProps = {
title: string;
description: string;
};
function ContentErrorFallback(
{ title, description }: ContentErrorBoundaryProps,
{ retry }: ErrorInfo,
) {
return (
<div
role="alert"
className="flex min-h-64 w-full items-center justify-center"
>
<EmptyState
title={title}
description={description}
media={<RiErrorWarningLine className="size-5 text-destructive" />}
mediaVariant="icon"
className="min-h-64 border border-dashed"
>
<Button type="button" variant="outline" onClick={retry}>
Tentar novamente
</Button>
</EmptyState>
</div>
);
}
export const ContentErrorBoundary =
catchError<ContentErrorBoundaryProps>(ContentErrorFallback);

View File

@@ -0,0 +1,69 @@
import { formatDecimalForDbRequired } from "@/shared/utils/currency";
export type OfxImportDestination = {
type: "account" | "card";
id: string;
};
export type OfxIdentityRow = {
externalId: string | null;
externalIdOccurrence: number;
date: string;
amount: number;
transactionType: "income" | "expense";
sourceDescription: string;
};
type OfxIdentityInput = {
source: string;
accountNumber: string | null;
destination: OfxImportDestination;
row: OfxIdentityRow;
};
export function normalizeOfxIdentityText(value: string): string {
return value.normalize("NFKC").trim().replace(/\s+/g, " ").toLowerCase();
}
export function buildOfxOccurrenceKey(
row: Omit<OfxIdentityRow, "externalIdOccurrence">,
): string | null {
if (!row.externalId) return null;
const signedAmount =
row.transactionType === "expense" ? -row.amount : row.amount;
return JSON.stringify([
row.externalId.trim(),
row.date,
formatDecimalForDbRequired(signedAmount),
row.transactionType,
normalizeOfxIdentityText(row.sourceDescription),
]);
}
export function buildOfxFingerprintPayload({
source,
accountNumber,
destination,
row,
}: OfxIdentityInput): string | null {
if (!row.externalId) return null;
const signedAmount =
row.transactionType === "expense" ? -row.amount : row.amount;
return JSON.stringify([
"openmonetis-ofx-v1",
normalizeOfxIdentityText(source),
normalizeOfxIdentityText(accountNumber ?? ""),
destination.type,
destination.id,
row.externalId.trim(),
row.date,
formatDecimalForDbRequired(signedAmount),
row.transactionType,
normalizeOfxIdentityText(row.sourceDescription),
row.externalIdOccurrence,
]);
}

View File

@@ -1,3 +1,4 @@
import { buildOfxOccurrenceKey } from "./ofx-identity";
import type { ImportedTransaction, ImportStatement } from "./types"; import type { ImportedTransaction, ImportStatement } from "./types";
// Extrai o valor de uma tag leaf do OFX SGML: <TAG>valor // Extrai o valor de uma tag leaf do OFX SGML: <TAG>valor
@@ -32,6 +33,7 @@ export function parseOfx(content: string): ImportStatement {
// Transações // Transações
const blocks = xml.match(/<STMTTRN>[\s\S]*?<\/STMTTRN>/g) ?? []; const blocks = xml.match(/<STMTTRN>[\s\S]*?<\/STMTTRN>/g) ?? [];
const occurrenceCounts = new Map<string, number>();
const transactions: ImportedTransaction[] = blocks.map((block) => { const transactions: ImportedTransaction[] = blocks.map((block) => {
const trnType = getField(block, "TRNTYPE") ?? "DEBIT"; const trnType = getField(block, "TRNTYPE") ?? "DEBIT";
const dtPosted = getField(block, "DTPOSTED") ?? ""; const dtPosted = getField(block, "DTPOSTED") ?? "";
@@ -41,16 +43,30 @@ export function parseOfx(content: string): ImportStatement {
const name = getField(block, "NAME"); const name = getField(block, "NAME");
const amount = Number.parseFloat(trnAmt.replace(",", ".")); const amount = Number.parseFloat(trnAmt.replace(",", "."));
const transactionType = const transactionType: ImportedTransaction["transactionType"] =
amount > 0 || trnType === "CREDIT" ? "income" : "expense"; amount > 0 || trnType === "CREDIT" ? "income" : "expense";
const description = memo ?? name ?? "";
return { const transaction = {
externalId: fitId, externalId: fitId,
date: parseOfxDate(dtPosted), date: parseOfxDate(dtPosted),
amount: Math.abs(amount), amount: Math.abs(amount),
description: memo ?? name ?? "", description,
sourceDescription: description,
transactionType, transactionType,
}; };
const occurrenceKey = buildOfxOccurrenceKey(transaction);
const externalIdOccurrence = occurrenceKey
? (occurrenceCounts.get(occurrenceKey) ?? 0)
: 0;
if (occurrenceKey) {
occurrenceCounts.set(occurrenceKey, externalIdOccurrence + 1);
}
return {
...transaction,
externalIdOccurrence,
};
}); });
const isCreditCard = xml.includes("<CREDITCARDMSGSRSV1>"); const isCreditCard = xml.includes("<CREDITCARDMSGSRSV1>");

View File

@@ -1,8 +1,10 @@
export type ImportedTransaction = { export type ImportedTransaction = {
externalId: string | null; // FITID do OFX externalId: string | null; // FITID do OFX
externalIdOccurrence: number; // posição entre registros OFX idênticos
date: string; // YYYY-MM-DD date: string; // YYYY-MM-DD
amount: number; // positivo = receita, negativo = despesa amount: number; // positivo = receita, negativo = despesa
description: string; // MEMO ou NAME description: string; // MEMO ou NAME
sourceDescription: string; // descrição original, preservada para deduplicação
transactionType: "income" | "expense"; transactionType: "income" | "expense";
categoryRaw?: string | null; categoryRaw?: string | null;
}; };

View File

@@ -105,9 +105,11 @@ export async function parseXls(buffer: ArrayBuffer): Promise<ImportStatement> {
transactions.push({ transactions.push({
externalId: null, externalId: null,
externalIdOccurrence: 0,
date, date,
amount, amount,
description, description,
sourceDescription: description,
transactionType, transactionType,
categoryRaw, categoryRaw,
}); });