Compare commits
155 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6fba5f953 | ||
|
|
18893bfe02 | ||
|
|
7fdf9e2876 | ||
|
|
7d0781b035 | ||
|
|
b9b843b9db | ||
|
|
01215b3124 | ||
|
|
d70223e7b3 | ||
|
|
6ea064e1bd | ||
|
|
9c0669a152 | ||
|
|
b2d4b29cb5 | ||
|
|
1df2ba787d | ||
|
|
e5d9b66cca | ||
|
|
37edb1b76d | ||
|
|
6288f5f8d4 | ||
|
|
57ac326c2a | ||
|
|
dccc18b1c1 | ||
|
|
0cb01a1d4c | ||
|
|
51652da4f8 | ||
|
|
7a74f9405e | ||
|
|
94bf93194f | ||
|
|
d55173e8c1 | ||
|
|
4a73088c09 | ||
|
|
eaa20448a8 | ||
|
|
367d78d43d | ||
|
|
2fc6d11d78 | ||
|
|
0f5c735be0 | ||
|
|
4bea6330bf | ||
|
|
8389752172 | ||
|
|
19b5aa00ee | ||
|
|
863ccc0fd2 | ||
|
|
29d99cbedb | ||
|
|
dbeb98bbe4 | ||
|
|
c0436dc2ac | ||
|
|
e1e76fadc0 | ||
|
|
9b2c15ef7d | ||
|
|
fbe3fceb9f | ||
|
|
39f3cd8b20 | ||
|
|
791fec7751 | ||
|
|
114e2b4011 | ||
|
|
f15a003cef | ||
|
|
7f07a9cbf6 | ||
|
|
5fa234884e | ||
|
|
b453b432ed | ||
|
|
7f05d2a681 | ||
|
|
b14f487824 | ||
|
|
5b03824a72 | ||
|
|
74dda549f5 | ||
|
|
137b63f256 | ||
|
|
f747405264 | ||
|
|
cbc17c8513 | ||
|
|
c41fafc319 | ||
|
|
0bc3f06b77 | ||
|
|
2f68bcf039 | ||
|
|
41dcd5cec9 | ||
|
|
6391f07eb6 | ||
|
|
ae9dd364c4 | ||
|
|
e005add233 | ||
|
|
6d81ff8b53 | ||
|
|
5d84ae928a | ||
|
|
ba05985725 | ||
|
|
3e80d5995b | ||
|
|
68daae7926 | ||
|
|
9413c470a8 | ||
|
|
ad1b0aa979 | ||
|
|
4d9a1c0a35 | ||
|
|
5635705c56 | ||
|
|
4c97ed569d | ||
|
|
22a88de993 | ||
|
|
9456aa98bc | ||
|
|
21c6a8d9d0 | ||
|
|
c29ffa9a12 | ||
|
|
8875de843b | ||
|
|
679ea752bb | ||
|
|
1161e97d9e | ||
|
|
55d7dedd9a | ||
|
|
ad2752b7b0 | ||
|
|
58db357cde | ||
|
|
99a9ff5512 | ||
|
|
5bcf4f69d3 | ||
|
|
95099c1a94 | ||
|
|
94912f7edc | ||
|
|
bf6adfa3f1 | ||
|
|
e4b9dd4254 | ||
|
|
f1907c8697 | ||
|
|
805bcb863d | ||
|
|
11b4f8940f | ||
|
|
fba9686fdb | ||
|
|
9b8ac9f71f | ||
|
|
fa41c78a39 | ||
|
|
5f7bfb98da | ||
|
|
9ecafdb15f | ||
|
|
e8cc673e52 | ||
|
|
3bd8117b65 | ||
|
|
a7268d8f05 | ||
|
|
1f9098879e | ||
|
|
7a3bff52ac | ||
|
|
dfb4126b12 | ||
|
|
ffead579fa | ||
|
|
aa85cf8b29 | ||
|
|
9a7ae0fa3d | ||
|
|
98fe6a0f4f | ||
|
|
d10eae13e5 | ||
|
|
43697b4fd2 | ||
|
|
27e3ba5f0d | ||
|
|
31485eec8f | ||
|
|
3be64aa8d0 | ||
|
|
85f6dcfc22 | ||
|
|
df996df93d | ||
|
|
10afef9fec | ||
|
|
fd4d90a53e | ||
|
|
a24406271c | ||
|
|
a09942e3d8 | ||
|
|
96febd5904 | ||
|
|
c3cfbc878c | ||
|
|
55bbfabe9f | ||
|
|
f5cdae4853 | ||
|
|
5c4995961c | ||
|
|
1b4dfaaba7 | ||
|
|
549a5bdba1 | ||
|
|
acaf9d5c27 | ||
|
|
e4c6a91350 | ||
|
|
ba369e8a83 | ||
|
|
d01bc8a669 | ||
|
|
e024e0d54e | ||
|
|
c44089169f | ||
|
|
d04e30e3c9 | ||
|
|
229b6c5bc0 | ||
|
|
c3b133d8d9 | ||
|
|
e9a2ab1782 | ||
|
|
c7d6e23398 | ||
|
|
0514efb1c4 | ||
|
|
e32fb85006 | ||
|
|
96df6a1798 | ||
|
|
1f8a97bd16 | ||
|
|
0ab3298cef | ||
|
|
cad41680eb | ||
|
|
3b00f328c5 | ||
|
|
20d0c3e0a7 | ||
|
|
71b5a004e3 | ||
|
|
65b1506d75 | ||
|
|
2a458d5a3c | ||
|
|
f418987f47 | ||
|
|
59b4dea071 | ||
|
|
6ce132fe0c | ||
|
|
49731238e4 | ||
|
|
c5df97f7aa | ||
|
|
3476fda4db | ||
|
|
519b673ae5 | ||
|
|
303b8bedd4 | ||
|
|
f2b9b16896 | ||
|
|
6eba35542b | ||
|
|
f5e95ffba6 | ||
|
|
a75bb86eec | ||
|
|
a3b858621f | ||
|
|
fee2a2c9f5 |
19
.env.example
@@ -3,10 +3,10 @@
|
|||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
# === Database ===
|
# === Database ===
|
||||||
# PostgreSQL local (Docker): use host "db"
|
# Desenvolvimento local (pnpm dev): use host "localhost" (padrão abaixo)
|
||||||
# PostgreSQL local (sem Docker): use host "localhost"
|
# Docker Compose completo: o compose.yml define DATABASE_URL automaticamente com host "db"
|
||||||
# PostgreSQL remoto: use URL completa do provider
|
# PostgreSQL remoto: use URL completa do provider
|
||||||
DATABASE_URL=postgresql://openmonetis:openmonetis_dev_password@db:5432/openmonetis_db
|
DATABASE_URL=postgresql://openmonetis:openmonetis_dev_password@localhost:5432/openmonetis_db
|
||||||
|
|
||||||
# Credenciais do PostgreSQL (apenas para Docker local) - Alterar
|
# Credenciais do PostgreSQL (apenas para Docker local) - Alterar
|
||||||
POSTGRES_USER=openmonetis
|
POSTGRES_USER=openmonetis
|
||||||
@@ -44,8 +44,19 @@ GOOGLE_CLIENT_SECRET=
|
|||||||
# Se não definido, todas as rotas ficam acessíveis.
|
# Se não definido, todas as rotas ficam acessíveis.
|
||||||
# PUBLIC_DOMAIN=openmonetis.com
|
# PUBLIC_DOMAIN=openmonetis.com
|
||||||
|
|
||||||
|
# === Analytics (Opcional) ===
|
||||||
|
# Umami: https://umami.is — self-hosted ou cloud
|
||||||
|
UMAMI_URL=
|
||||||
|
UMAMI_WEBSITE_ID=
|
||||||
|
UMAMI_DOMAINS=
|
||||||
|
|
||||||
# === AI Providers (Opcional) ===
|
# === AI Providers (Opcional) ===
|
||||||
ANTHROPIC_API_KEY=
|
ANTHROPIC_API_KEY=
|
||||||
OPENAI_API_KEY=
|
OPENAI_API_KEY=
|
||||||
GOOGLE_GENERATIVE_AI_API_KEY=
|
GOOGLE_GENERATIVE_AI_API_KEY=
|
||||||
OPENROUTER_API_KEY=
|
OPENROUTER_API_KEY=
|
||||||
|
|
||||||
|
# === Logo.dev (Opcional) ===
|
||||||
|
# Logos automáticos de estabelecimentos. Cadastre em https://www.logo.dev
|
||||||
|
LOGO_DEV_TOKEN=
|
||||||
|
LOGO_DEV_SECRET_KEY=
|
||||||
4
.gitattributes
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Força LF para arquivos que precisam de line endings Unix no container
|
||||||
|
*.sh text eol=lf
|
||||||
|
docker-entrypoint.sh text eol=lf
|
||||||
|
Dockerfile text eol=lf
|
||||||
58
.github/copilot-instructions.md
vendored
@@ -1,58 +0,0 @@
|
|||||||
# AI Coding Assistant Instructions for OpenMonetis
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
OpenMonetis is a self-hosted personal finance management application built with Next.js 16, TypeScript, PostgreSQL, and Drizzle ORM. It provides manual transaction tracking, account management, budgeting, and financial insights with a Portuguese interface.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
- **Frontend**: Next.js App Router with React 19, shadcn/ui components, Tailwind CSS
|
|
||||||
- **Backend**: Server actions in Next.js, API routes for auth/health
|
|
||||||
- **Database**: PostgreSQL with Drizzle ORM, schema in `db/schema.ts`
|
|
||||||
- **Auth**: Better Auth (OAuth + email magic links)
|
|
||||||
- **Deployment**: Docker multi-stage build, health checks
|
|
||||||
|
|
||||||
## Key Patterns
|
|
||||||
|
|
||||||
- **Server Actions**: Use `"use server"` for mutations, validate with Zod schemas, handle errors with `handleActionError`
|
|
||||||
- **Database Queries**: Use Drizzle's query API with relations, e.g., `db.query.lancamentos.findMany({ with: { categoria: true } })`
|
|
||||||
- **Authentication**: Import from `lib/auth/server`, redirect on failure
|
|
||||||
- **Revalidation**: Call `revalidateForEntity("lancamentos")` after mutations
|
|
||||||
- **Portuguese Naming**: DB fields like `nome`, `tipo_conta`, `pagador` (payer), `lancamento` (transaction)
|
|
||||||
- **Component Structure**: Feature-based folders in `components/`, shared UI in `components/ui/`
|
|
||||||
|
|
||||||
## Development Workflow
|
|
||||||
|
|
||||||
- **Start Dev**: `pnpm dev` (Turbopack), `docker compose up db -d` for DB
|
|
||||||
- **Database**: `pnpm db:push` to sync schema, `pnpm db:studio` for visual editor
|
|
||||||
- **Build**: `pnpm build`, `pnpm start` for production
|
|
||||||
- **Docker**: `pnpm docker:up` for full stack, `pnpm docker:logs` for monitoring
|
|
||||||
|
|
||||||
## Common Tasks
|
|
||||||
|
|
||||||
- **Add Transaction**: Create server action in `app/(dashboard)/lancamentos/actions.ts`, validate with Zod, insert via Drizzle
|
|
||||||
- **New Entity**: Add to `db/schema.ts`, define relations, create CRUD actions in `lib/[entity]/actions.ts`
|
|
||||||
- **UI Component**: Use shadcn/ui, place in `components/[feature]/`, export from `components/ui/`
|
|
||||||
- **API Route**: Add to `app/api/`, use `getUserSession()` for auth
|
|
||||||
|
|
||||||
## Conventions
|
|
||||||
|
|
||||||
- **Imports**: Absolute paths with `@/`, group by external/internal
|
|
||||||
- **Error Handling**: Return `{ success: false, error: string }` from actions
|
|
||||||
- **Currency**: Store as decimal strings (e.g., "123.45"), convert to cents for calculations
|
|
||||||
- **Periods**: Format as "YYYY-MM", use `parsePeriodParam()` for URL params
|
|
||||||
- **Notifications**: Send emails via `sendPagadorAutoEmails()` for payer updates
|
|
||||||
|
|
||||||
## External Integrations
|
|
||||||
|
|
||||||
- **Better Auth**: Config in `lib/auth/config.ts`, session handling
|
|
||||||
- **Drizzle**: Migrations in `drizzle/`, studio at `pnpm db:studio`
|
|
||||||
- **AI Features**: Use `@ai-sdk/*` for insights, configured in environment
|
|
||||||
- **Email**: Resend for notifications, configured via `RESEND_API_KEY`
|
|
||||||
|
|
||||||
## File Examples
|
|
||||||
|
|
||||||
- Schema: `db/schema.ts` (relations, indexes)
|
|
||||||
- Actions: `app/(dashboard)/lancamentos/actions.ts` (CRUD with validation)
|
|
||||||
- Components: `components/lancamentos/page/lancamentos-page.tsx` (client component)
|
|
||||||
- Utils: `lib/lancamentos/page-helpers.ts` (data transformation)
|
|
||||||
25
.github/workflows/docker-publish.yml
vendored
@@ -13,10 +13,35 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
DOCKER_IMAGE_NAME: openmonetis
|
DOCKER_IMAGE_NAME: openmonetis
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
quality:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10.33.0
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: pnpm run lint
|
||||||
|
|
||||||
build-and-push:
|
build-and-push:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
needs: quality
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
|||||||
3
.github/workflows/release.yml
vendored
@@ -5,6 +5,9 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
1
.gitignore
vendored
@@ -106,6 +106,7 @@ docker-compose.override.yml
|
|||||||
.cursor/
|
.cursor/
|
||||||
QWEN.md
|
QWEN.md
|
||||||
AGENTS.md
|
AGENTS.md
|
||||||
|
.codex
|
||||||
# === Backups locais ===
|
# === Backups locais ===
|
||||||
/backup/
|
/backup/
|
||||||
|
|
||||||
|
|||||||
3
.vscode/settings.json
vendored
@@ -12,7 +12,6 @@
|
|||||||
"**/.next": true,
|
"**/.next": true,
|
||||||
".next": true
|
".next": true
|
||||||
},
|
},
|
||||||
"explorerExclude.backup": {},
|
|
||||||
"editor.defaultFormatter": "biomejs.biome",
|
"editor.defaultFormatter": "biomejs.biome",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
@@ -25,9 +24,7 @@
|
|||||||
"[json]": {
|
"[json]": {
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
},
|
},
|
||||||
"eslint.enable": false,
|
|
||||||
"prettier.enable": false,
|
"prettier.enable": false,
|
||||||
"typescript.preferences.organizeImportsCollation": "ordinal",
|
|
||||||
"editor.fontSize": 15,
|
"editor.fontSize": 15,
|
||||||
"[jsonc]": {
|
"[jsonc]": {
|
||||||
"editor.defaultFormatter": "vscode.json-language-features"
|
"editor.defaultFormatter": "vscode.json-language-features"
|
||||||
|
|||||||
552
CHANGELOG.md
@@ -5,10 +5,504 @@ 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/).
|
||||||
|
|
||||||
## [Unreleased]
|
## [2.5.5] - 2026-05-06
|
||||||
|
|
||||||
|
Esta versão melhora a navegação por históricos e lançamentos. O changelog ganhou uma linha do tempo mais leve, colapsável e fácil de escanear; os filtros de lançamentos passam a aceitar múltiplas pessoas, categorias, formas de pagamento, condições e contas/cartões na mesma busca; e os diálogos adotam as animações compartilhadas do design system. Também há pequenos polimentos de texto e layout para deixar a interface mais consistente.
|
||||||
|
|
||||||
|
### Adicionado
|
||||||
|
- Lançamentos: filtros multi-seleção para condição, forma de pagamento, pessoa, categoria e conta/cartão, permitindo combinar vários valores no mesmo filtro (query string passa a aceitar múltiplos valores por chave).
|
||||||
|
- Changelog: parser passou a inferir o tipo de bump (major/minor/patch) a partir da numeração e a extrair o parágrafo de resumo abaixo do cabeçalho de versão; novo arquivo `src/features/settings/lib/changelog-types.ts` consolidando os tipos compartilhados.
|
||||||
|
- UI: dependência `tw-animate-css` para usar as mesmas animações utilitárias já presentes nos componentes shadcn/ui.
|
||||||
|
|
||||||
|
### Alterado
|
||||||
|
- Changelog: visual da página reformulado para linha do tempo com resumo sempre visível, detalhes colapsáveis por versão, agrupamento por mês e marcadores visuais por tipo de bump; componente migrado para `"use client"` com `Collapsible` e abertura via âncora (`#vX-Y-Z`).
|
||||||
|
- Lançamentos: botões "Nova Receita" e "Nova Despesa" agora usam os próprios triggers do `TransactionDialog` (via prop `createSlot`), reduzindo estado manual na página e eliminando o fluxo `setCreateOpen` + `transactionTypeForCreate`.
|
||||||
|
- Diálogos: animações customizadas em CSS (`@keyframes dialog-in/out` e `overlay-in/out`) substituídas pelas classes utilitárias compartilhadas em `Dialog`/`DialogOverlay` (`data-[state=open]:animate-in`, `zoom-in-95`, `fade-in-0`).
|
||||||
|
- BulkActionDialog: label do escopo "Todas as pessoas" passa a indicar a parcela atual (`Todas as pessoas desta parcela (N/Total)`) com descrição mais clara sobre o efeito da ação.
|
||||||
|
- Checkbox: `RiCheckLine`/`RiSubtractLine` agora herdam `text-current` para alinhar com a cor do indicator nativo.
|
||||||
|
- Landing page: remoção de fundos alternados (`bg-muted/40`) nas seções "Funcionalidades", "Stack" e "Para quem é" para uma leitura visual mais limpa.
|
||||||
|
- Navbar: aviso de atualização passa a usar o texto "Versão X disponível".
|
||||||
|
|
||||||
|
## [2.5.4] - 2026-05-06
|
||||||
|
|
||||||
|
Esta versão é uma faxina arquitetural de larga escala sem nenhuma mudança visível ao usuário. Removido código morto, padronizamos identificadores em inglês conforme a convenção do projeto, simplificamos o barrel de Server Actions e consolidamos os arquivos de helpers/queries soltos nas raízes das features dentro de pastas `lib/`. O resultado é uma estrutura previsível e consistente entre features (`actions.ts`, `queries.ts`, `actions/`, `components/`, `hooks/`, `lib/`) e um saldo líquido de −428 linhas de código com zero impacto em comportamento, performance ou banco de dados.
|
||||||
|
|
||||||
|
### Alterado
|
||||||
|
- Padronização da estrutura de `transactions/`: 14 helpers soltos na raiz movidos para `lib/`; barrel `actions.ts` reduzido de 76 linhas de wrappers redundantes para 14 linhas de re-exports puros; `anticipation-actions.ts` movido para `actions/anticipation.ts`.
|
||||||
|
- Reorganização de `dashboard/`: 8 helpers soltos consolidados em `dashboard/lib/`; orquestradores (`fetch-dashboard-data.ts`, `page-data-queries.ts`) permanecem na raiz como entry points.
|
||||||
|
- Reorganização de `reports/`: 5 query files na raiz consolidados em `reports/lib/`.
|
||||||
|
- Reorganização de `payers/`: god file `detail-actions.ts` (21KB) e `detail-queries.ts` movidos para `payers/lib/`.
|
||||||
|
- `shared/components/`: 9 dos 16 componentes soltos agrupados em 3 novas subpastas temáticas (`brand/`, `widgets/`, `feedback/`).
|
||||||
|
- `shared/lib/fetch-json.ts` movido para `shared/utils/fetch-json.ts` (categorização correta — utilitário genérico de transporte HTTP).
|
||||||
|
- Padronização EN dos identificadores remanescentes: 4 constantes globais (`LANCAMENTOS_*` → `TRANSACTIONS_*`), 12 tipos/interfaces (`Lancamento*`/`Pagador*`/`Estabelecimento*` → equivalentes em EN), 13 funções/components exportados (`fetchPagador*`, `EstabelecimentoInput`, `PagadorInfoCard`, etc.), 5 props cross-file (`preLancamentosCount` → `inboxPendingCount`, etc.).
|
||||||
|
- Server Actions de `insights/` simplificadas: barrel reduzido para re-exports puros.
|
||||||
|
- Mantidas intencionalmente em PT-BR conforme exceção do `CLAUDE.md`: variáveis locais (`pagador`, `categoria`, `lancamento`), accessor key `pagadorName` (persistida em preferências do usuário), strings de UI.
|
||||||
|
|
||||||
|
### Removido
|
||||||
|
- 14 funções/constantes mortas verificadas via `grep` em todo o repo: `validateCategoriaOwnership`, `getInstallmentAnticipationsAction`, `getAnticipationDetailsAction`, `formatDecimalForDb`, `currencyFormatterNoCents`, `optionalDecimalSchema`, `formatMonthLabel`, `getGoalProgressStatusColorClass`, `MONTH_PERIOD_PARAM`, `calculateRemainingInstallments`, e 5 funções `fetch*` não usadas em `inbox/queries.ts`.
|
||||||
|
- 1 tipo morto: `ImportRow` em `transactions/actions/import-action.ts`.
|
||||||
|
- 2 tipos órfãos consequentes: `InstallmentAnticipationWithRelations`, `GoalProgressStatus` (este último convertido em interno).
|
||||||
|
- ~30 `export` keywords desnecessários (símbolos usados apenas no próprio arquivo) — visibilidade reduzida sem mudar comportamento.
|
||||||
|
- Re-exports mortos em barrels: `EstablishmentLogoPicker` em `entity-avatar/index.ts`, `CategoryReportSkeleton` e `WidgetSkeleton` em `skeletons/index.ts`, `toNameKey` em `establishment-logo-queries.ts`.
|
||||||
|
- Arquivo `features/reports/types.ts` (barrel inteiro era órfão — todos os 5 tipos eram importados direto de `@/shared/lib/types/reports`).
|
||||||
|
|
||||||
|
## [2.5.3] - 2026-05-05
|
||||||
|
|
||||||
|
Esta versão foca em polimento do diálogo de detalhes do lançamento, refresh visual da linha do tempo de parcelas e limpeza terminológica em torno de contas/cartões inativos. O diálogo de detalhes ganhou logo da conta/cartão, ícone colorido por categoria e avatar do responsável; a barra de progresso de parcelas foi redesenhada num layout horizontal compacto; e o widget "Minhas Contas" do dashboard passou a ocultar automaticamente contas marcadas como inativas. Internamente, o termo "arquivadas" foi padronizado como "inativas" nas tabs de contas e cartões, surgiram constantes compartilhadas para formas de pagamento liquidáveis e um helper `isAccountInactive`, e o seed de mock data ganhou cobertura mais realista (novas pessoas, contas, cartões e assinaturas recorrentes).
|
||||||
|
|
||||||
|
### Adicionado
|
||||||
|
- Logo da conta/cartão, ícone colorido por categoria e avatar do responsável no diálogo de detalhes do lançamento.
|
||||||
|
- Constantes `SETTLEABLE_PAYMENT_METHODS` e `CREDIT_CARD_PAYMENT_METHOD` em `features/transactions/constants.ts`.
|
||||||
|
- Helper `isAccountInactive(status)` em `shared/lib/accounts/constants.ts`, reaproveitado em `account-card.tsx` e `my-accounts-widget.tsx`.
|
||||||
|
|
||||||
|
### Alterado
|
||||||
|
- Widget "Minhas Contas" do dashboard agora oculta contas inativas (filtra antes de aplicar a regra de "não consideradas") e ajusta o empty state quando o usuário só tem contas inativas.
|
||||||
|
- Linha do tempo de parcelas (`InstallmentTimeline`) redesenhada: layout horizontal com barra de progresso, datas de compra e quitação alinhadas nas pontas e contador "N restante(s)" / "Última parcela" abaixo.
|
||||||
|
- Diálogo de detalhes do lançamento: badge de status "Pendente" virou "Em aberto" com variante `info`, "Resumo" virou "Total" e ID do lançamento passou a exibir o UUID completo em fonte monoespaçada (sem truncar).
|
||||||
|
- Tabs em contas e cartões: "Arquivadas/Arquivados" renomeadas para "Inativas/Inativos".
|
||||||
|
- Legenda do calendário envolvida em `Card` para destacar visualmente do conteúdo da página.
|
||||||
|
- Páginas `cards`, `categories`, `inbox`, `notes`, `payers` perderam `items-start` no `<main>` (alinhamento natural à largura total); `calendar` ajustou gap de 3 para 4.
|
||||||
|
- Tabela de lançamentos: extraído IIFE de payment-method dos botões de liquidação com as novas constantes compartilhadas; bloco logo+label da coluna Conta/Cartão deduplicado via reuso de variável JSX; removido `capitalize` redundante do label "Venc.".
|
||||||
|
- Mock data renovado em `scripts/mock-data.ts`: novas pessoas (Mario), novas contas (Itaú Personnalité, Banco Inter), novo cartão Inter Black, e cobertura mais ampla de assinaturas recorrentes (Vivo, Sabesp, Disney+, HBO Max, Amazon Prime, OpenAI, Apple iCloud, Notion, YouTube Premium).
|
||||||
|
|
||||||
|
### Removido
|
||||||
|
- Comentário narrativo `{/* Opções de Antecipação */}` em `transactions-columns.tsx`.
|
||||||
|
- Helper local `shortTransactionId` em `transaction-details-dialog.tsx` (substituído pela exibição do UUID completo).
|
||||||
|
|
||||||
|
## [2.5.2] - 2026-05-04
|
||||||
|
|
||||||
|
Esta versão traz melhorias visuais e de usabilidade em contas, lançamentos, orçamentos, cartões e anotações: novos tipos de conta, ícones no seletor, feedback visual de limite excedido nas progress bars e refinamentos nos ícones de tarefas em anotações.
|
||||||
|
|
||||||
|
### Adicionado
|
||||||
|
- Novos tipos de conta `"Dinheiro"` e `"Outros"` na lista padrão do diálogo de contas (issue #50).
|
||||||
|
- Ícones por tipo de conta no seletor (Conta Corrente, Poupança, Carteira Digital, Investimento, Pré-Pago, Dinheiro, Outros).
|
||||||
|
- Filtro automático: ao selecionar `"Dinheiro"` como forma de pagamento em lançamentos, o select de conta exibe apenas contas do tipo `"Dinheiro"`.
|
||||||
|
- Sinal `+` no valor de transferências recebidas na tabela de lançamentos (mantém cor azul).
|
||||||
|
|
||||||
|
### Alterado
|
||||||
|
- Forma de pagamento de novas transferências entre contas alterada de `"Pix"` para `"Transferência bancária"`.
|
||||||
|
- Progress bar de orçamentos excedidos agora exibe indicador e fundo na cor `destructive`.
|
||||||
|
- Progress bar de cartões com 100% do limite utilizado agora exibe indicador e fundo na cor `destructive`.
|
||||||
|
- Ícone de tarefa não concluída no card e no modal de detalhes de anotações substituído por `RiSubtractLine` (locais sem interação de marcação).
|
||||||
|
|
||||||
|
## [2.5.1] - 2026-05-04
|
||||||
|
|
||||||
|
Versão de correção pontual focada na exibição do indicador de anexo nas tabelas de lançamentos da fatura do cartão. Em `/cards/[cardId]/invoice`, lançamentos com anexos não mostravam o ícone porque o fetcher dedicado da fatura não calculava o flag `hasAttachments`. A primeira tentativa de adicionar o EXISTS via `extras` na query relacional gerou SQL inválido (Drizzle re-aliasava `transactionAttachments.transactionId` para o alias da tabela externa). A correção definitiva troca o fetcher pela função compartilhada `fetchTransactionsWithRelations` de `features/transactions`, que já implementa o EXISTS corretamente via `select`.
|
||||||
|
|
||||||
|
### Corrigido
|
||||||
|
- Ícone de anexo voltou a aparecer na tabela de lançamentos da fatura do cartão (`/cards/[cardId]/invoice`). `fetchCardTransactions` em `features/invoices/queries.ts` agora delega para `fetchTransactionsWithRelations`, garantindo que o flag `hasAttachments` seja preenchido com a mesma EXISTS subquery usada no restante do app.
|
||||||
|
|
||||||
|
## [2.5.0] - 2026-05-01
|
||||||
|
|
||||||
|
Esta versão melhora o fechamento de faturas, a correção de lançamentos já registrados e a conferência de saldos contra o extrato do banco. O novo **ajuste de fatura** fecha a conta entre o total calculado pelo sistema e o valor real cobrado pelo banco, sem exigir que o usuário reabra lançamentos individuais. A mesma ideia foi estendida para **contas correntes**: na página do extrato, ao lado de "Saldo ao final do período", o usuário informa o saldo real e o sistema cria (ou atualiza) um lançamento de ajuste no período visualizado. Também entra o fluxo de **reembolso** para despesas à vista: pelo menu de ações do lançamento, o usuário informa a data do reembolso e o sistema cria uma receita espelhada no extrato ou na fatura correta. O widget de boletos do dashboard ganhou paridade com o widget de faturas — confirmação de pagamento agora pede conta de origem e data antes de quitar o boleto. Por fim, o **limite do cartão** passou a ser obrigatório e o sistema bloqueia despesas em cartão que ultrapassem o limite disponível, retornando uma mensagem com o valor exato disponível. As operações mantêm rastro no lançamento gerado e respeitam a proteção de faturas já pagas.
|
||||||
|
|
||||||
|
### Adicionado
|
||||||
|
- Nome do boleto no widget de Boletos agora é um link para `/transactions?q=<nome>`, incluindo `?periodo=<mes-ano>` automaticamente quando o período selecionado não é o atual. Ícone `RiExternalLinkLine` ao lado do nome, igual ao padrão do widget de Faturas.
|
||||||
|
- Botão "Ajustar fatura" ao lado do valor na página da fatura.
|
||||||
|
- Dialog `AdjustInvoiceDialog` com input de valor correto e preview da diferença.
|
||||||
|
- Action `adjustInvoiceAction` que faz upsert/delete idempotente do lançamento de ajuste.
|
||||||
|
- Botão "Ajustar saldo" ao lado do valor na página do extrato da conta.
|
||||||
|
- Dialog `AdjustBalanceDialog` com input do saldo correto e preview da diferença que será lançada (receita ou despesa).
|
||||||
|
- Action `adjustAccountBalanceAction` que faz upsert/delete idempotente do lançamento de ajuste por `(accountId, period)`.
|
||||||
|
- Opção "Reembolso" no dropdown de ações de despesas à vista, posicionada após "Copiar" e antes de "Remover".
|
||||||
|
- Dialog `RefundTransactionDialog` com seleção da data do reembolso e indicação do período de destino.
|
||||||
|
- Action `refundTransactionAction` que cria uma receita de reembolso vinculada ao lançamento original.
|
||||||
|
- Constantes compartilhadas `INVOICE_ADJUSTMENT_NAME`, `ACCOUNT_BALANCE_ADJUSTMENT_NAME`, `REFUND_NOTE_PREFIX` e `buildRefundNote()` em `shared/lib/accounts/constants.ts`.
|
||||||
|
- Validação de limite de cartão: `validateCardLimit()` em `transactions/actions/core.ts` calcula o uso atual do cartão (somando lançamentos não quitados, com a mesma regra usada em `cards/queries.ts` para recorrentes) e bloqueia criação ou edição de despesa em cartão que ultrapasse o disponível, retornando "Lançamento de R$ X excede o limite disponível do cartão (R$ Y)."
|
||||||
|
- Schema reutilizável `requiredDecimalSchema(fieldName)` em `shared/lib/schemas/common.ts` — número/string positiva (`> 0`) com mensagens parametrizáveis.
|
||||||
|
|
||||||
|
### Alterado
|
||||||
|
- **Limite do cartão é obrigatório**: campo `limite` em `cartoes` ganhou `NOT NULL DEFAULT 0` no schema, validação Zod com `requiredDecimalSchema("limite")`, atributo `required` no input do formulário e checagem client-side antes do submit. Tipos `Card.limit` e `Card.limitAvailable` deixam de ser nullable; branch "Ainda não há limite registrado" foi removido de `card-item.tsx` e a derivação defensiva em `cards/[cardId]/invoice` foi simplificada.
|
||||||
|
- Migration `0029_friendly_spitfire`: preenche com `0` registros legados antes do `SET NOT NULL` para não quebrar bancos com cartões sem limite.
|
||||||
|
- Métricas principais passam a tratar reembolsos como abatimento de despesa, não como receita comum.
|
||||||
|
- Cards de receitas/despesas, série histórica do dashboard e resumo do extrato agora preservam o efeito líquido do reembolso no balanço sem inflar entradas e saídas.
|
||||||
|
- Pagamento de fatura agora abre confirmação com conta de origem selecionável; por padrão vem a conta vinculada ao cartão, mas o usuário pode escolher outra conta antes de confirmar.
|
||||||
|
- Widget de faturas no dashboard ganhou a mesma confirmação: o modal "Confirmar pagamento" agora pede conta de origem e data antes de marcar a fatura como paga, alinhando o comportamento ao da página de fatura.
|
||||||
|
- Widget de boletos no dashboard ganhou a mesma paridade: o modal "Confirmar pagamento" passou a oferecer seleção de **conta de pagamento** e **data do pagamento**, com mesma estrutura de cards de detalhes, métricas, separator e formulário condicional do widget de faturas.
|
||||||
|
- `toggleTransactionSettlementAction` agora aceita `paymentAccountId` e `paymentDate` opcionais para boletos — quando informados, atualiza a `accountId` do lançamento e usa a data escolhida em `boletoPaymentDate` (em vez da data atual).
|
||||||
|
- `DashboardBill` passa a expor `accountId` para que o dialog inicialize a conta com o valor já vinculado ao boleto.
|
||||||
|
- Widget "Lançamentos por Categorias" agora ignora a categoria "Transferência interna" — transferências entre contas próprias deixam de poluir o ranking de categorias.
|
||||||
|
|
||||||
|
### Corrigido
|
||||||
|
- Erro de hidratação no widget de Anotações: `Intl.DateTimeFormat` sem `timeZone` usava o fuso do servidor (UTC) no SSR e o fuso do browser (BRT) no cliente, resultando em datas divergentes. Ambos os formatters passam a usar `timeZone: "America/Sao_Paulo"` explicitamente.
|
||||||
|
- Extrato da conta agora contabiliza transferências internas nos cards de **Entradas** e **Saídas**: transferência recebida soma em Entradas, transferência enviada soma em Saídas. Antes o saldo final refletia o movimento mas os cards permaneciam zerados, gerando inconsistência visível na tela (issue #47).
|
||||||
|
|
||||||
|
### Removido
|
||||||
|
- Seção "Veja o que você pode fazer" (galeria de screenshots com abas) da landing page, junto com o componente `ScreenshotTabs`, as 14 imagens `preview-*.webp`, o bloco `screenshots` em `images.ts`, o link `#telas` do nav e o export `pwaCompatList` sem uso.
|
||||||
|
- Exports mortos `dateFormatter` e `monthFormatter` de `features/transactions/formatting-helpers.ts`.
|
||||||
|
|
||||||
|
## [2.4.4] - 2026-04-27
|
||||||
|
|
||||||
|
Esta versão remove a dependência da extensão `pgcrypto` do PostgreSQL para a geração do `share_code` em pagadores. O default a nível de banco (`gen_random_bytes`) foi removido — agora a aplicação gera o código sempre via `crypto.randomBytes` do Node.js, num utilitário compartilhado. A consequência prática é que o setup inicial fica mais simples: não há mais script de habilitação de extensão, nem etapa extra no primeiro `db:push`, e bancos restaurados de dumps externos não precisam ter `pgcrypto` instalada. O script de backup também foi enxugado para gerar dumps focados nos schemas relevantes (`public` e `drizzle`), descartando os schemas internos do Supabase e eliminando os ~148 erros de restore em PostgreSQL padrão. Por fim, os logos da marca (ícone laranja e wordmark) foram vetorizados: as PNGs antigas foram substituídas por SVGs inline em componentes próprios e por arquivos `.svg` no `public/`, escalando perfeitamente em qualquer tamanho — inclusive nos PDFs exportados, que agora rasterizam o SVG em alta resolução.
|
||||||
|
|
||||||
|
### Alterado
|
||||||
|
|
||||||
|
- Schema: coluna `share_code` em `pagadores` perdeu o default `substr(encode(gen_random_bytes(24), 'base64'), 1, 24)` — campo continua `NOT NULL` e a aplicação passa a fornecer o valor explicitamente em todas as inserções
|
||||||
|
- Pagadores: nova função utilitária `generateShareCode()` em `src/shared/lib/payers/share-code.ts` (server-only) — usa `crypto.randomBytes(18).toString("base64url").slice(0, 24)`
|
||||||
|
- Pagadores: `createPayerAction`, `ensureDefaultPagadorForUser`, `resetUserAppData` (settings) e `mock-data.ts` agora chamam `generateShareCode()` ao inserir um pagador
|
||||||
|
- Backup: `scripts/backup.sh` agora dumpa apenas os schemas `public` e `drizzle` — schemas internos do Supabase (`auth`, `realtime`, `storage`, `vault`, `graphql`, `graphql_public`, `extensions`, `pgbouncer`) e suas extensions/roles deixam de poluir os dumps. Restaurações em PostgreSQL padrão passam a executar sem os ~148 erros de `role/extension does not exist`
|
||||||
|
- Logo: `Logo` foi quebrado em três arquivos — `src/shared/components/logo.tsx` (orquestrador), `logo-icon.tsx` (ícone laranja em SVG inline, viewBox `0 0 200 200`) e `logo-text.tsx` (wordmark em SVG inline, viewBox `0 0 574.201 89.6`). API pública (`variant`, `invertTextOnDark`, `colorIcon`, `iconClassName`, `textClassName`) preservada
|
||||||
|
- Assets: `public/images/logo_small.png` e `logo_text.png` substituídos por `logo_small.svg` e `logo_text.svg` (com `width`/`height` explícitos para compatibilidade com `<img>` em canvas)
|
||||||
|
- Exports: `loadExportLogoDataUrl` agora carrega SVG e rasteriza no canvas a 4× a resolução natural antes de gerar o data URL — mantém nitidez quando o PDF amplia a imagem
|
||||||
|
|
||||||
|
### Removido
|
||||||
|
|
||||||
|
- Pasta `scripts/postgres/` (continha `init.sql` e `enable-extensions.ts`)
|
||||||
|
- Script `pnpm db:extensions` no `package.json`
|
||||||
|
- Referências ao `pnpm db:extensions` no README
|
||||||
|
- `public/images/logo_small.png` e `public/images/logo_text.png` (substituídos pelos `.svg`)
|
||||||
|
|
||||||
|
### Corrigido
|
||||||
|
|
||||||
|
- Migrations: conflito de numeração resolvido — `0027_fancy_reaper` renomeado para `0028_fancy_reaper` (o número 0027 já estava ocupado pelo arquivo órfão `0027_glorious_mindworm`); journal e snapshot atualizados
|
||||||
|
- TS: removido `baseUrl` do `tsconfig.json` para evitar erro `TS5101` (deprecação no TS 7) — `moduleResolution: bundler` resolve os `paths` relativos ao próprio `tsconfig`, dispensando `baseUrl`
|
||||||
|
|
||||||
|
### Documentação
|
||||||
|
|
||||||
|
- README: seção Backup atualizada — arquivos gerados agora especificam que apenas os schemas `public` e `drizzle` são dumpados
|
||||||
|
- README: seção Restore reescrita com o fluxo correto para banco Docker (`DROP SCHEMA public CASCADE` + `pg_restore --clean --if-exists --disable-triggers`)
|
||||||
|
- README: comando rápido de Docker Compose de backup/restore substituído por `pnpm backup`
|
||||||
|
- README: header passa a apontar para `logo_small.svg`
|
||||||
|
|
||||||
|
## [2.4.3] - 2026-04-25
|
||||||
|
|
||||||
|
Esta versão amplia o trabalho com lançamentos divididos: anexos passam a ser visíveis para pessoas com acesso compartilhado, a importação para conta própria copia os arquivos de forma independente e a edição ganha a opção de aplicar a alteração nos dois lados do par. Três caminhos de deleção foram corrigidos para não deixar arquivos órfãos no storage. Também traz refresh visual nos badges de tipo e radio buttons, prefetch server-side de logos para reduzir chamadas de API no dashboard, e ajustes pontuais no healthcheck do container e em rótulos da UI.
|
||||||
|
|
||||||
|
### Adicionado
|
||||||
|
|
||||||
|
- Schema: coluna `split_group_id` (uuid, nullable) em `lancamentos` com índice `(user_id, split_group_id)` — liga as shares do mesmo evento de divisão
|
||||||
|
- Split: `buildLancamentoRecords` atribui um `splitGroupId` único por cycle (parcelado, recorrente ou único) para ambas as shares
|
||||||
|
- Split: edição cooperativa via `updateTransactionSplitPairAction` — ao editar um lançamento dividido, novo dialog `SplitPairDialog` permite escolher entre aplicar somente neste lado ou nos dois lados (nome, data, categoria e demais campos compartilhados; valor e payer permanecem por share)
|
||||||
|
- Importação: "Importar para Minha Conta" agora copia os anexos do lançamento-fonte para a conta de quem está importando (novo arquivo, novo `userId`, novo `fileKey` — cópia independente via S3 CopyObject). `createSchema` ganhou campo opcional `importFromTransactionId`; helper `copyAttachmentsForImport` valida acesso à fonte via ownership direto ou `payerShares`
|
||||||
|
- Importação: dialog "Importar para Minha Conta" exibe seção read-only "Anexos que serão copiados" listando os anexos do lançamento-fonte antes da confirmação
|
||||||
|
- Filtros: nova chave `isDivided` na tabela de lançamentos — toggle "Somente divididos" no drawer de filtros mantém o estado na URL
|
||||||
|
- Performance: prefetch server-side de mapeamentos Logo.dev no `/dashboard`, `/transactions` e `/payers/[payerId]` — uma única query SQL em batch (`fetchEstablishmentLogoMap`) semeia o cache do React Query antes do primeiro render, eliminando os N requests para `/api/logo/mapping`
|
||||||
|
|
||||||
|
### Alterado
|
||||||
|
|
||||||
|
- Anexos: `fetchTransactionAttachments` e `fetchTransactionAttachmentsAction` passam a autorizar leitura por acesso à transação (direto ou via `payerShares`), permitindo que pessoas com pagador compartilhado visualizem anexos de lançamentos divididos
|
||||||
|
- Anexos: upload (`confirmAttachmentUploadAction`) e detach em massa (`detachAttachmentBulkAction`) agora expandem `transactionIds` para incluir shares irmãs via `splitGroupId` — o vínculo em `transaction_attachments` é replicado para manter simetria
|
||||||
|
- Anexos: delete/detach continuam restritos ao criador (sem alteração de escrita); dashboard (`fetchAttachmentsForPeriod`) permanece listando apenas os anexos do próprio usuário
|
||||||
|
- Migração: lançamentos divididos criados antes desta versão ficam com `split_group_id` NULL e mantêm o comportamento antigo (anexos não visíveis para a contraparte); apenas splits novos são afetados
|
||||||
|
- Storage: `deleteS3Object` passa a ignorar `NoSuchKey` silenciosamente — providers S3-compatíveis (ex.: Cloudflare R2) lançam esse erro ao deletar objeto inexistente, ao contrário do comportamento idempotente do S3 padrão
|
||||||
|
- UI/Badges: `TransactionTypeBadge` redesenhado — substitui o `StatusDot` por ícones direcionais (`RiArrowRightDownLine` receita, `RiArrowRightUpLine` despesa, `RiArrowLeftRightLine` transferência), com borda visível, shadow sutil e variantes dark mode dessaturadas; rótulo "Transferência" abreviado para "Transf."
|
||||||
|
- UI/Forms: indicador do `RadioGroup` trocado de círculo (`RiCircleLine`) por check (`RiCheckLine`) com fundo sólido `primary` no estado selecionado
|
||||||
|
- UI/Antecipação: tabela de seleção de parcelas reduzida de quatro para três colunas (estabelecimento + fatura + valor) — informações de parcela e vencimento absorvidas pela coluna do estabelecimento
|
||||||
|
- Tipografia: fonte Inter agora carrega explicitamente os pesos 500, 600 e 700 (antes derivava de 400)
|
||||||
|
- Deps: better-auth 1.6.5 → 1.6.9, @aws-sdk/client-s3 3.1032 → 3.1037, @tanstack/react-query 5.99.2 → 5.100.3, @biomejs/biome 2.4.12 → 2.4.13, tailwindcss 4.2.2 → 4.2.4, resend 6.12.0 → 6.12.2
|
||||||
|
|
||||||
|
### Corrigido
|
||||||
|
|
||||||
|
- Anexos: deleção em massa por série (`deleteTransactionBulkAction`) não chamava cleanup de storage — arquivos ficavam órfãos no S3 após apagar "este e futuros" ou "todos" de uma série parcelada/recorrente com anexo
|
||||||
|
- Anexos: deleção múltipla por seleção (`deleteMultipleTransactionsAction`) não chamava cleanup de storage — mesmo problema ao selecionar vários lançamentos com anexo e deletar em lote
|
||||||
|
- Anexos: reset de conta em Ajustes (`resetUserAppData`) não limpava o storage — todos os arquivos do usuário ficavam órfãos no S3 após a operação de zeragem
|
||||||
|
- Página da pessoa (`/payers/[payerId]`): `fetchPagadorLancamentos` agora calcula `hasAttachments` via `EXISTS`, fazendo o ícone de clipe aparecer na tabela de lançamentos (antes só aparecia em `/transactions`)
|
||||||
|
- Categorias: mensagem de sucesso ao atualizar exibia "Category atualizada com sucesso." — corrigido para "Categoria atualizada com sucesso."
|
||||||
|
- Antecipação: rótulos "Category" e "Período" no dialog corrigidos para "Categoria" e "Fatura"
|
||||||
|
- Docker: healthcheck do container `app` agora usa `127.0.0.1:3000` em vez de `localhost:3000`, evitando connection timeout em hosts com IPv6 (resolvendo [#44](https://github.com/felipegcoutinho/openmonetis/issues/44))
|
||||||
|
|
||||||
|
## [2.4.2] - 2026-04-20
|
||||||
|
|
||||||
|
Esta versão é quase toda sobre organização e polimento. O código interno do Dashboard foi reestruturado — módulos espalhados pela raiz da feature foram agrupados em subdiretórios coesos e a arquitetura de widgets foi renovada com um novo `widget-registry`. A sidebar lateral foi aposentada em favor de uma navegação concentrada na navbar. A interface passou por um refinamento visual amplo: cards redesenhados, dark mode mais consistente e efeitos decorativos removidos para uma composição mais limpa. As imagens de preview da landing page foram atualizadas. Por fim, a integração com Logo.dev ganhou uma arquitetura mais segura — o token agora é lido apenas no servidor e nunca chega ao cliente. O conceito de "Pagador" foi renomeado para "Pessoa" em toda a interface.
|
||||||
|
|
||||||
|
### Adicionado
|
||||||
|
|
||||||
|
- Dashboard: nova arquitetura de widgets com `widget-registry` — módulos reorganizados em subdiretórios (`bills/`, `invoices/`, `notes/`, `notifications/`, `overview/`, `payments/`, `goals-progress/`, `categories/`)
|
||||||
|
- Dashboard: novos componentes `category-breakdown-chart`, `category-breakdown-list`, `goals-progress-item` e `percentage-change-indicator`
|
||||||
|
- Logo.dev: `server.ts` com `isLogoDevEnabled()` e `buildLogoDevUrl()` server-side; `LogoDevProvider` propaga flag `enabled` para Client Components
|
||||||
|
- Scripts: `mockup` adicionado ao `package.json` (`tsx scripts/mock-data.ts`)
|
||||||
|
|
||||||
|
### Alterado
|
||||||
|
|
||||||
|
- Nav: sidebar lateral removida — navegação unificada na navbar
|
||||||
|
- UI/Tema: raio de borda global 0.625rem → 0.7rem; ajustes finos em `--card` e `--border` (light e dark)
|
||||||
|
- UI: `DotPattern` removido do layout dashboard, tela de autenticação e landing page
|
||||||
|
- UI: account-card redesenhado com cores de saldo (success/destructive) e tooltip para flags de exclusão
|
||||||
|
- UI: budget-card, card-item e componentes do calendário (day-cell, event-modal) com layout revisado
|
||||||
|
- UI: auth-card-shell simplificado (removido glassmorphism e blob animado)
|
||||||
|
- Landing: imagens de preview atualizadas; `mainFeatures` + `extraFeatures` unificados em grid único; dark mode nos botões de CTA
|
||||||
|
- Navbar: dark mode corrigido no navbar-shell (`dark:bg-card`, `dark:border-b-border`)
|
||||||
|
- Logo.dev: `NEXT_PUBLIC_LOGO_DEV_TOKEN` renomeado para `LOGO_DEV_TOKEN` (agora lido em runtime server-side apenas)
|
||||||
|
- UI: conceito "Pagador/Pagadores" renomeado para **"Pessoa/Pessoas"** em toda a interface — labels, títulos, toasts, mensagens de erro, cabeçalhos de tabela e exportações. Código, rotas (`/payers`) e schema do banco (`pagadores`) permanecem inalterados; a divergência entre UI e código é intencional
|
||||||
|
- Deps: next 16.2.3 → 16.2.4, better-auth 1.6.2 → 1.6.5, ai 6.0.159 → 6.0.168 e outros patches menores
|
||||||
|
- Notas/Tarefas: ícone de tarefa concluída em visualização (card e detalhes) simplificado para `RiCheckLine` verde sem caixa; checkbox no modal de edição usa fundo e borda `success` com ícone `success-foreground` (claro no light, escuro no dark)
|
||||||
|
- Notas/Detalhes: botões do footer reordenados ("Cancelar" à esquerda, "Alterar" primário à direita)
|
||||||
|
|
||||||
|
### Removido
|
||||||
|
|
||||||
|
- Nav: componentes sidebar (`app-sidebar`, `nav-main`, `nav-secondary`, `nav-user`, `nav-link`), `sidebar.tsx` e `use-mobile.ts`
|
||||||
|
- Dashboard: ~25 widgets monolíticos obsoletos (`inbox-widget`, `bills-widget`, `notes-widget`, `payers-widget`, `my-accounts-widget` etc.)
|
||||||
|
- Dashboard: arquivos dispersos na raiz da feature movidos para subdiretórios (arquivos antigos removidos)
|
||||||
|
- CSS: variáveis `--data-7` a `--data-10` removidas do tema
|
||||||
|
- CI: build arg `NEXT_PUBLIC_LOGO_DEV_TOKEN` removido do `Dockerfile` e do workflow `docker-publish.yml` — basta configurar `LOGO_DEV_TOKEN` e `LOGO_DEV_SECRET_KEY` como variáveis de runtime no host (Coolify, Railway, etc.)
|
||||||
|
|
||||||
|
## [2.4.1] - 2026-04-16
|
||||||
|
|
||||||
|
Versão pequena com refresh visual nas telas de autenticação (efeito blob com três círculos coloridos em movimento e card com glassmorphism), capitalização dos labels da navbar para melhor legibilidade e otimização do banco com 17 índices novos em foreign keys — evitando sequential scans em deletes em tabelas grandes como `lancamentos`. Corrigida regressão no `postgres:18-alpine` que recusava iniciar em instalações existentes; adicionada variável `PGDATA` no compose para preservar dados de quem já tinha o volume populado.
|
||||||
|
|
||||||
|
### Adicionado
|
||||||
|
|
||||||
|
- UI/Auth: layout animado nas páginas de login e signup com efeito blob (3 círculos coloridos em movimento) e card com glassmorphism; layout compartilhado extraído para `app/(auth)/layout.tsx` eliminando duplicação (PR #42)
|
||||||
|
- DB: 17 índices em foreign keys — evita sequential scans em deletes nas tabelas pai. Impacto maior nas FKs de `lancamentos` (conta_id, categoria_id, antecipacao_id), onde deletes em `categorias` antes provocavam full scan na tabela de lançamentos
|
||||||
|
|
||||||
|
### Alterado
|
||||||
|
|
||||||
|
- UI/Navbar: labels capitalizados (Lançamentos, Categorias, Contas) em vez de caixa baixa — melhora legibilidade (PR #42)
|
||||||
|
|
||||||
|
### Removido
|
||||||
|
|
||||||
|
- DB: 7 índices sem uso — `tokens_api_user_id_idx`, `cartoes_user_id_status_idx`, `contas_user_id_status_idx`, `pagadores_user_id_status_idx`, `pagadores_user_id_role_idx`, `dashboard_notification_states_user_id_archived_idx`, `antecipacoes_parcelas_series_id_idx` (0 scans em 187 dias de estatísticas)
|
||||||
|
- UI/Settings: tab de Integrações órfã removida (não tinha `TabsContent` correspondente)
|
||||||
|
|
||||||
|
### Corrigido
|
||||||
|
|
||||||
|
- Docker: container do PostgreSQL falhava ao iniciar em instalações existentes após atualização da imagem `postgres:18-alpine` — entrypoint passou a recusar dados no caminho legado `/var/lib/postgresql/data`. Adicionada variável `PGDATA` no `docker-compose.yml` para fixar o caminho e preservar dados de quem já tinha o volume populado (resolve #41)
|
||||||
|
|
||||||
|
## [2.4.0] - 2026-04-13
|
||||||
|
|
||||||
|
Esta versão integra o serviço Logo.dev para exibir automaticamente logos de marcas na coluna de estabelecimentos dos lançamentos, com picker manual para fixar o domínio quando a sugestão automática não acerta. As consultas vão por novas rotas de API (`/api/logo/search` e `/api/logo/mapping`) que servem como proxy seguro — a secret key fica server-side. Inclui também tabela própria `establishment_logos` com PK composta `(user_id, name_key)` para persistir as preferências por usuário.
|
||||||
|
|
||||||
|
### Adicionado
|
||||||
|
|
||||||
|
- Estabelecimentos: integração com Logo.dev — logos automáticos de marcas exibidos na coluna de estabelecimentos nos lançamentos
|
||||||
|
- Estabelecimentos: picker de logo por estabelecimento — clique no avatar para buscar e fixar um domínio Logo.dev específico (salvo por usuário no banco)
|
||||||
|
- API: rotas `/api/logo/search` e `/api/logo/mapping` — proxy seguro para Logo.dev Brand Search API (secret key server-side) e consulta de mapeamentos salvos
|
||||||
|
- Schema: tabela `establishment_logos` com PK composta `(user_id, name_key)` para persistir preferências de logo por usuário
|
||||||
|
|
||||||
|
### Corrigido
|
||||||
|
|
||||||
|
- Dev: `.env.example` usava host `db` no `DATABASE_URL`, causando erro `EAI_AGAIN` ao rodar `pnpm dev` localmente — corrigido para `localhost`
|
||||||
|
|
||||||
|
### Documentação
|
||||||
|
|
||||||
|
- README: tabela comparativa entre Perfil 1 (Usar) e Perfil 2 (Desenvolver) com diferenças de setup, `DATABASE_URL` e instruções de atualização
|
||||||
|
- README: seção "Variáveis de Ambiente" esclarecida — distingue contexto Docker (Perfil 1) de desenvolvimento local (Perfil 2)
|
||||||
|
- Logo.dev: crie uma conta em logo.dev para obter as chaves `NEXT_PUBLIC_LOGO_DEV_TOKEN` e `LOGO_DEV_SECRET_KEY` — plano gratuito inclui 500.000 requisições/mês
|
||||||
|
|
||||||
|
## [2.3.8] - 2026-04-12
|
||||||
|
|
||||||
|
Refatoração do `docker-compose.yml` para virar standalone — agora basta um `curl` + `docker compose up -d`, sem dependências de arquivos externos ou profiles complexos. README reescrito em dois perfis claros (Usar com Docker e Desenvolver com hot-reload) e scripts npm reduzidos de 10 para 5.
|
||||||
|
|
||||||
|
### Alterado
|
||||||
|
|
||||||
|
- Docker: `docker-compose.yml` refatorado — removidos profiles, build e dependência de arquivo externo; compose agora é standalone (basta `curl` + `docker compose up -d`)
|
||||||
|
- Docker: `docker-entrypoint.sh` simplificado — extensão `pgcrypto` criada via Node.js antes das migrations; loop de retry reescrito; removido hack `@localhost → @db`
|
||||||
|
- Docker: scripts reduzidos de 10 para 5 — `docker:up`, `docker:db`, `docker:down`, `docker:logs`, `docker:update`
|
||||||
|
- Docs: README reestruturado em dois perfis claros — **Usar** (só Docker) e **Desenvolver** (hot-reload)
|
||||||
|
|
||||||
|
## [2.3.7] - 2026-04-11
|
||||||
|
|
||||||
|
Esta versão amplia significativamente o dashboard com três novos widgets configuráveis (Anexos, Inbox, Tendências de Categoria), adiciona filtros úteis na tabela de lançamentos (por status de pagamento e por presença de anexo) e moderniza a tipografia substituindo a fonte local por Inter (Google Fonts, self-hosted pelo Next.js) — eliminando arquivos `.woff2` do repositório. Pesos tipográficos foram padronizados para `font-semibold` em títulos, rótulos e valores monetários, e o card de grupo de parcelas foi redesenhado expandindo num dialog de detalhes com parcelas pagas/pendentes separadas. No backend, a CSP foi expandida para permitir preview de anexos PDF via S3, e o setup ganhou script `install-deps.sh` pra preparar servidores Ubuntu 24.04 limpos.
|
||||||
|
|
||||||
|
### Adicionado
|
||||||
|
|
||||||
|
- Dashboard: novos widgets configuráveis — Anexos (resumo de arquivos do período), Inbox (snapshot de pré-lançamentos pendentes) e Tendências de Categoria
|
||||||
|
- Lançamentos: filtro por status de pagamento (somente pagos / somente não pagos) e filtro por presença de anexo
|
||||||
|
- Lançamentos: indicador visual no status de liquidação para lançamentos de cartão de crédito com fatura paga — exibe ícone verde com tooltip explicativo
|
||||||
|
- Scripts: `scripts/install-deps.sh` — script de preparação para servidores Ubuntu 24.04 limpos (instala Docker, Node.js 22, pnpm via Homebrew)
|
||||||
|
- Docker: variáveis `PUBLIC_DOMAIN`, `UMAMI_URL`, `UMAMI_WEBSITE_ID` e `UMAMI_DOMAINS` passadas ao container da aplicação no `docker-compose.yml`
|
||||||
|
|
||||||
|
### Alterado
|
||||||
|
|
||||||
|
- Fonte: substituída fonte local `America` por `Inter` (Google Fonts, self-hosted pelo Next.js) — elimina arquivos `.woff2` do repositório
|
||||||
|
- Tipografia: peso tipográfico padronizado de `font-medium` para `font-semibold` em títulos, rótulos e valores monetários em toda a interface
|
||||||
|
- Parcelas: redesenho do card de grupo de parcelas — expandindo para dialog de detalhes com parcelas pagas/pendentes separadas
|
||||||
|
- Inbox: redesenho do card de pré-lançamento — logo maior, hierarquia tipográfica melhorada
|
||||||
|
- Lançamentos: filtros de tipo, condição e forma de pagamento agora usam slugs em URL (ex: `receita` em vez do valor literal com acentos)
|
||||||
|
- Estabelecimento: popover de autocomplete agora respeita a largura do input ao abrir
|
||||||
|
- CSP: adicionado `frame-src` para permitir preview de anexos PDF via S3
|
||||||
|
|
||||||
|
### Corrigido
|
||||||
|
|
||||||
|
- Docker: corrigido crash loop no container com mensagem `exec /app/docker-entrypoint.sh: no such file or directory` causado por CRLF no `docker-entrypoint.sh` em ambientes Windows/WSL2 — adicionado `sed -i 's/\r$//'` no Dockerfile e `.gitattributes` com `eol=lf` para scripts shell
|
||||||
|
- S3: corrigido `Error: Region is missing` ao usar o app sem S3 configurado — `S3_REGION` vazio (string vazia) não era tratado pelo operador `??`; substituído por `||` em todo o `s3-client.ts`
|
||||||
|
- i18n: corrigidas mensagens de erro que exibiam "Payer" em inglês em vez de "Pagador"
|
||||||
|
- Logos: corrigido modal seletor de logos de cartões e contas para renderizar miniaturas sem avisos de proporção
|
||||||
|
- Scripts: `install-deps.sh` — spinner travava o script por `wait` retornar código não-zero com `set -e` ativo; corrigido com `|| true`
|
||||||
|
- Scripts: `install-deps.sh` — prompt interativo do corepack suprimido com `COREPACK_ENABLE_DOWNLOAD_PROMPT=0`
|
||||||
|
- Scripts: `install-deps.sh` — PATH do Homebrew não estava configurado na seção de resumo
|
||||||
|
|
||||||
|
### Removido
|
||||||
|
|
||||||
|
- Scripts: removidos arquivos órfãos `scripts/dev.ts` e `scripts/setup-env.sh` (substituídos pelo `setup.mjs`)
|
||||||
|
- Docker: `docker-compose.yml` agora funciona sem arquivo `.env` — `DATABASE_URL` tem valor padrão com credenciais de desenvolvimento
|
||||||
|
- Docker: `docker-entrypoint.sh` converte automaticamente `@localhost:` para `@db:` na `DATABASE_URL` ao iniciar o container, eliminando a necessidade de usar hosts diferentes no `.env` para desenvolvimento local e Docker
|
||||||
|
|
||||||
|
## [2.3.6] - 2026-04-09
|
||||||
|
|
||||||
|
Correção pontual no Docker — adicionado `NODE_PATH=/app/migrate/node_modules` no entrypoint para o `drizzle-kit` resolver corretamente o `drizzle-orm` ao executar as migrations no container.
|
||||||
|
|
||||||
|
### Corrigido
|
||||||
|
|
||||||
|
- Docker: adicionado `NODE_PATH=/app/migrate/node_modules` no entrypoint para que o `drizzle-kit` consiga resolver `drizzle-orm` ao executar as migrations no container
|
||||||
|
|
||||||
|
## [2.3.5] - 2026-04-07
|
||||||
|
|
||||||
|
Correção crítica na CSP: regra movida do `next.config.ts` (build time) para `proxy.ts` (runtime), desbloqueando uploads de anexos quando o `S3_ENDPOINT` ainda não estava disponível durante o build da imagem Docker.
|
||||||
|
|
||||||
|
### Corrigido
|
||||||
|
|
||||||
|
- CSP: movido `Content-Security-Policy` do `next.config.ts` (build time) para `proxy.ts` (runtime), corrigindo bloqueio de upload de anexos quando `S3_ENDPOINT` não estava disponível durante o build do Docker
|
||||||
|
|
||||||
|
## [2.3.4] - 2026-04-05
|
||||||
|
|
||||||
|
Correção pontual no upload de anexos — a CSP `connect-src` bloqueava o fetch para o storage, gerando `NetworkError` na hora de subir o arquivo.
|
||||||
|
|
||||||
|
### Corrigido
|
||||||
|
|
||||||
|
- Anexos: corrigido upload que falhava com `NetworkError` — CSP `connect-src` bloqueava fetch para o Storage
|
||||||
|
|
||||||
|
## [2.3.3] - 2026-04-05
|
||||||
|
|
||||||
|
Correção do fluxo de tokens da API: `/api/auth/device/verify` voltou a aceitar tokens criados pela tela de Settings (revertido de JWT para hash lookup). O prefixo dos tokens também foi renomeado de `os_` para `opm_` (OpenMonetis) e rotas JWT não utilizadas foram removidas — usuários precisam recriar os tokens existentes.
|
||||||
|
|
||||||
|
### Corrigido
|
||||||
|
|
||||||
|
- Tokens: corrigido `/api/auth/device/verify` que rejeitava tokens criados via Settings (revertido de JWT para hash lookup)
|
||||||
|
|
||||||
|
### Alterado
|
||||||
|
|
||||||
|
- Tokens: prefixo renomeado de `os_` para `opm_` (OpenMonetis); tokens existentes precisam ser recriados
|
||||||
|
- Tokens: removidas rotas JWT não utilizadas (`/api/auth/device/token` e `/api/auth/device/refresh`)
|
||||||
|
- Tokens: `api-token.ts` simplificado para conter apenas `hashToken` e `extractBearerToken`
|
||||||
|
|
||||||
|
## [2.3.2] - 2026-04-04
|
||||||
|
|
||||||
|
Esta versão concentra hardening de segurança. Tokens da API ganharam expiração obrigatória de 1 ano (sem mais tokens eternos) e o refresh foi corrigido para validar JWT por assinatura. A CSP foi expandida com `default-src`, `script-src`, `style-src`, `img-src`, `font-src` e `connect-src` (no lugar de uma regra única ampla), e foi adicionada mitigação para CVE-2024-44294 desabilitando parsing de fórmulas em `xlsx`. Inclui ainda novos headers (`Referrer-Policy`, `X-Permitted-Cross-Domain-Policies`), respostas `401 JSON` em vez de redirect 302 em rotas autenticadas, `security.txt` (RFC 9116) e correção de URL com protocolo duplicado no sitemap.
|
||||||
|
|
||||||
|
### Segurança
|
||||||
|
|
||||||
|
- Tokens: removido aceite de tokens sem expiração (`expiresAt NULL`); tokens criados via settings agora expiram em 1 ano
|
||||||
|
- Tokens: corrigido refresh que sobrescrevia hash e invalidava access token anterior; verify agora valida JWT por assinatura
|
||||||
|
- xlsx: desabilitado parsing de fórmulas (`cellFormula: false`) para mitigar CVE-2024-44294
|
||||||
|
- CSP: expandida Content-Security-Policy com `default-src`, `script-src`, `style-src`, `img-src`, `font-src` e `connect-src`
|
||||||
|
- Headers: adicionados `Referrer-Policy` e `X-Permitted-Cross-Domain-Policies`
|
||||||
|
- API: rotas autenticadas agora retornam `401 JSON` em vez de redirect `302` para clientes não autenticados
|
||||||
|
- Health: removido campo `version` da resposta do `/api/health`
|
||||||
|
- robots.txt: simplificado para não expor mapa de rotas internas
|
||||||
|
- Sitemap: corrigida URL com protocolo duplicado (`https://https://`)
|
||||||
|
- Criado `security.txt` (RFC 9116)
|
||||||
|
|
||||||
|
## [2.3.1] - 2026-04-03
|
||||||
|
|
||||||
|
Correção pontual de infraestrutura — dependências do `drizzle-kit` passaram a ser instaladas em `/app/migrate/` separadamente do `node_modules` do build standalone, corrigindo o erro `Cannot find module 'next'` no startup do container.
|
||||||
|
|
||||||
|
### Corrigido
|
||||||
|
|
||||||
|
- Infraestrutura: deps do drizzle-kit agora são instaladas em `/app/migrate/` separado do `node_modules` do standalone, corrigindo erro `Cannot find module 'next'` no startup do container
|
||||||
|
|
||||||
|
## [2.3.0] - 2026-04-03
|
||||||
|
|
||||||
|
Esta versão introduz `@tanstack/react-query` no projeto, padronizando cache, deduplicação e invalidação de leituras client-side. Várias features (anexos, insights, antecipação de parcelas) passaram a usar React Query no lugar de `useEffect` manual sobre rotas GET dedicadas. O dashboard ganhou ajuda contextual em cada métrica e configuração persistida pra ocultar contas marcadas como não consideradas no saldo total; o menu do usuário na navbar passou a avisar quando há release nova publicada no GitHub; e o Docker passou a rodar migrations automaticamente no startup via `docker-entrypoint.sh`. Internamente, o `knip` foi adicionado pra auditar arquivos/exports/tipos sem uso, várias rotas e actions ganharam validações extras (filtros por `userId` em joins, rate limits explícitos no Better Auth, headers `Cache-Control: private, no-store` em rotas privadas) e o projeto foi atualizado para Next.js 16.2.2 e Biome 2.4.10.
|
||||||
|
|
||||||
|
### Adicionado
|
||||||
|
|
||||||
|
- Dependências: adiciona `@tanstack/react-query` e um provider global para padronizar cache, deduplicação e invalidação de leituras client-side
|
||||||
|
- Dashboard: widget "Minhas Contas" ganha preferência persistida para mostrar ou ocultar contas marcadas como não consideradas no saldo total
|
||||||
|
- Dashboard: cards de métricas ganham botão de ajuda com explicação do cálculo exibido no app
|
||||||
|
- Versionamento: menu do usuário na navbar passa a avisar quando existe release mais recente publicada no GitHub
|
||||||
|
- Qualidade: adiciona `knip` ao projeto com o script `pnpm run lint:deadcode` para auditar arquivos, exports e tipos sem uso
|
||||||
|
- Infraestrutura: imagem Docker passa a rodar migrations automaticamente via `docker-entrypoint.sh` antes de iniciar a aplicação
|
||||||
|
|
||||||
|
### Alterado
|
||||||
|
|
||||||
|
- Anexos: listagem no modal de edição/detalhes, URLs temporárias da galeria e preview deixam de depender de `useEffect` para data fetching direto no componente e passam a usar React Query sobre rotas GET dedicadas
|
||||||
|
- Insights: carregamento de análises salvas passa a usar React Query com cache por período, mantendo estado draft local apenas para análises recém-geradas ou removidas
|
||||||
|
- Parcelamentos: histórico de antecipações no diálogo passa a usar React Query com invalidação automática após cancelamento
|
||||||
|
- Dashboard, insights e relatórios passam a excluir movimentações de contas marcadas como não consideradas no saldo total; balanço e previsto também passam a considerar ajustes de transferências entre contas consideradas e não consideradas
|
||||||
|
- UX: boletos e faturas passam a exibir labels relativas como "vence hoje", "vence amanhã" e "pago ontem", com tooltip para a data completa
|
||||||
|
- Lançamentos: diálogo foi reorganizado em blocos mais claros; a criação passa a aceitar múltiplos anexos e a edição em lote preserva `purchaseDate` e `period` ao propagar alterações por série
|
||||||
|
- Inbox e tabela de lançamentos foram componentizados em partes menores, mantendo paginação e ações em lote mais simples de evoluir
|
||||||
|
- Infraestrutura: workflow de publish ganha etapa obrigatória de qualidade; `docker-compose` passa a suportar perfil local ou banco remoto; build fixa `pnpm@10.33.0`; projeto atualizado para `Next.js 16.2.2`, `Biome 2.4.10` e dependências correlatas
|
||||||
|
- Qualidade: `knip` ganha configuração inicial para reduzir falsos positivos, ignorando `src/shared/components/ui/**`, o worker público de PDF, `setup.mjs` e o falso positivo de `postcss`
|
||||||
|
|
||||||
|
### Corrigido
|
||||||
|
|
||||||
|
- Segurança: criação de antecipações agora valida se `payerId` e `categoryId` informados pertencem ao usuário autenticado antes de persistir referências cruzadas
|
||||||
|
- Segurança: histórico de antecipações endurece os joins de `transactions`, `payers` e `categories` com filtro por `userId`, evitando exposição de nomes relacionados caso exista referência inconsistente no banco
|
||||||
|
- Segurança: domínio público deixa de responder rotas `/api/*`, e o Better Auth passa a aplicar rate limits explícitos para login e cadastro por e-mail
|
||||||
|
- APIs privadas: rotas de anexos, insights salvos, histórico de antecipações e presign de download passam a responder com `Cache-Control: private, no-store`; a rota de antecipações também deixa de devolver mensagens internas de erro ao cliente
|
||||||
|
- Build: rotas web de tokens do Companion passam a ser explicitamente dinâmicas, removendo o warning de prerender no `next build`
|
||||||
|
- Lançamentos: edição em série de compras parceladas volta a persistir `purchaseDate` e `period`, permitindo mover parcelas para a fatura ou competência correta conforme o escopo escolhido
|
||||||
|
- Lançamentos: edições que tentam mover compras de cartão para faturas já pagas agora são bloqueadas com mensagem clara também no fluxo de atualização e propagação em lote
|
||||||
|
- Imagens: logos institucionais, avatares padrão e componentes com `next/image` em modo `fill` passam a usar containers fixos com `sizes`, removendo avisos de proporção e performance
|
||||||
|
- Gráficos: `ChartContainer` passa a definir `initialDimension` no `ResponsiveContainer` do Recharts, evitando avisos `width(-1)` e `height(-1)` durante a medição inicial em widgets e relatórios
|
||||||
|
|
||||||
|
## [2.2.1] - 2026-04-01
|
||||||
|
|
||||||
|
Correção pontual no build da imagem Docker — removido `chown -R /app` do stage final (que travava o build/push da GitHub Action por lentidão excessiva); permissões agora definidas via `COPY --chown` direto.
|
||||||
|
|
||||||
|
### Corrigido
|
||||||
|
|
||||||
|
- Docker: imagem de produção deixa de executar `chown -R /app` no stage final; as permissões passam a ser definidas nos `COPY --chown`, reduzindo o risco de travamento e lentidão excessiva no build/push da GitHub Action
|
||||||
|
|
||||||
|
## [2.2.0] - 2026-04-01
|
||||||
|
|
||||||
|
Esta versão entrega uma nova página dedicada de galeria de anexos em `/attachments` com miniaturas, visualização inline (incluindo PDF via `pdfjs-dist`), download direto e acesso a partir do lançamento. As páginas de login e cadastro foram redesenhadas com sidebar mockup de faturas, três blocos de funcionalidade e gradiente decorativo. O dashboard passou a notificar boletos e faturas com vencimento dentro de 5 dias, e o cache do dashboard migrou de `unstable_cache` para a diretiva `use cache` (com `cacheTag` e `cacheLife`), com `cacheComponents: true` no `next.config.ts` e `connection()` em todas as páginas para forçar render dinâmico. A tipografia ganhou peso 500 (Medium) padronizado em títulos, valores e rótulos.
|
||||||
|
|
||||||
|
### Adicionado
|
||||||
|
|
||||||
|
- Anexos: nova página de galeria em `/attachments` com miniaturas, visualização inline de imagem e PDF, download direto e acesso a partir do lançamento
|
||||||
|
- Anexos: suporte a visualização de PDF diretamente no app via `pdfjs-dist`
|
||||||
|
- Autenticação: sidebar redesenhado com mockup de faturas e três itens de funcionalidade; páginas de login e cadastro ganham gradiente decorativo e logo visível no mobile
|
||||||
|
- Notificações: alertas de vencimento para boletos e faturas do período seguinte exibidos quando o vencimento está dentro de 5 dias
|
||||||
|
- Documentação: novo arquivo público `public/llms.txt` com resumo do projeto e links curados para documentação, setup e arquitetura
|
||||||
|
|
||||||
|
### Alterado
|
||||||
|
|
||||||
|
- Performance: queries de cache do dashboard migradas de `unstable_cache` para a diretiva `use cache` com `cacheTag` e `cacheLife`; todas as páginas do dashboard passam a chamar `connection()` para renderização dinâmica; `next.config.ts` adota `cacheComponents: true`
|
||||||
|
- Tipografia: adicionada fonte America Medium (weight 500); pesos tipográficos padronizados para `font-medium` em títulos, valores e rótulos em todos os componentes
|
||||||
|
- Anexos: `AttachmentPreview` foi simplificado para exibir apenas nome da transação, nome do arquivo, navegação entre anexos e ações de download, abrir em nova aba e fechar com ícone `X`
|
||||||
|
|
||||||
|
### Corrigido
|
||||||
|
|
||||||
|
- Lançamentos: uploads e remoções de anexo agora funcionam para todos os lançamentos, não apenas os pertencentes a séries
|
||||||
|
|
||||||
|
## [2.1.2] - 2026-03-30
|
||||||
|
|
||||||
|
Pequena versão de polimento: novo escopo `"period"` na ação em lote de lançamentos (aplica alteração a todos os lançamentos do período sem sobrescrever o pagador individual de cada um), preferência de tamanho máximo por arquivo de anexo (5/10/25/50/100 MB) persistida no banco e respeitada em todos os pontos de upload, e redesign visual da página de Configurações com separadores entre seções e títulos maiores.
|
||||||
|
|
||||||
|
### Adicionado
|
||||||
|
|
||||||
|
- Preferências: nova configuração de tamanho máximo por arquivo de anexo (5, 10, 25, 50 ou 100 MB), persistida no banco e respeitada em todos os pontos de upload
|
||||||
|
- Lançamentos: novo escopo `"period"` na ação em lote, que aplica a alteração a todos os lançamentos do período sem sobrescrever o pagador individual de cada um
|
||||||
|
### Corrigido
|
||||||
|
|
||||||
|
- Lançamentos: ao editar um lançamento de série, uploads e remoções de anexo agora aguardam a escolha de escopo da ação em lote antes de serem executados, evitando que o anexo fosse aplicado no lançamento errado
|
||||||
|
- Lançamentos: ação em lote com escopo `"period"` não sobrescreve mais o `payerId` individual de cada lançamento ao alterar o pagador
|
||||||
|
|
||||||
|
### Alterado
|
||||||
|
|
||||||
|
- Configurações: redesign visual da página com separadores entre seções e títulos maiores
|
||||||
|
- Configurações: seção "Extrato e lançamentos" renomeada para "Lançamentos"
|
||||||
|
|
||||||
|
## [2.1.1] - 2026-03-29
|
||||||
|
|
||||||
|
Esta versão extrai a navbar pra um componente `NavbarShell` compartilhado entre app e landing page e cria uma variante `navbar` no Button pra centralizar os estilos antes duplicados em `nav-styles.ts`. A integração com `@vercel/analytics`/`@vercel/speed-insights` foi substituída por Umami self-hosted via script tag no layout raiz.
|
||||||
|
|
||||||
|
### Adicionado
|
||||||
|
|
||||||
|
- Navbar: novo componente `NavbarShell` que unifica a estrutura da barra de navegação entre o app e a landing page
|
||||||
|
- UI: nova variante `navbar` no componente `Button`, centralizando os estilos de botões usados dentro da navbar
|
||||||
|
- Analytics: integração com Umami self-hosted via script tag no layout raiz
|
||||||
|
|
||||||
|
### Alterado
|
||||||
|
|
||||||
|
- Navbar: `AnimatedThemeToggler` e `RefreshPageButton` passam a aceitar prop `variant` para adaptar estilos ao contexto (navbar ou sidebar)
|
||||||
|
- Navbar: estilos inline duplicados de `nav-styles.ts` migrados para a variante `navbar` do Button
|
||||||
|
- Logo: prop `showVersion` removida; prop `colorIcon` passa a aplicar filtro de cor também no variant `compact`
|
||||||
|
- Scripts: `mockup` renomeado para `db:seed`; `db:enableExtensions` renomeado para `db:extensions`; script `dev-env` removido
|
||||||
|
- Landing: `MobileNav` simplificado com a remoção da prop `triggerClassName`
|
||||||
|
|
||||||
|
### Removido
|
||||||
|
|
||||||
|
- Navbar: arquivo `nav-styles.ts` removido após migração dos estilos para a variante `navbar`
|
||||||
|
- Dependências: `@vercel/analytics` e `@vercel/speed-insights` removidos (substituídos pelo Umami self-hosted)
|
||||||
|
|
||||||
## [2.1.0] - 2026-03-28
|
## [2.1.0] - 2026-03-28
|
||||||
|
|
||||||
|
Esta versão adiciona suporte a anexos em transações, com upload direto para storage compatível com S3, persistência em tabelas dedicadas (`anexos` e `lancamento_anexos`) e ações de visualizar/remover no detalhe do lançamento. O upload exige token assinado por arquivo, valida ownership da transação na leitura/remoção e confere tamanho/tipo do objeto no storage antes de persistir o vínculo no banco. Inclui também novo workflow `release.yml` que cria tag e GitHub Release automaticamente a partir da versão do `package.json` e da entrada correspondente no `CHANGELOG.md`.
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Lançamentos: suporte a anexos em transações com upload direto para storage compatível com S3, persistência em tabelas dedicadas (`anexos` e `lancamento_anexos`) e ações de visualizar/remover no detalhe do lançamento
|
- Lançamentos: suporte a anexos em transações com upload direto para storage compatível com S3, persistência em tabelas dedicadas (`anexos` e `lancamento_anexos`) e ações de visualizar/remover no detalhe do lançamento
|
||||||
@@ -24,12 +518,16 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR
|
|||||||
|
|
||||||
## [2.0.3] - 2026-03-26
|
## [2.0.3] - 2026-03-26
|
||||||
|
|
||||||
|
Correção pontual em `/transactions` — removida dependência de `crypto.randomUUID()` no carregamento inicial, que falhava em ambientes self-hosted sem HTTPS (a API só está disponível em contextos seguros).
|
||||||
|
|
||||||
### Corrigido
|
### Corrigido
|
||||||
|
|
||||||
- Lançamentos: `/transactions` deixa de depender de `crypto.randomUUID()` no carregamento inicial, corrigindo a falha em ambientes self-hosted sem HTTPS ao abrir a página
|
- Lançamentos: `/transactions` deixa de depender de `crypto.randomUUID()` no carregamento inicial, corrigindo a falha em ambientes self-hosted sem HTTPS ao abrir a página
|
||||||
|
|
||||||
## [2.0.2] - 2026-03-25
|
## [2.0.2] - 2026-03-25
|
||||||
|
|
||||||
|
Versão focada nas notificações da navbar: novo estado persistido permite marcar alertas de fatura, boleto e orçamento como lidos ou arquivados por usuário; o snapshot global passa a usar o período corrente do negócio (não mais o `periodo` da URL), itens lidos saem do badge e arquivados somem da lista padrão do sino. O filtro foi refinado para um seletor explícito entre `Ativas` e `Arquivadas`. Inclui ajustes pontuais no detalhamento por categoria do dashboard (oculta categorias sem movimentação no período), na arte decorativa do cabeçalho de boas-vindas e na edição em lote de lançamentos em série (que agora propaga também o status de pagamento para transações fora do cartão).
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Scripts: novo comando `mockup` no `package.json` para executar `scripts/mock-data.ts`
|
- Scripts: novo comando `mockup` no `package.json` para executar `scripts/mock-data.ts`
|
||||||
@@ -58,6 +556,8 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR
|
|||||||
|
|
||||||
## [2.0.1] - 2026-03-21
|
## [2.0.1] - 2026-03-21
|
||||||
|
|
||||||
|
Versão de correções na inbox de pré-lançamentos: filtro por app passa a montar a lista completa a partir de todos os itens do status atual (sem depender da página carregada), notificações de cartões/apps sem logo cadastrado passam a usar `default_icon.png` como fallback, e o select de apps exibe os logos. Inclui também correção de divergência entre a versão exibida no UI e a reportada pelo `/api/health` (que agora reporta a versão atual do `package.json`).
|
||||||
|
|
||||||
### Corrigido
|
### Corrigido
|
||||||
|
|
||||||
- Inbox: filtro por app em `/inbox` agora monta a lista completa de apps da aba a partir de todos os itens do status atual, sem depender apenas da página carregada, e o SSR deixa de quebrar quando `sourceApps` vier inconsistente
|
- Inbox: filtro por app em `/inbox` agora monta a lista completa de apps da aba a partir de todos os itens do status atual, sem depender apenas da página carregada, e o SSR deixa de quebrar quando `sourceApps` vier inconsistente
|
||||||
@@ -68,6 +568,8 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR
|
|||||||
|
|
||||||
## [2.0.0] - 2026-03-21
|
## [2.0.0] - 2026-03-21
|
||||||
|
|
||||||
|
Marco importante do projeto. Esta versão consolida ganhos de performance, segurança e organização interna. No backend, paginação server-side real foi implementada em transações, extrato e inbox; o dashboard reduziu de 19 fetchers para 7 blocos com agregações compartilhadas; exportações de PDF/Excel passaram a carregar libs sob demanda apenas no clique; e o cache de dashboard/insights ganhou invalidação segmentada por `userId` (sem fallback global). Internamente, identificadores foram migrados de PT-BR para inglês (`lancamento` → `transaction`, `pagador` → `payer`, `conta` → `account`, etc.) e helpers foram consolidados em módulos de domínio. Visualmente, a navbar e os cards de auth ganharam dot pattern + brilho em primary, faturas tiveram refinamento na hierarquia visual, e a tipografia foi unificada na família America. Inclui ainda script `scripts/backup.sh` para backup automático do PostgreSQL, importação de extratos OFX e XLS/XLSX com tela de revisão e dedup por FITID, e nova opção de zerar dados financeiros sem excluir o usuário.
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Infraestrutura: script `scripts/backup.sh` para backup automático do banco PostgreSQL; configuração de destino (rclone, cron, retenção) feita separadamente; passa a gerar também `*.data.sql.gz` com dados puros de todas as tabelas públicas (`--data-only --schema=public`)
|
- Infraestrutura: script `scripts/backup.sh` para backup automático do banco PostgreSQL; configuração de destino (rclone, cron, retenção) feita separadamente; passa a gerar também `*.data.sql.gz` com dados puros de todas as tabelas públicas (`--data-only --schema=public`)
|
||||||
@@ -124,6 +626,8 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR
|
|||||||
|
|
||||||
## [1.7.7] - 2026-03-05
|
## [1.7.7] - 2026-03-05
|
||||||
|
|
||||||
|
Versão de organização interna sem mudanças visíveis grandes. Períodos e navegação mensal passaram a usar os helpers centrais de período (`YYYY-MM`), hooks locais (calculadora, month-picker, logo picker) foram movidos pra perto das respectivas features e `components/navbar`/`sidebar` foram consolidados em `components/navigation/*`. Análise de parcelas migrou para `/relatorios/analise-parcelas`, exportações em PDF/CSV/Excel ganharam melhor branding e apresentação, e a calculadora teve ajustes de estabilidade no arrasto.
|
||||||
|
|
||||||
### Alterado
|
### Alterado
|
||||||
|
|
||||||
- Períodos e navegação mensal: `useMonthPeriod` passou a usar os helpers centrais de período (`YYYY-MM`), o month-picker foi simplificado e o rótulo visual agora segue o formato `Março 2026`.
|
- Períodos e navegação mensal: `useMonthPeriod` passou a usar os helpers centrais de período (`YYYY-MM`), o month-picker foi simplificado e o rótulo visual agora segue o formato `Março 2026`.
|
||||||
@@ -138,6 +642,8 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR
|
|||||||
|
|
||||||
## [1.7.6] - 2026-03-02
|
## [1.7.6] - 2026-03-02
|
||||||
|
|
||||||
|
Esta versão adiciona suporte completo a Passkeys (WebAuthn) via `@better-auth/passkey`: nova aba em `/ajustes` permite listar, adicionar, renomear e remover credenciais, e a tela de login ganhou ação dedicada para passkey. O dashboard ganhou widget de Anotações e atalhos rápidos na toolbar de widgets pra criar Receita, Despesa ou Anotação direto. Top Estabelecimentos foi unificado num único widget com abas, e o widget "Lançamentos recentes" foi substituído por "Progresso de metas" com lista de orçamentos do período (gasto, limite e percentual de uso por categoria).
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Suporte completo a Passkeys (WebAuthn) com plugin `@better-auth/passkey` no servidor e `passkeyClient` no cliente de autenticação
|
- Suporte completo a Passkeys (WebAuthn) com plugin `@better-auth/passkey` no servidor e `passkeyClient` no cliente de autenticação
|
||||||
@@ -172,6 +678,8 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR
|
|||||||
|
|
||||||
## [1.7.5] - 2026-02-28
|
## [1.7.5] - 2026-02-28
|
||||||
|
|
||||||
|
Versão pequena de polimento: ações para excluir item individual (processado/descartado) e limpar itens em lote por status na inbox de pré-lançamentos, redesign dos cards e diálogos dos widgets de boletos e faturas com indicação "Atrasado / Pagar" quando vencidos e não pagos, e migração da página de categorias de cards pra layout em tabela com link direto para detalhe e ações inline.
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Inbox de pré-lançamentos: ações para excluir item individual (processado/descartado) e limpar itens em lote por status
|
- Inbox de pré-lançamentos: ações para excluir item individual (processado/descartado) e limpar itens em lote por status
|
||||||
@@ -190,6 +698,8 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR
|
|||||||
|
|
||||||
## [1.7.4] - 2026-02-28
|
## [1.7.4] - 2026-02-28
|
||||||
|
|
||||||
|
Versão de polimento de responsividade no mobile: 26 componentes ajustados (navbar, filtros, skeletons, widgets, dialogs), card de análise de parcelas empilhado verticalmente em telas pequenas e cards do top estabelecimentos reorganizados em coluna única no mobile. Inclui também regra mais inteligente em "Remover selecionados" — quando todos os itens pertencem à mesma série, abre dialog de escopo com 3 opções; e ajuste no consumo de limite por despesa recorrente no cartão (só consome quando a data já passou).
|
||||||
|
|
||||||
### Alterado
|
### Alterado
|
||||||
|
|
||||||
- Card de análise de parcelas (`/dashboard/analise-parcelas`): layout empilhado no mobile — nome/cartão e valores Total/Pendente em linhas separadas ao invés de lado-a-lado, evitando truncamento
|
- Card de análise de parcelas (`/dashboard/analise-parcelas`): layout empilhado no mobile — nome/cartão e valores Total/Pendente em linhas separadas ao invés de lado-a-lado, evitando truncamento
|
||||||
@@ -201,6 +711,8 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR
|
|||||||
|
|
||||||
## [1.7.3] - 2026-02-27
|
## [1.7.3] - 2026-02-27
|
||||||
|
|
||||||
|
Versão pequena com nova prop `compact` no DatePicker (formato abreviado "28 fev", sem "de" e sem ano) e modal de múltiplos lançamentos reformulado: selects de conta e cartão separados por forma de pagamento, InlinePeriodPicker ao escolher cartão de crédito e DatePicker compacto.
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Prop `compact` no DatePicker para formato abreviado "28 fev" (sem "de" e sem ano)
|
- Prop `compact` no DatePicker para formato abreviado "28 fev" (sem "de" e sem ano)
|
||||||
@@ -212,6 +724,8 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR
|
|||||||
|
|
||||||
## [1.7.2] - 2026-02-26
|
## [1.7.2] - 2026-02-26
|
||||||
|
|
||||||
|
Versão de polimento dos diálogos: padding maior (p-10), largura padronizada em `max-w-xl` e botões do footer com largura igual; o lançamento dialog ganhou seção colapsável "Condições e anotações" e cálculo automático do período da fatura via `deriveCreditCardPeriod()`. Inclui também uma faxina de tipos (non-null assertions removidas, `any` substituído por tipos explícitos em 15+ arquivos) e remoção de 6 componentes e 20+ funções/tipos sem uso.
|
||||||
|
|
||||||
### Alterado
|
### Alterado
|
||||||
|
|
||||||
- Dialogs padronizados: padding maior (p-10), largura max-w-xl, botões do footer com largura igual (flex-1)
|
- Dialogs padronizados: padding maior (p-10), largura max-w-xl, botões do footer com largura igual (flex-1)
|
||||||
@@ -235,6 +749,8 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR
|
|||||||
|
|
||||||
## [1.7.1] - 2026-02-24
|
## [1.7.1] - 2026-02-24
|
||||||
|
|
||||||
|
Esta versão substitui o header lateral por uma topbar de navegação com backdrop blur e links agrupados em 5 seções (Dashboard, Lançamentos, Cartões, Relatórios, Ferramentas), expande o sino de notificações pra exibir orçamentos estourados e pré-lançamentos pendentes em seções separadas, e cria página dedicada de changelog em `/changelog` (acessível pelo menu do usuário com a versão atual exibida ao lado).
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Topbar de navegação substituindo o header fixo: backdrop blur, links agrupados em 5 seções (Dashboard, Lançamentos, Cartões, Relatórios, Ferramentas)
|
- Topbar de navegação substituindo o header fixo: backdrop blur, links agrupados em 5 seções (Dashboard, Lançamentos, Cartões, Relatórios, Ferramentas)
|
||||||
@@ -258,6 +774,8 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR
|
|||||||
|
|
||||||
## [1.6.3] - 2026-02-19
|
## [1.6.3] - 2026-02-19
|
||||||
|
|
||||||
|
Correção pontual: variável `RESEND_FROM_EMAIL` não era lida corretamente do `.env` quando o valor continha espaços (precisa estar entre aspas). Leitura centralizada em `lib/email/resend.ts` com `getResendFromEmail()` e carregamento explícito do `.env` no contexto de Server Actions.
|
||||||
|
|
||||||
### Corrigido
|
### Corrigido
|
||||||
|
|
||||||
- E-mail Resend: variável `RESEND_FROM_EMAIL` não era lida do `.env` (valores com espaço precisam estar entre aspas). Leitura centralizada em `lib/email/resend.ts` com `getResendFromEmail()` e carregamento explícito do `.env` no contexto de Server Actions
|
- E-mail Resend: variável `RESEND_FROM_EMAIL` não era lida do `.env` (valores com espaço precisam estar entre aspas). Leitura centralizada em `lib/email/resend.ts` com `getResendFromEmail()` e carregamento explícito do `.env` no contexto de Server Actions
|
||||||
@@ -269,12 +787,16 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR
|
|||||||
|
|
||||||
## [1.6.2] - 2026-02-19
|
## [1.6.2] - 2026-02-19
|
||||||
|
|
||||||
|
Correção pontual no mobile: ao selecionar um logo no diálogo de criação de conta/cartão, o diálogo principal fechava inesperadamente. Adicionado `stopPropagation` nos eventos de click/touch dos botões e delay com `requestAnimationFrame` antes de fechar o seletor.
|
||||||
|
|
||||||
### Corrigido
|
### Corrigido
|
||||||
|
|
||||||
- Bug no mobile onde, ao selecionar um logo no diálogo de criação de conta/cartão, o diálogo principal fechava inesperadamente: adicionado `stopPropagation` nos eventos de click/touch dos botões de logo e delay com `requestAnimationFrame` antes de fechar o seletor de logo
|
- Bug no mobile onde, ao selecionar um logo no diálogo de criação de conta/cartão, o diálogo principal fechava inesperadamente: adicionado `stopPropagation` nos eventos de click/touch dos botões de logo e delay com `requestAnimationFrame` antes de fechar o seletor de logo
|
||||||
|
|
||||||
## [1.6.1] - 2026-02-18
|
## [1.6.1] - 2026-02-18
|
||||||
|
|
||||||
|
Versão pequena: nome do estabelecimento padronizado para transferências entre contas ("Saída - Transf. entre contas" e "Entrada - Transf. entre contas") com anotação no formato "de {origem} -> {destino}", e correção de avisos `width(-1) and height(-1)` do `ChartContainer` no console.
|
||||||
|
|
||||||
### Alterado
|
### Alterado
|
||||||
|
|
||||||
- Transferências entre contas: nome do estabelecimento passa a ser "Saída - Transf. entre contas" na saída e "Entrada - Transf. entre contas" na entrada e adicionando em anotação no formato "de {conta origem} -> {conta destino}"
|
- Transferências entre contas: nome do estabelecimento passa a ser "Saída - Transf. entre contas" na saída e "Entrada - Transf. entre contas" na entrada e adicionando em anotação no formato "de {conta origem} -> {conta destino}"
|
||||||
@@ -282,6 +804,8 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR
|
|||||||
|
|
||||||
## [1.6.0] - 2026-02-18
|
## [1.6.0] - 2026-02-18
|
||||||
|
|
||||||
|
Versão de personalização da tabela de lançamentos. Duas novas preferências em Ajustes > Extrato e lançamentos: "Anotações em coluna" (controla se a anotação aparece como coluna ou tooltip no ícone) e "Ordem das colunas" (lista ordenável por arrasto pra reordenar Estabelecimento, Transação, Valor etc.). Inclui ajustes mobile no header do dashboard (fixo só no mobile) e na rolagem horizontal de tabs e botões de ação.
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Preferência "Anotações em coluna" em Ajustes > Extrato e lançamentos: quando ativa, a anotação dos lançamentos aparece em coluna na tabela; quando inativa, permanece no balão (tooltip) no ícone
|
- Preferência "Anotações em coluna" em Ajustes > Extrato e lançamentos: quando ativa, a anotação dos lançamentos aparece em coluna na tabela; quando inativa, permanece no balão (tooltip) no ícone
|
||||||
@@ -302,6 +826,8 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR
|
|||||||
|
|
||||||
## [1.5.3] - 2026-02-21
|
## [1.5.3] - 2026-02-21
|
||||||
|
|
||||||
|
Versão focada no painel do pagador (novo card "Status de Pagamento" com totais pagos/pendentes e listagem individual de boletos com data de vencimento, data de pagamento e status), além de SEO completo na landing page (Open Graph, Twitter Card, JSON-LD Schema.org, sitemap.xml e robots.txt) e layout específico com metadados ricos. Imagens da landing convertidas de PNG para WebP para melhor performance.
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Painel do pagador: card "Status de Pagamento" com totais pagos/pendentes e listagem individual de boletos com data de vencimento, data de pagamento e status
|
- Painel do pagador: card "Status de Pagamento" com totais pagos/pendentes e listagem individual de boletos com data de vencimento, data de pagamento e status
|
||||||
@@ -323,6 +849,8 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR
|
|||||||
|
|
||||||
## [1.5.2] - 2026-02-16
|
## [1.5.2] - 2026-02-16
|
||||||
|
|
||||||
|
Reforma visual da landing page: hero com gradient sutil e tipografia responsiva, dashboard preview sem bordas pra visual mais limpo, seção "Funcionalidades" reorganizada em 6 cards principais + 6 extras compactos, seção "Como usar" com tabs Docker (Recomendado) vs Manual e footer simplificado em 3 colunas. Inclui menu hamburger mobile com Sheet drawer, animações fade-in via Intersection Observer e seção dedicada ao OpenMonetis Companion com screenshots e fluxo de captura.
|
||||||
|
|
||||||
### Alterado
|
### Alterado
|
||||||
|
|
||||||
- Landing page reformulada: visual modernizado, melhor experiência mobile e novas seções
|
- Landing page reformulada: visual modernizado, melhor experiência mobile e novas seções
|
||||||
@@ -345,6 +873,8 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR
|
|||||||
|
|
||||||
## [1.5.1] - 2026-02-16
|
## [1.5.1] - 2026-02-16
|
||||||
|
|
||||||
|
Esta versão renomeia o projeto de **OpenSheets** para **OpenMonetis** em todo o codebase (~40 arquivos: package.json, manifests, layouts, componentes, server actions, emails, Docker, docs e landing page). URLs do repositório atualizados de `opensheets-app` para `openmonetis`, image Docker renomeada para `felipegcoutinho/openmonetis` e logo textual atualizado. Inclui também suporte a multi-domínio via `PUBLIC_DOMAIN` (domínio público serve apenas a landing page, com middleware bloqueando rotas do app).
|
||||||
|
|
||||||
### Alterado
|
### Alterado
|
||||||
|
|
||||||
- Projeto renomeado de **OpenSheets** para **OpenMonetis** em todo o codebase (~40 arquivos): package.json, manifests, layouts, componentes, server actions, emails, Docker, docs e landing page
|
- Projeto renomeado de **OpenSheets** para **OpenMonetis** em todo o codebase (~40 arquivos): package.json, manifests, layouts, componentes, server actions, emails, Docker, docs e landing page
|
||||||
@@ -359,6 +889,8 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR
|
|||||||
|
|
||||||
## [1.5.0] - 2026-02-15
|
## [1.5.0] - 2026-02-15
|
||||||
|
|
||||||
|
Versão de personalização tipográfica: 13 fontes disponíveis (incluindo SF Pro Display, SF Pro Rounded, Inter, Geist Sans, Roboto, Reddit Sans, JetBrains Mono e outras) configuráveis por usuário tanto pra interface quanto pros valores monetários, com FontProvider que aplica a troca instantaneamente via CSS variables sem necessidade de reload. Fontes Apple SF Pro carregadas localmente com 4 pesos (Regular, Medium, Semibold, Bold) e novas colunas `system_font` e `money_font` na tabela `preferencias_usuario`.
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Customização de fontes nas preferências — fonte da interface e fonte de valores monetários configuráveis por usuário
|
- Customização de fontes nas preferências — fonte da interface e fonte de valores monetários configuráveis por usuário
|
||||||
@@ -378,6 +910,8 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR
|
|||||||
|
|
||||||
## [1.4.1] - 2026-02-15
|
## [1.4.1] - 2026-02-15
|
||||||
|
|
||||||
|
Versão focada na inbox de pré-lançamentos: novas abas "Pendentes", "Processados" e "Descartados" (antes só pendentes), logo do cartão/conta exibido automaticamente nos cards via matching por nome do app, pre-fill automático do cartão de crédito ao processar e badges de status com data nos itens já processados/descartados em modo readonly. Cor `--warning` ajustada para melhor contraste (mais alaranjada).
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Abas "Pendentes", "Processados" e "Descartados" na página de pré-lançamentos (antes exibia apenas pendentes)
|
- Abas "Pendentes", "Processados" e "Descartados" na página de pré-lançamentos (antes exibia apenas pendentes)
|
||||||
@@ -399,6 +933,8 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR
|
|||||||
|
|
||||||
## [1.4.0] - 2026-02-07
|
## [1.4.0] - 2026-02-07
|
||||||
|
|
||||||
|
Reforma do design system: ~60+ componentes migrados de cores hardcoded do Tailwind (`green-500`, `red-600`, `amber-500`, `blue-500` etc.) pra tokens semânticos (`success`, `destructive`, `warning`, `info`); adicionados novos tokens `--success`, `--warning`, `--info` (com foregrounds) tanto em light quanto dark mode, novas variantes `success` e `info` no Badge, e cores de chart estendidas de 6 para 10. Inclui também correção do bug de invalidação de cache do dashboard que impedia widgets de boleto/fatura de atualizar após pagamento, e fix de scroll em listas Popover+Command (estabelecimento, categorias, filtros) com a prop `modal`.
|
||||||
|
|
||||||
### Corrigido
|
### Corrigido
|
||||||
|
|
||||||
- Widgets de boleto/fatura não atualizavam após pagamento: actions de fatura (`updateInvoicePaymentStatusAction`, `updatePaymentDateAction`) e antecipação de parcelas não invalidavam o cache do dashboard
|
- Widgets de boleto/fatura não atualizavam após pagamento: actions de fatura (`updateInvoicePaymentStatusAction`, `updatePaymentDateAction`) e antecipação de parcelas não invalidavam o cache do dashboard
|
||||||
@@ -429,6 +965,8 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR
|
|||||||
|
|
||||||
## [1.3.1] - 2026-02-06
|
## [1.3.1] - 2026-02-06
|
||||||
|
|
||||||
|
Versão pequena: calculadora arrastável via drag handle no header do dialog, callback `onSelectValue` pra inserir valor diretamente no campo de lançamento, e nova aba "Changelog" em Ajustes com histórico parseado do `CHANGELOG.md`. As páginas de itens ativos e arquivados em Cartões, Contas e Anotações foram unificadas com sistema de tabs (mesmo padrão de Categorias), eliminando rotas separadas e nomenclatura inconsistente.
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Calculadora arrastável via drag handle no header do dialog
|
- Calculadora arrastável via drag handle no header do dialog
|
||||||
@@ -444,6 +982,8 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR
|
|||||||
|
|
||||||
## [1.3.0] - 2026-02-06
|
## [1.3.0] - 2026-02-06
|
||||||
|
|
||||||
|
Versão de performance no dashboard: indexes compostos em `lancamentos`, cache cross-request via `unstable_cache` com tag `"dashboard"` e TTL de 120s, e invalidação automática em mutations financeiras via `revalidateTag`. Eliminados ~20 JOINs com a tabela `pagadores` (substituídos por filtro direto via `pagadorId`) e queries consolidadas (income-expense-balance: 12→1 com GROUP BY; payment-status: 2→1; expenses/income por categoria: 4→2). Auth session deduplicada por request via `React.cache()` e scan de métricas limitado a 24 meses.
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Indexes compostos em `lancamentos`: `(userId, period, transactionType)` e `(pagadorId, period)`
|
- Indexes compostos em `lancamentos`: `(userId, period, transactionType)` e `(pagadorId, period)`
|
||||||
@@ -464,6 +1004,8 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR
|
|||||||
|
|
||||||
## [1.2.6] - 2025-02-04
|
## [1.2.6] - 2025-02-04
|
||||||
|
|
||||||
|
Versão de adaptação ao React 19 compiler: removidos ~60 `useCallback`/`useMemo` desnecessários, wrappers `React.memo` redundantes e simplificação de padrões de hidratação com `useSyncExternalStore`. Sem mudanças visíveis ao usuário — só faxina interna alinhada às novas otimizações automáticas do compilador.
|
||||||
|
|
||||||
### Alterado
|
### Alterado
|
||||||
|
|
||||||
- Refatoração para otimização do React 19 compiler
|
- Refatoração para otimização do React 19 compiler
|
||||||
@@ -492,6 +1034,8 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR
|
|||||||
|
|
||||||
## [1.2.5] - 2025-02-01
|
## [1.2.5] - 2025-02-01
|
||||||
|
|
||||||
|
Versão pequena: novo widget de pagadores no dashboard com avatares atualizados.
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Widget de pagadores no dashboard
|
- Widget de pagadores no dashboard
|
||||||
@@ -499,6 +1043,8 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR
|
|||||||
|
|
||||||
## [1.2.4] - 2025-01-22
|
## [1.2.4] - 2025-01-22
|
||||||
|
|
||||||
|
Correção pontual: preservação de formatação nas anotações e ajuste no layout do card de anotações.
|
||||||
|
|
||||||
### Corrigido
|
### Corrigido
|
||||||
|
|
||||||
- Preservar formatação nas anotações
|
- Preservar formatação nas anotações
|
||||||
@@ -506,6 +1052,8 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR
|
|||||||
|
|
||||||
## [1.2.3] - 2025-01-22
|
## [1.2.3] - 2025-01-22
|
||||||
|
|
||||||
|
Versão pequena: versão do app passa a aparecer na sidebar e atualização da documentação.
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|
||||||
- Versão exibida na sidebar
|
- Versão exibida na sidebar
|
||||||
@@ -513,6 +1061,8 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR
|
|||||||
|
|
||||||
## [1.2.2] - 2025-01-22
|
## [1.2.2] - 2025-01-22
|
||||||
|
|
||||||
|
Versão de manutenção: atualização de dependências e formatação aplicada em todo o código.
|
||||||
|
|
||||||
### Alterado
|
### Alterado
|
||||||
|
|
||||||
- Atualização de dependências
|
- Atualização de dependências
|
||||||
|
|||||||
103
CLAUDE.md
@@ -16,9 +16,10 @@
|
|||||||
3. **Periods** usam formato `YYYY-MM` (ex: `"2025-11"`). Utils em `src/shared/utils/period/`.
|
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`.
|
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.
|
5. **Revalidation**: usar `revalidateForEntity("entity")` de `src/shared/lib/actions/helpers.ts` apos mutations.
|
||||||
6. **Versionamento**: registrar mudancas no `CHANGELOG.md` seguindo Keep a Changelog.
|
6. **Versionamento**: registrar mudancas no `CHANGELOG.md` seguindo Keep a Changelog, também altere o `package.json` e `readme.md` (Badges 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...").
|
||||||
7. **Comunicacao**: responder em portugues clara e direta com o time.
|
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.
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -44,6 +45,10 @@ Use esta pergunta:
|
|||||||
|
|
||||||
Se um contrato cruza dominios, ele deve morar em `src/shared/`.
|
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:
|
Exemplos comuns:
|
||||||
|
|
||||||
- auth: `src/shared/lib/auth/*`
|
- auth: `src/shared/lib/auth/*`
|
||||||
@@ -80,6 +85,7 @@ src/
|
|||||||
│ │ ├── insights/
|
│ │ ├── insights/
|
||||||
│ │ ├── calendar/
|
│ │ ├── calendar/
|
||||||
│ │ ├── inbox/
|
│ │ ├── inbox/
|
||||||
|
│ │ ├── attachments/
|
||||||
│ │ ├── changelog/
|
│ │ ├── changelog/
|
||||||
│ │ ├── reports/
|
│ │ ├── reports/
|
||||||
│ │ │ ├── category-trends/
|
│ │ │ ├── category-trends/
|
||||||
@@ -91,7 +97,7 @@ src/
|
|||||||
│ ├── api/
|
│ ├── api/
|
||||||
│ ├── globals.css
|
│ ├── globals.css
|
||||||
│ └── layout.tsx
|
│ └── layout.tsx
|
||||||
├── features/
|
├── features/ # cada feature segue: actions.ts, queries.ts, actions/, components/, hooks/, lib/
|
||||||
│ ├── auth/
|
│ ├── auth/
|
||||||
│ ├── landing/
|
│ ├── landing/
|
||||||
│ ├── dashboard/
|
│ ├── dashboard/
|
||||||
@@ -106,13 +112,17 @@ src/
|
|||||||
│ ├── insights/
|
│ ├── insights/
|
||||||
│ ├── calendar/
|
│ ├── calendar/
|
||||||
│ ├── inbox/
|
│ ├── inbox/
|
||||||
|
│ ├── attachments/
|
||||||
│ ├── reports/
|
│ ├── reports/
|
||||||
│ └── settings/
|
│ └── settings/
|
||||||
├── shared/
|
├── shared/
|
||||||
│ ├── components/
|
│ ├── components/
|
||||||
│ │ ├── ui/
|
│ │ ├── ui/ # shadcn/ui primitives
|
||||||
│ │ ├── navigation/
|
│ │ ├── navigation/ # navbar, sidebar, breadcrumbs
|
||||||
│ │ ├── providers/
|
│ │ ├── 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/
|
│ │ ├── month-picker/
|
||||||
│ │ ├── logo-picker/
|
│ │ ├── logo-picker/
|
||||||
│ │ ├── calculator/
|
│ │ ├── calculator/
|
||||||
@@ -127,34 +137,56 @@ src/
|
|||||||
│ │ ├── calculator/
|
│ │ ├── calculator/
|
||||||
│ │ ├── categories/
|
│ │ ├── categories/
|
||||||
│ │ ├── email/
|
│ │ ├── email/
|
||||||
|
│ │ ├── import/
|
||||||
│ │ ├── installments/
|
│ │ ├── installments/
|
||||||
│ │ ├── invoices/
|
│ │ ├── invoices/
|
||||||
│ │ ├── logo/
|
│ │ ├── logo/
|
||||||
|
│ │ ├── notifications/
|
||||||
│ │ ├── payers/
|
│ │ ├── payers/
|
||||||
│ │ ├── schemas/
|
│ │ ├── schemas/
|
||||||
|
│ │ ├── storage/
|
||||||
│ │ ├── transfers/
|
│ │ ├── transfers/
|
||||||
│ │ ├── types/
|
│ │ ├── types/
|
||||||
|
│ │ ├── version/
|
||||||
│ │ └── db.ts
|
│ │ └── db.ts
|
||||||
│ └── utils/
|
│ └── utils/
|
||||||
│ ├── period/
|
│ ├── period/
|
||||||
|
│ ├── calculator.ts
|
||||||
|
│ ├── calendar.ts
|
||||||
|
│ ├── category-colors.ts
|
||||||
│ ├── currency.ts
|
│ ├── currency.ts
|
||||||
│ ├── date.ts
|
│ ├── date.ts
|
||||||
|
│ ├── export-branding.ts
|
||||||
|
│ ├── fetch-json.ts
|
||||||
│ ├── financial-dates.ts
|
│ ├── financial-dates.ts
|
||||||
│ ├── percentage.ts
|
│ ├── icons.tsx
|
||||||
│ ├── category-colors.ts
|
│ ├── id.ts
|
||||||
│ ├── calendar.ts
|
│ ├── initials.ts
|
||||||
│ ├── math.ts
|
│ ├── math.ts
|
||||||
│ ├── number.ts
|
│ ├── number.ts
|
||||||
|
│ ├── percentage.ts
|
||||||
│ ├── string.ts
|
│ ├── string.ts
|
||||||
│ ├── initials.ts
|
│ └── ui.ts
|
||||||
│ ├── icons.tsx
|
|
||||||
│ ├── export-branding.ts
|
|
||||||
│ ├── ui.ts
|
|
||||||
│ └── calculator.ts
|
|
||||||
└── db/
|
└── db/
|
||||||
└── schema.ts
|
└── 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
|
## Import Patterns
|
||||||
@@ -210,7 +242,9 @@ Layouts, `loading.tsx` e metadata continuam em `src/app/`.
|
|||||||
| `contas` | `accounts` |
|
| `contas` | `accounts` |
|
||||||
| `categorias` | `categories` |
|
| `categorias` | `categories` |
|
||||||
| `orcamentos` | `budgets` |
|
| `orcamentos` | `budgets` |
|
||||||
| `pagadores` | `payers` |
|
| `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` |
|
| `anotacoes` | `notes` |
|
||||||
| `calendario` | `calendar` |
|
| `calendario` | `calendar` |
|
||||||
| `ajustes` | `settings` |
|
| `ajustes` | `settings` |
|
||||||
@@ -290,9 +324,11 @@ export async function fetchData(userId: string, period: string) {
|
|||||||
2. Criar a feature em `src/features/<feature>/`
|
2. Criar a feature em `src/features/<feature>/`
|
||||||
3. Separar:
|
3. Separar:
|
||||||
- `components/`
|
- `components/`
|
||||||
- `queries.ts`
|
- `queries.ts` (entry point de leitura)
|
||||||
- `actions.ts`
|
- `actions.ts` (entry point de Server Actions; vira barrel quando crescer e migrar para `actions/`)
|
||||||
- `types.ts` ou `schemas.ts` quando fizer sentido
|
- `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
|
4. Extrair para `src/shared/` tudo que for reutilizavel
|
||||||
5. Atualizar navegacao e `revalidateForEntity()` se a feature tiver CRUD
|
5. Atualizar navegacao e `revalidateForEntity()` se a feature tiver CRUD
|
||||||
6. Rodar:
|
6. Rodar:
|
||||||
@@ -302,18 +338,29 @@ export async function fetchData(userId: string, period: string) {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Response Style
|
## Security Rules
|
||||||
|
|
||||||
Quando o time pedir avaliacao de plano ou feature:
|
Regras aplicadas automaticamente ao gerar codigo.
|
||||||
|
|
||||||
1. Responder em portugues simples.
|
### Secrets
|
||||||
2. Listar 3-5 problemas principais.
|
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.
|
||||||
3. Fechar com decisao pratica:
|
|
||||||
- aprova agora
|
|
||||||
- nao aprova agora
|
|
||||||
- o que ajustar antes de comecar codigo
|
|
||||||
|
|
||||||
Exemplo:
|
### 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.
|
||||||
|
|
||||||
- "Nao aprovaria para comecar codigo imediatamente."
|
### Input & Output
|
||||||
- "Primeiro ajustaria o doc com estes 5 pontos."
|
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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
389
DESIGN.md
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
# Design System Inspired by OpenMonetis
|
||||||
|
|
||||||
|
## 1. Visual Theme & Atmosphere
|
||||||
|
|
||||||
|
OpenMonetis embodies a warm, approachable financial management aesthetic grounded in trust and transparency. The design system combines a rich warm-orange accent palette with a sophisticated warm-neutral foundation, creating an interface that feels both professional and inviting. The typography and spacing work together to emphasize clarity and hierarchy, supporting the open-source ethos of personal financial control. The visual atmosphere prioritizes legibility and calm navigation, with generous whitespace and deliberate color restraint—the bold orange is reserved for critical calls-to-action and highlights, while the warm grays and blacks anchor the interface with stability and focus.
|
||||||
|
|
||||||
|
**Key Characteristics**
|
||||||
|
- Warm, approachable color story with a dominant orange accent (`#FF7733`)
|
||||||
|
- Generous whitespace and breathing room between sections
|
||||||
|
- High contrast between backgrounds and text for accessibility
|
||||||
|
- Clear typographic hierarchy using Inter for all text and UI
|
||||||
|
- Minimal elevation and shadow treatment—mostly flat design
|
||||||
|
- Subtle border accents in warm grays to define surfaces
|
||||||
|
- Open-source transparency reflected in straightforward, honest design language
|
||||||
|
|
||||||
|
## 2. Color Palette & Roles
|
||||||
|
|
||||||
|
### Primary
|
||||||
|
- **Primary Accent** (`#FF7733`): Used for primary call-to-action buttons, highlights, and key interactive elements throughout the interface; draws user attention to the most important actions
|
||||||
|
- **Primary Dark** (`#443732`): Warm-brown anchor color used extensively for text, headings, and interactive elements; provides the primary text color across the system
|
||||||
|
|
||||||
|
### Interactive
|
||||||
|
- **Interactive Neutral** (`#0F0D0C`): Near-black used for primary text and strong emphasis elements; highest contrast state
|
||||||
|
- **Interactive Overlay** (`#0006`): Transparent black overlay at 24% opacity; used for hover states, modals, and depth layering
|
||||||
|
|
||||||
|
### Neutral Scale
|
||||||
|
- **Neutral 900** (`#2A2827`): Very dark warm gray; used for secondary text and disabled states
|
||||||
|
- **Neutral 800** (`#322C2A`): Dark warm gray; alternative text color for lower-emphasis content
|
||||||
|
- **Neutral 700** (`#676260`): Medium warm gray; used for tertiary text, captions, and metadata
|
||||||
|
- **Neutral 50** (`#FCF7F6`): Almost-white warm cream; primary background color for light surfaces
|
||||||
|
- **Neutral 100** (`#F8F6F4`): Very light warm gray; secondary background and subtle surface distinction
|
||||||
|
- **Neutral 200** (`#F5F2EF`): Light warm gray; tertiary background and card interiors
|
||||||
|
- **Neutral 300** (`#F0EEEC`): Pale warm gray; border and divider lines
|
||||||
|
|
||||||
|
### Surface & Borders
|
||||||
|
- **Surface** (`#FFFFFF`): Pure white; primary card and container background; high-contrast surface
|
||||||
|
- **Border Light** (`#F0EEEC`): Pale warm gray used for subtle borders between elements
|
||||||
|
- **Border Dark** (`#2A2827`): Dark warm gray; stronger borders for defined card boundaries
|
||||||
|
|
||||||
|
### Semantic / Status
|
||||||
|
- **Success** (`#0E9D6E`): Green used for positive status indicators, confirmation states, and success messages
|
||||||
|
- **Warning** (`#F7A439`): Amber used for cautionary states, warnings, and attention-drawing alerts
|
||||||
|
- **Error** (`#F53F2D`): Red-orange used for error states, destructive actions, and validation failures
|
||||||
|
- **Error Alt** (`#D40C1A`): Deep red alternative for critical errors and danger states
|
||||||
|
|
||||||
|
## 3. Typography Rules
|
||||||
|
|
||||||
|
### Font Family
|
||||||
|
**Primary:** Inter (sans-serif)
|
||||||
|
Fallback: `Inter, system-ui, -apple-system, sans-serif`
|
||||||
|
|
||||||
|
**Monospace:** ui-monospace
|
||||||
|
Fallback: `ui-monospace, 'Courier New', monospace`
|
||||||
|
|
||||||
|
### Hierarchy
|
||||||
|
|
||||||
|
| Role | Font | Size | Weight | Line Height | Letter Spacing | Notes |
|
||||||
|
|------|------|------|--------|-------------|----------------|-------|
|
||||||
|
| Display / H1 | Inter | 60px | 600 | 60px | 0px | Page titles and hero headlines; maximum visual impact |
|
||||||
|
| Heading H2 | Inter | 36px | 600 | 40px | 0px | Section headers and major subsections |
|
||||||
|
| Heading H3 | Inter | 16px | 600 | 20px | 0px | Card titles and smaller section headers |
|
||||||
|
| Body | Inter | 20px | 400 | 28px | 0px | Descriptive text, paragraphs, and long-form content |
|
||||||
|
| Body Secondary | Inter | 16px | 400 | 24px | 0px | Standard body text, list items, and descriptions |
|
||||||
|
| Emphasis / Span | Inter | 14px | 500 | 20px | 0px | Emphasized text, labels, and badge content |
|
||||||
|
| Button | Inter | 14px | 500 | 20px | 0px | All button text; medium weight for clarity |
|
||||||
|
| Caption | Inter | 14px | 400 | 20px | 0px | Metadata, timestamps, and small supporting text |
|
||||||
|
| Code | ui-monospace | 14px | 400 | 20px | 0px | Code blocks and technical content |
|
||||||
|
|
||||||
|
### Principles
|
||||||
|
- **Contrast & Clarity:** Text color `#0F0D0C` on light backgrounds; `#FFFFFF` on dark backgrounds
|
||||||
|
- **Weight Hierarchy:** Use 600 weight for all headings; 500 for interactive/emphasized text; 400 for body
|
||||||
|
- **Scale Progression:** Sizes increase in meaningful increments (14 → 16 → 20 → 36 → 60); maintain consistent rhythm
|
||||||
|
- **Line Height:** Body text uses 1.4× line height multiplier for comfortable reading; headings use tighter ratio (1:1) for impact
|
||||||
|
- **Readability First:** Avoid line lengths over 80 characters for long-form content; increase line height on smaller screens
|
||||||
|
|
||||||
|
## 4. Component Stylings
|
||||||
|
|
||||||
|
### Buttons
|
||||||
|
|
||||||
|
#### Primary Button
|
||||||
|
- **Background:** `#FF7733`
|
||||||
|
- **Text Color:** `#FFFFFF`
|
||||||
|
- **Font Size:** `14px`
|
||||||
|
- **Font Weight:** `500`
|
||||||
|
- **Font Family:** `Inter`
|
||||||
|
- **Padding:** `8px 16px`
|
||||||
|
- **Border Radius:** `9.2px`
|
||||||
|
- **Border:** `0px solid transparent`
|
||||||
|
- **Height:** `40px`
|
||||||
|
- **Box Shadow:** `none`
|
||||||
|
- **Hover State:** Darken background to `#E55F1F`; add subtle shadow `0px 2px 8px rgba(0, 0, 0, 0.12)`
|
||||||
|
- **Active State:** Darken further to `#CC5118`; increase shadow
|
||||||
|
- **Disabled State:** Background `#E8E3E0`; text color `#999890`; cursor not-allowed
|
||||||
|
|
||||||
|
#### Secondary Button
|
||||||
|
- **Background:** `#FFFFFF`
|
||||||
|
- **Text Color:** `#2A2827`
|
||||||
|
- **Font Size:** `14px`
|
||||||
|
- **Font Weight:** `500`
|
||||||
|
- **Font Family:** `Inter`
|
||||||
|
- **Padding:** `8px 24px`
|
||||||
|
- **Border Radius:** `9.2px`
|
||||||
|
- **Border:** `1px solid #F0EEEC`
|
||||||
|
- **Height:** `40px`
|
||||||
|
- **Box Shadow:** `0px 1px 3px rgba(0, 0, 0, 0.06)`
|
||||||
|
- **Hover State:** Background `#F8F6F4`; border color `#E8E3E0`
|
||||||
|
- **Active State:** Background `#F0EEEC`; border color `#D5CCCA`
|
||||||
|
- **Disabled State:** Background `#FAFAF8`; text color `#BFBAB7`; border color `#F0EEEC`
|
||||||
|
|
||||||
|
#### Ghost Button
|
||||||
|
- **Background:** `transparent`
|
||||||
|
- **Text Color:** `#443732`
|
||||||
|
- **Font Size:** `14px`
|
||||||
|
- **Font Weight:** `500`
|
||||||
|
- **Font Family:** `Inter`
|
||||||
|
- **Padding:** `6px 8px`
|
||||||
|
- **Border Radius:** `9.2px`
|
||||||
|
- **Border:** `0px solid transparent`
|
||||||
|
- **Height:** `32px`
|
||||||
|
- **Box Shadow:** `none`
|
||||||
|
- **Hover State:** Background `rgba(68, 55, 50, 0.05)`
|
||||||
|
- **Active State:** Background `rgba(68, 55, 50, 0.1)`
|
||||||
|
- **Disabled State:** Text color `#BFBAB7`; cursor not-allowed
|
||||||
|
|
||||||
|
#### Icon Button
|
||||||
|
- **Background:** `transparent`
|
||||||
|
- **Icon Color:** `#443732`
|
||||||
|
- **Size:** `32px` × `32px`
|
||||||
|
- **Border Radius:** `9.2px`
|
||||||
|
- **Border:** `0px solid transparent`
|
||||||
|
- **Padding:** `0px`
|
||||||
|
- **Box Shadow:** `none`
|
||||||
|
- **Hover State:** Background `rgba(68, 55, 50, 0.08)`
|
||||||
|
- **Active State:** Background `rgba(68, 55, 50, 0.12)`
|
||||||
|
|
||||||
|
### Cards & Containers
|
||||||
|
|
||||||
|
#### Standard Card
|
||||||
|
- **Background:** `#FFFFFF`
|
||||||
|
- **Border:** `1px solid #F0EEEC`
|
||||||
|
- **Border Radius:** `11.2px`
|
||||||
|
- **Padding:** `24px`
|
||||||
|
- **Box Shadow:** `none`
|
||||||
|
- **Text Color:** `#2A2827`
|
||||||
|
- **Hover State:** Border color `#E8E3E0`; box-shadow `0px 4px 12px rgba(0, 0, 0, 0.08)`
|
||||||
|
|
||||||
|
#### Card with Top Border
|
||||||
|
- **Background:** `#FFFFFF`
|
||||||
|
- **Border:** `1px solid #F0EEEC`
|
||||||
|
- **Border Radius:** `15.2px 15.2px 0px 0px` (top corners only)
|
||||||
|
- **Padding:** `24px`
|
||||||
|
- **Box Shadow:** `none`
|
||||||
|
- **Top Border Color:** `#FF7733` (3px height implied)
|
||||||
|
|
||||||
|
#### Surface Container (Header/Nav)
|
||||||
|
- **Background:** `#FF7733`
|
||||||
|
- **Height:** `64px`
|
||||||
|
- **Padding:** `0px 24px`
|
||||||
|
- **Box Shadow:** `none`
|
||||||
|
- **Text Color:** `#FFFFFF`
|
||||||
|
- **Border:** `0px solid transparent`
|
||||||
|
|
||||||
|
#### Light Surface
|
||||||
|
- **Background:** `#F8F6F4`
|
||||||
|
- **Border:** `0px solid transparent`
|
||||||
|
- **Border Radius:** `11.2px`
|
||||||
|
- **Padding:** `16px`
|
||||||
|
- **Box Shadow:** `none`
|
||||||
|
|
||||||
|
### Inputs & Forms
|
||||||
|
|
||||||
|
#### Text Input
|
||||||
|
- **Background:** `#FFFFFF`
|
||||||
|
- **Border:** `1px solid #F0EEEC`
|
||||||
|
- **Border Radius:** `9.2px`
|
||||||
|
- **Padding:** `12px 16px`
|
||||||
|
- **Font Size:** `16px`
|
||||||
|
- **Text Color:** `#2A2827`
|
||||||
|
- **Line Height:** `24px`
|
||||||
|
- **Placeholder Color:** `#999890`
|
||||||
|
- **Focus State:** Border color `#FF7733`; box-shadow `0px 0px 0px 3px rgba(255, 119, 51, 0.1)`
|
||||||
|
- **Error State:** Border color `#F53F2D`; background `#FEF5F3`
|
||||||
|
- **Disabled State:** Background `#F8F6F4`; text color `#BFBAB7`; border color `#F0EEEC`
|
||||||
|
|
||||||
|
#### Select / Dropdown
|
||||||
|
- **Background:** `#FFFFFF`
|
||||||
|
- **Border:** `1px solid #F0EEEC`
|
||||||
|
- **Border Radius:** `9.2px`
|
||||||
|
- **Padding:** `12px 16px`
|
||||||
|
- **Font Size:** `16px`
|
||||||
|
- **Text Color:** `#2A2827`
|
||||||
|
- **Focus State:** Border color `#FF7733`; outline `0px`
|
||||||
|
- **Hover State:** Background `#FAFAF8`
|
||||||
|
|
||||||
|
#### Checkbox & Radio
|
||||||
|
- **Size:** `20px` × `20px`
|
||||||
|
- **Border Radius:** `4px` (checkbox), `50%` (radio)
|
||||||
|
- **Border:** `2px solid #F0EEEC`
|
||||||
|
- **Background:** `#FFFFFF`
|
||||||
|
- **Checked Background:** `#FF7733`
|
||||||
|
- **Checked Border:** `2px solid #FF7733`
|
||||||
|
- **Checked Icon Color:** `#FFFFFF`
|
||||||
|
- **Focus:** Border `2px solid #FF7733`; box-shadow `0px 0px 0px 3px rgba(255, 119, 51, 0.1)`
|
||||||
|
|
||||||
|
### Navigation
|
||||||
|
|
||||||
|
#### Primary Navigation
|
||||||
|
- **Background:** `#FF7733`
|
||||||
|
- **Height:** `64px`
|
||||||
|
- **Padding:** `0px 48px`
|
||||||
|
- **Display:** flex; align-items: center; gap `32px`
|
||||||
|
- **Link Color:** `#FFFFFF`
|
||||||
|
- **Link Font Size:** `16px`
|
||||||
|
- **Link Font Weight:** `400`
|
||||||
|
- **Link Hover:** Opacity `0.8`
|
||||||
|
- **Link Active:** Text decoration underline; opacity `1.0`
|
||||||
|
|
||||||
|
#### Secondary Navigation / Tabs
|
||||||
|
- **Background:** `transparent`
|
||||||
|
- **Border Bottom:** `2px solid #F0EEEC`
|
||||||
|
- **Tab Padding:** `16px 24px`
|
||||||
|
- **Tab Color:** `#676260`
|
||||||
|
- **Tab Font Size:** `16px`
|
||||||
|
- **Tab Hover:** Color `#443732`
|
||||||
|
- **Tab Active:** Color `#FF7733`; border-bottom color `#FF7733`
|
||||||
|
|
||||||
|
#### Breadcrumb Navigation
|
||||||
|
- **Font Size:** `14px`
|
||||||
|
- **Color:** `#676260`
|
||||||
|
- **Separator:** `/` with `0px 8px` margin
|
||||||
|
- **Link Color:** `#443732`
|
||||||
|
- **Link Hover:** Color `#FF7733`
|
||||||
|
- **Current (Active):** Color `#2A2827`; font-weight `500`
|
||||||
|
|
||||||
|
### Badges & Status Indicators
|
||||||
|
|
||||||
|
#### Badge – Default
|
||||||
|
- **Background:** `#F8F6F4`
|
||||||
|
- **Text Color:** `#443732`
|
||||||
|
- **Padding:** `4px 12px`
|
||||||
|
- **Border Radius:** `20px`
|
||||||
|
- **Font Size:** `12px`
|
||||||
|
- **Font Weight:** `500`
|
||||||
|
- **Border:** `0px solid transparent`
|
||||||
|
|
||||||
|
#### Badge – Success
|
||||||
|
- **Background:** `#E8F5F0`
|
||||||
|
- **Text Color:** `#0E9D6E`
|
||||||
|
- **Padding:** `4px 12px`
|
||||||
|
- **Border Radius:** `20px`
|
||||||
|
- **Font Size:** `12px`
|
||||||
|
- **Font Weight:** `500`
|
||||||
|
|
||||||
|
#### Badge – Warning
|
||||||
|
- **Background:** `#FEF5E8`
|
||||||
|
- **Text Color:** `#F7A439`
|
||||||
|
- **Padding:** `4px 12px`
|
||||||
|
- **Border Radius:** `20px`
|
||||||
|
- **Font Size:** `12px`
|
||||||
|
- **Font Weight:** `500`
|
||||||
|
|
||||||
|
#### Badge – Error
|
||||||
|
- **Background:** `#FEF5F3`
|
||||||
|
- **Text Color:** `#F53F2D`
|
||||||
|
- **Padding:** `4px 12px`
|
||||||
|
- **Border Radius:** `20px`
|
||||||
|
- **Font Size:** `12px`
|
||||||
|
- **Font Weight:** `500`
|
||||||
|
|
||||||
|
## 5. Layout Principles
|
||||||
|
|
||||||
|
### Spacing System
|
||||||
|
- **Base Unit:** `4px`
|
||||||
|
- **Scale:** `4px`, `8px`, `12px`, `16px`, `24px`, `32px`, `48px`, `56px`, `64px`, `80px`, `96px`, `128px`
|
||||||
|
|
||||||
|
**Usage Contexts:**
|
||||||
|
- **4–8px:** Tight spacing within compact components (icon-text pairs, inline elements)
|
||||||
|
- **12–16px:** Standard padding inside cards, inputs, and buttons
|
||||||
|
- **24–32px:** Section gaps, spacing between components on a page
|
||||||
|
- **48–64px:** Large section separations, hero spacing
|
||||||
|
- **80–128px:** Hero margins, page-level vertical rhythm
|
||||||
|
|
||||||
|
### Grid & Container
|
||||||
|
- **Max Width:** `1440px` for full-width containers
|
||||||
|
- **Content Width:** `1152px` for typical page layouts
|
||||||
|
- **Column Strategy:** 12-column grid system; gutter `24px`
|
||||||
|
- **Container Padding:** `48px` on desktop (left + right)
|
||||||
|
- **Section Pattern:** Full-width containers with internal max-width constraint
|
||||||
|
|
||||||
|
### Whitespace Philosophy
|
||||||
|
OpenMonetis prioritizes breathing room and visual clarity. Whitespace is intentional and strategic—surrounding headings, separating card groups, and framing key messages. The warm neutral backgrounds (`#F8F6F4`, `#F5F2EF`) create natural visual separation without hard borders. Minimum margin between major sections is `64px` vertically; minimum padding inside containers is `16px`.
|
||||||
|
|
||||||
|
### Border Radius Scale
|
||||||
|
- **Sharp Corners:** `0px` (utility container tops, category selectors)
|
||||||
|
- **Subtle Radius:** `9.2px` (buttons, small inputs, icon buttons)
|
||||||
|
- **Standard Radius:** `11.2px` (cards, standard containers, modals)
|
||||||
|
- **Rounded Top:** `15.2px 15.2px 0px 0px` (card headers, sheet-style containers)
|
||||||
|
- **Pill Shape:** `24px` (badges, full-rounded tags, avatar images)
|
||||||
|
- **Circle:** `50%` (avatar images, radial elements)
|
||||||
|
|
||||||
|
## 6. Depth & Elevation
|
||||||
|
|
||||||
|
| Level | Treatment | Use |
|
||||||
|
|-------|-----------|-----|
|
||||||
|
| Flat (None) | No shadow, `border: 1px solid #F0EEEC` | Default cards, inputs, containers; baseline surfaces |
|
||||||
|
| Subtle (sm) | `0px 1px 3px rgba(0, 0, 0, 0.06)` | Secondary buttons, hover states on light surfaces |
|
||||||
|
| Medium (md) | `0px 4px 12px rgba(0, 0, 0, 0.08)` | Elevated cards on hover, floating actions |
|
||||||
|
| Deep (lg) | `0px 10px 24px rgba(0, 0, 0, 0.12)` | Modals, dropdowns, popover menus |
|
||||||
|
|
||||||
|
**Shadow Philosophy:**
|
||||||
|
The design system uses restrained shadow treatment aligned with a flat-modern aesthetic. Shadows emerge subtly on interaction (hover, focus) rather than as default styling. The primary depth cue is border color (`#F0EEEC`), which maintains visual hierarchy without excessive z-depth. When shadows are used, they employ warm-tinted blacks (`rgba(0, 0, 0, 0.06–0.12)`) to harmonize with the warm neutral palette.
|
||||||
|
|
||||||
|
## 7. Do's and Don'ts
|
||||||
|
|
||||||
|
### Do
|
||||||
|
- Use the primary orange (`#FF7733`) exclusively for the most important call-to-action buttons
|
||||||
|
- Apply generous padding (`24px–48px`) around sections and inside cards for breathing room
|
||||||
|
- Stack elements vertically with `24–32px` gaps for clear visual rhythm
|
||||||
|
- Use warm grays (`#443732`, `#2A2827`) for all body text to maintain the warm aesthetic
|
||||||
|
- Apply `9.2px` border radius consistently to all interactive elements (buttons, inputs)
|
||||||
|
- Keep line heights at `1.4×` or greater for comfortable reading on body text
|
||||||
|
- Use semantic colors (`#0E9D6E` success, `#F7A439` warning, `#F53F2D` error) intentionally
|
||||||
|
- Test contrast ratios; maintain WCAG AA minimum (4.5:1 for body text)
|
||||||
|
- Use the `Inter` typeface exclusively for consistency
|
||||||
|
- Implement focus states with a `3px` colored outline or border
|
||||||
|
|
||||||
|
### Don't
|
||||||
|
- Don't use orange anywhere except primary CTAs and critical highlights
|
||||||
|
- Don't reduce padding below `12px` inside cards or `8px` inside compact buttons
|
||||||
|
- Don't use dark backgrounds (`#0F0D0C`) for body text on light surfaces; stick to `#2A2827` or `#443732`
|
||||||
|
- Don't apply shadows as default styling; reserve them for elevated states (hover, focus, modal)
|
||||||
|
- Don't mix border radius values on the same component type; stick to defined scale
|
||||||
|
- Don't increase line height above `1.6×` for headings; tighten for impact
|
||||||
|
- Don't use the error color (`#F53F2D`) for general emphasis; reserve for genuine errors
|
||||||
|
- Don't create new colors outside the palette; use opacity if gradation is needed
|
||||||
|
- Don't apply multiple shadows to a single element; layer a maximum of two shadow values
|
||||||
|
- Don't forget to include focus/keyboard navigation states on all interactive elements
|
||||||
|
|
||||||
|
## 8. Responsive Behavior
|
||||||
|
|
||||||
|
### Breakpoints
|
||||||
|
|
||||||
|
| Breakpoint | Width | Key Changes |
|
||||||
|
|-----------|-------|-------------|
|
||||||
|
| Mobile | `375px–599px` | Single column; container padding `16px`; font sizes reduce 1–2 sizes; gap scale halved |
|
||||||
|
| Tablet | `600px–1023px` | Two-column grid; container padding `32px`; button height `36px`; heading sizes reduce slightly |
|
||||||
|
| Desktop | `1024px+` | Full 12-column grid; container padding `48px`; full-scale typography; max-width `1440px` |
|
||||||
|
|
||||||
|
### Touch Targets
|
||||||
|
- **Minimum Interactive Size:** `44px` × `44px` for mobile; `40px` × `40px` for desktop
|
||||||
|
- **Button Padding:** `12px` vertical, `16px` horizontal (minimum)
|
||||||
|
- **Link Padding:** `6px` vertical, `8px` horizontal minimum
|
||||||
|
- **Icon Button:** `32px` × `32px` minimum (32px on mobile preferred)
|
||||||
|
- **Spacing Between Targets:** `8px` minimum to avoid accidental activation
|
||||||
|
|
||||||
|
### Collapsing Strategy
|
||||||
|
- **Navigation:** Horizontal nav on desktop collapses to hamburger menu on tablet; menu items stack vertically with `12px` gap
|
||||||
|
- **Grid:** 12-column layout on desktop → 6-column on tablet → 2-column (stacked) on mobile
|
||||||
|
- **Cards:** Three-column card layouts collapse to single column on mobile; padding reduces from `24px` to `16px`
|
||||||
|
- **Typography:** Display (60px) → 36px on tablet → 28px on mobile; body (20px) → 18px on tablet → 16px on mobile
|
||||||
|
- **Spacing:** All spacing scale values reduce by 25–33% on mobile (e.g., `24px` gap → `16px` on tablet, `12px` on mobile)
|
||||||
|
- **Buttons:** Full-width on mobile (padding `0px`); inline (auto-width) on desktop
|
||||||
|
- **Inputs:** Full-width on mobile; constrained width on desktop
|
||||||
|
|
||||||
|
## 9. Agent Prompt Guide
|
||||||
|
|
||||||
|
### Quick Color Reference
|
||||||
|
- **Primary CTA:** Warm Orange (`#FF7733`) — Buttons, highlights, key interactions
|
||||||
|
- **Primary Text:** Warm Brown (`#443732`) — Headings, strong emphasis
|
||||||
|
- **Secondary Text:** Dark Neutral (`#2A2827`) — Body text, card content
|
||||||
|
- **Background:** White (`#FFFFFF`) — Cards, primary surfaces
|
||||||
|
- **Background Alt:** Cream (`#FCF7F6`) — Alternative surfaces, light containers
|
||||||
|
- **Border:** Pale Gray (`#F0EEEC`) — Card borders, divider lines
|
||||||
|
- **Success:** Green (`#0E9D6E`) — Confirmation, positive states
|
||||||
|
- **Warning:** Amber (`#F7A439`) — Cautions, attention states
|
||||||
|
- **Error:** Red-Orange (`#F53F2D`) — Errors, destructive actions
|
||||||
|
- **Disabled:** Light Gray (`#E8E3E0`) — Inactive elements, inaccessible states
|
||||||
|
|
||||||
|
### Iteration Guide
|
||||||
|
1. **Always use `#FF7733` for primary buttons** and all main call-to-action elements; secondary buttons use `#FFFFFF` with `#F0EEEC` border
|
||||||
|
2. **Typography is always `Inter`** with weights 400 (body), 500 (emphasis), and 600 (headings); size hierarchy: 14 → 16 → 20 → 36 → 60px
|
||||||
|
3. **Spacing base is `4px`**; use multiples from the scale (8, 12, 16, 24, 32, 48, 64, 80, 96, 128px); never arbitrary values
|
||||||
|
4. **Border radius:** Apply `9.2px` to all buttons and inputs, `11.2px` to cards, `15.2px 15.2px 0px 0px` to top-bordered containers
|
||||||
|
5. **Cards default to `#FFFFFF` background with `1px solid #F0EEEC` border**; add shadow only on hover (0px 4px 12px rgba(0, 0, 0, 0.08))
|
||||||
|
6. **Form inputs:** Padding `12px 16px`, border `1px solid #F0EEEC`, focus state `border: 1px solid #FF7733` + `box-shadow: 0px 0px 0px 3px rgba(255, 119, 51, 0.1)`
|
||||||
|
7. **Navigation background is always `#FF7733`** with white text; links in content use `#443732` with underline on active
|
||||||
|
8. **Maintain 1.4× line height minimum for body text**; tighten headings to 1:1 or 1.2:1 ratio
|
||||||
|
9. **Contrast validation:** Text `#0F0D0C` or `#2A2827` on light backgrounds (WCAG AA); text `#FFFFFF` on `#FF7733` (WCAG AAA)
|
||||||
|
10. **Responsive collapse:** Reduce padding and font by 25% on mobile; stack multi-column layouts to single column; full-width buttons on mobile only
|
||||||
62
Dockerfile
@@ -5,14 +5,16 @@
|
|||||||
# ============================================
|
# ============================================
|
||||||
FROM node:22-alpine AS deps
|
FROM node:22-alpine AS deps
|
||||||
|
|
||||||
# Instalar pnpm globalmente
|
RUN corepack enable && corepack prepare pnpm@10.33.0 --activate
|
||||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copiar apenas arquivos de dependências para aproveitar cache
|
# Copiar apenas arquivos de dependências para aproveitar cache
|
||||||
COPY package.json pnpm-lock.yaml* ./
|
COPY package.json pnpm-lock.yaml* ./
|
||||||
|
|
||||||
|
# Criar pasta public para o postinstall do pdfjs-dist
|
||||||
|
RUN mkdir -p public
|
||||||
|
|
||||||
# Instalar dependências (production + dev para o build)
|
# Instalar dependências (production + dev para o build)
|
||||||
RUN pnpm install --frozen-lockfile
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
@@ -21,8 +23,7 @@ RUN pnpm install --frozen-lockfile
|
|||||||
# ============================================
|
# ============================================
|
||||||
FROM node:22-alpine AS builder
|
FROM node:22-alpine AS builder
|
||||||
|
|
||||||
# Instalar pnpm globalmente
|
RUN corepack enable && corepack prepare pnpm@10.33.0 --activate
|
||||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -32,13 +33,18 @@ COPY --from=deps /app/node_modules ./node_modules
|
|||||||
# Copiar todo o código fonte
|
# Copiar todo o código fonte
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
# Garantir que o pdf.worker vem da versão instalada no stage 1, não do host
|
||||||
|
COPY --from=deps /app/public/pdf.worker.min.mjs ./public/pdf.worker.min.mjs
|
||||||
|
|
||||||
# Variáveis de ambiente necessárias para o build
|
# Variáveis de ambiente necessárias para o build
|
||||||
# DATABASE_URL será fornecida em runtime, mas precisa estar definida para validação
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1 \
|
ENV NEXT_TELEMETRY_DISABLED=1 \
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
|
|
||||||
|
# Nota: a integração Logo.dev não precisa mais de build args. O token
|
||||||
|
# `LOGO_DEV_TOKEN` é lido em runtime no servidor — basta configurá-lo no
|
||||||
|
# host (Coolify, Railway, etc.) junto com `LOGO_DEV_SECRET_KEY`.
|
||||||
|
|
||||||
# Build da aplicação Next.js
|
# Build da aplicação Next.js
|
||||||
# Nota: Se houver erros de tipo, ajuste typescript.ignoreBuildErrors no next.config.ts
|
|
||||||
RUN pnpm build
|
RUN pnpm build
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
@@ -46,8 +52,7 @@ RUN pnpm build
|
|||||||
# ============================================
|
# ============================================
|
||||||
FROM node:22-alpine AS runner
|
FROM node:22-alpine AS runner
|
||||||
|
|
||||||
# Instalar pnpm globalmente
|
RUN corepack enable && corepack prepare pnpm@10.33.0 --activate
|
||||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -55,12 +60,27 @@ WORKDIR /app
|
|||||||
RUN addgroup --system --gid 1001 nodejs && \
|
RUN addgroup --system --gid 1001 nodejs && \
|
||||||
adduser --system --uid 1001 nextjs
|
adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
# Copiar apenas arquivos necessários para produção
|
# Instalar deps do drizzle-kit em diretório separado ANTES de copiar o standalone
|
||||||
COPY --from=builder /app/public ./public
|
# Isso evita que o pnpm install sobrescreva o node_modules do Next.js standalone
|
||||||
COPY --from=builder /app/package.json ./package.json
|
COPY --from=builder /app/package.json /tmp/pkg.json
|
||||||
COPY --from=builder /app/pnpm-lock.yaml ./pnpm-lock.yaml
|
RUN mkdir -p /app/migrate && \
|
||||||
|
node -e "\
|
||||||
|
const p=JSON.parse(require('fs').readFileSync('/tmp/pkg.json','utf8'));\
|
||||||
|
require('fs').writeFileSync('/app/migrate/package.json',JSON.stringify({\
|
||||||
|
name:'openmonetis-migrate',version:p.version,\
|
||||||
|
dependencies:{\
|
||||||
|
'drizzle-orm':p.dependencies['drizzle-orm'],\
|
||||||
|
'pg':p.dependencies['pg']\
|
||||||
|
},\
|
||||||
|
devDependencies:{'drizzle-kit':p.devDependencies['drizzle-kit']}\
|
||||||
|
}));" && \
|
||||||
|
cd /app/migrate && pnpm install --no-frozen-lockfile --ignore-scripts && \
|
||||||
|
chown -R nextjs:nodejs /app/migrate
|
||||||
|
|
||||||
# Copiar arquivos de build do Next.js
|
# Copiar apenas arquivos necessários para produção
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
|
||||||
|
|
||||||
|
# Copiar arquivos de build do Next.js (inclui node_modules standalone com next)
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
|
|
||||||
@@ -69,8 +89,11 @@ COPY --from=builder --chown=nextjs:nodejs /app/drizzle ./drizzle
|
|||||||
COPY --from=builder --chown=nextjs:nodejs /app/drizzle.config.ts ./drizzle.config.ts
|
COPY --from=builder --chown=nextjs:nodejs /app/drizzle.config.ts ./drizzle.config.ts
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/src/db ./src/db
|
COPY --from=builder --chown=nextjs:nodejs /app/src/db ./src/db
|
||||||
|
|
||||||
# Copiar node_modules para ter drizzle-kit disponível para migrations
|
# Copiar entrypoint de migrations
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
|
COPY docker-entrypoint.sh ./
|
||||||
|
RUN sed -i 's/\r$//' /app/docker-entrypoint.sh && \
|
||||||
|
chmod +x /app/docker-entrypoint.sh && \
|
||||||
|
chown nextjs:nodejs /app/docker-entrypoint.sh
|
||||||
|
|
||||||
# Definir variáveis de ambiente de produção
|
# Definir variáveis de ambiente de produção
|
||||||
ENV NODE_ENV=production \
|
ENV NODE_ENV=production \
|
||||||
@@ -81,16 +104,13 @@ ENV NODE_ENV=production \
|
|||||||
# Expor porta
|
# Expor porta
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
# Ajustar permissões para o usuário nextjs
|
|
||||||
RUN chown -R nextjs:nodejs /app
|
|
||||||
|
|
||||||
# Mudar para usuário não-root
|
# Mudar para usuário não-root
|
||||||
USER nextjs
|
USER nextjs
|
||||||
|
|
||||||
# Health check
|
# Health check
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||||
CMD node -e "require('http').get('http://localhost:3000/api/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})" || exit 1
|
CMD wget --quiet --tries=1 --spider http://127.0.0.1:3000/api/health || exit 1
|
||||||
|
|
||||||
# Comando de inicialização
|
# Entrypoint: roda migrations e depois executa o CMD
|
||||||
# Nota: Em produção com standalone build, o servidor é iniciado pelo arquivo server.js
|
ENTRYPOINT ["/app/docker-entrypoint.sh"]
|
||||||
CMD ["node", "server.js"]
|
CMD ["node", "server.js"]
|
||||||
|
|||||||
360
README.md
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="./public/images/logo_small.png" alt="OpenMonetis Logo" height="80" />
|
<img src="./public/images/logo_small.svg" alt="OpenMonetis Logo" height="80" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -8,7 +8,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.
|
||||||
|
|
||||||
[](CHANGELOG.md)
|
[](CHANGELOG.md)
|
||||||
[](https://nextjs.org/)
|
[](https://nextjs.org/)
|
||||||
[](https://www.typescriptlang.org/)
|
[](https://www.typescriptlang.org/)
|
||||||
[](https://www.postgresql.org/)
|
[](https://www.postgresql.org/)
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="./public/images/dashboard-preview-light.webp" alt="Dashboard Preview" width="800" />
|
<img src="./public/images/dashboard-preview-light.png" alt="Dashboard Preview" width="800" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -28,10 +28,12 @@
|
|||||||
## 📖 Índice
|
## 📖 Índice
|
||||||
|
|
||||||
- [Sobre o Projeto](#-sobre-o-projeto)
|
- [Sobre o Projeto](#-sobre-o-projeto)
|
||||||
- [Instalação via Script](#-instalação-via-script)
|
- [Como rodar o OpenMonetis](#-como-rodar-o-openmonetis)
|
||||||
- [Início Rápido (manual)](#-início-rápido)
|
- [Perfil 1 — Usar](#perfil-1--usar-self-hosting)
|
||||||
|
- [Perfil 2 — Desenvolver](#perfil-2--desenvolver)
|
||||||
- [Scripts Disponíveis](#-scripts-disponíveis)
|
- [Scripts Disponíveis](#-scripts-disponíveis)
|
||||||
- [Docker](#-docker)
|
- [Docker](#-docker)
|
||||||
|
- [Backup](#-backup)
|
||||||
- [Storage S3 Compatível](#-storage-s3-compatível)
|
- [Storage S3 Compatível](#-storage-s3-compatível)
|
||||||
- [Variáveis de Ambiente](#-variáveis-de-ambiente)
|
- [Variáveis de Ambiente](#-variáveis-de-ambiente)
|
||||||
- [Arquitetura](#-arquitetura)
|
- [Arquitetura](#-arquitetura)
|
||||||
@@ -53,13 +55,13 @@ A ideia é simples: ter um lugar onde consigo ver todas as minhas contas, cartõ
|
|||||||
|
|
||||||
**1. Não há versão hospedada online** — Este projeto é self-hosted. Você precisa rodar no seu próprio computador ou servidor.
|
**1. Não há versão hospedada online** — Este projeto é self-hosted. Você precisa rodar no seu próprio computador ou servidor.
|
||||||
|
|
||||||
**2. Não há Open Finance** — Não há conexão automática com bancos. Você pode registrar transações manualmente ou importar extratos nos formatos OFX e XLS/XLSX.
|
**2. Não há Open Finance** — Não há conexão automática com bancos. Você pode registrar transações manualmente, usar o app companion para capturar notificações bancárias ou importar extratos nos formatos OFX e XLS/XLSX.
|
||||||
|
|
||||||
**3. Requer disciplina** — O OpenMonetis funciona melhor para quem tem disciplina de registrar os gastos regularmente, quer controle total sobre seus dados e gosta de entender exatamente onde o dinheiro está indo.
|
**3. Requer disciplina** — O OpenMonetis funciona melhor para quem tem disciplina de registrar os gastos regularmente, quer controle total sobre seus dados e gosta de entender exatamente onde o dinheiro está indo.
|
||||||
|
|
||||||
### Funcionalidades
|
### Funcionalidades
|
||||||
|
|
||||||
💰 **Contas e transações** — Contas bancárias, cartões, dinheiro. Receitas, despesas e transferências. Categorização, extratos detalhados 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 e transferências. Categorização, filtros combináveis, extratos detalhados e importação de extratos OFX e XLS/XLSX com detecção automática de categoria.
|
||||||
|
|
||||||
📊 **Dashboard e relatórios** — Widgets interativos de métricas, gráficos de evolução, comparativos por categoria, tendências, uso de cartões, top estabelecimentos. Exportação em PDF e Excel.
|
📊 **Dashboard e relatórios** — Widgets interativos de métricas, gráficos de evolução, comparativos por categoria, tendências, uso de cartões, top estabelecimentos. Exportação em PDF e Excel.
|
||||||
|
|
||||||
@@ -77,9 +79,13 @@ A ideia é simples: ter um lugar onde consigo ver todas as minhas contas, cartõ
|
|||||||
|
|
||||||
📅 **Calendário financeiro** — Visualize todos os lançamentos em um calendário mensal.
|
📅 **Calendário financeiro** — Visualize todos os lançamentos em um calendário mensal.
|
||||||
|
|
||||||
📲 **OpenMonetis Companion** — App Android que captura notificações bancárias (Nubank, Itaú, Bradesco, Inter, C6 e outros) e envia como pré-lançamentos para revisão. [Repositório](https://github.com/felipegcoutinho/openmonetis-companion).
|
📲 **OpenMonetis Companion** — App Android que captura notificações bancárias (Nubank, Itaú, Bradesco, Inter, C6 e outros) e envia automaticamente como pré-lançamentos para revisão — sem digitar nada. [Repositório](https://github.com/felipegcoutinho/openmonetis-companion).
|
||||||
|
|
||||||
⚙️ **Personalização** — Tema dark/light e modo privacidade.
|
<p align="center">
|
||||||
|
<img src="./public/images/companion-preview-light.webp" alt="OpenMonetis Companion" width="300" height="600" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
⚙️ **Personalização** — Tema dark/light, modo privacidade e changelog visual para acompanhar as novidades do app.
|
||||||
|
|
||||||
### Stack técnica
|
### Stack técnica
|
||||||
|
|
||||||
@@ -93,82 +99,118 @@ A ideia é simples: ter um lugar onde consigo ver todas as minhas contas, cartõ
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ⚡ Instalação via Script
|
## 🚀 Como rodar o OpenMonetis
|
||||||
|
|
||||||
A forma mais rápida de instalar. O script verifica dependências, configura o `.env` interativamente e sobe o banco automaticamente.
|
Escolha o perfil que corresponde ao seu objetivo:
|
||||||
|
|
||||||
**Pré-requisito:** Node.js 22+
|
| | Perfil 1 — Usar | Perfil 2 — Desenvolver |
|
||||||
|
|---|---|---|
|
||||||
```bash
|
| **Objetivo** | Rodar o app pronto | Modificar o código |
|
||||||
# Mac / Linux / WSL
|
| **Clonar repositório** | Não | Sim |
|
||||||
curl -fsSL https://raw.githubusercontent.com/felipegcoutinho/openmonetis/main/setup.mjs -o setup.mjs && node setup.mjs
|
| **Node.js / pnpm** | Não | Sim (Node 22+) |
|
||||||
|
| **Docker** | Sim | Sim |
|
||||||
# Windows (PowerShell)
|
| **Como iniciar** | `docker compose up -d` | `pnpm docker:db` + `pnpm dev` |
|
||||||
curl -o setup.mjs https://raw.githubusercontent.com/felipegcoutinho/openmonetis/main/setup.mjs ; node setup.mjs
|
| **App roda em** | Container Docker | Host local (hot-reload) |
|
||||||
```
|
| **Banco roda em** | Container Docker | Container Docker |
|
||||||
|
| **`DATABASE_URL` (host)** | `db` (automático pelo compose) | `localhost` |
|
||||||
O script irá:
|
| **Banco remoto (Supabase, Neon...)** | Sim (`docker compose up -d app`) | Sim (ajustar `DATABASE_URL`) |
|
||||||
- Verificar Node, pnpm, Git e Docker
|
| **Como atualizar** | `pnpm docker:update` | `git pull` + `pnpm install` + `pnpm db:push` |
|
||||||
- Perguntar se quer banco local (Docker) ou remoto (Supabase, Neon, etc.)
|
| **Indicado para** | Self-hosting, VPS, servidor | Contribuidores, customizações |
|
||||||
- Gerar o `BETTER_AUTH_SECRET` automaticamente
|
|
||||||
- Configurar opcionais: Google OAuth, e-mail, IA, domínio público
|
|
||||||
- Clonar o repositório, instalar dependências e aplicar o schema
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 Início Rápido (manual)
|
### Perfil 1 — Usar (self-hosting)
|
||||||
|
|
||||||
### Pré-requisitos
|
Só quer rodar o OpenMonetis. **Não precisa clonar o repositório nem instalar Node.js** — apenas Docker.
|
||||||
|
|
||||||
- Node.js 22+ e pnpm
|
```bash
|
||||||
- Docker e Docker Compose
|
# 1. Baixe o compose
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/felipegcoutinho/openmonetis/main/docker-compose.yml -o docker-compose.yml
|
||||||
|
|
||||||
### Passo a Passo
|
# 2. Crie um .en na mesma pasta.
|
||||||
|
# .env mínimo recomendado para produção
|
||||||
|
BETTER_AUTH_SECRET=gere-um-valor-com-openssl-rand-base64-32
|
||||||
|
BETTER_AUTH_URL=http://seu-dominio.com
|
||||||
|
|
||||||
1. **Clone e instale**
|
# 3. Suba tudo
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
Acesse em: `http://localhost:3000`
|
||||||
git clone https://github.com/felipegcoutinho/openmonetis.git
|
|
||||||
cd openmonetis
|
|
||||||
pnpm install
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Configure o `.env`**
|
O banco sobe com credenciais padrão. Para personalizar (senha, Google OAuth, e-mail, IA...), crie um `.env` na mesma pasta **antes** de subir.
|
||||||
|
|
||||||
```bash
|
Mais sobre .env em [Variáveis de Ambiente](#-variáveis-de-ambiente).
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
Edite o `.env` com suas credenciais. O principal é o `DATABASE_URL` e o `BETTER_AUTH_SECRET`:
|
**Banco remoto (Supabase, Neon, Railway...):** defina `DATABASE_URL` no `.env` e suba só o app:
|
||||||
|
|
||||||
```env
|
```bash
|
||||||
# Banco local (Docker): use host "localhost"
|
docker compose up -d app
|
||||||
DATABASE_URL=postgresql://openmonetis:openmonetis_dev_password@localhost:5432/openmonetis_db
|
```
|
||||||
|
|
||||||
# Banco remoto (Supabase, Neon, etc): use a URL completa do provider
|
**Não tem Docker instalado?** Em servidores Ubuntu 24.04 limpos, use o script de instalação:
|
||||||
# DATABASE_URL=postgresql://user:password@host:5432/database?sslmode=require
|
|
||||||
|
|
||||||
BETTER_AUTH_SECRET=seu-secret-aqui # gere com: openssl rand -base64 32
|
```bash
|
||||||
BETTER_AUTH_URL=http://localhost:3000
|
curl -fsSL https://raw.githubusercontent.com/felipegcoutinho/openmonetis/main/scripts/install-deps.sh -o install-deps.sh
|
||||||
```
|
sudo sh install-deps.sh
|
||||||
|
```
|
||||||
|
|
||||||
3. **Suba o banco de dados** (pule se estiver usando banco remoto)
|
> Ao final, faça **logout e login** para as permissões do grupo `docker` terem efeito.
|
||||||
|
|
||||||
```bash
|
#### Atualizando (Perfil 1)
|
||||||
docker compose up db -d
|
|
||||||
pnpm db:enableExtensions
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Execute as migrations e inicie**
|
```bash
|
||||||
|
pnpm docker:update
|
||||||
|
# ou equivalente:
|
||||||
|
docker compose pull && docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
O schema do banco é aplicado automaticamente no startup — nenhum passo extra necessário.
|
||||||
pnpm db:push
|
|
||||||
pnpm dev
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Acesse `http://localhost:3000`
|
---
|
||||||
|
|
||||||
> **Docker completo** (app + banco em containers): use `pnpm docker:up` ao invés dos passos 3-4.
|
### Perfil 2 — Desenvolver
|
||||||
|
|
||||||
|
Quer modificar o código com hot-reload. O banco roda no Docker, o app roda direto no seu servidor.
|
||||||
|
|
||||||
|
**Requisitos:** Docker + Node.js 22+ + pnpm
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clone o repositório
|
||||||
|
git clone https://github.com/felipegcoutinho/openmonetis.git
|
||||||
|
cd openmonetis
|
||||||
|
|
||||||
|
# 2. Instale as dependências
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# 3. Configure o ambiente
|
||||||
|
cp .env.example .env
|
||||||
|
# O DATABASE_URL já vem com host "localhost" (correto para dev local).
|
||||||
|
# Edite o .env com suas configurações (BETTER_AUTH_SECRET, etc.)
|
||||||
|
|
||||||
|
# 4. Suba o banco
|
||||||
|
pnpm docker:db
|
||||||
|
|
||||||
|
# 5. Aplique o schema no banco (apenas no primeiro setup)
|
||||||
|
pnpm db:push
|
||||||
|
|
||||||
|
# 6. Inicie o app com hot-reload
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Acesse em: `http://localhost:3000`
|
||||||
|
|
||||||
|
Toda vez que salvar um arquivo, o app atualiza automaticamente sem precisar reiniciar.
|
||||||
|
|
||||||
|
#### Atualizando (Perfil 2)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git pull
|
||||||
|
pnpm install # instala dependências novas, se houver
|
||||||
|
pnpm db:push # aplica mudanças de schema, se houver
|
||||||
|
```
|
||||||
|
|
||||||
|
O `pnpm dev` já em execução detecta as mudanças de código automaticamente — não precisa reiniciar.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -196,41 +238,54 @@ pnpm db:studio # Drizzle Studio (UI visual)
|
|||||||
### Utilitários
|
### Utilitários
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm backup # Backup do banco (requer scripts/backup.sh configurado)
|
pnpm backup # Backup completo do banco (ver seção Backup)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm docker:up # Subir app + banco
|
pnpm docker:up # Sobe app (Docker Hub) + banco em background
|
||||||
pnpm docker:up:d # Subir em background
|
pnpm docker:db # Sobe apenas o banco em background (usar com pnpm dev)
|
||||||
pnpm docker:up:db # Subir apenas o banco
|
pnpm docker:down # Para e remove os containers
|
||||||
pnpm docker:down # Parar containers
|
pnpm docker:logs # Logs em tempo real (todos os containers)
|
||||||
pnpm docker:down:volumes # Parar e remover volumes (⚠️ apaga dados!)
|
pnpm docker:update # Atualiza para a imagem mais recente do Hub e reinicia
|
||||||
pnpm docker:logs # Logs em tempo real
|
|
||||||
pnpm docker:restart # Reiniciar
|
|
||||||
pnpm docker:rebuild # Rebuild completo
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🐳 Docker
|
## 🐳 Docker
|
||||||
|
|
||||||
O `Dockerfile` usa multi-stage build (deps → builder → runner) com imagem final ~200MB rodando como usuário não-root.
|
O `Dockerfile` usa multi-stage build (deps → builder → runner) com imagem final ~200MB rodando como usuário não-root. Health checks configurados para ambos os serviços (PostgreSQL via `pg_isready`, app via `GET /api/health`).
|
||||||
|
|
||||||
Health checks configurados para ambos os serviços (PostgreSQL via `pg_isready`, app via `GET /api/health`).
|
### Self-hosting (recomendado)
|
||||||
|
|
||||||
|
Baixe apenas o `docker-compose.yml` e suba tudo — sem clonar o repositório, sem instalar dependências:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/felipegcoutinho/openmonetis/main/docker-compose.yml -o docker-compose.yml
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
As credenciais padrão do banco já estão configuradas. Para personalizar (senhas, opcionais), crie um `.env` na mesma pasta antes de subir — veja [Variáveis de Ambiente](#-variáveis-de-ambiente).
|
||||||
|
|
||||||
|
### Banco remoto (Supabase, Neon, Railway...)
|
||||||
|
|
||||||
|
Suba apenas o app e aponte para o banco externo via `DATABASE_URL` no `.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d app
|
||||||
|
```
|
||||||
|
|
||||||
### Comandos úteis
|
### Comandos úteis
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose exec app sh # Shell da aplicação
|
docker compose exec app sh # Shell da aplicação
|
||||||
docker compose exec db psql -U openmonetis -d openmonetis_db # Shell do banco
|
docker compose exec db psql -U openmonetis -d openmonetis_db # Shell do banco
|
||||||
docker compose ps # Status
|
docker compose ps # Status
|
||||||
docker compose exec db pg_dump -U openmonetis openmonetis_db > backup.sql # Backup
|
pnpm backup # Backup (ver seção Backup)
|
||||||
docker compose exec -T db psql -U openmonetis -d openmonetis_db < backup.sql # Restore
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Customizando Portas
|
### Customizando portas
|
||||||
|
|
||||||
```env
|
```env
|
||||||
APP_PORT=3001 # Padrão: 3000
|
APP_PORT=3001 # Padrão: 3000
|
||||||
@@ -239,6 +294,71 @@ DB_PORT=5433 # Padrão: 5432
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 💾 Backup
|
||||||
|
|
||||||
|
O backup é uma rotina de infraestrutura — não é uma tela no app. Ele opera diretamente sobre o banco PostgreSQL e é executado via linha de comando.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm backup
|
||||||
|
```
|
||||||
|
|
||||||
|
### O que é salvo
|
||||||
|
|
||||||
|
Cada execução gera **3 arquivos** em `backup/`:
|
||||||
|
|
||||||
|
| Arquivo | Conteúdo | Uso |
|
||||||
|
|---|---|---|
|
||||||
|
| `openmonetis_YYYY-MM-DD_HH-MM.dump` | Dump custom dos schemas `public` + `drizzle` | Restore completo via `pg_restore` |
|
||||||
|
| `openmonetis_YYYY-MM-DD_HH-MM.sql.gz` | Dump SQL compactado dos schemas `public` + `drizzle` | Inspeção manual, portabilidade |
|
||||||
|
| `openmonetis_YYYY-MM-DD_HH-MM.data.sql.gz` | Apenas os dados do schema `public` (sem DDL) | Migração parcial, seed de outro ambiente |
|
||||||
|
|
||||||
|
### Modos de conexão
|
||||||
|
|
||||||
|
Configure `DB_MODE` no topo de `scripts/backup.sh`:
|
||||||
|
|
||||||
|
| Modo | Quando usar | Fonte de dados |
|
||||||
|
|---|---|---|
|
||||||
|
| `remote` (padrão) | Banco em Supabase, Neon, Railway, etc. | `DATABASE_URL` do `.env` |
|
||||||
|
| `docker` | Banco no container local | Container `openmonetis_postgres` |
|
||||||
|
|
||||||
|
### Upload para Google Drive (opcional)
|
||||||
|
|
||||||
|
Se o [rclone](https://rclone.org/) estiver instalado e configurado com um remote chamado `gdrive`, os arquivos são enviados automaticamente para `gdrive:BACKUP OPENMONETIS`. Sem o rclone, o backup funciona normalmente e fica apenas local.
|
||||||
|
|
||||||
|
**Retenção:**
|
||||||
|
- Local: 7 dias
|
||||||
|
- Google Drive: 30 dias
|
||||||
|
|
||||||
|
### Automatizar com cron
|
||||||
|
|
||||||
|
Para rodar o backup automaticamente todo dia às 3h:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
crontab -e
|
||||||
|
```
|
||||||
|
|
||||||
|
```cron
|
||||||
|
0 3 * * * cd /caminho/para/openmonetis && pnpm backup >> /var/log/openmonetis-backup.log 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restore
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Zerar o banco
|
||||||
|
docker exec <container-db> psql -U openmonetis -d openmonetis_db \
|
||||||
|
-c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
|
||||||
|
|
||||||
|
# 2. Restaurar schema + dados (um comando)
|
||||||
|
docker exec -i <container-db> pg_restore \
|
||||||
|
-U openmonetis -d openmonetis_db \
|
||||||
|
--clean --if-exists --disable-triggers --no-owner --no-privileges \
|
||||||
|
< backup/openmonetis_YYYY-MM-DD_HH-MM.dump
|
||||||
|
```
|
||||||
|
|
||||||
|
> `--disable-triggers` é necessário para evitar erros de FK durante o restore (os dados são inseridos fora de ordem). O usuário `openmonetis` tem permissão para isso.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## ☁️ Storage S3 Compatível
|
## ☁️ Storage S3 Compatível
|
||||||
|
|
||||||
O suporte a anexos de lançamentos usa upload direto com URL pré-assinada. Essa configuração é opcional, mas passa a ser necessária se você quiser habilitar anexos no app.
|
O suporte a anexos de lançamentos usa upload direto com URL pré-assinada. Essa configuração é opcional, mas passa a ser necessária se você quiser habilitar anexos no app.
|
||||||
@@ -256,20 +376,60 @@ S3_BUCKET=
|
|||||||
### Compatibilidade
|
### Compatibilidade
|
||||||
|
|
||||||
- O código atual espera um provider com API compatível com S3 e suporte a `PutObject`, `GetObject`, `HeadObject`, `DeleteObject` e URLs pré-assinadas.
|
- O código atual espera um provider com API compatível com S3 e suporte a `PutObject`, `GetObject`, `HeadObject`, `DeleteObject` e URLs pré-assinadas.
|
||||||
- A implementação usa `endpoint` customizado e `forcePathStyle: true` em [`src/shared/lib/storage/s3-client.ts`](/home/ubuntu/github/openmonetis/src/shared/lib/storage/s3-client.ts).
|
- A implementação usa `endpoint` customizado e `forcePathStyle: true` em [`src/shared/lib/storage/s3-client.ts`](./src/shared/lib/storage/s3-client.ts).
|
||||||
- Em geral isso cobre MinIO, Cloudflare R2, Backblaze B2 S3-Compatible, DigitalOcean Spaces e AWS S3. Mas foi testado apenas no Supabase Storage.
|
- Em geral isso cobre MinIO, Cloudflare R2, Backblaze B2 S3-Compatible, DigitalOcean Spaces e AWS S3. Mas foi testado apenas no Supabase Storage.
|
||||||
- Se o seu provider exigir `virtual-hosted-style` em vez de `path-style`, você vai precisar ajustar essa configuração antes de usar anexos.
|
- Se o seu provider exigir `virtual-hosted-style` em vez de `path-style`, você vai precisar ajustar essa configuração antes de usar anexos.
|
||||||
- Se as variáveis de S3 não forem configuradas, mantenha os anexos desabilitados no seu fluxo de uso.
|
- Se as variáveis de S3 não forem configuradas, mantenha os anexos desabilitados no seu fluxo de uso.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🏷️ Logos de Estabelecimentos (Logo.dev)
|
||||||
|
|
||||||
|
O app exibe logos automáticos de marcas na coluna de estabelecimentos nos lançamentos. A integração usa a [Logo.dev](https://www.logo.dev) e é opcional — sem ela, o app exibe as iniciais coloridas normalmente.
|
||||||
|
|
||||||
|
### Variáveis
|
||||||
|
|
||||||
|
```env
|
||||||
|
LOGO_DEV_TOKEN=pk_... # token público (obrigatório para exibir logos)
|
||||||
|
LOGO_DEV_SECRET_KEY=sk_... # chave secreta (obrigatório para o picker de busca)
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Atualizando da v2.4.1 ou anterior:** a variável foi renomeada de `NEXT_PUBLIC_LOGO_DEV_TOKEN` para `LOGO_DEV_TOKEN`. Renomeie no seu `.env` (ou nas variáveis do Coolify/host) e remova o secret homônimo do GitHub Actions — ele não é mais usado. Não há outra etapa de migração.
|
||||||
|
|
||||||
|
### Como configurar
|
||||||
|
|
||||||
|
Ambas as variáveis são lidas em **runtime** pelo servidor Next.js. Não há mais nenhuma etapa no CI nem `--build-arg` no Docker.
|
||||||
|
|
||||||
|
**Self-hosted via Docker Hub (Coolify, Railway, etc.):**
|
||||||
|
|
||||||
|
1. Adicione `LOGO_DEV_TOKEN` e `LOGO_DEV_SECRET_KEY` nas variáveis de ambiente do host
|
||||||
|
2. Reinicie o container — pronto
|
||||||
|
|
||||||
|
**Desenvolvimento local:**
|
||||||
|
|
||||||
|
Adicione as duas no `.env` e rode `pnpm dev`.
|
||||||
|
|
||||||
|
### Como usar
|
||||||
|
|
||||||
|
Após configurado, passe o mouse sobre o avatar de qualquer estabelecimento nos lançamentos — um ícone de lápis aparece. Clique para abrir o picker, busque pelo nome da marca e selecione o logo desejado. O mapeamento fica salvo por usuário no banco.
|
||||||
|
|
||||||
|
### Arquitetura
|
||||||
|
|
||||||
|
O token **nunca chega ao cliente**. O servidor constrói a URL `https://img.logo.dev/{domain}?token=...` nos endpoints `/api/logo/mapping` e `/api/logo/search`, e o cliente apenas consome a URL pronta. Um Context Provider (`LogoDevProvider`) propaga a flag `enabled` para os componentes que decidem se renderizam o picker.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🔐 Variáveis de Ambiente
|
## 🔐 Variáveis de Ambiente
|
||||||
|
|
||||||
Copie `.env.example` para `.env` e configure:
|
**Perfil 2 (dev):** copie `.env.example` para `.env` — o `DATABASE_URL` já vem com `localhost`, pronto para uso com `pnpm dev`.
|
||||||
|
|
||||||
|
**Perfil 1 (Docker):** não precisa definir `DATABASE_URL` — o compose já configura automaticamente com host `db`. Só defina se usar banco remoto (Supabase, Neon, etc.).
|
||||||
|
|
||||||
### Obrigatórias
|
### Obrigatórias
|
||||||
|
|
||||||
```env
|
```env
|
||||||
|
# Perfil 2 (dev): host "localhost" — o banco roda em container, o app no host
|
||||||
|
# Perfil 1 (Docker): não precisa definir — o compose usa "db" automaticamente
|
||||||
DATABASE_URL=postgresql://openmonetis:openmonetis_dev_password@localhost:5432/openmonetis_db
|
DATABASE_URL=postgresql://openmonetis:openmonetis_dev_password@localhost:5432/openmonetis_db
|
||||||
BETTER_AUTH_SECRET=seu-secret-aqui # openssl rand -base64 32
|
BETTER_AUTH_SECRET=seu-secret-aqui # openssl rand -base64 32
|
||||||
BETTER_AUTH_URL=http://localhost:3000
|
BETTER_AUTH_URL=http://localhost:3000
|
||||||
@@ -306,6 +466,11 @@ ANTHROPIC_API_KEY=
|
|||||||
OPENAI_API_KEY=
|
OPENAI_API_KEY=
|
||||||
GOOGLE_GENERATIVE_AI_API_KEY=
|
GOOGLE_GENERATIVE_AI_API_KEY=
|
||||||
OPENROUTER_API_KEY=
|
OPENROUTER_API_KEY=
|
||||||
|
|
||||||
|
# Logo.dev (opcional, necessário para logos automáticos de estabelecimentos)
|
||||||
|
# Ambas as variáveis são runtime — basta definir no host; nenhum build arg necessário.
|
||||||
|
LOGO_DEV_TOKEN=
|
||||||
|
LOGO_DEV_SECRET_KEY=
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -342,7 +507,18 @@ openmonetis/
|
|||||||
│ │ └── auth/ # Formulários de autenticação
|
│ │ └── auth/ # Formulários de autenticação
|
||||||
│ │
|
│ │
|
||||||
│ ├── shared/ # Código reutilizado entre features
|
│ ├── shared/ # Código reutilizado entre features
|
||||||
│ │ ├── components/ # UI compartilhada (shadcn/ui, navigation, skeletons...)
|
│ │ ├── components/ # UI compartilhada
|
||||||
|
│ │ │ ├── ui/ # shadcn/ui primitives
|
||||||
|
│ │ │ ├── navigation/ # navbar, sidebar, breadcrumbs
|
||||||
|
│ │ │ ├── brand/ # logos do app
|
||||||
|
│ │ │ ├── widgets/ # widget-card e variantes
|
||||||
|
│ │ │ ├── feedback/ # empty-state, status-dot, payment-success
|
||||||
|
│ │ │ ├── entity-avatar/ # avatares de categoria/estabelecimento
|
||||||
|
│ │ │ ├── month-picker/ # seletor de período
|
||||||
|
│ │ │ ├── logo-picker/ # seletor de logos
|
||||||
|
│ │ │ ├── calculator/ # calculadora de cálculos rápidos
|
||||||
|
│ │ │ ├── skeletons/ # loading skeletons
|
||||||
|
│ │ │ └── providers/ # React context providers
|
||||||
│ │ ├── hooks/ # React hooks globais
|
│ │ ├── hooks/ # React hooks globais
|
||||||
│ │ ├── lib/ # Helpers de domínio (auth, db, payers, schemas, email...)
|
│ │ ├── lib/ # Helpers de domínio (auth, db, payers, schemas, email...)
|
||||||
│ │ └── utils/ # Utilitários (currency, date, period, math, string...)
|
│ │ └── utils/ # Utilitários (currency, date, period, math, string...)
|
||||||
@@ -358,6 +534,22 @@ openmonetis/
|
|||||||
└── proxy.ts # Middleware (auth + multi-domínio)
|
└── proxy.ts # Middleware (auth + multi-domínio)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Estrutura interna de uma feature
|
||||||
|
|
||||||
|
Toda feature em `src/features/<nome>/` segue o mesmo padrão:
|
||||||
|
|
||||||
|
```
|
||||||
|
<feature>/
|
||||||
|
├── actions.ts # Server Actions (entry point — barrel re-export quando há actions/)
|
||||||
|
├── queries.ts # Funções de leitura do banco (entry point)
|
||||||
|
├── 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
|
||||||
|
```
|
||||||
|
|
||||||
|
A regra é: `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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🤝 Contribuindo
|
## 🤝 Contribuindo
|
||||||
@@ -397,12 +589,6 @@ Para o texto legal completo, consulte o arquivo [LICENSE](LICENSE) ou visite [cr
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🙏 Agradecimentos
|
|
||||||
|
|
||||||
[Next.js](https://nextjs.org/) · [Better Auth](https://better-auth.com/) · [Drizzle ORM](https://orm.drizzle.team/) · [shadcn/ui](https://ui.shadcn.com/) · [Biome](https://biomejs.dev/) · [Vercel](https://vercel.com/)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Desenvolvido por:** Felipe Coutinho — [@felipegcoutinho](https://github.com/felipegcoutinho)
|
**Desenvolvido por:** Felipe Coutinho — [@felipegcoutinho](https://github.com/felipegcoutinho)
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/2.4.8/schema.json",
|
"$schema": "https://biomejs.dev/schemas/2.4.14/schema.json",
|
||||||
"vcs": {
|
"vcs": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"clientKind": "git",
|
"clientKind": "git",
|
||||||
|
|||||||
@@ -1,138 +1,52 @@
|
|||||||
# Docker Compose para Next.js + PostgreSQL
|
|
||||||
name: openmonetis
|
name: openmonetis
|
||||||
|
|
||||||
# MODOS DE USO:
|
|
||||||
# 1. Banco LOCAL (PostgreSQL em container):
|
|
||||||
# - Configure DATABASE_URL com host "db" no .env
|
|
||||||
# - Execute: docker compose up
|
|
||||||
#
|
|
||||||
# 2. Banco REMOTO (ex: Supabase, Neon, etc):
|
|
||||||
# - Configure DATABASE_URL com a URL do banco remoto no .env
|
|
||||||
# - Execute: docker compose up app (apenas o serviço app)
|
|
||||||
#
|
|
||||||
# 3. Para parar todos os serviços:
|
|
||||||
# - Execute: docker compose down
|
|
||||||
#
|
|
||||||
# 4. Para remover volumes (CUIDADO: apaga dados do banco local):
|
|
||||||
# - Execute: docker compose down -v
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# ============================================
|
|
||||||
# Serviço: PostgreSQL (Banco de dados local)
|
|
||||||
# ============================================
|
|
||||||
db:
|
db:
|
||||||
image: postgres:18-alpine
|
image: postgres:18-alpine
|
||||||
container_name: openmonetis_postgres
|
container_name: openmonetis_postgres
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: ${POSTGRES_USER:-openmonetis}
|
POSTGRES_USER: ${POSTGRES_USER:-openmonetis}
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-openmonetis_dev_password}
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-openmonetis_dev_password}
|
||||||
POSTGRES_DB: ${POSTGRES_DB:-openmonetis_db}
|
POSTGRES_DB: ${POSTGRES_DB:-openmonetis_db}
|
||||||
PGDATA: /var/lib/postgresql/data
|
|
||||||
POSTGRES_INITDB_ARGS: "-E UTF8 --locale=C"
|
POSTGRES_INITDB_ARGS: "-E UTF8 --locale=C"
|
||||||
|
PGDATA: /var/lib/postgresql/data
|
||||||
ports:
|
ports:
|
||||||
- "${DB_PORT:-5432}:5432"
|
- "${DB_PORT:-5432}:5432"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
# Cria extensão pgcrypto inline (necessária para gen_random_bytes no schema)
|
|
||||||
entrypoint: ["/bin/sh", "-c"]
|
|
||||||
command:
|
|
||||||
- |
|
|
||||||
echo 'CREATE EXTENSION IF NOT EXISTS pgcrypto;' > /docker-entrypoint-initdb.d/init.sql
|
|
||||||
exec docker-entrypoint.sh postgres
|
|
||||||
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-openmonetis} -d ${POSTGRES_DB:-openmonetis_db}"]
|
||||||
[
|
|
||||||
"CMD-SHELL",
|
|
||||||
"pg_isready -U ${POSTGRES_USER:-openmonetis} -d ${POSTGRES_DB:-openmonetis_db}",
|
|
||||||
]
|
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
start_period: 10s
|
start_period: 10s
|
||||||
|
|
||||||
# Para ativar logs de queries (debug), adicione ao command acima:
|
|
||||||
# exec docker-entrypoint.sh postgres -c log_statement=all
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# Serviço: Aplicação Next.js
|
|
||||||
# ============================================
|
|
||||||
app:
|
app:
|
||||||
image: felipegcoutinho/openmonetis:latest
|
image: felipegcoutinho/openmonetis:latest
|
||||||
|
|
||||||
container_name: openmonetis_app
|
container_name: openmonetis_app
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
- "${APP_PORT:-3000}:3000"
|
- "${APP_PORT:-3000}:3000"
|
||||||
|
env_file:
|
||||||
|
- path: .env
|
||||||
|
required: false
|
||||||
environment:
|
environment:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
|
DATABASE_URL: ${DATABASE_URL:-postgresql://openmonetis:openmonetis_dev_password@db:5432/openmonetis_db}
|
||||||
# Banco local: use host "db" | Banco remoto: URL completa do provider
|
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:-}
|
||||||
DATABASE_URL: ${DATABASE_URL}
|
|
||||||
|
|
||||||
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
|
|
||||||
BETTER_AUTH_URL: ${BETTER_AUTH_URL:-http://localhost:3000}
|
BETTER_AUTH_URL: ${BETTER_AUTH_URL:-http://localhost:3000}
|
||||||
|
|
||||||
# Email (opcional)
|
|
||||||
RESEND_API_KEY: ${RESEND_API_KEY:-}
|
|
||||||
RESEND_FROM_EMAIL: ${RESEND_FROM_EMAIL:-}
|
|
||||||
|
|
||||||
# OAuth (opcional)
|
|
||||||
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
|
|
||||||
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}
|
|
||||||
GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID:-}
|
|
||||||
GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET:-}
|
|
||||||
|
|
||||||
# AI providers (opcional)
|
|
||||||
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
|
|
||||||
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
|
|
||||||
GOOGLE_GENERATIVE_AI_API_KEY: ${GOOGLE_GENERATIVE_AI_API_KEY:-}
|
|
||||||
OPENROUTER_API_KEY: ${OPENROUTER_API_KEY:-}
|
|
||||||
|
|
||||||
# Só depende do 'db' se estiver usando banco local
|
|
||||||
# Para banco remoto, comente as linhas abaixo
|
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
required: false
|
||||||
# Script de inicialização: roda migrations antes de iniciar o app
|
|
||||||
entrypoint: ["/bin/sh", "-c"]
|
|
||||||
command:
|
|
||||||
- |
|
|
||||||
echo "Aguardando banco de dados..."
|
|
||||||
sleep 5
|
|
||||||
|
|
||||||
echo "Rodando migrations..."
|
|
||||||
pnpm db:push || echo "Migrations falharam ou já estão atualizadas"
|
|
||||||
|
|
||||||
echo "Iniciando aplicação Next.js..."
|
|
||||||
node server.js
|
|
||||||
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1:3000/api/health"]
|
||||||
[
|
|
||||||
"CMD",
|
|
||||||
"wget",
|
|
||||||
"--quiet",
|
|
||||||
"--tries=1",
|
|
||||||
"--spider",
|
|
||||||
"http://localhost:3000/api/health",
|
|
||||||
]
|
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
start_period: 40s
|
start_period: 40s
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# Volumes
|
|
||||||
# ============================================
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
driver: local
|
driver: local
|
||||||
|
|||||||
16
docker-entrypoint.sh
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
echo "Rodando migrations..."
|
||||||
|
MIGRATED=0
|
||||||
|
for i in 1 2 3 4 5; do
|
||||||
|
if NODE_PATH=/app/migrate/node_modules /app/migrate/node_modules/.bin/drizzle-kit push; then
|
||||||
|
MIGRATED=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo "Tentativa $i/5 falhou. Aguardando 5s..."
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
|
||||||
|
[ "$MIGRATED" -eq 0 ] && echo "Aviso: migrations não foram aplicadas."
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
1
drizzle/0024_petite_lucky_pierre.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "preferencias_usuario" ADD COLUMN "attachment_max_size_mb" integer DEFAULT 50 NOT NULL;
|
||||||
24
drizzle/0025_burly_colonel_america.sql
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
DROP INDEX "tokens_api_user_id_idx";--> statement-breakpoint
|
||||||
|
DROP INDEX "cartoes_user_id_status_idx";--> statement-breakpoint
|
||||||
|
DROP INDEX "dashboard_notification_states_user_id_archived_idx";--> statement-breakpoint
|
||||||
|
DROP INDEX "contas_user_id_status_idx";--> statement-breakpoint
|
||||||
|
DROP INDEX "antecipacoes_parcelas_series_id_idx";--> statement-breakpoint
|
||||||
|
DROP INDEX "pagadores_user_id_status_idx";--> statement-breakpoint
|
||||||
|
DROP INDEX "pagadores_user_id_role_idx";--> statement-breakpoint
|
||||||
|
CREATE INDEX "account_user_id_idx" ON "account" USING btree ("userId");--> statement-breakpoint
|
||||||
|
CREATE INDEX "anexos_user_id_idx" ON "anexos" USING btree ("user_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "orcamentos_categoria_id_idx" ON "orcamentos" USING btree ("categoria_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "cartoes_conta_id_idx" ON "cartoes" USING btree ("conta_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "import_category_mappings_category_id_idx" ON "import_category_mappings" USING btree ("category_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "pre_lancamentos_lancamento_id_idx" ON "pre_lancamentos" USING btree ("lancamento_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "antecipacoes_parcelas_lancamento_id_idx" ON "antecipacoes_parcelas" USING btree ("lancamento_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "antecipacoes_parcelas_pagador_id_idx" ON "antecipacoes_parcelas" USING btree ("pagador_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "antecipacoes_parcelas_categoria_id_idx" ON "antecipacoes_parcelas" USING btree ("categoria_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "anotacoes_user_id_idx" ON "anotacoes" USING btree ("user_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "passkey_user_id_idx" ON "passkey" USING btree ("userId");--> statement-breakpoint
|
||||||
|
CREATE INDEX "compartilhamentos_pagador_shared_with_user_id_idx" ON "compartilhamentos_pagador" USING btree ("shared_with_user_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "compartilhamentos_pagador_created_by_user_id_idx" ON "compartilhamentos_pagador" USING btree ("created_by_user_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "session_user_id_idx" ON "session" USING btree ("userId");--> statement-breakpoint
|
||||||
|
CREATE INDEX "lancamentos_conta_id_idx" ON "lancamentos" USING btree ("conta_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "lancamentos_categoria_id_idx" ON "lancamentos" USING btree ("categoria_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "lancamentos_antecipacao_id_idx" ON "lancamentos" USING btree ("antecipacao_id");
|
||||||
2
drizzle/0026_bored_eternity.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "lancamentos" ADD COLUMN "split_group_id" uuid;--> statement-breakpoint
|
||||||
|
CREATE INDEX "lancamentos_user_id_split_group_id_idx" ON "lancamentos" USING btree ("user_id","split_group_id");
|
||||||
1
drizzle/0028_fancy_reaper.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "pagadores" ALTER COLUMN "share_code" DROP DEFAULT;
|
||||||
3
drizzle/0029_friendly_spitfire.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE "cartoes" ALTER COLUMN "limite" SET DEFAULT '0';--> statement-breakpoint
|
||||||
|
UPDATE "cartoes" SET "limite" = '0' WHERE "limite" IS NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "cartoes" ALTER COLUMN "limite" SET NOT NULL;
|
||||||
2711
drizzle/meta/0024_snapshot.json
Normal file
2889
drizzle/meta/0025_snapshot.json
Normal file
2916
drizzle/meta/0026_snapshot.json
Normal file
2915
drizzle/meta/0028_snapshot.json
Normal file
2916
drizzle/meta/0029_snapshot.json
Normal file
@@ -169,6 +169,41 @@
|
|||||||
"when": 1774529878374,
|
"when": 1774529878374,
|
||||||
"tag": "0023_sturdy_wolfpack",
|
"tag": "0023_sturdy_wolfpack",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 24,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1774891206703,
|
||||||
|
"tag": "0024_petite_lucky_pierre",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 25,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1776351838548,
|
||||||
|
"tag": "0025_burly_colonel_america",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 26,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1777042423451,
|
||||||
|
"tag": "0026_bored_eternity",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 28,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1777153372633,
|
||||||
|
"tag": "0028_fancy_reaper",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 29,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1777648189399,
|
||||||
|
"tag": "0029_friendly_spitfire",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
24
knip.jsonc
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://unpkg.com/knip@6/schema-jsonc.json",
|
||||||
|
// Exclude shared UI primitives from dead code reporting while we focus the
|
||||||
|
// cleanup on feature and domain code first.
|
||||||
|
"ignore": [
|
||||||
|
"src/shared/components/ui/**"
|
||||||
|
],
|
||||||
|
// Runtime asset referenced by string in the PDF viewer.
|
||||||
|
"ignoreFiles": [
|
||||||
|
"public/pdf.worker.min.mjs",
|
||||||
|
"setup.mjs"
|
||||||
|
],
|
||||||
|
// PostCSS is inferred from the config file, but the project only depends on
|
||||||
|
// the Tailwind PostCSS plugin directly.
|
||||||
|
// `server-only` is provided implicitly by Next.js — no install needed.
|
||||||
|
"ignoreDependencies": [
|
||||||
|
"postcss",
|
||||||
|
"server-only"
|
||||||
|
],
|
||||||
|
"next": true,
|
||||||
|
"postcss": true,
|
||||||
|
"biome": true,
|
||||||
|
"drizzle": true
|
||||||
|
}
|
||||||
@@ -6,16 +6,24 @@ dotenv.config();
|
|||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
experimental: {
|
cacheComponents: true,
|
||||||
turbopackFileSystemCacheForDev: true,
|
|
||||||
},
|
|
||||||
reactCompiler: true,
|
reactCompiler: true,
|
||||||
images: {
|
images: {
|
||||||
remotePatterns: [new URL("https://lh3.googleusercontent.com/**")],
|
remotePatterns: [
|
||||||
|
new URL("https://lh3.googleusercontent.com/**"),
|
||||||
|
{ protocol: "https", hostname: "**" },
|
||||||
|
{ protocol: "http", hostname: "**" },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
devIndicators: {
|
devIndicators: {
|
||||||
position: "bottom-right",
|
position: "bottom-right",
|
||||||
},
|
},
|
||||||
|
experimental: {
|
||||||
|
prefetchInlining: true,
|
||||||
|
turbopackFileSystemCacheForDev: true,
|
||||||
|
optimizePackageImports: ["@remixicon/react"],
|
||||||
|
},
|
||||||
|
|
||||||
// Headers for Safari compatibility
|
// Headers for Safari compatibility
|
||||||
async headers() {
|
async headers() {
|
||||||
return [
|
return [
|
||||||
@@ -39,8 +47,12 @@ const nextConfig: NextConfig = {
|
|||||||
value: "DENY",
|
value: "DENY",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Content-Security-Policy",
|
key: "Referrer-Policy",
|
||||||
value: "frame-ancestors 'none';",
|
value: "strict-origin-when-cross-origin",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "X-Permitted-Cross-Domain-Policies",
|
||||||
|
value: "none",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Permissions-Policy",
|
key: "Permissions-Policy",
|
||||||
|
|||||||
88
package.json
@@ -1,44 +1,46 @@
|
|||||||
{
|
{
|
||||||
"name": "openmonetis",
|
"name": "openmonetis",
|
||||||
"version": "2.1.0",
|
"version": "2.5.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"packageManager": "pnpm@10.33.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
"dev-env": "tsx scripts/dev.ts",
|
"db:seed": "tsx scripts/mock-data.ts",
|
||||||
"mockup": "tsx scripts/mock-data.ts",
|
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "biome check .",
|
"lint": "biome check .",
|
||||||
|
"lint:deadcode": "knip --reporter compact",
|
||||||
"lint:fix": "biome check --write .",
|
"lint:fix": "biome check --write .",
|
||||||
"env:setup": "bash scripts/setup-env.sh",
|
"env:setup": "node setup.mjs",
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "drizzle-kit migrate",
|
"db:migrate": "drizzle-kit migrate",
|
||||||
"db:push": "drizzle-kit push",
|
"db:push": "drizzle-kit push",
|
||||||
"db:enableExtensions": "tsx scripts/postgres/enable-extensions.ts",
|
|
||||||
"db:studio": "drizzle-kit studio",
|
"db:studio": "drizzle-kit studio",
|
||||||
"docker:up": "docker compose up --build",
|
"postinstall": "cp node_modules/pdfjs-dist/build/pdf.worker.min.mjs public/pdf.worker.min.mjs",
|
||||||
"docker:up:db": "docker compose up -d db",
|
"docker:up": "docker compose up -d",
|
||||||
"docker:up:d": "docker compose up --build -d",
|
"//docker:up": "Sobe app (Docker Hub) + banco PostgreSQL em background",
|
||||||
|
"docker:db": "docker compose up -d db",
|
||||||
|
"//docker:db": "Sobe apenas o banco em background (para usar com pnpm dev)",
|
||||||
"docker:down": "docker compose down",
|
"docker:down": "docker compose down",
|
||||||
"docker:down:volumes": "docker compose down -v",
|
"//docker:down": "Para e remove os containers",
|
||||||
"docker:logs": "docker compose logs -f",
|
"docker:logs": "docker compose logs -f",
|
||||||
"docker:logs:app": "docker compose logs -f app",
|
"//docker:logs": "Acompanha logs de todos os containers em tempo real",
|
||||||
"docker:logs:db": "docker compose logs -f db",
|
"docker:update": "docker compose pull && docker compose up -d",
|
||||||
"docker:restart": "docker compose restart",
|
"//docker:update": "Atualiza para a imagem mais recente do Docker Hub e reinicia",
|
||||||
"docker:rebuild": "docker compose up --build --force-recreate",
|
"backup": "bash scripts/backup.sh",
|
||||||
"backup": "bash scripts/backup.sh"
|
"mockup": "tsx scripts/mock-data.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "^3.0.64",
|
"@ai-sdk/anthropic": "^3.0.74",
|
||||||
"@ai-sdk/google": "^3.0.53",
|
"@ai-sdk/google": "^3.0.67",
|
||||||
"@ai-sdk/openai": "^3.0.48",
|
"@ai-sdk/openai": "^3.0.60",
|
||||||
"@aws-sdk/client-s3": "^3.1019.0",
|
"@aws-sdk/client-s3": "^3.1042.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.1019.0",
|
"@aws-sdk/s3-request-presigner": "^3.1042.0",
|
||||||
"@better-auth/passkey": "^1.5.6",
|
"@better-auth/passkey": "^1.6.9",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@openrouter/ai-sdk-provider": "^2.3.3",
|
"@openrouter/ai-sdk-provider": "^2.9.0",
|
||||||
"@radix-ui/react-alert-dialog": "1.1.15",
|
"@radix-ui/react-alert-dialog": "1.1.15",
|
||||||
"@radix-ui/react-avatar": "1.1.11",
|
"@radix-ui/react-avatar": "1.1.11",
|
||||||
"@radix-ui/react-checkbox": "1.3.3",
|
"@radix-ui/react-checkbox": "1.3.3",
|
||||||
@@ -47,11 +49,13 @@
|
|||||||
"@radix-ui/react-dropdown-menu": "2.1.16",
|
"@radix-ui/react-dropdown-menu": "2.1.16",
|
||||||
"@radix-ui/react-hover-card": "^1.1.15",
|
"@radix-ui/react-hover-card": "^1.1.15",
|
||||||
"@radix-ui/react-label": "2.1.8",
|
"@radix-ui/react-label": "2.1.8",
|
||||||
|
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||||
"@radix-ui/react-popover": "^1.1.15",
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
"@radix-ui/react-progress": "1.1.8",
|
"@radix-ui/react-progress": "1.1.8",
|
||||||
"@radix-ui/react-radio-group": "^1.3.8",
|
"@radix-ui/react-radio-group": "^1.3.8",
|
||||||
"@radix-ui/react-select": "2.2.6",
|
"@radix-ui/react-select": "2.2.6",
|
||||||
"@radix-ui/react-separator": "1.1.8",
|
"@radix-ui/react-separator": "1.1.8",
|
||||||
|
"@radix-ui/react-slider": "^1.3.6",
|
||||||
"@radix-ui/react-slot": "1.2.4",
|
"@radix-ui/react-slot": "1.2.4",
|
||||||
"@radix-ui/react-switch": "1.2.6",
|
"@radix-ui/react-switch": "1.2.6",
|
||||||
"@radix-ui/react-tabs": "1.1.13",
|
"@radix-ui/react-tabs": "1.1.13",
|
||||||
@@ -59,47 +63,53 @@
|
|||||||
"@radix-ui/react-toggle-group": "1.1.11",
|
"@radix-ui/react-toggle-group": "1.1.11",
|
||||||
"@radix-ui/react-tooltip": "1.2.8",
|
"@radix-ui/react-tooltip": "1.2.8",
|
||||||
"@remixicon/react": "4.9.0",
|
"@remixicon/react": "4.9.0",
|
||||||
|
"@tanstack/react-query": "^5.100.9",
|
||||||
"@tanstack/react-table": "8.21.3",
|
"@tanstack/react-table": "8.21.3",
|
||||||
"@tanstack/react-virtual": "^3.13.23",
|
"@tanstack/react-virtual": "^3.13.24",
|
||||||
"@vercel/analytics": "^2.0.1",
|
"ai": "^6.0.175",
|
||||||
"@vercel/speed-insights": "^2.0.0",
|
"better-auth": "1.6.9",
|
||||||
"ai": "^6.0.141",
|
|
||||||
"better-auth": "1.5.6",
|
|
||||||
"canvas-confetti": "^1.9.4",
|
"canvas-confetti": "^1.9.4",
|
||||||
"class-variance-authority": "0.7.1",
|
"class-variance-authority": "0.7.1",
|
||||||
"clsx": "2.1.1",
|
"clsx": "2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"drizzle-orm": "0.45.2",
|
"drizzle-orm": "0.45.2",
|
||||||
|
"exceljs": "^4.4.0",
|
||||||
"jspdf": "^4.2.1",
|
"jspdf": "^4.2.1",
|
||||||
"jspdf-autotable": "^5.0.7",
|
"jspdf-autotable": "^5.0.7",
|
||||||
"next": "16.1.7",
|
"next": "16.2.4",
|
||||||
"next-themes": "0.4.6",
|
"next-themes": "0.4.6",
|
||||||
|
"pdfjs-dist": "^5.7.284",
|
||||||
"pg": "8.20.0",
|
"pg": "8.20.0",
|
||||||
"radix-ui": "^1.4.3",
|
"react": "19.2.5",
|
||||||
"react": "19.2.4",
|
|
||||||
"react-day-picker": "^9.14.0",
|
"react-day-picker": "^9.14.0",
|
||||||
"react-dom": "19.2.4",
|
"react-dom": "19.2.5",
|
||||||
"recharts": "3.8.1",
|
"recharts": "3.8.1",
|
||||||
"resend": "^6.9.4",
|
"resend": "^6.12.2",
|
||||||
"sonner": "2.0.7",
|
"sonner": "2.0.7",
|
||||||
"tailwind-merge": "3.5.0",
|
"tailwind-merge": "3.5.0",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
"vaul": "1.1.2",
|
"vaul": "1.1.2",
|
||||||
"xlsx": "^0.18.5",
|
"zod": "4.4.3"
|
||||||
"zod": "4.3.6"
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"overrides": {
|
||||||
|
"defu": "6.1.7"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.4.9",
|
"@biomejs/biome": "2.4.14",
|
||||||
"@tailwindcss/postcss": "4.2.2",
|
"@tailwindcss/postcss": "4.2.4",
|
||||||
"@types/canvas-confetti": "^1.9.0",
|
"@types/canvas-confetti": "^1.9.0",
|
||||||
"@types/node": "25.5.0",
|
"@types/node": "25.6.0",
|
||||||
"@types/pg": "^8.20.0",
|
"@types/pg": "^8.20.0",
|
||||||
"@types/react": "19.2.14",
|
"@types/react": "19.2.14",
|
||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "19.2.3",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.4.2",
|
||||||
"drizzle-kit": "0.31.10",
|
"drizzle-kit": "0.31.10",
|
||||||
"tailwindcss": "4.2.2",
|
"knip": "^6.11.0",
|
||||||
|
"tailwindcss": "4.2.4",
|
||||||
"tsx": "4.21.0",
|
"tsx": "4.21.0",
|
||||||
"typescript": "6.0.2"
|
"typescript": "6.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4855
pnpm-lock.yaml
generated
4
public/.well-known/security.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Contact: https://github.com/felipegcoutinho/openmonetis/security/advisories
|
||||||
|
Expires: 2027-04-04T00:00:00.000Z
|
||||||
|
Preferred-Languages: pt-BR, en
|
||||||
|
Canonical: https://openmonetis.com/.well-known/security.txt
|
||||||
@@ -1,15 +1,10 @@
|
|||||||
import localFont from "next/font/local";
|
import { Inter } from "next/font/google";
|
||||||
|
|
||||||
export const america = localFont({
|
export const inter = Inter({
|
||||||
src: [
|
subsets: ["latin"],
|
||||||
{
|
|
||||||
path: "./america-regular.woff2",
|
|
||||||
weight: "400",
|
|
||||||
style: "normal",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
display: "swap",
|
display: "swap",
|
||||||
variable: "--font-america",
|
variable: "--font-inter",
|
||||||
|
fallback: ["ui-sans-serif", "system-ui"],
|
||||||
|
weight: ["500", "600", "700"],
|
||||||
|
preload: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const americaFontVariable = america.variable;
|
|
||||||
|
|||||||
BIN
public/images/dashboard-preview-dark.png
Normal file
|
After Width: | Height: | Size: 571 KiB |
|
Before Width: | Height: | Size: 165 KiB |
BIN
public/images/dashboard-preview-light.png
Normal file
|
After Width: | Height: | Size: 562 KiB |
|
Before Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
3
public/images/logo_small.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200" role="img" aria-label="OpenMonetis">
|
||||||
|
<path fill="#ff7733" d="M 77.66,165.64 L 37.77,141.54 L 63.30,108.72 L 27.13,97.44 L 46.81,50.77 L 77.66,63.08 L 81.91,30.26 L 126.40,29.23 L 126.06,33.85 L 122.87,67.69 L 158.51,50.77 L 178.19,90.26 L 140.96,104.62 L 162.23,127.18 L 132.98,162.56 L 103.19,131.79 Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 402 B |
|
Before Width: | Height: | Size: 21 KiB |
3
public/images/logo_text.svg
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 382 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 189 KiB |
|
Before Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 92 KiB |
37
public/llms.txt
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# OpenMonetis
|
||||||
|
|
||||||
|
> OpenMonetis is a self-hosted personal finance web app for manual financial control. It helps users manage accounts, cards, invoices, budgets, notes, reports, attachments, and AI-generated insights. The product UI is in Brazilian Portuguese, the codebase uses English folder and import names, and there is no hosted SaaS version.
|
||||||
|
>
|
||||||
|
> **Stack:** Next.js 16, React 19, PostgreSQL, Drizzle ORM, Better Auth, Tailwind CSS 4, shadcn/ui. Package manager: pnpm. Linter: Biome.
|
||||||
|
|
||||||
|
OpenMonetis is meant to be deployed by the user on their own machine or server.
|
||||||
|
There is no Open Finance or automatic bank synchronization.
|
||||||
|
Transactions can be entered manually or imported from OFX and XLS/XLSX files.
|
||||||
|
Attachments are optional and require S3-compatible storage.
|
||||||
|
The public website is mainly a landing page; the main technical documentation lives in the GitHub repository.
|
||||||
|
|
||||||
|
## Docs
|
||||||
|
|
||||||
|
- [Landing page](/): Public homepage and high-level product overview
|
||||||
|
- [README](https://github.com/felipegcoutinho/openmonetis/blob/main/README.md): Main project documentation covering features, installation, Docker, environment variables, architecture, contributing, and license
|
||||||
|
- [CHANGELOG](https://github.com/felipegcoutinho/openmonetis/blob/main/CHANGELOG.md): Release history and notable changes
|
||||||
|
- [LICENSE](https://github.com/felipegcoutinho/openmonetis/blob/main/LICENSE): CC BY-NC-SA 4.0 license terms
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
- [Setup script](https://raw.githubusercontent.com/felipegcoutinho/openmonetis/main/setup.mjs): Interactive installer for local or self-hosted setup
|
||||||
|
- [Environment example](https://github.com/felipegcoutinho/openmonetis/blob/main/.env.example): Required and optional environment variables
|
||||||
|
- [Docker Compose](https://github.com/felipegcoutinho/openmonetis/blob/main/docker-compose.yml): Local app and PostgreSQL stack definition
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
- [CLAUDE.md](https://github.com/felipegcoutinho/openmonetis/blob/main/CLAUDE.md): Project architecture, naming rules, query rules, and feature checklist
|
||||||
|
|
||||||
|
## Optional
|
||||||
|
|
||||||
|
- [robots.txt](/robots.txt): Crawl policy for the public site
|
||||||
|
|
||||||
|
## Related Projects
|
||||||
|
|
||||||
|
- [OpenMonetis Companion](https://github.com/felipegcoutinho/openmonetis-companion): Android app that captures bank notifications and sends them to the OpenMonetis inbox for review
|
||||||
|
|
||||||
BIN
public/logos/dinheiro.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
31
public/pdf.worker.min.mjs
Normal file
@@ -58,21 +58,26 @@ DATA_FILE="$BACKUP_DIR/openmonetis_${TIMESTAMP}.data.sql.gz"
|
|||||||
|
|
||||||
log "Iniciando backup (modo: $DB_MODE)..."
|
log "Iniciando backup (modo: $DB_MODE)..."
|
||||||
|
|
||||||
|
# Schemas relevantes do OpenMonetis (descarta lixo Supabase: auth, realtime, storage, vault, graphql, etc.)
|
||||||
|
SCHEMA_FLAGS=(--schema=public --schema=drizzle)
|
||||||
|
|
||||||
# --- Dump ---
|
# --- Dump ---
|
||||||
if [[ "$DB_MODE" == "remote" ]]; then
|
if [[ "$DB_MODE" == "remote" ]]; then
|
||||||
# --no-owner --no-privileges: necessário no Supabase (roles gerenciados internamente)
|
# --no-owner --no-privileges: necessário no Supabase (roles gerenciados internamente)
|
||||||
pg_dump --format=custom --no-owner --no-privileges \
|
pg_dump --format=custom --no-owner --no-privileges \
|
||||||
|
"${SCHEMA_FLAGS[@]}" \
|
||||||
"$REMOTE_DB_URL" > "$DUMP_FILE"
|
"$REMOTE_DB_URL" > "$DUMP_FILE"
|
||||||
|
|
||||||
pg_dump --no-owner --no-privileges \
|
pg_dump --no-owner --no-privileges \
|
||||||
|
"${SCHEMA_FLAGS[@]}" \
|
||||||
"$REMOTE_DB_URL" | gzip > "$SQL_FILE"
|
"$REMOTE_DB_URL" | gzip > "$SQL_FILE"
|
||||||
|
|
||||||
elif [[ "$DB_MODE" == "docker" ]]; then
|
elif [[ "$DB_MODE" == "docker" ]]; then
|
||||||
docker exec "$DOCKER_CONTAINER" pg_dump \
|
docker exec "$DOCKER_CONTAINER" pg_dump \
|
||||||
-U "$DOCKER_DB_USER" -Fc "$DOCKER_DB_NAME" > "$DUMP_FILE"
|
-U "$DOCKER_DB_USER" -Fc "${SCHEMA_FLAGS[@]}" "$DOCKER_DB_NAME" > "$DUMP_FILE"
|
||||||
|
|
||||||
docker exec "$DOCKER_CONTAINER" pg_dump \
|
docker exec "$DOCKER_CONTAINER" pg_dump \
|
||||||
-U "$DOCKER_DB_USER" "$DOCKER_DB_NAME" | gzip > "$SQL_FILE"
|
-U "$DOCKER_DB_USER" "${SCHEMA_FLAGS[@]}" "$DOCKER_DB_NAME" | gzip > "$SQL_FILE"
|
||||||
|
|
||||||
else
|
else
|
||||||
log "ERRO: DB_MODE inválido ('$DB_MODE'). Use 'remote' ou 'docker'."
|
log "ERRO: DB_MODE inválido ('$DB_MODE'). Use 'remote' ou 'docker'."
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
#!/usr/bin/env tsx
|
|
||||||
|
|
||||||
import { execSync } from "node:child_process";
|
|
||||||
import { config } from "dotenv";
|
|
||||||
|
|
||||||
// Carregar variáveis de ambiente
|
|
||||||
config();
|
|
||||||
|
|
||||||
const port = process.env.PORT || "3000";
|
|
||||||
|
|
||||||
console.log(`Starting Next.js development server on port ${port}...`);
|
|
||||||
|
|
||||||
// Executar next dev com a porta especificada
|
|
||||||
execSync(`npx next dev --turbopack --port ${port}`, {
|
|
||||||
stdio: "inherit",
|
|
||||||
env: { ...process.env, PORT: port },
|
|
||||||
});
|
|
||||||
245
scripts/install-deps.sh
Executable file
@@ -0,0 +1,245 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# install-deps.sh — Instala pré-requisitos do OpenMonetis
|
||||||
|
# Testado apenas em Ubuntu Server 24.04 LTS
|
||||||
|
# Uso: curl -fsSL https://raw.githubusercontent.com/felipegcoutinho/openmonetis/main/scripts/install-deps.sh -o install-deps.sh
|
||||||
|
# sudo sh install-deps.sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
LOG_FILE="/tmp/openmonetis-install.log"
|
||||||
|
> "$LOG_FILE"
|
||||||
|
|
||||||
|
# Suprimir prompt interativo do corepack ao chamar pnpm/node versioning
|
||||||
|
export COREPACK_ENABLE_DOWNLOAD_PROMPT=0
|
||||||
|
|
||||||
|
# ── Cores ──────────────────────────────────────────────────────────────────────
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
BOLD='\033[1m'
|
||||||
|
RESET='\033[0m'
|
||||||
|
|
||||||
|
ok() { printf "${GREEN}✔${RESET} %s\n" "$1"; }
|
||||||
|
warn() { printf "${YELLOW}!${RESET} %s\n" "$1"; }
|
||||||
|
info() { printf "${CYAN}→${RESET} %s\n" "$1"; }
|
||||||
|
fail() { printf "${RED}✗${RESET} %s\n" "$1"; exit 1; }
|
||||||
|
|
||||||
|
# ── Contador de etapas ─────────────────────────────────────────────────────────
|
||||||
|
_STEP=0
|
||||||
|
_TOTAL=5
|
||||||
|
|
||||||
|
section() {
|
||||||
|
_STEP=$((_STEP + 1))
|
||||||
|
printf "\n${BOLD}[%d/%d] %s${RESET}\n" "$_STEP" "$_TOTAL" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Spinner ────────────────────────────────────────────────────────────────────
|
||||||
|
_spin_pid=""
|
||||||
|
|
||||||
|
spinner_start() {
|
||||||
|
_spin_label="$1"
|
||||||
|
( i=0
|
||||||
|
while true; do
|
||||||
|
case $((i % 4)) in
|
||||||
|
0) d=" " ;; 1) d=". " ;; 2) d=".. " ;; *) d="..." ;;
|
||||||
|
esac
|
||||||
|
printf "\r${CYAN}→${RESET} %s%s" "$_spin_label" "$d"
|
||||||
|
i=$((i + 1))
|
||||||
|
sleep 0.4
|
||||||
|
done
|
||||||
|
) &
|
||||||
|
_spin_pid=$!
|
||||||
|
}
|
||||||
|
|
||||||
|
spinner_stop() {
|
||||||
|
if [ -n "$_spin_pid" ]; then
|
||||||
|
kill "$_spin_pid" 2>/dev/null || true
|
||||||
|
wait "$_spin_pid" 2>/dev/null || true
|
||||||
|
_spin_pid=""
|
||||||
|
printf "\r\033[2K"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Executores silenciosos com spinner ─────────────────────────────────────────
|
||||||
|
|
||||||
|
# run_quiet "label" cmd [args...] — roda comando com spinner, falha mostra log
|
||||||
|
run_quiet() {
|
||||||
|
_rq_label="$1"; shift
|
||||||
|
spinner_start "$_rq_label"
|
||||||
|
if ! "$@" >> "$LOG_FILE" 2>&1; then
|
||||||
|
spinner_stop
|
||||||
|
printf "${RED}✗ Falha em: %s${RESET}\n" "$_rq_label"
|
||||||
|
printf " Log completo: %s\n\n" "$LOG_FILE"
|
||||||
|
tail -20 "$LOG_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
spinner_stop
|
||||||
|
}
|
||||||
|
|
||||||
|
# run_as_user "label" "comando_shell" — roda comando como $CURRENT_USER com spinner
|
||||||
|
run_as_user() {
|
||||||
|
_ru_label="$1"; shift
|
||||||
|
spinner_start "$_ru_label"
|
||||||
|
if ! su - "$CURRENT_USER" -c "$*" >> "$LOG_FILE" 2>&1; then
|
||||||
|
spinner_stop
|
||||||
|
printf "${RED}✗ Falha em: %s${RESET}\n" "$_ru_label"
|
||||||
|
printf " Log completo: %s\n\n" "$LOG_FILE"
|
||||||
|
tail -20 "$LOG_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
spinner_stop
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Cleanup no Ctrl+C ──────────────────────────────────────────────────────────
|
||||||
|
cleanup() {
|
||||||
|
spinner_stop
|
||||||
|
printf "\n${YELLOW}Instalação interrompida.${RESET} Log em: %s\n" "$LOG_FILE"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
trap cleanup INT TERM
|
||||||
|
|
||||||
|
# ── Tempo total ────────────────────────────────────────────────────────────────
|
||||||
|
_START=$(date +%s)
|
||||||
|
elapsed() {
|
||||||
|
_secs=$(( $(date +%s) - _START ))
|
||||||
|
printf "%dm%ds" $((_secs / 60)) $((_secs % 60))
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Root check ─────────────────────────────────────────────────────────────────
|
||||||
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
|
fail "Execute como root ou com sudo: sudo sh install-deps.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
CURRENT_USER="${SUDO_USER:-$(whoami)}"
|
||||||
|
|
||||||
|
printf "\n${BOLD}OpenMonetis — Instalação de Dependências${RESET}\n"
|
||||||
|
printf "Usuário: ${CYAN}%s${RESET} | Log: %s\n" "$CURRENT_USER" "$LOG_FILE"
|
||||||
|
|
||||||
|
# ── [1/5] Dependências base ────────────────────────────────────────────────────
|
||||||
|
section "Dependências base"
|
||||||
|
run_quiet "Atualizando lista de pacotes" apt-get update -qq
|
||||||
|
run_quiet "Instalando git, curl, ca-certificates" apt-get install -y -qq ca-certificates curl git
|
||||||
|
ok "git $(git --version | cut -d' ' -f3) · curl · ca-certificates"
|
||||||
|
|
||||||
|
# ── [2/5] Docker ───────────────────────────────────────────────────────────────
|
||||||
|
section "Docker"
|
||||||
|
|
||||||
|
if command -v docker > /dev/null 2>&1; then
|
||||||
|
ok "Docker já instalado: $(docker --version | cut -d',' -f1)"
|
||||||
|
else
|
||||||
|
info "Adicionando repositório oficial do Docker..."
|
||||||
|
install -m 0755 -d /etc/apt/keyrings
|
||||||
|
run_quiet "Baixando chave GPG do Docker" \
|
||||||
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
|
||||||
|
chmod a+r /etc/apt/keyrings/docker.asc
|
||||||
|
|
||||||
|
. /etc/os-release
|
||||||
|
mkdir -p /etc/apt/sources.list.d
|
||||||
|
printf 'deb [arch=%s signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu %s stable\n' \
|
||||||
|
"$(dpkg --print-architecture)" "$VERSION_CODENAME" \
|
||||||
|
> /etc/apt/sources.list.d/docker.list
|
||||||
|
|
||||||
|
run_quiet "Atualizando lista de pacotes" apt-get update -qq
|
||||||
|
run_quiet "Instalando Docker Engine (pode levar alguns minutos)" \
|
||||||
|
apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||||
|
|
||||||
|
systemctl enable docker > /dev/null 2>&1 || true
|
||||||
|
systemctl start docker > /dev/null 2>&1 || true
|
||||||
|
ok "Docker $(docker --version | cut -d',' -f1 | cut -d' ' -f3) instalado"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if docker compose version > /dev/null 2>&1; then
|
||||||
|
ok "Docker Compose $(docker compose version | cut -d' ' -f4)"
|
||||||
|
else
|
||||||
|
run_quiet "Instalando Docker Compose plugin" \
|
||||||
|
sh -c 'mkdir -p /usr/local/lib/docker/cli-plugins && curl -fsSL "https://github.com/docker/compose/releases/latest/download/docker-compose-linux-$(uname -m)" -o /usr/local/lib/docker/cli-plugins/docker-compose && chmod +x /usr/local/lib/docker/cli-plugins/docker-compose'
|
||||||
|
ok "Docker Compose $(docker compose version | cut -d' ' -f4) instalado"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$CURRENT_USER" ] && [ "$CURRENT_USER" != "root" ]; then
|
||||||
|
if ! groups "$CURRENT_USER" | grep -q docker; then
|
||||||
|
usermod -aG docker "$CURRENT_USER"
|
||||||
|
warn "Usuário '$CURRENT_USER' adicionado ao grupo docker — faça logout/login para aplicar"
|
||||||
|
else
|
||||||
|
ok "Usuário '$CURRENT_USER' já está no grupo docker"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── [3/5] Homebrew ─────────────────────────────────────────────────────────────
|
||||||
|
section "Homebrew"
|
||||||
|
|
||||||
|
if command -v brew > /dev/null 2>&1; then
|
||||||
|
ok "Homebrew já instalado: $(brew --version | head -1)"
|
||||||
|
else
|
||||||
|
warn "Esta etapa pode levar de 5 a 10 minutos."
|
||||||
|
run_quiet "Instalando dependências de compilação" \
|
||||||
|
apt-get install -y -qq build-essential procps file
|
||||||
|
|
||||||
|
if [ -n "$CURRENT_USER" ] && [ "$CURRENT_USER" != "root" ]; then
|
||||||
|
run_as_user "Instalando Homebrew" \
|
||||||
|
'NONINTERACTIVE=1 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
|
||||||
|
|
||||||
|
BREW_PROFILE="/home/$CURRENT_USER/.bashrc"
|
||||||
|
BREW_EVAL='eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"'
|
||||||
|
grep -qxF "$BREW_EVAL" "$BREW_PROFILE" 2>/dev/null || echo "$BREW_EVAL" >> "$BREW_PROFILE"
|
||||||
|
export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH"
|
||||||
|
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
fail "Homebrew não pode ser instalado como root. Use sudo com um usuário normal."
|
||||||
|
fi
|
||||||
|
|
||||||
|
ok "Homebrew instalado"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── [4/5] Node.js 22 ───────────────────────────────────────────────────────────
|
||||||
|
section "Node.js 22"
|
||||||
|
|
||||||
|
NODE_MAJOR=0
|
||||||
|
if command -v node > /dev/null 2>&1; then
|
||||||
|
NODE_MAJOR=$(node -e "process.stdout.write(String(parseInt(process.versions.node)))")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$NODE_MAJOR" -ge 22 ] 2>/dev/null; then
|
||||||
|
ok "Node.js já instalado: $(node --version)"
|
||||||
|
else
|
||||||
|
warn "Node.js via Homebrew pode levar alguns minutos."
|
||||||
|
if [ -n "$CURRENT_USER" ] && [ "$CURRENT_USER" != "root" ]; then
|
||||||
|
run_as_user "Instalando Node.js 22" \
|
||||||
|
'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" && brew install node@22 && brew link node@22 --force --overwrite'
|
||||||
|
export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH"
|
||||||
|
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
fail "Node.js via Homebrew não pode ser instalado como root."
|
||||||
|
fi
|
||||||
|
ok "Node.js $(node --version) instalado"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── [5/5] pnpm ─────────────────────────────────────────────────────────────────
|
||||||
|
section "pnpm"
|
||||||
|
|
||||||
|
if command -v pnpm > /dev/null 2>&1; then
|
||||||
|
ok "pnpm já instalado: $(pnpm --version)"
|
||||||
|
else
|
||||||
|
if [ -n "$CURRENT_USER" ] && [ "$CURRENT_USER" != "root" ]; then
|
||||||
|
run_as_user "Instalando pnpm via corepack" \
|
||||||
|
'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" && corepack enable && COREPACK_ENABLE_DOWNLOAD_PROMPT=0 corepack prepare pnpm@latest --activate'
|
||||||
|
else
|
||||||
|
run_quiet "Instalando pnpm via corepack" \
|
||||||
|
sh -c 'corepack enable && COREPACK_ENABLE_DOWNLOAD_PROMPT=0 corepack prepare pnpm@latest --activate'
|
||||||
|
fi
|
||||||
|
ok "pnpm instalado"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Resumo ─────────────────────────────────────────────────────────────────────
|
||||||
|
# Garantir que node/pnpm do brew estejam no PATH para o resumo
|
||||||
|
export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH"
|
||||||
|
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" 2>/dev/null || true
|
||||||
|
|
||||||
|
printf "\n${BOLD}Concluído em $(elapsed)${RESET}\n"
|
||||||
|
|
||||||
|
ok "git: $(git --version | cut -d' ' -f3)"
|
||||||
|
ok "docker: $(docker --version | cut -d',' -f1 | cut -d' ' -f3)"
|
||||||
|
ok "docker compose: $(docker compose version | cut -d' ' -f4)"
|
||||||
|
ok "node: $(node --version)"
|
||||||
|
ok "pnpm: $(pnpm --version)"
|
||||||
1092
scripts/mock-data.ts
@@ -1,45 +0,0 @@
|
|||||||
import * as fs from "node:fs";
|
|
||||||
import * as path from "node:path";
|
|
||||||
import { config } from "dotenv";
|
|
||||||
import { drizzle } from "drizzle-orm/node-postgres";
|
|
||||||
import { Pool } from "pg";
|
|
||||||
|
|
||||||
// Load environment variables from .env
|
|
||||||
config();
|
|
||||||
|
|
||||||
async function initDatabase() {
|
|
||||||
const databaseUrl = process.env.DATABASE_URL;
|
|
||||||
|
|
||||||
if (!databaseUrl) {
|
|
||||||
console.error("DATABASE_URL environment variable is required");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const pool = new Pool({ connectionString: databaseUrl });
|
|
||||||
const db = drizzle(pool);
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log("🔧 Initializing database extensions...");
|
|
||||||
|
|
||||||
// Read and execute init.sql as a single query
|
|
||||||
const initSqlPath = path.join(
|
|
||||||
process.cwd(),
|
|
||||||
"scripts",
|
|
||||||
"postgres",
|
|
||||||
"init.sql",
|
|
||||||
);
|
|
||||||
const initSql = fs.readFileSync(initSqlPath, "utf-8");
|
|
||||||
|
|
||||||
console.log("Executing init.sql...");
|
|
||||||
await db.execute(initSql);
|
|
||||||
|
|
||||||
console.log("✅ Database initialization completed");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Database initialization failed:", error);
|
|
||||||
process.exit(1);
|
|
||||||
} finally {
|
|
||||||
await pool.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
initDatabase();
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
-- Script de inicialização do PostgreSQL para Docker
|
|
||||||
-- Este script é executado automaticamente quando o banco é criado pela primeira vez
|
|
||||||
|
|
||||||
-- Habilitar extensão pgcrypto (necessária para gen_random_bytes usado pelo Drizzle)
|
|
||||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
||||||
-- Log de sucesso
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
RAISE NOTICE '✅ Extensão pgcrypto habilitada com sucesso';
|
|
||||||
END $$;
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Script para configurar ambiente de forma segura
|
|
||||||
# Cria backup do .env atual antes de sobrescrever
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "🔧 Configurando ambiente..."
|
|
||||||
|
|
||||||
# Se .env já existe, criar backup
|
|
||||||
if [ -f .env ]; then
|
|
||||||
BACKUP_FILE=".env.backup.$(date +%Y%m%d_%H%M%S)"
|
|
||||||
echo "⚠️ Arquivo .env existente detectado!"
|
|
||||||
echo "📦 Criando backup em: $BACKUP_FILE"
|
|
||||||
cp .env "$BACKUP_FILE"
|
|
||||||
echo "✅ Backup criado com sucesso!"
|
|
||||||
echo ""
|
|
||||||
read -p "Deseja sobrescrever o .env atual com .env.example? (s/N) " -n 1 -r
|
|
||||||
echo
|
|
||||||
if [[ ! $REPLY =~ ^[Ss]$ ]]; then
|
|
||||||
echo "❌ Operação cancelada. Seu .env não foi modificado."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Copiar .env.example para .env
|
|
||||||
if [ -f .env.example ]; then
|
|
||||||
cp .env.example .env
|
|
||||||
echo "✅ Arquivo .env criado a partir de .env.example"
|
|
||||||
else
|
|
||||||
echo "❌ Erro: .env.example não encontrado!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Gerar BETTER_AUTH_SECRET automaticamente
|
|
||||||
if command -v openssl &> /dev/null; then
|
|
||||||
SECRET=$(openssl rand -base64 32)
|
|
||||||
sed -i.bak "s|BETTER_AUTH_SECRET=.*|BETTER_AUTH_SECRET=$SECRET|" .env && rm -f .env.bak
|
|
||||||
echo "✅ BETTER_AUTH_SECRET gerado automaticamente"
|
|
||||||
else
|
|
||||||
echo "⚠️ openssl não encontrado — configure BETTER_AUTH_SECRET manualmente:"
|
|
||||||
echo " openssl rand -base64 32"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "⚠️ IMPORTANTE: Edite o arquivo .env e configure:"
|
|
||||||
echo " - DATABASE_URL"
|
|
||||||
echo " - BETTER_AUTH_URL"
|
|
||||||
echo " - Demais variáveis opcionais (OAuth, e-mail, IA)"
|
|
||||||
39
setup.mjs
@@ -21,6 +21,7 @@ const c = {
|
|||||||
red: "\x1b[31m",
|
red: "\x1b[31m",
|
||||||
yellow: "\x1b[33m",
|
yellow: "\x1b[33m",
|
||||||
cyan: "\x1b[36m",
|
cyan: "\x1b[36m",
|
||||||
|
orange: "\x1b[38;5;214m",
|
||||||
};
|
};
|
||||||
|
|
||||||
const sym = {
|
const sym = {
|
||||||
@@ -81,10 +82,38 @@ function abort(msg) {
|
|||||||
|
|
||||||
// ─── Header ──────────────────────────────────────────────────────────────────
|
// ─── Header ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
console.log(`
|
const logoLines = [
|
||||||
${c.bold}${c.cyan} OpenMonetis — Setup${c.reset}
|
".............................+@@@@@@@@@@=.............................",
|
||||||
${c.dim}Gestão financeira self-hosted${c.reset}
|
".............................@@@@@@@@@@@:.............................",
|
||||||
`);
|
"...................+@@@@@@*-:@@@@@@@@@@%...=@@@@@@-...................",
|
||||||
|
"..................@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%..................",
|
||||||
|
"................=@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+................",
|
||||||
|
"...................-=+%@@@@@@@@@@@@@@@@@@@@@*:........................",
|
||||||
|
".......................#@@@@@@@@@@@@@@@@@@@@@@@+......................",
|
||||||
|
"....................%@@@@@@@@@@@@@%#@@@@@@@@@@@@*.....................",
|
||||||
|
"....................+@@@@@@@@@@@......*@@@@@@#........................",
|
||||||
|
".........................:#@@=...........+#...........................",
|
||||||
|
];
|
||||||
|
|
||||||
|
const nameLines = [
|
||||||
|
" ___ __ __ _ _ ",
|
||||||
|
" / _ \\ _ __ ___ _ __ | \\/ | ___ _ __ ___| |_(_)___ ",
|
||||||
|
" | | | | '_ \\ / _ \\ '_ \\| |\\/| |/ _ \\| '_ \\ / _ \\ __| / __|",
|
||||||
|
" | |_| | |_) | __/ | | | | | | (_) | | | | __/ |_| \\__ \\",
|
||||||
|
" \\___/| .__/ \\___|_| |_|_| |_|\\___/|_| |_|\\___|\\__|_|___/",
|
||||||
|
" |_| ",
|
||||||
|
];
|
||||||
|
|
||||||
|
const nameStart = Math.floor((logoLines.length - nameLines.length) / 2);
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
for (let i = 0; i < logoLines.length; i++) {
|
||||||
|
const logoCol = c.orange + logoLines[i].replaceAll(".", " ").substring(14, 56).padEnd(42) + c.reset;
|
||||||
|
const nameIdx = i - nameStart;
|
||||||
|
const nameCol = nameIdx >= 0 && nameIdx < nameLines.length ? nameLines[nameIdx] : "";
|
||||||
|
console.log(logoCol + " " + nameCol);
|
||||||
|
}
|
||||||
|
console.log(`\n${" ".repeat(46)}${c.dim}Gestão financeira · self-hosted${c.reset}\n`);
|
||||||
|
|
||||||
// ─── ETAPA 1: Verificações do sistema ────────────────────────────────────────
|
// ─── ETAPA 1: Verificações do sistema ────────────────────────────────────────
|
||||||
|
|
||||||
@@ -329,7 +358,7 @@ if (useLocalDocker) {
|
|||||||
// Extensões
|
// Extensões
|
||||||
s = spinner("Habilitando extensões do banco...");
|
s = spinner("Habilitando extensões do banco...");
|
||||||
try {
|
try {
|
||||||
run("pnpm db:enableExtensions", { cwd: targetDir });
|
run("pnpm db:extensions", { cwd: targetDir });
|
||||||
s.stop("Extensões habilitadas");
|
s.stop("Extensões habilitadas");
|
||||||
} catch {
|
} catch {
|
||||||
s.fail("Falha ao habilitar extensões");
|
s.fail("Falha ao habilitar extensões");
|
||||||
|
|||||||
23
src/app/(auth)/layout.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Logo } from "@/shared/components/brand/logo";
|
||||||
|
|
||||||
|
export default function AuthLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="relative flex min-h-svh flex-col items-center justify-center overflow-hidden bg-linear-to-b from-background via-background to-muted/20 px-5 py-8 md:px-8 md:py-10">
|
||||||
|
<div className="pointer-events-none absolute inset-0 overflow-hidden flex items-center justify-center">
|
||||||
|
<div className="absolute -right-32 top-0 h-96 w-96 rounded-full bg-primary/10 blur-3xl animate-blob mix-blend-multiply" />
|
||||||
|
<div className="absolute -left-32 bottom-0 h-96 w-96 rounded-full bg-primary/7 blur-3xl animate-blob animation-delay-2000 mix-blend-multiply" />
|
||||||
|
<div className="absolute -bottom-32 left-1/2 h-80 w-80 rounded-full bg-secondary/30 blur-3xl animate-blob animation-delay-4000 mix-blend-multiply" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative mb-6 flex md:hidden z-20">
|
||||||
|
<Logo variant="compact" colorIcon />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative w-full max-w-sm md:max-w-5xl">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,11 +1,5 @@
|
|||||||
import { LoginForm } from "@/features/auth/components/login-form";
|
import { LoginForm } from "@/features/auth/components/login-form";
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
return (
|
return <LoginForm />;
|
||||||
<div className="flex min-h-svh flex-col items-center justify-center bg-linear-to-b from-background via-background to-muted/20 px-5 py-8 md:px-8 md:py-10">
|
|
||||||
<div className="w-full max-w-sm md:max-w-5xl">
|
|
||||||
<LoginForm />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
import { SignupForm } from "@/features/auth/components/signup-form";
|
import { SignupForm } from "@/features/auth/components/signup-form";
|
||||||
|
|
||||||
export default function Page() {
|
export default function SignupPage() {
|
||||||
return (
|
return <SignupForm />;
|
||||||
<div className="flex min-h-svh flex-col items-center justify-center bg-linear-to-b from-background via-background to-muted/20 px-5 py-8 md:px-8 md:py-10">
|
|
||||||
<div className="w-full max-w-sm md:max-w-5xl">
|
|
||||||
<SignupForm />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { RiPencilLine } from "@remixicon/react";
|
import { RiPencilLine } from "@remixicon/react";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
|
import { connection } from "next/server";
|
||||||
import { AccountDialog } from "@/features/accounts/components/account-dialog";
|
import { AccountDialog } from "@/features/accounts/components/account-dialog";
|
||||||
import { AccountStatementCard } from "@/features/accounts/components/account-statement-card";
|
import { AccountStatementCard } from "@/features/accounts/components/account-statement-card";
|
||||||
|
import { AdjustBalanceDialog } from "@/features/accounts/components/adjust-balance-dialog";
|
||||||
import type { Account } from "@/features/accounts/components/types";
|
import type { Account } from "@/features/accounts/components/types";
|
||||||
import {
|
import {
|
||||||
fetchAccountData,
|
fetchAccountData,
|
||||||
fetchAccountLancamentosPage,
|
|
||||||
fetchAccountSummary,
|
fetchAccountSummary,
|
||||||
|
fetchAccountTransactionsPage,
|
||||||
} from "@/features/accounts/statement-queries";
|
} from "@/features/accounts/statement-queries";
|
||||||
import { fetchUserPreferences } from "@/features/settings/queries";
|
import { fetchUserPreferences } from "@/features/settings/queries";
|
||||||
import { TransactionsPage as LancamentosSection } from "@/features/transactions/components/page/transactions-page";
|
import { TransactionsPage as LancamentosSection } from "@/features/transactions/components/page/transactions-page";
|
||||||
@@ -20,7 +22,7 @@ import {
|
|||||||
mapTransactionsData,
|
mapTransactionsData,
|
||||||
type ResolvedSearchParams,
|
type ResolvedSearchParams,
|
||||||
resolveTransactionPagination,
|
resolveTransactionPagination,
|
||||||
} from "@/features/transactions/page-helpers";
|
} from "@/features/transactions/lib/page-helpers";
|
||||||
import {
|
import {
|
||||||
fetchRecentEstablishments,
|
fetchRecentEstablishments,
|
||||||
fetchTransactionFilterSources,
|
fetchTransactionFilterSources,
|
||||||
@@ -42,6 +44,7 @@ const capitalize = (value: string) =>
|
|||||||
value.length > 0 ? value[0]?.toUpperCase().concat(value.slice(1)) : value;
|
value.length > 0 ? value[0]?.toUpperCase().concat(value.slice(1)) : value;
|
||||||
|
|
||||||
export default async function Page({ params, searchParams }: PageProps) {
|
export default async function Page({ params, searchParams }: PageProps) {
|
||||||
|
await connection();
|
||||||
const { accountId } = await params;
|
const { accountId } = await params;
|
||||||
const userId = await getUserId();
|
const userId = await getUserId();
|
||||||
const resolvedSearchParams = searchParams ? await searchParams : undefined;
|
const resolvedSearchParams = searchParams ? await searchParams : undefined;
|
||||||
@@ -86,7 +89,7 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
accountId: account.id,
|
accountId: account.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const transactionsPage = await fetchAccountLancamentosPage(
|
const transactionsPage = await fetchAccountTransactionsPage(
|
||||||
filters,
|
filters,
|
||||||
pagination,
|
pagination,
|
||||||
);
|
);
|
||||||
@@ -139,6 +142,13 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
totalIncomes={totalIncomes}
|
totalIncomes={totalIncomes}
|
||||||
totalExpenses={totalExpenses}
|
totalExpenses={totalExpenses}
|
||||||
logo={account.logo}
|
logo={account.logo}
|
||||||
|
balanceAdjustment={
|
||||||
|
<AdjustBalanceDialog
|
||||||
|
accountId={account.id}
|
||||||
|
period={selectedPeriod}
|
||||||
|
currentBalance={currentBalance}
|
||||||
|
/>
|
||||||
|
}
|
||||||
actions={
|
actions={
|
||||||
<AccountDialog
|
<AccountDialog
|
||||||
mode="update"
|
mode="update"
|
||||||
@@ -190,6 +200,7 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
allowCreate={false}
|
allowCreate={false}
|
||||||
noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
|
noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
|
||||||
columnOrder={userPreferences?.transactionsColumnOrder ?? null}
|
columnOrder={userPreferences?.transactionsColumnOrder ?? null}
|
||||||
|
attachmentMaxSizeMb={userPreferences?.attachmentMaxSizeMb ?? 50}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="space-y-6 pt-4">
|
<section className="space-y-6">
|
||||||
<PageDescription
|
<PageDescription
|
||||||
icon={<RiBankLine />}
|
icon={<RiBankLine />}
|
||||||
title="Contas"
|
title="Contas"
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import { connection } from "next/server";
|
||||||
import { AccountsPage } from "@/features/accounts/components/accounts-page";
|
import { AccountsPage } from "@/features/accounts/components/accounts-page";
|
||||||
import { fetchAllAccountsForUser } from "@/features/accounts/queries";
|
import { fetchAllAccountsForUser } from "@/features/accounts/queries";
|
||||||
import { getUserId } from "@/shared/lib/auth/server";
|
import { getUserId } from "@/shared/lib/auth/server";
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
|
await connection();
|
||||||
const userId = await getUserId();
|
const userId = await getUserId();
|
||||||
const { activeAccounts, archivedAccounts, logoOptions } =
|
const { activeAccounts, archivedAccounts, logoOptions } =
|
||||||
await fetchAllAccountsForUser(userId);
|
await fetchAllAccountsForUser(userId);
|
||||||
|
|||||||
25
src/app/(dashboard)/attachments/layout.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { RiAttachmentLine } from "@remixicon/react";
|
||||||
|
import MonthNavigation from "@/shared/components/month-picker/month-navigation";
|
||||||
|
import PageDescription from "@/shared/components/page-description";
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: "Anexos",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<section className="space-y-6">
|
||||||
|
<PageDescription
|
||||||
|
icon={<RiAttachmentLine />}
|
||||||
|
title="Anexos"
|
||||||
|
subtitle="Gerencie os anexos das suas transações"
|
||||||
|
/>
|
||||||
|
<MonthNavigation />
|
||||||
|
{children}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
src/app/(dashboard)/attachments/loading.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Skeleton } from "@/shared/components/ui/skeleton";
|
||||||
|
|
||||||
|
export default function AnexosLoading() {
|
||||||
|
return (
|
||||||
|
<main className="flex flex-col gap-6">
|
||||||
|
<div className="w-full space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<Skeleton className="h-10 w-40 rounded-md bg-foreground/10" />
|
||||||
|
|
||||||
|
{/* Month navigation */}
|
||||||
|
<Skeleton className="h-10 w-64 rounded-md bg-foreground/10" />
|
||||||
|
|
||||||
|
{/* Count */}
|
||||||
|
<Skeleton className="h-4 w-20 rounded-md bg-foreground/10" />
|
||||||
|
|
||||||
|
{/* Grid */}
|
||||||
|
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
||||||
|
{Array.from({ length: 10 }).map((_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="flex flex-col overflow-hidden rounded-lg border"
|
||||||
|
>
|
||||||
|
<Skeleton className="aspect-square w-full bg-foreground/10" />
|
||||||
|
<div className="space-y-1.5 p-2.5">
|
||||||
|
<Skeleton className="h-3 w-3/4 rounded bg-foreground/10" />
|
||||||
|
<Skeleton className="h-3 w-full rounded bg-foreground/10" />
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<Skeleton className="h-3 w-16 rounded bg-foreground/10" />
|
||||||
|
<Skeleton className="h-3 w-12 rounded bg-foreground/10" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
src/app/(dashboard)/attachments/page.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { connection } from "next/server";
|
||||||
|
import { AttachmentsPage } from "@/features/attachments/components/attachments-page";
|
||||||
|
import { fetchAttachmentsForPeriod } from "@/features/attachments/queries";
|
||||||
|
import { getUserId } from "@/shared/lib/auth/server";
|
||||||
|
import { parsePeriodParam } from "@/shared/utils/period";
|
||||||
|
|
||||||
|
type PageSearchParams = Promise<Record<string, string | string[] | undefined>>;
|
||||||
|
|
||||||
|
type PageProps = {
|
||||||
|
searchParams?: PageSearchParams;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSingleParam = (
|
||||||
|
params: Record<string, string | string[] | undefined> | undefined,
|
||||||
|
key: string,
|
||||||
|
) => {
|
||||||
|
const value = params?.[key];
|
||||||
|
if (!value) return null;
|
||||||
|
return Array.isArray(value) ? (value[0] ?? null) : value;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function Page({ searchParams }: PageProps) {
|
||||||
|
await connection();
|
||||||
|
const userId = await getUserId();
|
||||||
|
const resolvedSearchParams = searchParams ? await searchParams : undefined;
|
||||||
|
const periodoParam = getSingleParam(resolvedSearchParams, "periodo");
|
||||||
|
const { period } = parsePeriodParam(periodoParam);
|
||||||
|
|
||||||
|
const attachments = await fetchAttachmentsForPeriod(userId, period);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="flex flex-col gap-6">
|
||||||
|
<AttachmentsPage attachments={attachments} />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="space-y-6 pt-4">
|
<section className="space-y-6">
|
||||||
<PageDescription
|
<PageDescription
|
||||||
icon={<RiBarChart2Line />}
|
icon={<RiBarChart2Line />}
|
||||||
title="Orçamentos"
|
title="Orçamentos"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { connection } from "next/server";
|
||||||
import { BudgetsPage } from "@/features/budgets/components/budgets-page";
|
import { BudgetsPage } from "@/features/budgets/components/budgets-page";
|
||||||
import { fetchBudgetsForUser } from "@/features/budgets/queries";
|
import { fetchBudgetsForUser } from "@/features/budgets/queries";
|
||||||
import MonthNavigation from "@/shared/components/month-picker/month-navigation";
|
import MonthNavigation from "@/shared/components/month-picker/month-navigation";
|
||||||
@@ -19,22 +20,12 @@ const getSingleParam = (
|
|||||||
return Array.isArray(value) ? (value[0] ?? null) : value;
|
return Array.isArray(value) ? (value[0] ?? null) : value;
|
||||||
};
|
};
|
||||||
|
|
||||||
const capitalize = (value: string) =>
|
|
||||||
value.length === 0 ? value : value[0]?.toUpperCase() + value.slice(1);
|
|
||||||
|
|
||||||
export default async function Page({ searchParams }: PageProps) {
|
export default async function Page({ searchParams }: PageProps) {
|
||||||
|
await connection();
|
||||||
const userId = await getUserId();
|
const userId = await getUserId();
|
||||||
const resolvedSearchParams = searchParams ? await searchParams : undefined;
|
const resolvedSearchParams = searchParams ? await searchParams : undefined;
|
||||||
const periodoParam = getSingleParam(resolvedSearchParams, "periodo");
|
const periodoParam = getSingleParam(resolvedSearchParams, "periodo");
|
||||||
|
const { period: selectedPeriod } = parsePeriodParam(periodoParam);
|
||||||
const {
|
|
||||||
period: selectedPeriod,
|
|
||||||
monthName: rawMonthName,
|
|
||||||
year,
|
|
||||||
} = parsePeriodParam(periodoParam);
|
|
||||||
|
|
||||||
const periodLabel = `${capitalize(rawMonthName)} ${year}`;
|
|
||||||
|
|
||||||
const { budgets, categoriesOptions } = await fetchBudgetsForUser(
|
const { budgets, categoriesOptions } = await fetchBudgetsForUser(
|
||||||
userId,
|
userId,
|
||||||
selectedPeriod,
|
selectedPeriod,
|
||||||
@@ -47,7 +38,6 @@ export default async function Page({ searchParams }: PageProps) {
|
|||||||
budgets={budgets}
|
budgets={budgets}
|
||||||
categories={categoriesOptions}
|
categories={categoriesOptions}
|
||||||
selectedPeriod={selectedPeriod}
|
selectedPeriod={selectedPeriod}
|
||||||
periodLabel={periodLabel}
|
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="space-y-6 pt-4">
|
<section className="space-y-6">
|
||||||
<PageDescription
|
<PageDescription
|
||||||
icon={<RiCalendarEventLine />}
|
icon={<RiCalendarEventLine />}
|
||||||
title="Calendário"
|
title="Calendário"
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import { connection } from "next/server";
|
||||||
import { MonthlyCalendar } from "@/features/calendar/components/monthly-calendar";
|
import { MonthlyCalendar } from "@/features/calendar/components/monthly-calendar";
|
||||||
import { fetchCalendarData } from "@/features/calendar/queries";
|
import { fetchCalendarData } from "@/features/calendar/queries";
|
||||||
import {
|
import {
|
||||||
getSingleParam,
|
getSingleParam,
|
||||||
type ResolvedSearchParams,
|
type ResolvedSearchParams,
|
||||||
} from "@/features/transactions/page-helpers";
|
} from "@/features/transactions/lib/page-helpers";
|
||||||
import MonthNavigation from "@/shared/components/month-picker/month-navigation";
|
import MonthNavigation from "@/shared/components/month-picker/month-navigation";
|
||||||
import { getUserId } from "@/shared/lib/auth/server";
|
import { getUserId } from "@/shared/lib/auth/server";
|
||||||
import type { CalendarPeriod } from "@/shared/lib/types/calendar";
|
import type { CalendarPeriod } from "@/shared/lib/types/calendar";
|
||||||
@@ -16,6 +17,7 @@ type PageProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function Page({ searchParams }: PageProps) {
|
export default async function Page({ searchParams }: PageProps) {
|
||||||
|
await connection();
|
||||||
const userId = await getUserId();
|
const userId = await getUserId();
|
||||||
const resolvedParams = searchParams ? await searchParams : undefined;
|
const resolvedParams = searchParams ? await searchParams : undefined;
|
||||||
|
|
||||||
@@ -34,7 +36,7 @@ export default async function Page({ searchParams }: PageProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col gap-3">
|
<main className="flex flex-col gap-4">
|
||||||
<MonthNavigation />
|
<MonthNavigation />
|
||||||
<MonthlyCalendar
|
<MonthlyCalendar
|
||||||
period={calendarPeriod}
|
period={calendarPeriod}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { RiPencilLine } from "@remixicon/react";
|
import { RiPencilLine } from "@remixicon/react";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
|
import { connection } from "next/server";
|
||||||
import type { FinancialAccount } from "@/db/schema";
|
import type { FinancialAccount } from "@/db/schema";
|
||||||
import { CardDialog } from "@/features/cards/components/card-dialog";
|
import { CardDialog } from "@/features/cards/components/card-dialog";
|
||||||
import type { Card } from "@/features/cards/components/types";
|
import type { Card } from "@/features/cards/components/types";
|
||||||
@@ -20,7 +21,7 @@ import {
|
|||||||
getSingleParam,
|
getSingleParam,
|
||||||
mapTransactionsData,
|
mapTransactionsData,
|
||||||
type ResolvedSearchParams,
|
type ResolvedSearchParams,
|
||||||
} from "@/features/transactions/page-helpers";
|
} from "@/features/transactions/lib/page-helpers";
|
||||||
import {
|
import {
|
||||||
fetchRecentEstablishments,
|
fetchRecentEstablishments,
|
||||||
fetchTransactionFilterSources,
|
fetchTransactionFilterSources,
|
||||||
@@ -39,6 +40,7 @@ type PageProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function Page({ params, searchParams }: PageProps) {
|
export default async function Page({ params, searchParams }: PageProps) {
|
||||||
|
await connection();
|
||||||
const { cardId } = await params;
|
const { cardId } = await params;
|
||||||
const userId = await getUserId();
|
const userId = await getUserId();
|
||||||
const resolvedSearchParams = searchParams ? await searchParams : undefined;
|
const resolvedSearchParams = searchParams ? await searchParams : undefined;
|
||||||
@@ -116,6 +118,8 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
financialAccount.id === card.accountId,
|
financialAccount.id === card.accountId,
|
||||||
)?.name ?? "Conta";
|
)?.name ?? "Conta";
|
||||||
|
|
||||||
|
const limitAmount = Number(card.limit);
|
||||||
|
|
||||||
const cardDialogData: Card = {
|
const cardDialogData: Card = {
|
||||||
id: card.id,
|
id: card.id,
|
||||||
name: card.name,
|
name: card.name,
|
||||||
@@ -125,19 +129,14 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
dueDay: card.dueDay,
|
dueDay: card.dueDay,
|
||||||
note: card.note ?? null,
|
note: card.note ?? null,
|
||||||
logo: card.logo,
|
logo: card.logo,
|
||||||
limit:
|
limit: limitAmount,
|
||||||
card.limit !== null && card.limit !== undefined
|
|
||||||
? Number(card.limit)
|
|
||||||
: null,
|
|
||||||
accountId: card.accountId,
|
accountId: card.accountId,
|
||||||
accountName,
|
accountName,
|
||||||
limitInUse: 0,
|
limitInUse: 0,
|
||||||
limitAvailable: null,
|
limitAvailable: limitAmount,
|
||||||
};
|
};
|
||||||
|
|
||||||
const { totalAmount, invoiceStatus, paymentDate } = invoiceData;
|
const { totalAmount, invoiceStatus, paymentDate } = invoiceData;
|
||||||
const limitAmount =
|
|
||||||
card.limit !== null && card.limit !== undefined ? Number(card.limit) : null;
|
|
||||||
|
|
||||||
const periodLabel = `${monthName.charAt(0).toUpperCase()}${monthName.slice(
|
const periodLabel = `${monthName.charAt(0).toUpperCase()}${monthName.slice(
|
||||||
1,
|
1,
|
||||||
@@ -161,6 +160,12 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
limitAmount={limitAmount}
|
limitAmount={limitAmount}
|
||||||
invoiceStatus={invoiceStatus}
|
invoiceStatus={invoiceStatus}
|
||||||
paymentDate={paymentDate}
|
paymentDate={paymentDate}
|
||||||
|
defaultPaymentAccountId={card.accountId}
|
||||||
|
paymentAccountOptions={accountOptions.map((option) => ({
|
||||||
|
value: option.value,
|
||||||
|
label: option.label,
|
||||||
|
logo: option.logo ?? null,
|
||||||
|
}))}
|
||||||
logo={card.logo}
|
logo={card.logo}
|
||||||
actions={
|
actions={
|
||||||
<CardDialog
|
<CardDialog
|
||||||
@@ -202,6 +207,7 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
allowCreate
|
allowCreate
|
||||||
noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
|
noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
|
||||||
columnOrder={userPreferences?.transactionsColumnOrder ?? null}
|
columnOrder={userPreferences?.transactionsColumnOrder ?? null}
|
||||||
|
attachmentMaxSizeMb={userPreferences?.attachmentMaxSizeMb ?? 50}
|
||||||
defaultCardId={card.id}
|
defaultCardId={card.id}
|
||||||
defaultPaymentMethod="Cartão de crédito"
|
defaultPaymentMethod="Cartão de crédito"
|
||||||
lockCardSelection
|
lockCardSelection
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="space-y-6 pt-4">
|
<section className="space-y-6">
|
||||||
<PageDescription
|
<PageDescription
|
||||||
icon={<RiBankCard2Line />}
|
icon={<RiBankCard2Line />}
|
||||||
title="Cartões"
|
title="Cartões"
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
|
import { connection } from "next/server";
|
||||||
import { CardsPage } from "@/features/cards/components/cards-page";
|
import { CardsPage } from "@/features/cards/components/cards-page";
|
||||||
import { fetchAllCardsForUser } from "@/features/cards/queries";
|
import { fetchAllCardsForUser } from "@/features/cards/queries";
|
||||||
import { getUserId } from "@/shared/lib/auth/server";
|
import { getUserId } from "@/shared/lib/auth/server";
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
|
await connection();
|
||||||
const userId = await getUserId();
|
const userId = await getUserId();
|
||||||
const { activeCards, archivedCards, accounts, logoOptions } =
|
const { activeCards, archivedCards, accounts, logoOptions } =
|
||||||
await fetchAllCardsForUser(userId);
|
await fetchAllCardsForUser(userId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col items-start gap-6">
|
<main className="flex flex-col gap-6">
|
||||||
<CardsPage
|
<CardsPage
|
||||||
cards={activeCards}
|
cards={activeCards}
|
||||||
archivedCards={archivedCards}
|
archivedCards={archivedCards}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
|
import { connection } from "next/server";
|
||||||
import { CategoryDetailHeader } from "@/features/categories/components/category-detail-header";
|
import { CategoryDetailHeader } from "@/features/categories/components/category-detail-header";
|
||||||
import { fetchCategoryDetails } from "@/features/dashboard/categories/category-details-queries";
|
import { fetchCategoryDetails } from "@/features/dashboard/categories/category-details-queries";
|
||||||
import { fetchUserPreferences } from "@/features/settings/queries";
|
import { fetchUserPreferences } from "@/features/settings/queries";
|
||||||
@@ -6,7 +7,7 @@ import { TransactionsPage } from "@/features/transactions/components/page/transa
|
|||||||
import {
|
import {
|
||||||
buildOptionSets,
|
buildOptionSets,
|
||||||
buildSluggedFilters,
|
buildSluggedFilters,
|
||||||
} from "@/features/transactions/page-helpers";
|
} from "@/features/transactions/lib/page-helpers";
|
||||||
import {
|
import {
|
||||||
fetchRecentEstablishments,
|
fetchRecentEstablishments,
|
||||||
fetchTransactionFilterSources,
|
fetchTransactionFilterSources,
|
||||||
@@ -32,6 +33,7 @@ const getSingleParam = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function Page({ params, searchParams }: PageProps) {
|
export default async function Page({ params, searchParams }: PageProps) {
|
||||||
|
await connection();
|
||||||
const { categoryId } = await params;
|
const { categoryId } = await params;
|
||||||
const userId = await getUserId();
|
const userId = await getUserId();
|
||||||
const resolvedSearchParams = searchParams ? await searchParams : undefined;
|
const resolvedSearchParams = searchParams ? await searchParams : undefined;
|
||||||
@@ -99,6 +101,7 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
allowCreate={true}
|
allowCreate={true}
|
||||||
noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
|
noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
|
||||||
columnOrder={userPreferences?.transactionsColumnOrder ?? null}
|
columnOrder={userPreferences?.transactionsColumnOrder ?? null}
|
||||||
|
attachmentMaxSizeMb={userPreferences?.attachmentMaxSizeMb ?? 50}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import { connection } from "next/server";
|
||||||
import { fetchCategoryHistory } from "@/features/dashboard/categories/category-history-queries";
|
import { fetchCategoryHistory } from "@/features/dashboard/categories/category-history-queries";
|
||||||
import { CategoryHistoryWidget } from "@/features/dashboard/components/category-history-widget";
|
import { CategoryHistoryWidget } from "@/features/dashboard/components/widgets/category-history-widget";
|
||||||
import { getUser } from "@/shared/lib/auth/server";
|
import { getUser } from "@/shared/lib/auth/server";
|
||||||
import { getCurrentPeriod } from "@/shared/utils/period";
|
import { getCurrentPeriod } from "@/shared/utils/period";
|
||||||
|
|
||||||
export default async function HistoricoCategoriasPage() {
|
export default async function HistoricoCategoriasPage() {
|
||||||
|
await connection();
|
||||||
const user = await getUser();
|
const user = await getUser();
|
||||||
const currentPeriod = getCurrentPeriod();
|
const currentPeriod = getCurrentPeriod();
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="space-y-6 pt-4">
|
<section className="space-y-6">
|
||||||
<PageDescription
|
<PageDescription
|
||||||
icon={<RiPriceTag3Line />}
|
icon={<RiPriceTag3Line />}
|
||||||
title="Categorias"
|
title="Categorias"
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
|
import { connection } from "next/server";
|
||||||
import { CategoriesPage } from "@/features/categories/components/categories-page";
|
import { CategoriesPage } from "@/features/categories/components/categories-page";
|
||||||
import { fetchCategoriesForUser } from "@/features/categories/queries";
|
import { fetchCategoriesForUser } from "@/features/categories/queries";
|
||||||
import { getUserId } from "@/shared/lib/auth/server";
|
import { getUserId } from "@/shared/lib/auth/server";
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
|
await connection();
|
||||||
const userId = await getUserId();
|
const userId = await getUserId();
|
||||||
const categories = await fetchCategoriesForUser(userId);
|
const categories = await fetchCategoriesForUser(userId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col items-start gap-6">
|
<main className="flex flex-col gap-6">
|
||||||
<CategoriesPage categories={categories} />
|
<CategoriesPage categories={categories} />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="space-y-6 pt-4">
|
<section className="space-y-6">
|
||||||
<PageDescription
|
<PageDescription
|
||||||
icon={<RiHistoryLine />}
|
icon={<RiHistoryLine />}
|
||||||
title="Changelog"
|
title="Changelog"
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
|
import { connection } from "next/server";
|
||||||
import { DashboardGridEditable } from "@/features/dashboard/components/dashboard-grid-editable";
|
import { DashboardGridEditable } from "@/features/dashboard/components/dashboard-grid-editable";
|
||||||
import { DashboardMetricsCards } from "@/features/dashboard/components/dashboard-metrics-cards";
|
import { DashboardMetricsCards } from "@/features/dashboard/components/dashboard-metrics-cards";
|
||||||
import { DashboardWelcome } from "@/features/dashboard/components/dashboard-welcome";
|
import { DashboardWelcome } from "@/features/dashboard/components/dashboard-welcome";
|
||||||
|
import { extractDashboardLogoNames } from "@/features/dashboard/lib/extract-logo-names";
|
||||||
import { fetchDashboardPageData } from "@/features/dashboard/page-data-queries";
|
import { fetchDashboardPageData } from "@/features/dashboard/page-data-queries";
|
||||||
import { getSingleParam } from "@/features/transactions/page-helpers";
|
import { getSingleParam } from "@/features/transactions/lib/page-helpers";
|
||||||
|
import { LogoPrefetchProvider } from "@/shared/components/entity-avatar";
|
||||||
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 { parsePeriodParam } from "@/shared/utils/period";
|
import { parsePeriodParam } from "@/shared/utils/period";
|
||||||
|
|
||||||
type PageSearchParams = Promise<Record<string, string | string[] | undefined>>;
|
type PageSearchParams = Promise<Record<string, string | string[] | undefined>>;
|
||||||
@@ -14,6 +18,7 @@ type PageProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function Page({ searchParams }: PageProps) {
|
export default async function Page({ searchParams }: PageProps) {
|
||||||
|
await connection();
|
||||||
const user = await getUser();
|
const user = await getUser();
|
||||||
const resolvedSearchParams = searchParams ? await searchParams : undefined;
|
const resolvedSearchParams = searchParams ? await searchParams : undefined;
|
||||||
const periodoParam = getSingleParam(resolvedSearchParams, "periodo");
|
const periodoParam = getSingleParam(resolvedSearchParams, "periodo");
|
||||||
@@ -23,17 +28,24 @@ export default async function Page({ searchParams }: PageProps) {
|
|||||||
await fetchDashboardPageData(user.id, selectedPeriod);
|
await fetchDashboardPageData(user.id, selectedPeriod);
|
||||||
const { dashboardWidgets } = preferences;
|
const { dashboardWidgets } = preferences;
|
||||||
|
|
||||||
|
const logoMappings = await prefetchLogoMappings(
|
||||||
|
user.id,
|
||||||
|
extractDashboardLogoNames(dashboardData),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col gap-4">
|
<main className="flex flex-col gap-4">
|
||||||
<DashboardWelcome name={user.name} />
|
<DashboardWelcome name={user.name} />
|
||||||
<MonthNavigation />
|
<MonthNavigation />
|
||||||
<DashboardMetricsCards metrics={dashboardData.metrics} />
|
<DashboardMetricsCards metrics={dashboardData.metrics} />
|
||||||
<DashboardGridEditable
|
<LogoPrefetchProvider mappings={logoMappings}>
|
||||||
data={dashboardData}
|
<DashboardGridEditable
|
||||||
period={selectedPeriod}
|
data={dashboardData}
|
||||||
initialPreferences={dashboardWidgets}
|
period={selectedPeriod}
|
||||||
quickActionOptions={quickActionOptions}
|
initialPreferences={dashboardWidgets}
|
||||||
/>
|
quickActionOptions={quickActionOptions}
|
||||||
|
/>
|
||||||
|
</LogoPrefetchProvider>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="space-y-6 pt-4">
|
<section className="space-y-6">
|
||||||
<PageDescription
|
<PageDescription
|
||||||
icon={<RiAtLine />}
|
icon={<RiAtLine />}
|
||||||
title="Pré-Lançamentos"
|
title="Pré-Lançamentos"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { connection } from "next/server";
|
||||||
import { InboxPage } from "@/features/inbox/components/inbox-page";
|
import { InboxPage } from "@/features/inbox/components/inbox-page";
|
||||||
import {
|
import {
|
||||||
type ResolvedInboxSearchParams,
|
type ResolvedInboxSearchParams,
|
||||||
@@ -31,6 +32,7 @@ const EMPTY_DIALOG_DATA = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function Page({ searchParams }: PageProps) {
|
export default async function Page({ searchParams }: PageProps) {
|
||||||
|
await connection();
|
||||||
const userId = await getUserId();
|
const userId = await getUserId();
|
||||||
const resolvedSearchParams = searchParams ? await searchParams : undefined;
|
const resolvedSearchParams = searchParams ? await searchParams : undefined;
|
||||||
const activeStatus = resolveInboxStatus(resolvedSearchParams);
|
const activeStatus = resolveInboxStatus(resolvedSearchParams);
|
||||||
@@ -54,7 +56,7 @@ export default async function Page({ searchParams }: PageProps) {
|
|||||||
const normalizedSourceApps = Array.isArray(sourceApps) ? sourceApps : [];
|
const normalizedSourceApps = Array.isArray(sourceApps) ? sourceApps : [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col items-start gap-6">
|
<main className="flex flex-col gap-6">
|
||||||
<InboxPage
|
<InboxPage
|
||||||
activeStatus={activeStatus}
|
activeStatus={activeStatus}
|
||||||
activeApp={activeApp}
|
activeApp={activeApp}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="space-y-6 pt-4">
|
<section className="space-y-6">
|
||||||
<PageDescription
|
<PageDescription
|
||||||
icon={<RiSparklingLine />}
|
icon={<RiSparklingLine />}
|
||||||
title="Insights"
|
title="Insights"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { connection } from "next/server";
|
||||||
import { InsightsPage } from "@/features/insights/components/insights-page";
|
import { InsightsPage } from "@/features/insights/components/insights-page";
|
||||||
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,6 +19,7 @@ const getSingleParam = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function Page({ searchParams }: PageProps) {
|
export default async function Page({ searchParams }: PageProps) {
|
||||||
|
await connection();
|
||||||
const resolvedSearchParams = searchParams ? await searchParams : undefined;
|
const resolvedSearchParams = searchParams ? await searchParams : undefined;
|
||||||
const periodoParam = getSingleParam(resolvedSearchParams, "periodo");
|
const periodoParam = getSingleParam(resolvedSearchParams, "periodo");
|
||||||
const { period: selectedPeriod } = parsePeriodParam(periodoParam);
|
const { period: selectedPeriod } = parsePeriodParam(periodoParam);
|
||||||
|
|||||||
@@ -1,43 +1,38 @@
|
|||||||
import { fetchDashboardNavbarData } from "@/features/dashboard/navbar-queries";
|
import { connection } from "next/server";
|
||||||
|
import { fetchDashboardNavbarData } from "@/features/dashboard/lib/navbar-queries";
|
||||||
import { AppNavbar } from "@/shared/components/navigation/navbar/app-navbar";
|
import { AppNavbar } from "@/shared/components/navigation/navbar/app-navbar";
|
||||||
|
import { LogoDevProvider } from "@/shared/components/providers/logo-dev-provider";
|
||||||
import { PrivacyProvider } from "@/shared/components/providers/privacy-provider";
|
import { PrivacyProvider } from "@/shared/components/providers/privacy-provider";
|
||||||
import { DotPattern } from "@/shared/components/ui/dot-pattern";
|
|
||||||
import { getUserSession } from "@/shared/lib/auth/server";
|
import { getUserSession } from "@/shared/lib/auth/server";
|
||||||
|
import { isLogoDevEnabled } from "@/shared/lib/logo/server";
|
||||||
|
|
||||||
export default async function DashboardLayout({
|
export default async function DashboardLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
|
await connection();
|
||||||
const session = await getUserSession();
|
const session = await getUserSession();
|
||||||
const navbarData = await fetchDashboardNavbarData(session.user.id);
|
const navbarData = await fetchDashboardNavbarData(session.user.id);
|
||||||
|
const logoDevEnabled = isLogoDevEnabled();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PrivacyProvider>
|
<LogoDevProvider enabled={logoDevEnabled}>
|
||||||
<AppNavbar
|
<PrivacyProvider>
|
||||||
user={{ ...session.user, image: session.user.image ?? null }}
|
<AppNavbar
|
||||||
pagadorAvatarUrl={navbarData.pagadorAvatarUrl}
|
user={{ ...session.user, image: session.user.image ?? null }}
|
||||||
preLancamentosCount={navbarData.preLancamentosCount}
|
payerAvatarUrl={navbarData.payerAvatarUrl}
|
||||||
notificationsSnapshot={navbarData.notificationsSnapshot}
|
inboxPendingCount={navbarData.inboxPendingCount}
|
||||||
/>
|
notificationsSnapshot={navbarData.notificationsSnapshot}
|
||||||
<div className="relative flex flex-1 flex-col pt-16">
|
/>
|
||||||
<div className="pointer-events-none absolute inset-x-0 top-0 h-32 overflow-hidden md:h-36">
|
<div className="relative flex flex-1 flex-col pt-16">
|
||||||
<DotPattern
|
<div className="@container/main flex flex-1 flex-col gap-2">
|
||||||
width={20}
|
<div className="flex flex-col gap-4 py-5 md:gap-6 w-full max-w-8xl mx-auto px-4 ">
|
||||||
height={20}
|
{children}
|
||||||
cx={1.25}
|
</div>
|
||||||
cy={1.25}
|
|
||||||
cr={1.25}
|
|
||||||
className="text-primary/10 mask-[linear-gradient(to_bottom,black_0%,transparent_100%)]"
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 bg-linear-to-b from-primary/6 to-transparent" />
|
|
||||||
</div>
|
|
||||||
<div className="@container/main flex flex-1 flex-col gap-2">
|
|
||||||
<div className="flex flex-col gap-4 py-5 md:gap-6 w-full max-w-8xl mx-auto px-4 ">
|
|
||||||
{children}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PrivacyProvider>
|
||||||
</PrivacyProvider>
|
</LogoDevProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="space-y-6 pt-4">
|
<section className="space-y-6">
|
||||||
<PageDescription
|
<PageDescription
|
||||||
icon={<RiTodoLine />}
|
icon={<RiTodoLine />}
|
||||||
title="Anotações"
|
title="Anotações"
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
|
import { connection } from "next/server";
|
||||||
import { NotesPage } from "@/features/notes/components/notes-page";
|
import { NotesPage } from "@/features/notes/components/notes-page";
|
||||||
import { fetchAllNotesForUser } from "@/features/notes/queries";
|
import { fetchAllNotesForUser } from "@/features/notes/queries";
|
||||||
import { getUserId } from "@/shared/lib/auth/server";
|
import { getUserId } from "@/shared/lib/auth/server";
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
|
await connection();
|
||||||
const userId = await getUserId();
|
const userId = await getUserId();
|
||||||
const { activeNotes, archivedNotes } = await fetchAllNotesForUser(userId);
|
const { activeNotes, archivedNotes } = await fetchAllNotesForUser(userId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col items-start gap-6">
|
<main className="flex flex-col gap-6">
|
||||||
<NotesPage notes={activeNotes} archivedNotes={archivedNotes} />
|
<NotesPage notes={activeNotes} archivedNotes={archivedNotes} />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Skeleton } from "@/shared/components/ui/skeleton";
|
|||||||
* Loading state para a página de detalhes do pagador.
|
* Loading state para a página de detalhes do pagador.
|
||||||
* Layout: navegação mensal + tabs com card compartilhado do pagador.
|
* Layout: navegação mensal + tabs com card compartilhado do pagador.
|
||||||
*/
|
*/
|
||||||
export default function PagadorDetailsLoading() {
|
export default function PayerDetailsLoading() {
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col gap-6">
|
<main className="flex flex-col gap-6">
|
||||||
<div className="h-[60px] animate-pulse rounded-md bg-foreground/10" />
|
<div className="h-[60px] animate-pulse rounded-md bg-foreground/10" />
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import {
|
|||||||
RiWallet3Line,
|
RiWallet3Line,
|
||||||
} from "@remixicon/react";
|
} from "@remixicon/react";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
|
import { connection } from "next/server";
|
||||||
import { PayerCardUsageCard } from "@/features/payers/components/details/payer-card-usage-card";
|
import { PayerCardUsageCard } from "@/features/payers/components/details/payer-card-usage-card";
|
||||||
import { PayerHeaderCard } from "@/features/payers/components/details/payer-header-card";
|
import { PayerHeaderCard } from "@/features/payers/components/details/payer-header-card";
|
||||||
import { PayerHistoryCard } from "@/features/payers/components/details/payer-history-card";
|
import { PayerHistoryCard } from "@/features/payers/components/details/payer-history-card";
|
||||||
import { PagadorInfoCard } from "@/features/payers/components/details/payer-info-card";
|
import { PayerInfoCard } from "@/features/payers/components/details/payer-info-card";
|
||||||
import { PayerLeaveShareCard } from "@/features/payers/components/details/payer-leave-share-card";
|
import { PayerLeaveShareCard } from "@/features/payers/components/details/payer-leave-share-card";
|
||||||
import { PayerMonthlySummaryCard } from "@/features/payers/components/details/payer-monthly-summary-card";
|
import { PayerMonthlySummaryCard } from "@/features/payers/components/details/payer-monthly-summary-card";
|
||||||
import {
|
import {
|
||||||
@@ -15,12 +16,12 @@ import {
|
|||||||
PayerPaymentStatusCard,
|
PayerPaymentStatusCard,
|
||||||
} from "@/features/payers/components/details/payer-payment-method-cards";
|
} from "@/features/payers/components/details/payer-payment-method-cards";
|
||||||
import { PayerSharingCard } from "@/features/payers/components/details/payer-sharing-card";
|
import { PayerSharingCard } from "@/features/payers/components/details/payer-sharing-card";
|
||||||
|
import { buildReadOnlyOptionSets } from "@/features/payers/lib/build-readonly-option-sets";
|
||||||
import {
|
import {
|
||||||
fetchCurrentUserShare,
|
fetchCurrentUserShare,
|
||||||
fetchPagadorLancamentos,
|
|
||||||
fetchPayerShares,
|
fetchPayerShares,
|
||||||
} from "@/features/payers/detail-queries";
|
fetchPayerTransactions,
|
||||||
import { buildReadOnlyOptionSets } from "@/features/payers/lib/build-readonly-option-sets";
|
} from "@/features/payers/lib/detail-queries";
|
||||||
import { fetchUserPreferences } from "@/features/settings/queries";
|
import { fetchUserPreferences } from "@/features/settings/queries";
|
||||||
import { TransactionsPage as LancamentosSection } from "@/features/transactions/components/page/transactions-page";
|
import { TransactionsPage as LancamentosSection } from "@/features/transactions/components/page/transactions-page";
|
||||||
import {
|
import {
|
||||||
@@ -35,12 +36,12 @@ import {
|
|||||||
type SluggedFilters,
|
type SluggedFilters,
|
||||||
type SlugMaps,
|
type SlugMaps,
|
||||||
type TransactionSearchFilters,
|
type TransactionSearchFilters,
|
||||||
} from "@/features/transactions/page-helpers";
|
} from "@/features/transactions/lib/page-helpers";
|
||||||
import {
|
import {
|
||||||
fetchRecentEstablishments,
|
fetchRecentEstablishments,
|
||||||
fetchTransactionFilterSources,
|
fetchTransactionFilterSources,
|
||||||
} from "@/features/transactions/queries";
|
} from "@/features/transactions/queries";
|
||||||
import { ExpandableWidgetCard } from "@/shared/components/expandable-widget-card";
|
import { LogoPrefetchProvider } from "@/shared/components/entity-avatar";
|
||||||
import MonthNavigation from "@/shared/components/month-picker/month-navigation";
|
import MonthNavigation from "@/shared/components/month-picker/month-navigation";
|
||||||
import {
|
import {
|
||||||
Tabs,
|
Tabs,
|
||||||
@@ -48,15 +49,17 @@ import {
|
|||||||
TabsList,
|
TabsList,
|
||||||
TabsTrigger,
|
TabsTrigger,
|
||||||
} from "@/shared/components/ui/tabs";
|
} from "@/shared/components/ui/tabs";
|
||||||
|
import { ExpandableWidgetCard } from "@/shared/components/widgets/expandable-widget-card";
|
||||||
import { getUserId } from "@/shared/lib/auth/server";
|
import { getUserId } from "@/shared/lib/auth/server";
|
||||||
|
import { prefetchLogoMappings } from "@/shared/lib/logo/prefetch-server";
|
||||||
import { getPayerAccess } from "@/shared/lib/payers/access";
|
import { getPayerAccess } from "@/shared/lib/payers/access";
|
||||||
import {
|
import {
|
||||||
fetchPagadorBoletoItems,
|
fetchPayerBoletoItems,
|
||||||
fetchPagadorBoletoStats,
|
fetchPayerBoletoStats,
|
||||||
fetchPagadorCardUsage,
|
fetchPayerCardUsage,
|
||||||
fetchPagadorPaymentStatus,
|
|
||||||
fetchPayerHistory,
|
fetchPayerHistory,
|
||||||
fetchPayerMonthlyBreakdown,
|
fetchPayerMonthlyBreakdown,
|
||||||
|
fetchPayerPaymentStatus,
|
||||||
type PayerCardUsageItem,
|
type PayerCardUsageItem,
|
||||||
} from "@/shared/lib/payers/details";
|
} from "@/shared/lib/payers/details";
|
||||||
import { parsePeriodParam } from "@/shared/utils/period";
|
import { parsePeriodParam } from "@/shared/utils/period";
|
||||||
@@ -73,12 +76,15 @@ const capitalize = (value: string) =>
|
|||||||
|
|
||||||
const EMPTY_FILTERS: TransactionSearchFilters = {
|
const EMPTY_FILTERS: TransactionSearchFilters = {
|
||||||
transactionFilter: null,
|
transactionFilter: null,
|
||||||
conditionFilter: null,
|
conditionFilters: [],
|
||||||
paymentFilter: null,
|
paymentFilters: [],
|
||||||
payerFilter: null,
|
payerFilters: [],
|
||||||
categoryFilter: null,
|
categoryFilters: [],
|
||||||
accountCardFilter: null,
|
accountCardFilters: [],
|
||||||
searchFilter: null,
|
searchFilter: null,
|
||||||
|
settledFilter: null,
|
||||||
|
attachmentFilter: null,
|
||||||
|
dividedFilter: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const createEmptySlugMaps = (): SlugMaps => ({
|
const createEmptySlugMaps = (): SlugMaps => ({
|
||||||
@@ -91,6 +97,7 @@ const createEmptySlugMaps = (): SlugMaps => ({
|
|||||||
type OptionSet = ReturnType<typeof buildOptionSets>;
|
type OptionSet = ReturnType<typeof buildOptionSets>;
|
||||||
|
|
||||||
export default async function Page({ params, searchParams }: PageProps) {
|
export default async function Page({ params, searchParams }: PageProps) {
|
||||||
|
await connection();
|
||||||
const { payerId } = await params;
|
const { payerId } = await params;
|
||||||
const userId = await getUserId();
|
const userId = await getUserId();
|
||||||
const resolvedSearchParams = searchParams ? await searchParams : undefined;
|
const resolvedSearchParams = searchParams ? await searchParams : undefined;
|
||||||
@@ -175,7 +182,7 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
estabelecimentos,
|
estabelecimentos,
|
||||||
userPreferences,
|
userPreferences,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
fetchPagadorLancamentos(filters),
|
fetchPayerTransactions(filters),
|
||||||
fetchPayerMonthlyBreakdown({
|
fetchPayerMonthlyBreakdown({
|
||||||
userId: dataOwnerId,
|
userId: dataOwnerId,
|
||||||
payerId: pagador.id,
|
payerId: pagador.id,
|
||||||
@@ -186,22 +193,22 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
payerId: pagador.id,
|
payerId: pagador.id,
|
||||||
period: selectedPeriod,
|
period: selectedPeriod,
|
||||||
}),
|
}),
|
||||||
fetchPagadorCardUsage({
|
fetchPayerCardUsage({
|
||||||
userId: dataOwnerId,
|
userId: dataOwnerId,
|
||||||
payerId: pagador.id,
|
payerId: pagador.id,
|
||||||
period: selectedPeriod,
|
period: selectedPeriod,
|
||||||
}),
|
}),
|
||||||
fetchPagadorBoletoStats({
|
fetchPayerBoletoStats({
|
||||||
userId: dataOwnerId,
|
userId: dataOwnerId,
|
||||||
payerId: pagador.id,
|
payerId: pagador.id,
|
||||||
period: selectedPeriod,
|
period: selectedPeriod,
|
||||||
}),
|
}),
|
||||||
fetchPagadorBoletoItems({
|
fetchPayerBoletoItems({
|
||||||
userId: dataOwnerId,
|
userId: dataOwnerId,
|
||||||
payerId: pagador.id,
|
payerId: pagador.id,
|
||||||
period: selectedPeriod,
|
period: selectedPeriod,
|
||||||
}),
|
}),
|
||||||
fetchPagadorPaymentStatus({
|
fetchPayerPaymentStatus({
|
||||||
userId: dataOwnerId,
|
userId: dataOwnerId,
|
||||||
payerId: pagador.id,
|
payerId: pagador.id,
|
||||||
period: selectedPeriod,
|
period: selectedPeriod,
|
||||||
@@ -303,103 +310,113 @@ export default async function Page({ params, searchParams }: PageProps) {
|
|||||||
lancamentoCount: transactionData.length,
|
lancamentoCount: transactionData.length,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const logoMappings = await prefetchLogoMappings(dataOwnerId, [
|
||||||
|
...transactionData.map((t) => t.name),
|
||||||
|
...boletoItems.map((b) => b.name),
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col gap-6">
|
<main className="flex flex-col gap-6">
|
||||||
<MonthNavigation />
|
<MonthNavigation />
|
||||||
|
|
||||||
<Tabs defaultValue="profile" className="w-full">
|
<LogoPrefetchProvider mappings={logoMappings}>
|
||||||
<TabsList className="mb-2">
|
<Tabs defaultValue="profile" className="w-full">
|
||||||
<TabsTrigger value="profile">Perfil</TabsTrigger>
|
<TabsList className="mb-2">
|
||||||
<TabsTrigger value="painel">Painel</TabsTrigger>
|
<TabsTrigger value="profile">Perfil</TabsTrigger>
|
||||||
<TabsTrigger value="lancamentos">Lançamentos</TabsTrigger>
|
<TabsTrigger value="painel">Painel</TabsTrigger>
|
||||||
</TabsList>
|
<TabsTrigger value="lancamentos">Lançamentos</TabsTrigger>
|
||||||
<PayerHeaderCard
|
</TabsList>
|
||||||
payer={payerData}
|
<PayerHeaderCard
|
||||||
selectedPeriod={selectedPeriod}
|
payer={payerData}
|
||||||
summary={summaryPreview}
|
selectedPeriod={selectedPeriod}
|
||||||
/>
|
summary={summaryPreview}
|
||||||
|
/>
|
||||||
|
|
||||||
<TabsContent value="profile" className="space-y-4">
|
<TabsContent value="profile" className="space-y-4">
|
||||||
<PagadorInfoCard payer={payerData} />
|
<PayerInfoCard payer={payerData} />
|
||||||
{canEdit && payerData.shareCode ? (
|
{canEdit && payerData.shareCode ? (
|
||||||
<PayerSharingCard
|
<PayerSharingCard
|
||||||
payerId={pagador.id}
|
payerId={pagador.id}
|
||||||
shareCode={payerData.shareCode}
|
shareCode={payerData.shareCode}
|
||||||
shares={payerSharesData}
|
shares={payerSharesData}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{!canEdit && currentUserShare ? (
|
{!canEdit && currentUserShare ? (
|
||||||
<PayerLeaveShareCard
|
<PayerLeaveShareCard
|
||||||
shareId={currentUserShare.id}
|
shareId={currentUserShare.id}
|
||||||
pagadorName={payerData.name}
|
pagadorName={payerData.name}
|
||||||
createdAt={currentUserShare.createdAt}
|
createdAt={currentUserShare.createdAt}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="painel" className="space-y-4">
|
<TabsContent value="painel" className="space-y-4">
|
||||||
<section className="grid gap-3 lg:grid-cols-2">
|
<section className="grid gap-3 lg:grid-cols-2">
|
||||||
<PayerMonthlySummaryCard
|
<PayerMonthlySummaryCard
|
||||||
periodLabel={periodLabel}
|
periodLabel={periodLabel}
|
||||||
breakdown={monthlyBreakdown}
|
breakdown={monthlyBreakdown}
|
||||||
/>
|
/>
|
||||||
<PayerHistoryCard data={historyData} />
|
<PayerHistoryCard data={historyData} />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="grid gap-3 lg:grid-cols-3">
|
<section className="grid gap-3 lg:grid-cols-3">
|
||||||
<ExpandableWidgetCard
|
<ExpandableWidgetCard
|
||||||
title="Minhas Faturas"
|
title="Minhas Faturas"
|
||||||
subtitle="Valores por cartão neste período"
|
subtitle="Valores por cartão neste período"
|
||||||
icon={<RiBankCard2Line className="size-4" />}
|
icon={<RiBankCard2Line className="size-4" />}
|
||||||
>
|
>
|
||||||
<PayerCardUsageCard items={cardUsage} />
|
<PayerCardUsageCard items={cardUsage} />
|
||||||
</ExpandableWidgetCard>
|
</ExpandableWidgetCard>
|
||||||
<ExpandableWidgetCard
|
<ExpandableWidgetCard
|
||||||
title="Boletos"
|
title="Boletos"
|
||||||
subtitle="Boletos registrados neste período"
|
subtitle="Boletos registrados neste período"
|
||||||
icon={<RiBarcodeLine className="size-4" />}
|
icon={<RiBarcodeLine className="size-4" />}
|
||||||
>
|
>
|
||||||
<PayerBoletoCard items={boletoItems} />
|
<PayerBoletoCard items={boletoItems} />
|
||||||
</ExpandableWidgetCard>
|
</ExpandableWidgetCard>
|
||||||
<ExpandableWidgetCard
|
<ExpandableWidgetCard
|
||||||
title="Status de Pagamento"
|
title="Status de Pagamento"
|
||||||
subtitle="Situação das despesas no período"
|
subtitle="Situação das despesas no período"
|
||||||
icon={<RiWallet3Line className="size-4" />}
|
icon={<RiWallet3Line className="size-4" />}
|
||||||
>
|
>
|
||||||
<PayerPaymentStatusCard data={paymentStatus} />
|
<PayerPaymentStatusCard data={paymentStatus} />
|
||||||
</ExpandableWidgetCard>
|
</ExpandableWidgetCard>
|
||||||
</section>
|
</section>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="lancamentos">
|
<TabsContent value="lancamentos">
|
||||||
<section className="flex flex-col gap-4">
|
<section className="flex flex-col gap-4">
|
||||||
<LancamentosSection
|
<LancamentosSection
|
||||||
currentUserId={userId}
|
currentUserId={userId}
|
||||||
transactions={transactionData}
|
transactions={transactionData}
|
||||||
payerOptions={optionSets.payerOptions}
|
payerOptions={optionSets.payerOptions}
|
||||||
splitPayerOptions={optionSets.splitPayerOptions}
|
splitPayerOptions={optionSets.splitPayerOptions}
|
||||||
defaultPayerId={pagador.id}
|
defaultPayerId={pagador.id}
|
||||||
accountOptions={optionSets.accountOptions}
|
accountOptions={optionSets.accountOptions}
|
||||||
cardOptions={optionSets.cardOptions}
|
cardOptions={optionSets.cardOptions}
|
||||||
categoryOptions={optionSets.categoryOptions}
|
categoryOptions={optionSets.categoryOptions}
|
||||||
payerFilterOptions={payerFilterOptions}
|
payerFilterOptions={payerFilterOptions}
|
||||||
categoryFilterOptions={optionSets.categoryFilterOptions}
|
categoryFilterOptions={optionSets.categoryFilterOptions}
|
||||||
accountCardFilterOptions={optionSets.accountCardFilterOptions}
|
accountCardFilterOptions={optionSets.accountCardFilterOptions}
|
||||||
selectedPeriod={selectedPeriod}
|
selectedPeriod={selectedPeriod}
|
||||||
estabelecimentos={estabelecimentos}
|
estabelecimentos={estabelecimentos}
|
||||||
allowCreate={canEdit}
|
allowCreate={canEdit}
|
||||||
noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
|
noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
|
||||||
columnOrder={userPreferences?.transactionsColumnOrder ?? null}
|
columnOrder={userPreferences?.transactionsColumnOrder ?? null}
|
||||||
importPayerOptions={loggedUserOptionSets?.payerOptions}
|
attachmentMaxSizeMb={userPreferences?.attachmentMaxSizeMb ?? 50}
|
||||||
importSplitPayerOptions={loggedUserOptionSets?.splitPayerOptions}
|
importPayerOptions={loggedUserOptionSets?.payerOptions}
|
||||||
importDefaultPayerId={loggedUserOptionSets?.defaultPayerId}
|
importSplitPayerOptions={
|
||||||
importAccountOptions={loggedUserOptionSets?.accountOptions}
|
loggedUserOptionSets?.splitPayerOptions
|
||||||
importCardOptions={loggedUserOptionSets?.cardOptions}
|
}
|
||||||
importCategoryOptions={loggedUserOptionSets?.categoryOptions}
|
importDefaultPayerId={loggedUserOptionSets?.defaultPayerId}
|
||||||
/>
|
importAccountOptions={loggedUserOptionSets?.accountOptions}
|
||||||
</section>
|
importCardOptions={loggedUserOptionSets?.cardOptions}
|
||||||
</TabsContent>
|
importCategoryOptions={loggedUserOptionSets?.categoryOptions}
|
||||||
</Tabs>
|
/>
|
||||||
|
</section>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</LogoPrefetchProvider>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||