Compare commits

..

9 Commits
v2.7.1 ... main

Author SHA1 Message Date
Felipe Coutinho
c81584095b docs: informa manutencao reduzida do projeto 2026-06-06 17:06:18 -03:00
Felipe Coutinho
8ccc4479be docs: registra release 2.7.3 2026-06-06 16:31:57 -03:00
Felipe Coutinho
2cead626ab chore: atualiza dependencias e build de PDF 2026-06-06 16:31:42 -03:00
Felipe Coutinho
811a035cb0 refactor: remove exports internos sem uso 2026-06-06 16:31:38 -03:00
Felipe Coutinho
356801324c feat: destaca fatura paga nos cartoes 2026-06-06 16:31:33 -03:00
Felipe Coutinho
b443fb010a config: adiciona origins confiaveis do Better Auth 2026-06-06 16:31:27 -03:00
Felipe Coutinho
026dff5399 Merge pull request #78 from felipegcoutinho/imgbot
[ImgBot] Optimize images
2026-05-31 18:16:42 -03:00
ImgBotApp
18b6a6a470 [ImgBot] Optimize images
*Total -- 1,696.54kb -> 1,060.14kb (37.51%)

/public/logos/dinheiro.png -- 72.04kb -> 17.17kb (76.17%)
/public/images/dashboard-preview-dark.png -- 597.68kb -> 350.27kb (41.4%)
/public/images/dashboard-preview-light.png -- 589.20kb -> 355.11kb (39.73%)
/public/avatars/default_icon.png -- 5.41kb -> 3.37kb (37.69%)
/public/logos/bipa.png -- 47.72kb -> 33.57kb (29.64%)
/public/images/pwa-preview-dark.png -- 181.92kb -> 138.71kb (23.75%)
/public/images/pwa-preview-light.png -- 177.18kb -> 136.59kb (22.91%)
/public/images/logo_small.svg -- 0.39kb -> 0.38kb (1.99%)
/public/images/logo_text.svg -- 6.74kb -> 6.73kb (0.12%)
/public/providers/minimax.svg -- 1.52kb -> 1.52kb (0.06%)
/public/providers/ollama_dark.svg -- 8.36kb -> 8.36kb (0.06%)
/public/providers/ollama_light.svg -- 8.36kb -> 8.36kb (0.06%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
2026-05-31 21:11:31 +00:00
Felipe Coutinho
78e778311d chore(release): prepara versao 2.7.2 2026-05-31 18:06:04 -03:00
38 changed files with 1455 additions and 1401 deletions

View File

@@ -17,8 +17,15 @@ POSTGRES_DB=openmonetis_db
# Gere com: openssl rand -base64 32 # Gere com: openssl rand -base64 32
BETTER_AUTH_SECRET=your-secret-key-here-change-this BETTER_AUTH_SECRET=your-secret-key-here-change-this
BETTER_AUTH_URL=http://localhost:3000 BETTER_AUTH_URL=http://localhost:3000
# Origins adicionais confiáveis para o Better Auth.
# Útil para Cloudflare Tunnel, reverse proxy e URLs diferentes de BETTER_AUTH_URL.
# Separe múltiplas origins por vírgula.
# Exemplo: https://*.trycloudflare.com,https://openmonetis.seudominio.com
BETTER_AUTH_TRUSTED_ORIGINS=
# Defina como true para bloquear novos cadastros # Defina como true para bloquear novos cadastros
DISABLE_SIGNUP=false DISABLE_SIGNUP=false
# Duração de sessões persistentes quando "Manter conectado" estiver marcado # Duração de sessões persistentes quando "Manter conectado" estiver marcado
AUTH_SESSION_EXPIRES_IN_DAYS=30 AUTH_SESSION_EXPIRES_IN_DAYS=30
AUTH_SESSION_UPDATE_AGE_HOURS=24 AUTH_SESSION_UPDATE_AGE_HOURS=24

View File

@@ -5,6 +5,29 @@ Todas as mudanças notáveis deste projeto serão documentadas neste arquivo.
O formato é baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/), O formato é baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/),
e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR/). e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR/).
## [2.7.3] - 2026-06-05
Esta versão melhora pequenos pontos de leitura e configuração para o uso diário e self-hosted. As faturas pagas ficam mais fáceis de identificar na lista de cartões, a configuração de origins confiáveis do Better Auth passa a ficar documentada para Docker e túneis, o dashboard corrige a leitura de tempo dos pré-lançamentos e as dependências seguem atualizadas sem quebrar o build da imagem.
### Adicionado
- Cartões: a lista de cartões agora exibe a etiqueta `Paga` ao lado do valor da fatura atual quando ela já foi quitada.
- Self-hosting: adicionada a variável `BETTER_AUTH_TRUSTED_ORIGINS` ao `.env.example`, ao `docker-compose.yml` e ao README para permitir origins adicionais confiáveis em cenários com Cloudflare Tunnel, reverse proxy ou URLs diferentes de `BETTER_AUTH_URL`.
### Alterado
- Dependências: atualizados Next.js, React, Better Auth, AI SDK, AWS SDK, pdf.js e ferramentas de desenvolvimento usadas no build.
### Corrigido
- Dashboard: o widget `Pré-lançamentos` agora calcula o rótulo `há X` a partir da chegada do item ao OpenMonetis, evitando deslocamentos causados por timestamps de notificação enviados com timezone incorreto.
- Anexos: o preview de PDFs foi ajustado para a API atual do `pdfjs-dist`, evitando falha de TypeScript durante o build da imagem Docker.
## [2.7.2] - 2026-05-31
Esta versão atualiza as imagens de apresentação do OpenMonetis na landing page e no compartilhamento em redes sociais.
### Alterado
- Landing page: atualizadas as imagens de preview do dashboard e da versão PWA.
- Compartilhamento: ajustada a imagem usada nos metadados sociais da landing page.
## [2.7.1] - 2026-05-30 ## [2.7.1] - 2026-05-30
Esta versão melhora a clareza dos fluxos de lançamento e a experiência do dashboard. Boletos de receita agora diferenciam pagamentos de recebimentos, a navegação mensal ficou mais direta e o painel ganhou atalhos mais úteis com personalização simplificada. Esta versão melhora a clareza dos fluxos de lançamento e a experiência do dashboard. Boletos de receita agora diferenciam pagamentos de recebimentos, a navegação mensal ficou mais direta e o painel ganhou atalhos mais úteis com personalização simplificada.

View File

@@ -6,9 +6,11 @@
Projeto pessoal de gestão financeira. Self-hosted, manual e open source. Projeto pessoal de gestão financeira. Self-hosted, manual e open source.
</p> </p>
> **⚠️ Não há versão online hospedada.** Você precisa clonar o repositório e rodar localmente ou no seu próprio servidor. > **⚠️ Nota:** o OpenMonetis não está sendo encerrado, mas o desenvolvimento deve reduzir para quase zero daqui em diante. O app já cobre minhas demandas atuais de gerenciamento financeiro, então novas mudanças tendem a ser pontuais: correções, ajustes necessários e pequenas melhorias quando fizerem bastante sentido para meu uso.
[![Version](https://img.shields.io/badge/version-2.7.1-blue?style=flat-square)](CHANGELOG.md) > **Não há versão online hospedada.** Você precisa clonar o repositório e rodar localmente ou no seu próprio servidor.
[![Version](https://img.shields.io/badge/version-2.7.3-blue?style=flat-square)](CHANGELOG.md)
[![Next.js](https://img.shields.io/badge/Next.js-black?style=flat-square&logo=next.js)](https://nextjs.org/) [![Next.js](https://img.shields.io/badge/Next.js-black?style=flat-square&logo=next.js)](https://nextjs.org/)
[![TypeScript](https://img.shields.io/badge/TypeScript-blue?style=flat-square&logo=typescript)](https://www.typescriptlang.org/) [![TypeScript](https://img.shields.io/badge/TypeScript-blue?style=flat-square&logo=typescript)](https://www.typescriptlang.org/)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-blue?style=flat-square&logo=postgresql)](https://www.postgresql.org/) [![PostgreSQL](https://img.shields.io/badge/PostgreSQL-blue?style=flat-square&logo=postgresql)](https://www.postgresql.org/)
@@ -451,6 +453,7 @@ POSTGRES_DB=openmonetis_db
DISABLE_SIGNUP=false # true bloqueia novos cadastros DISABLE_SIGNUP=false # true bloqueia novos cadastros
AUTH_SESSION_EXPIRES_IN_DAYS=30 # duração de sessões persistentes AUTH_SESSION_EXPIRES_IN_DAYS=30 # duração de sessões persistentes
AUTH_SESSION_UPDATE_AGE_HOURS=24 # frequência de renovação da sessão AUTH_SESSION_UPDATE_AGE_HOURS=24 # frequência de renovação da sessão
BETTER_AUTH_TRUSTED_ORIGINS= # origins adicionais confiáveis, separadas por vírgula
# S3 Server (opcional, necessario para anexos) # S3 Server (opcional, necessario para anexos)
S3_ENDPOINT= S3_ENDPOINT=
@@ -485,6 +488,19 @@ LOGO_DEV_TOKEN=
LOGO_DEV_SECRET_KEY= LOGO_DEV_SECRET_KEY=
``` ```
### BETTER_AUTH_TRUSTED_ORIGINS
Use `BETTER_AUTH_TRUSTED_ORIGINS` quando o OpenMonetis for acessado por uma URL diferente de `BETTER_AUTH_URL`, como Cloudflare Tunnel, reverse proxy, domínio local ou subdomínios temporários. Isso evita falhas de login como `Invalid origin` sem precisar alterar a imagem Docker.
Informe apenas origins confiáveis, separadas por vírgula:
```env
BETTER_AUTH_URL=http://localhost:3000
BETTER_AUTH_TRUSTED_ORIGINS=https://*.trycloudflare.com,https://openmonetis.seudominio.com
```
Para Google OAuth e outros callbacks externos, mantenha `BETTER_AUTH_URL` apontando para a URL pública/canônica configurada no provedor.
### IA local com Ollama ### IA local com Ollama
O provider Ollama permite gerar insights usando modelos locais. Instale e suba o Ollama no host onde o modelo ficará disponível: O provider Ollama permite gerar insights usando modelos locais. Instale e suba o Ollama no host onde o modelo ficará disponível:

View File

@@ -1,5 +1,5 @@
{ {
"$schema": "https://biomejs.dev/schemas/2.4.15/schema.json", "$schema": "https://biomejs.dev/schemas/2.4.16/schema.json",
"vcs": { "vcs": {
"enabled": true, "enabled": true,
"clientKind": "git", "clientKind": "git",

View File

@@ -36,6 +36,7 @@ services:
DATABASE_URL: ${DATABASE_URL:-postgresql://openmonetis:openmonetis_dev_password@db:5432/openmonetis_db} DATABASE_URL: ${DATABASE_URL:-postgresql://openmonetis:openmonetis_dev_password@db:5432/openmonetis_db}
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:-} BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:-}
BETTER_AUTH_URL: ${BETTER_AUTH_URL:-http://localhost:3000} BETTER_AUTH_URL: ${BETTER_AUTH_URL:-http://localhost:3000}
BETTER_AUTH_TRUSTED_ORIGINS: ${BETTER_AUTH_TRUSTED_ORIGINS:-}
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy

View File

@@ -1,6 +1,6 @@
{ {
"name": "openmonetis", "name": "openmonetis",
"version": "2.7.1", "version": "2.7.3",
"private": true, "private": true,
"packageManager": "pnpm@11.1.3", "packageManager": "pnpm@11.1.3",
"scripts": { "scripts": {
@@ -31,13 +31,13 @@
"mockup": "tsx scripts/mock-data.ts" "mockup": "tsx scripts/mock-data.ts"
}, },
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "^3.0.79", "@ai-sdk/anthropic": "^3.0.81",
"@ai-sdk/google": "^3.0.79", "@ai-sdk/google": "^3.0.80",
"@ai-sdk/openai": "^3.0.65", "@ai-sdk/openai": "^3.0.67",
"@ai-sdk/openai-compatible": "^2.0.48", "@ai-sdk/openai-compatible": "^2.0.48",
"@aws-sdk/client-s3": "^3.1050.0", "@aws-sdk/client-s3": "^3.1059.0",
"@aws-sdk/s3-request-presigner": "^3.1050.0", "@aws-sdk/s3-request-presigner": "^3.1059.0",
"@better-auth/passkey": "^1.6.11", "@better-auth/passkey": "^1.6.14",
"@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",
@@ -64,27 +64,27 @@
"@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.14", "@tanstack/react-query": "^5.101.0",
"@tanstack/react-table": "8.21.3", "@tanstack/react-table": "8.21.3",
"@tanstack/react-virtual": "^3.13.26", "@tanstack/react-virtual": "^3.14.2",
"ai": "^6.0.191", "ai": "^6.0.195",
"better-auth": "1.6.11", "better-auth": "1.6.14",
"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.3.0", "date-fns": "^4.4.0",
"drizzle-orm": "0.45.2", "drizzle-orm": "0.45.2",
"exceljs": "^4.4.0", "exceljs": "^4.4.0",
"jspdf": "^4.2.1", "jspdf": "^4.2.1",
"jspdf-autotable": "^5.0.8", "jspdf-autotable": "^5.0.8",
"next": "16.2.6", "next": "16.2.7",
"next-themes": "0.4.6", "next-themes": "0.4.6",
"pdfjs-dist": "^5.7.284", "pdfjs-dist": "^6.0.227",
"pg": "8.21.0", "pg": "8.21.0",
"react": "19.2.6", "react": "19.2.7",
"react-day-picker": "^10.0.1", "react-day-picker": "^10.0.1",
"react-dom": "19.2.6", "react-dom": "19.2.7",
"recharts": "3.8.1", "recharts": "3.8.1",
"resend": "^6.12.4", "resend": "^6.12.4",
"sonner": "2.0.7", "sonner": "2.0.7",
@@ -95,19 +95,19 @@
"zod": "4.4.3" "zod": "4.4.3"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.4.15", "@biomejs/biome": "2.4.16",
"@tailwindcss/postcss": "4.3.0", "@tailwindcss/postcss": "4.3.0",
"@types/canvas-confetti": "^1.9.0", "@types/canvas-confetti": "^1.9.0",
"@types/node": "25.9.1", "@types/node": "25.9.1",
"@types/pg": "^8.20.0", "@types/pg": "^8.20.0",
"@types/react": "19.2.15", "@types/react": "19.2.16",
"@types/react-dom": "19.2.3", "@types/react-dom": "19.2.3",
"babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-compiler": "^1.0.0",
"dotenv": "^17.4.2", "dotenv": "^17.4.2",
"drizzle-kit": "0.31.10", "drizzle-kit": "0.31.10",
"knip": "^6.14.2", "knip": "^6.15.0",
"tailwindcss": "4.3.0", "tailwindcss": "4.3.0",
"tsx": "4.22.3", "tsx": "4.22.4",
"typescript": "6.0.3" "typescript": "6.0.3"
} }
} }

2446
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,29 +7,7 @@ allowBuilds:
sharp: true sharp: true
unrs-resolver: true unrs-resolver: true
minimumReleaseAgeExclude: minimumReleaseAge: 0
- '@aws-sdk/client-s3@3.1050.0'
- '@aws-sdk/s3-request-presigner@3.1050.0'
- '@types/node@25.9.1'
- '@types/react@19.2.15'
- '@aws-sdk/client-s3@3.1054.0'
- '@aws-sdk/core@3.974.14'
- '@aws-sdk/credential-provider-env@3.972.40'
- '@aws-sdk/credential-provider-http@3.972.42'
- '@aws-sdk/credential-provider-ini@3.972.44'
- '@aws-sdk/credential-provider-login@3.972.44'
- '@aws-sdk/credential-provider-node@3.972.45'
- '@aws-sdk/credential-provider-process@3.972.40'
- '@aws-sdk/credential-provider-sso@3.972.44'
- '@aws-sdk/credential-provider-web-identity@3.972.44'
- '@aws-sdk/middleware-bucket-endpoint@3.972.16'
- '@aws-sdk/middleware-flexible-checksums@3.974.22'
- '@aws-sdk/middleware-sdk-s3@3.972.43'
- '@aws-sdk/nested-clients@3.997.12'
- '@aws-sdk/s3-request-presigner@3.1054.0'
- '@aws-sdk/signature-v4-multi-region@3.996.29'
- '@aws-sdk/token-providers@3.1054.0'
- '@aws-sdk/xml-builder@3.972.26'
overrides: overrides:
defu: 6.1.7 defu: 6.1.7

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 571 KiB

After

Width:  |  Height:  |  Size: 350 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 562 KiB

After

Width:  |  Height:  |  Size: 355 KiB

View File

@@ -1,3 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200" role="img" aria-label="OpenMonetis"> <svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" aria-label="OpenMonetis" role="img" viewBox="0 0 200 200"><path fill="#f73" 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>
<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>

Before

Width:  |  Height:  |  Size: 402 B

After

Width:  |  Height:  |  Size: 394 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>MiniMax</title><defs><linearGradient id="lobe-icons-minimax-gradient" x1="0%" x2="100.182%" y1="50.057%" y2="50.057%"><stop offset="0%" stop-color="#E2167E"/><stop offset="100%" stop-color="#FE603C"/></linearGradient></defs><path d="M16.278 2c1.156 0 2.093.927 2.093 2.07v12.501a.74.74 0 00.744.709.74.74 0 00.743-.709V9.099a2.06 2.06 0 012.071-2.049A2.06 2.06 0 0124 9.1v6.561a.649.649 0 01-.652.645.649.649 0 01-.653-.645V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v7.472a2.037 2.037 0 01-2.048 2.026 2.037 2.037 0 01-2.048-2.026v-12.5a.785.785 0 00-.788-.753.785.785 0 00-.789.752l-.001 15.904A2.037 2.037 0 0113.441 22a2.037 2.037 0 01-2.048-2.026V18.04c0-.356.292-.645.652-.645.36 0 .652.289.652.645v1.934c0 .263.142.506.372.638.23.131.514.131.744 0a.734.734 0 00.372-.638V4.07c0-1.143.937-2.07 2.093-2.07zm-5.674 0c1.156 0 2.093.927 2.093 2.07v11.523a.648.648 0 01-.652.645.648.648 0 01-.652-.645V4.07a.785.785 0 00-.789-.78.785.785 0 00-.789.78v14.013a2.06 2.06 0 01-2.07 2.048 2.06 2.06 0 01-2.071-2.048V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v3.8a2.06 2.06 0 01-2.071 2.049A2.06 2.06 0 010 12.9v-1.378c0-.357.292-.646.652-.646.36 0 .653.29.653.646V12.9c0 .418.343.757.766.757s.766-.339.766-.757V9.099a2.06 2.06 0 012.07-2.048 2.06 2.06 0 012.071 2.048v8.984c0 .419.343.758.767.758.423 0 .766-.339.766-.758V4.07c0-1.143.937-2.07 2.093-2.07z" fill="url(#lobe-icons-minimax-gradient)" fill-rule="nonzero"/></svg> <svg xmlns="http://www.w3.org/2000/svg" height="1em" style="flex:none;line-height:1" width="1em" viewBox="0 0 24 24"><title>MiniMax</title><defs><linearGradient id="lobe-icons-minimax-gradient" x1="0%" x2="100.182%" y1="50.057%" y2="50.057%"><stop offset="0%" stop-color="#E2167E"/><stop offset="100%" stop-color="#FE603C"/></linearGradient></defs><path fill="url(#lobe-icons-minimax-gradient)" fill-rule="nonzero" d="M16.278 2c1.156 0 2.093.927 2.093 2.07v12.501a.74.74 0 00.744.709.74.74 0 00.743-.709V9.099a2.06 2.06 0 012.071-2.049A2.06 2.06 0 0124 9.1v6.561a.649.649 0 01-.652.645.649.649 0 01-.653-.645V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v7.472a2.037 2.037 0 01-2.048 2.026 2.037 2.037 0 01-2.048-2.026v-12.5a.785.785 0 00-.788-.753.785.785 0 00-.789.752l-.001 15.904A2.037 2.037 0 0113.441 22a2.037 2.037 0 01-2.048-2.026V18.04c0-.356.292-.645.652-.645.36 0 .652.289.652.645v1.934c0 .263.142.506.372.638.23.131.514.131.744 0a.734.734 0 00.372-.638V4.07c0-1.143.937-2.07 2.093-2.07zm-5.674 0c1.156 0 2.093.927 2.093 2.07v11.523a.648.648 0 01-.652.645.648.648 0 01-.652-.645V4.07a.785.785 0 00-.789-.78.785.785 0 00-.789.78v14.013a2.06 2.06 0 01-2.07 2.048 2.06 2.06 0 01-2.071-2.048V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v3.8a2.06 2.06 0 01-2.071 2.049A2.06 2.06 0 010 12.9v-1.378c0-.357.292-.646.652-.646.36 0 .653.29.653.646V12.9c0 .418.343.757.766.757s.766-.339.766-.757V9.099a2.06 2.06 0 012.07-2.048 2.06 2.06 0 012.071 2.048v8.984c0 .419.343.758.767.758.423 0 .766-.339.766-.758V4.07c0-1.143.937-2.07 2.093-2.07z"/></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@@ -136,6 +136,7 @@ export default async function Page({ params, searchParams }: PageProps) {
limitAvailable: limitAmount, limitAvailable: limitAmount,
currentInvoiceAmount: 0, currentInvoiceAmount: 0,
currentInvoiceLabel: "", currentInvoiceLabel: "",
currentInvoiceStatus: null,
}; };
const { totalAmount, invoiceStatus, paymentDate } = invoiceData; const { totalAmount, invoiceStatus, paymentDate } = invoiceData;

View File

@@ -38,7 +38,7 @@ export const metadata: Metadata = {
description: DESCRIPTION, description: DESCRIPTION,
images: [ images: [
{ {
url: "/images/dashboard-preview-light.webp", url: "/images/dashboard-preview-light.png",
width: 1920, width: 1920,
height: 1080, height: 1080,
alt: "OpenMonetis — Dashboard de finanças pessoais", alt: "OpenMonetis — Dashboard de finanças pessoais",
@@ -49,7 +49,7 @@ export const metadata: Metadata = {
card: "summary_large_image", card: "summary_large_image",
title: TITLE, title: TITLE,
description: DESCRIPTION, description: DESCRIPTION,
images: ["/images/dashboard-preview-light.webp"], images: ["/images/dashboard-preview-light.png"],
}, },
robots: { robots: {
index: true, index: true,

View File

@@ -32,7 +32,7 @@ function PdfCanvas({ url }: PdfCanvasProps) {
let pdf: Awaited<ReturnType<typeof pdfjsLib.getDocument>["promise"]>; let pdf: Awaited<ReturnType<typeof pdfjsLib.getDocument>["promise"]>;
try { try {
pdf = await pdfjsLib.getDocument(url).promise; pdf = await pdfjsLib.getDocument({ url }).promise;
} catch (err) { } catch (err) {
if ((err as { name?: string }).name === "PasswordException") { if ((err as { name?: string }).name === "PasswordException") {
if (!cancelled) setLocked(true); if (!cancelled) setLocked(true);

View File

@@ -10,6 +10,7 @@ import {
} from "@remixicon/react"; } from "@remixicon/react";
import Image from "next/image"; import Image from "next/image";
import MoneyValues from "@/shared/components/money-values"; import MoneyValues from "@/shared/components/money-values";
import { Badge } from "@/shared/components/ui/badge";
import { import {
Card, Card,
CardContent, CardContent,
@@ -23,6 +24,10 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/shared/components/ui/tooltip"; } from "@/shared/components/ui/tooltip";
import { resolveCardBrandAsset } from "@/shared/lib/cards/brand-assets"; import { resolveCardBrandAsset } from "@/shared/lib/cards/brand-assets";
import {
INVOICE_PAYMENT_STATUS,
type InvoicePaymentStatus,
} from "@/shared/lib/invoices";
import { resolveLogoSrc } from "@/shared/lib/logo"; import { resolveLogoSrc } from "@/shared/lib/logo";
import { cn } from "@/shared/utils/ui"; import { cn } from "@/shared/utils/ui";
@@ -37,6 +42,7 @@ interface CardItemProps {
limitAvailable?: number; limitAvailable?: number;
currentInvoiceAmount: number; currentInvoiceAmount: number;
currentInvoiceLabel: string; currentInvoiceLabel: string;
currentInvoiceStatus: InvoicePaymentStatus | null;
accountName: string; accountName: string;
logo?: string | null; logo?: string | null;
note?: string | null; note?: string | null;
@@ -58,6 +64,7 @@ export function CardItem({
limitAvailable, limitAvailable,
currentInvoiceAmount, currentInvoiceAmount,
currentInvoiceLabel, currentInvoiceLabel,
currentInvoiceStatus,
accountName: _accountName, accountName: _accountName,
logo, logo,
note, note,
@@ -80,6 +87,8 @@ export function CardItem({
const logoPath = resolveLogoSrc(logo); const logoPath = resolveLogoSrc(logo);
const brandAsset = resolveCardBrandAsset(brand); const brandAsset = resolveCardBrandAsset(brand);
const isInactive = status?.toLowerCase() === "inativo"; const isInactive = status?.toLowerCase() === "inativo";
const isCurrentInvoicePaid =
currentInvoiceStatus === INVOICE_PAYMENT_STATUS.PAID;
return ( return (
<Card className="flex flex-col p-6 w-full"> <Card className="flex flex-col p-6 w-full">
@@ -175,10 +184,17 @@ export function CardItem({
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
{currentInvoiceLabel} {currentInvoiceLabel}
</span> </span>
<MoneyValues <div className="flex flex-wrap items-center gap-2">
amount={currentInvoiceAmount} <MoneyValues
className="text-xl font-semibold text-info" amount={currentInvoiceAmount}
/> className="text-xl font-semibold text-info"
/>
{isCurrentInvoicePaid ? (
<Badge variant="success" className="text-xs">
Paga
</Badge>
) : null}
</div>
</div> </div>
<div className="flex gap-2 justify-between w-full"> <div className="flex gap-2 justify-between w-full">

View File

@@ -144,6 +144,7 @@ export function CardsPage({
limitAvailable={card.limitAvailable ?? card.limit ?? null} limitAvailable={card.limitAvailable ?? card.limit ?? null}
currentInvoiceAmount={card.currentInvoiceAmount} currentInvoiceAmount={card.currentInvoiceAmount}
currentInvoiceLabel={card.currentInvoiceLabel} currentInvoiceLabel={card.currentInvoiceLabel}
currentInvoiceStatus={card.currentInvoiceStatus}
accountName={card.accountName} accountName={card.accountName}
logo={card.logo} logo={card.logo}
note={card.note} note={card.note}

View File

@@ -1,3 +1,5 @@
import type { InvoicePaymentStatus } from "@/shared/lib/invoices";
export type Card = { export type Card = {
id: string; id: string;
name: string; name: string;
@@ -14,6 +16,7 @@ export type Card = {
limitAvailable: number; limitAvailable: number;
currentInvoiceAmount: number; currentInvoiceAmount: number;
currentInvoiceLabel: string; currentInvoiceLabel: string;
currentInvoiceStatus: InvoicePaymentStatus | null;
}; };
export type CardFormValues = { export type CardFormValues = {

View File

@@ -11,7 +11,11 @@ import {
} from "drizzle-orm"; } from "drizzle-orm";
import { cards, financialAccounts, invoices, transactions } from "@/db/schema"; import { cards, financialAccounts, invoices, transactions } from "@/db/schema";
import { db } from "@/shared/lib/db"; import { db } from "@/shared/lib/db";
import { INVOICE_PAYMENT_STATUS } from "@/shared/lib/invoices"; import {
INVOICE_PAYMENT_STATUS,
INVOICE_STATUS_VALUES,
type InvoicePaymentStatus,
} from "@/shared/lib/invoices";
import { loadLogoOptions } from "@/shared/lib/logo/options"; import { loadLogoOptions } from "@/shared/lib/logo/options";
import { import {
formatPeriodMonthShort, formatPeriodMonthShort,
@@ -33,6 +37,7 @@ type CardData = {
limitAvailable: number; limitAvailable: number;
currentInvoiceAmount: number; currentInvoiceAmount: number;
currentInvoiceLabel: string; currentInvoiceLabel: string;
currentInvoiceStatus: InvoicePaymentStatus | null;
accountId: string; accountId: string;
accountName: string; accountName: string;
}; };
@@ -48,6 +53,12 @@ function formatCurrentInvoiceLabel(period: string) {
return `Fatura ${formatPeriodMonthShort(period)}. ${year}`; return `Fatura ${formatPeriodMonthShort(period)}. ${year}`;
} }
function parseInvoiceStatus(value: unknown): InvoicePaymentStatus | null {
return INVOICE_STATUS_VALUES.includes(value as InvoicePaymentStatus)
? (value as InvoicePaymentStatus)
: null;
}
async function fetchCardsByStatus( async function fetchCardsByStatus(
userId: string, userId: string,
archived: boolean, archived: boolean,
@@ -58,79 +69,94 @@ async function fetchCardsByStatus(
}> { }> {
const currentPeriod = getCurrentPeriod(); const currentPeriod = getCurrentPeriod();
const currentInvoiceLabel = formatCurrentInvoiceLabel(currentPeriod); const currentInvoiceLabel = formatCurrentInvoiceLabel(currentPeriod);
const [cardRows, accountRows, logoOptions, usageRows, invoiceRows] = const [
await Promise.all([ cardRows,
db.query.cards.findMany({ accountRows,
orderBy: (table, { desc }) => [desc(table.name)], logoOptions,
where: and( usageRows,
eq(cards.userId, userId), invoiceRows,
archived invoiceStatusRows,
? ilike(cards.status, "inativo") ] = await Promise.all([
: not(ilike(cards.status, "inativo")), db.query.cards.findMany({
), orderBy: (table, { desc }) => [desc(table.name)],
with: { where: and(
financialAccount: { eq(cards.userId, userId),
columns: { archived
id: true, ? ilike(cards.status, "inativo")
name: true, : not(ilike(cards.status, "inativo")),
}, ),
with: {
financialAccount: {
columns: {
id: true,
name: true,
}, },
}, },
}), },
db.query.financialAccounts.findMany({ }),
orderBy: (table, { desc }) => [desc(table.name)], db.query.financialAccounts.findMany({
where: eq(financialAccounts.userId, userId), orderBy: (table, { desc }) => [desc(table.name)],
columns: { where: eq(financialAccounts.userId, userId),
id: true, columns: {
name: true, id: true,
logo: true, name: true,
}, logo: true,
}), },
loadLogoOptions(), }),
db loadLogoOptions(),
.select({ db
cardId: transactions.cardId, .select({
total: sql<number>`coalesce(sum(${transactions.amount}), 0)`, cardId: transactions.cardId,
}) total: sql<number>`coalesce(sum(${transactions.amount}), 0)`,
.from(transactions) })
.leftJoin( .from(transactions)
invoices, .leftJoin(
and( invoices,
eq(invoices.userId, transactions.userId), and(
eq(invoices.cardId, transactions.cardId), eq(invoices.userId, transactions.userId),
eq(invoices.period, transactions.period), eq(invoices.cardId, transactions.cardId),
eq(invoices.period, transactions.period),
),
)
.where(
and(
eq(transactions.userId, userId),
isNotNull(transactions.cardId),
or(
isNull(invoices.paymentStatus),
ne(invoices.paymentStatus, INVOICE_PAYMENT_STATUS.PAID),
), ),
) // Recorrente no cartão: só consome limite quando a data da ocorrência já passou
.where( or(
and( ne(transactions.condition, "Recorrente"),
eq(transactions.userId, userId), sql`${transactions.purchaseDate} <= current_date`,
isNotNull(transactions.cardId),
or(
isNull(invoices.paymentStatus),
ne(invoices.paymentStatus, INVOICE_PAYMENT_STATUS.PAID),
),
// Recorrente no cartão: só consome limite quando a data da ocorrência já passou
or(
ne(transactions.condition, "Recorrente"),
sql`${transactions.purchaseDate} <= current_date`,
),
), ),
) ),
.groupBy(transactions.cardId), )
db .groupBy(transactions.cardId),
.select({ db
cardId: transactions.cardId, .select({
total: sql<number>`coalesce(sum(${transactions.amount}), 0)`, cardId: transactions.cardId,
}) total: sql<number>`coalesce(sum(${transactions.amount}), 0)`,
.from(transactions) })
.where( .from(transactions)
and( .where(
eq(transactions.userId, userId), and(
eq(transactions.period, currentPeriod), eq(transactions.userId, userId),
), eq(transactions.period, currentPeriod),
) ),
.groupBy(transactions.cardId), )
]); .groupBy(transactions.cardId),
db
.select({
cardId: invoices.cardId,
paymentStatus: invoices.paymentStatus,
})
.from(invoices)
.where(
and(eq(invoices.userId, userId), eq(invoices.period, currentPeriod)),
),
]);
const usageMap = new Map<string, number>(); const usageMap = new Map<string, number>();
usageRows.forEach((row: { cardId: string | null; total: number | null }) => { usageRows.forEach((row: { cardId: string | null; total: number | null }) => {
@@ -144,6 +170,13 @@ async function fetchCardsByStatus(
invoiceMap.set(row.cardId, Math.abs(Number(row.total ?? 0))); invoiceMap.set(row.cardId, Math.abs(Number(row.total ?? 0)));
}, },
); );
const invoiceStatusMap = new Map<string, InvoicePaymentStatus>();
invoiceStatusRows.forEach((row) => {
if (!row.cardId) return;
const status = parseInvoiceStatus(row.paymentStatus);
if (!status) return;
invoiceStatusMap.set(row.cardId, status);
});
const cardList = cardRows.map((card) => ({ const cardList = cardRows.map((card) => ({
id: card.id, id: card.id,
@@ -166,6 +199,7 @@ async function fetchCardsByStatus(
})(), })(),
currentInvoiceAmount: invoiceMap.get(card.id) ?? 0, currentInvoiceAmount: invoiceMap.get(card.id) ?? 0,
currentInvoiceLabel, currentInvoiceLabel,
currentInvoiceStatus: invoiceStatusMap.get(card.id) ?? null,
accountId: card.accountId, accountId: card.accountId,
accountName: accountName:
(card.financialAccount as { name?: string } | null)?.name ?? (card.financialAccount as { name?: string } | null)?.name ??

View File

@@ -221,7 +221,7 @@ export function InboxWidget({
<span className="truncate">{item.sourceAppName}</span> <span className="truncate">{item.sourceAppName}</span>
)} )}
<span className="text-muted-foreground/60"> <span className="text-muted-foreground/60">
{relativeTime(item.notificationTimestamp)} {relativeTime(item.createdAt)}
</span> </span>
</div> </div>
</div> </div>

View File

@@ -4,8 +4,8 @@ export const landingImages = {
dark: "/images/dashboard-preview-dark.png", dark: "/images/dashboard-preview-dark.png",
}, },
pwa: { pwa: {
light: "/images/pwa-preview-light.webp", light: "/images/pwa-preview-light.png",
dark: "/images/pwa-preview-dark.webp", dark: "/images/pwa-preview-dark.png",
}, },
companion: { companion: {
light: "/images/companion-preview-light.webp", light: "/images/companion-preview-light.webp",

View File

@@ -1,6 +1,6 @@
export type SectionType = "Adicionado" | "Alterado" | "Corrigido" | "Removido"; export type SectionType = "Adicionado" | "Alterado" | "Corrigido" | "Removido";
export const SECTION_TYPES: readonly SectionType[] = [ const SECTION_TYPES: readonly SectionType[] = [
"Adicionado", "Adicionado",
"Alterado", "Alterado",
"Corrigido", "Corrigido",

View File

@@ -31,19 +31,6 @@ import { addMonthsToPeriod, MONTH_NAMES } from "@/shared/utils/period";
// Authorization Validation Functions // Authorization Validation Functions
// ============================================================================ // ============================================================================
export async function validatePayerOwnership(
userId: string,
payerId: string | null | undefined,
): Promise<boolean> {
if (!payerId) return true;
const pagador = await db.query.payers.findFirst({
where: and(eq(payers.id, payerId), eq(payers.userId, userId)),
});
return !!pagador;
}
const normalizeIds = (ids: Array<string | null | undefined>) => [ const normalizeIds = (ids: Array<string | null | undefined>) => [
...new Set(ids.filter((id): id is string => Boolean(id))), ...new Set(ids.filter((id): id is string => Boolean(id))),
]; ];
@@ -592,7 +579,7 @@ type SplitShareInput = {
amount: number; amount: number;
}; };
export const resolveSplitShares = (data: { const resolveSplitShares = (data: {
payerId?: string | null; payerId?: string | null;
secondaryPayerId?: string | null; secondaryPayerId?: string | null;
splitShares?: SplitShareInput[]; splitShares?: SplitShareInput[];

View File

@@ -137,24 +137,6 @@ export function getSplitSummaryData(
}; };
} }
export function getSplitSummaryLabel(
formState: FormState,
payerOptions: SplitSummaryPayerOption[],
totalAmount: number,
) {
const summary = getSplitSummaryData(formState, payerOptions, totalAmount);
if (summary.type === "text") return summary.label;
const namesLabel = summary.participants
.map((participant) => participant.firstName)
.join(" ");
const remainingLabel =
summary.remainingCount > 0 ? ` +${summary.remainingCount}` : "";
return `${summary.count} pessoas: ${namesLabel}${remainingLabel} · ${summary.totalLabel}`;
}
export function SplitConfigDialog({ export function SplitConfigDialog({
open, open,
onOpenChange, onOpenChange,

View File

@@ -61,13 +61,3 @@ export function groupAndSortCategories(
), ),
})); }));
} }
/**
* Filters secondary payer options to exclude the primary payer
*/
export function filterSecondaryPayerOptions(
allOptions: SelectOption[],
primaryPayerId?: string,
): SelectOption[] {
return allOptions.filter((option) => option.value !== primaryPayerId);
}

View File

@@ -150,10 +150,7 @@ export const getSingleParam = (
return Array.isArray(value) ? (value[0] ?? null) : value; return Array.isArray(value) ? (value[0] ?? null) : value;
}; };
export const getMultiParam = ( const getMultiParam = (params: ResolvedSearchParams, key: string): string[] => {
params: ResolvedSearchParams,
key: string,
): string[] => {
const value = params?.[key]; const value = params?.[key];
if (!value) { if (!value) {
return []; return [];

View File

@@ -36,7 +36,7 @@ export const getLogoDisplayName = (logo?: string | null): string => {
* @example * @example
* deriveNameFromLogo("my-company-logo.png") // "My Company Logo" * deriveNameFromLogo("my-company-logo.png") // "My Company Logo"
*/ */
export const deriveNameFromLogo = (logo?: string | null) => { const deriveNameFromLogo = (logo?: string | null) => {
if (!logo) { if (!logo) {
return ""; return "";
} }