6 Commits

Author SHA1 Message Date
Felipe Coutinho
60a52b9873 fix(inbox): alinhar horario da tooltip do card 2026-03-21 19:42:55 +00:00
Felipe Coutinho
c9205f2be9 style(drizzle): normalizar snapshots gerados 2026-03-21 19:32:49 +00:00
Felipe Coutinho
1d36b12109 style: normalizar formatacao de importacao e suporte 2026-03-21 19:32:38 +00:00
Felipe Coutinho
19a1b1e943 chore(release): preparar versao 2.0.1 2026-03-21 19:31:53 +00:00
Felipe Coutinho
d3fc81db73 fix(inbox): melhorar filtros e identidade visual 2026-03-21 19:31:38 +00:00
Felipe Coutinho
80de9501f6 fix: move proxy.ts para src/ e atualiza dependências
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 17:52:20 +00:00
27 changed files with 5643 additions and 5581 deletions

View File

@@ -5,6 +5,16 @@ 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.0.1] - 2026-03-21
### 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: notificações de cartões/apps sem logo cadastrado agora exibem `default_icon.png` como fallback visual nos cards
- Inbox: select de apps em `/inbox` agora exibe os logos dos apps/cartões, com fallback para `default_icon.png` quando não houver logo mapeado
- Inbox: cabeçalhos de data entre grupos de cards agora exibem ícone e tipografia um pouco maior para melhorar a leitura
- Versionamento: `/api/health` passa a reportar a versão atual do `package.json`, evitando divergência entre healthcheck, UI e release publicada
## [2.0.0] - 2026-03-21 ## [2.0.0] - 2026-03-21
### Adicionado ### Adicionado

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,167 +1,167 @@
{ {
"version": "7", "version": "7",
"dialect": "postgresql", "dialect": "postgresql",
"entries": [ "entries": [
{ {
"idx": 0, "idx": 0,
"version": "7", "version": "7",
"when": 1762993507299, "when": 1762993507299,
"tag": "0000_flashy_manta", "tag": "0000_flashy_manta",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 1, "idx": 1,
"version": "7", "version": "7",
"when": 1765199006435, "when": 1765199006435,
"tag": "0001_young_mister_fear", "tag": "0001_young_mister_fear",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 2, "idx": 2,
"version": "7", "version": "7",
"when": 1765200545692, "when": 1765200545692,
"tag": "0002_slimy_flatman", "tag": "0002_slimy_flatman",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 3, "idx": 3,
"version": "7", "version": "7",
"when": 1767102605526, "when": 1767102605526,
"tag": "0003_green_korg", "tag": "0003_green_korg",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 4, "idx": 4,
"version": "7", "version": "7",
"when": 1767104066872, "when": 1767104066872,
"tag": "0004_acoustic_mach_iv", "tag": "0004_acoustic_mach_iv",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 5, "idx": 5,
"version": "7", "version": "7",
"when": 1767106121811, "when": 1767106121811,
"tag": "0005_adorable_bruce_banner", "tag": "0005_adorable_bruce_banner",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 6, "idx": 6,
"version": "7", "version": "7",
"when": 1767107487318, "when": 1767107487318,
"tag": "0006_youthful_mister_fear", "tag": "0006_youthful_mister_fear",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 7, "idx": 7,
"version": "7", "version": "7",
"when": 1767118780033, "when": 1767118780033,
"tag": "0007_sturdy_kate_bishop", "tag": "0007_sturdy_kate_bishop",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 8, "idx": 8,
"version": "7", "version": "7",
"when": 1767125796314, "when": 1767125796314,
"tag": "0008_fat_stick", "tag": "0008_fat_stick",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 9, "idx": 9,
"version": "7", "version": "7",
"when": 1768925100873, "when": 1768925100873,
"tag": "0009_add_dashboard_widgets", "tag": "0009_add_dashboard_widgets",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 10, "idx": 10,
"version": "7", "version": "7",
"when": 1769369834242, "when": 1769369834242,
"tag": "0010_lame_psynapse", "tag": "0010_lame_psynapse",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 11, "idx": 11,
"version": "7", "version": "7",
"when": 1769447087678, "when": 1769447087678,
"tag": "0011_remove_unused_inbox_columns", "tag": "0011_remove_unused_inbox_columns",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 12, "idx": 12,
"version": "7", "version": "7",
"when": 1769533200000, "when": 1769533200000,
"tag": "0012_rename_tables_to_portuguese", "tag": "0012_rename_tables_to_portuguese",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 13, "idx": 13,
"version": "7", "version": "7",
"when": 1769523352777, "when": 1769523352777,
"tag": "0013_fancy_rick_jones", "tag": "0013_fancy_rick_jones",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 14, "idx": 14,
"version": "7", "version": "7",
"when": 1769619226903, "when": 1769619226903,
"tag": "0014_yielding_jack_flag", "tag": "0014_yielding_jack_flag",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 15, "idx": 15,
"version": "7", "version": "7",
"when": 1770332054481, "when": 1770332054481,
"tag": "0015_concerned_kat_farrell", "tag": "0015_concerned_kat_farrell",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 16, "idx": 16,
"version": "7", "version": "7",
"when": 1771166328908, "when": 1771166328908,
"tag": "0016_complete_randall", "tag": "0016_complete_randall",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 17, "idx": 17,
"version": "7", "version": "7",
"when": 1772400510326, "when": 1772400510326,
"tag": "0017_previous_warstar", "tag": "0017_previous_warstar",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 18, "idx": 18,
"version": "7", "version": "7",
"when": 1773020417482, "when": 1773020417482,
"tag": "0018_rainy_epoch", "tag": "0018_rainy_epoch",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 19, "idx": 19,
"version": "7", "version": "7",
"when": 1773699152928, "when": 1773699152928,
"tag": "0019_ordinary_wild_pack", "tag": "0019_ordinary_wild_pack",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 20, "idx": 20,
"version": "7", "version": "7",
"when": 1773841892114, "when": 1773841892114,
"tag": "0020_add-budget-invoice-unique-constraints", "tag": "0020_add-budget-invoice-unique-constraints",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 21, "idx": 21,
"version": "7", "version": "7",
"when": 1774033320053, "when": 1774033320053,
"tag": "0021_careful_malcolm_colcord", "tag": "0021_careful_malcolm_colcord",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 22, "idx": 22,
"version": "7", "version": "7",
"when": 1748000000000, "when": 1748000000000,
"tag": "0022_import-category-mappings", "tag": "0022_import-category-mappings",
"breakpoints": true "breakpoints": true
} }
] ]
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "openmonetis", "name": "openmonetis",
"version": "2.0.0", "version": "2.0.1",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --turbopack", "dev": "next dev --turbopack",
@@ -28,9 +28,9 @@
"backup": "bash scripts/backup.sh" "backup": "bash scripts/backup.sh"
}, },
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "^3.0.62", "@ai-sdk/anthropic": "^3.0.63",
"@ai-sdk/google": "^3.0.51", "@ai-sdk/google": "^3.0.52",
"@ai-sdk/openai": "^3.0.46", "@ai-sdk/openai": "^3.0.47",
"@better-auth/passkey": "^1.5.5", "@better-auth/passkey": "^1.5.5",
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
@@ -60,7 +60,7 @@
"@tanstack/react-virtual": "^3.13.23", "@tanstack/react-virtual": "^3.13.23",
"@vercel/analytics": "^2.0.1", "@vercel/analytics": "^2.0.1",
"@vercel/speed-insights": "^2.0.0", "@vercel/speed-insights": "^2.0.0",
"ai": "^6.0.127", "ai": "^6.0.134",
"better-auth": "1.5.5", "better-auth": "1.5.5",
"canvas-confetti": "^1.9.4", "canvas-confetti": "^1.9.4",
"class-variance-authority": "0.7.1", "class-variance-authority": "0.7.1",
@@ -90,12 +90,12 @@
"@tailwindcss/postcss": "4.2.2", "@tailwindcss/postcss": "4.2.2",
"@types/canvas-confetti": "^1.9.0", "@types/canvas-confetti": "^1.9.0",
"@types/node": "25.5.0", "@types/node": "25.5.0",
"@types/pg": "^8.18.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.3.1",
"drizzle-kit": "0.31.10", "drizzle-kit": "0.31.10",
"tailwindcss": "4.2.1", "tailwindcss": "4.2.2",
"tsx": "4.21.0", "tsx": "4.21.0",
"typescript": "5.9.3" "typescript": "5.9.3"
} }

113
pnpm-lock.yaml generated
View File

@@ -9,17 +9,17 @@ importers:
.: .:
dependencies: dependencies:
'@ai-sdk/anthropic': '@ai-sdk/anthropic':
specifier: ^3.0.62 specifier: ^3.0.63
version: 3.0.62(zod@4.3.6) version: 3.0.63(zod@4.3.6)
'@ai-sdk/google': '@ai-sdk/google':
specifier: ^3.0.51 specifier: ^3.0.52
version: 3.0.51(zod@4.3.6) version: 3.0.52(zod@4.3.6)
'@ai-sdk/openai': '@ai-sdk/openai':
specifier: ^3.0.46 specifier: ^3.0.47
version: 3.0.46(zod@4.3.6) version: 3.0.47(zod@4.3.6)
'@better-auth/passkey': '@better-auth/passkey':
specifier: ^1.5.5 specifier: ^1.5.5
version: 1.5.5(933ec2e58dee4f4ee115209e94e4bc6e) version: 1.5.5(67982ffe784494dfa72893bff94217f8)
'@dnd-kit/core': '@dnd-kit/core':
specifier: ^6.3.1 specifier: ^6.3.1
version: 6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) version: 6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -31,7 +31,7 @@ importers:
version: 3.2.2(react@19.2.4) version: 3.2.2(react@19.2.4)
'@openrouter/ai-sdk-provider': '@openrouter/ai-sdk-provider':
specifier: ^2.3.3 specifier: ^2.3.3
version: 2.3.3(ai@6.0.129(zod@4.3.6))(zod@4.3.6) version: 2.3.3(ai@6.0.134(zod@4.3.6))(zod@4.3.6)
'@radix-ui/react-alert-dialog': '@radix-ui/react-alert-dialog':
specifier: 1.1.15 specifier: 1.1.15
version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -105,11 +105,11 @@ importers:
specifier: ^2.0.0 specifier: ^2.0.0
version: 2.0.0(next@16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) version: 2.0.0(next@16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)
ai: ai:
specifier: ^6.0.127 specifier: ^6.0.134
version: 6.0.129(zod@4.3.6) version: 6.0.134(zod@4.3.6)
better-auth: better-auth:
specifier: 1.5.5 specifier: 1.5.5
version: 1.5.5(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.18.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)))(mongodb@7.1.0)(mysql2@3.15.3)(next@16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.20.0)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) version: 1.5.5(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)))(mongodb@7.1.0)(mysql2@3.15.3)(next@16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.20.0)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
canvas-confetti: canvas-confetti:
specifier: ^1.9.4 specifier: ^1.9.4
version: 1.9.4 version: 1.9.4
@@ -127,7 +127,7 @@ importers:
version: 4.1.0 version: 4.1.0
drizzle-orm: drizzle-orm:
specifier: 0.45.1 specifier: 0.45.1
version: 0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.18.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)) version: 0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))
jspdf: jspdf:
specifier: ^4.2.1 specifier: ^4.2.1
version: 4.2.1 version: 4.2.1
@@ -190,8 +190,8 @@ importers:
specifier: 25.5.0 specifier: 25.5.0
version: 25.5.0 version: 25.5.0
'@types/pg': '@types/pg':
specifier: ^8.18.0 specifier: ^8.20.0
version: 8.18.0 version: 8.20.0
'@types/react': '@types/react':
specifier: 19.2.14 specifier: 19.2.14
version: 19.2.14 version: 19.2.14
@@ -205,8 +205,8 @@ importers:
specifier: 0.31.10 specifier: 0.31.10
version: 0.31.10 version: 0.31.10
tailwindcss: tailwindcss:
specifier: 4.2.1 specifier: 4.2.2
version: 4.2.1 version: 4.2.2
tsx: tsx:
specifier: 4.21.0 specifier: 4.21.0
version: 4.21.0 version: 4.21.0
@@ -216,32 +216,32 @@ importers:
packages: packages:
'@ai-sdk/anthropic@3.0.62': '@ai-sdk/anthropic@3.0.63':
resolution: {integrity: sha512-CkShXR8tmNO7QQnvpKbSMe2Vr1zUUcpqlp69iR+DYrbHm+tDJO9u6zZsjEHjcoRU9/e9z++p0W6NiuLC3aZ4Bg==} resolution: {integrity: sha512-SiLosFr0FfKfrNpAAj8mD/i3S5YBB/z5orb1DH3pN1yATuBNjjPMLnRE4P3Dn7Y5cQsro0uzw5g5117hkShWoQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
zod: ^3.25.76 || ^4.1.8 zod: ^3.25.76 || ^4.1.8
'@ai-sdk/gateway@3.0.75': '@ai-sdk/gateway@3.0.77':
resolution: {integrity: sha512-7Hwa0VdH+l85NFS7zqZhRRaiwZMStDxEwUoTPxPNEH6V0Vgw9wi9OGopIsYdywmfSOPfSAsPL8XXPAuaSLGchw==} resolution: {integrity: sha512-UdwIG2H2YMuntJQ5L+EmED5XiwnlvDT3HOmKfVFxR4Nq/RSLFA/HcchhwfNXHZ5UJjyuL2VO0huLbWSZ9ijemQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
zod: ^3.25.76 || ^4.1.8 zod: ^3.25.76 || ^4.1.8
'@ai-sdk/google@3.0.51': '@ai-sdk/google@3.0.52':
resolution: {integrity: sha512-S5pG/iRt+E12a4TSnquBFnkHkbS+rcAJ2lRzds59vdnVqTsZGGIncaLefpGmq/MZNfbSo6JIO60duoZIpZXOqg==} resolution: {integrity: sha512-HiFB4VlHnv55k9xIbgQW9tHw5OsLXzbAghnDUqrnk/S94QpQuyrDwLSDsk/tUkxJeT00B+wvhL1y6/SARdLeXw==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
zod: ^3.25.76 || ^4.1.8 zod: ^3.25.76 || ^4.1.8
'@ai-sdk/openai@3.0.46': '@ai-sdk/openai@3.0.47':
resolution: {integrity: sha512-8grBb4sAMU0MAC6uOOD/wP/+SyX/3MMS/Lf+ToGgeUzoF9oWU9dWBLkvgjXpn7Ro81bPDeycW2GCCT63V/Vnvg==} resolution: {integrity: sha512-bRsb2sDN5u+pKO3Kdr0flpxtL+cPwQ2uCo/pVyzIbj2I4AkKAokJHhw5JWLVOeEwdlYzWfmv+hzaiGarzUcTFQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
zod: ^3.25.76 || ^4.1.8 zod: ^3.25.76 || ^4.1.8
'@ai-sdk/provider-utils@4.0.20': '@ai-sdk/provider-utils@4.0.21':
resolution: {integrity: sha512-gpUIj9uDhIGbuo9afKEgQ074BWmhvK4THJAAeBjRnroTy2yQYo6rbtGD7pQDMZM8ouXPYmT/SCdkWVJ0KcpX8A==} resolution: {integrity: sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
zod: ^3.25.76 || ^4.1.8 zod: ^3.25.76 || ^4.1.8
@@ -2240,8 +2240,8 @@ packages:
'@types/pako@2.0.4': '@types/pako@2.0.4':
resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==} resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==}
'@types/pg@8.18.0': '@types/pg@8.20.0':
resolution: {integrity: sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==} resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==}
'@types/raf@3.4.3': '@types/raf@3.4.3':
resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==} resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==}
@@ -2329,8 +2329,8 @@ packages:
resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==} resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==}
engines: {node: '>=0.8'} engines: {node: '>=0.8'}
ai@6.0.129: ai@6.0.134:
resolution: {integrity: sha512-5nGckqbzwUBZD7wV9jsA8qaoYRwGpU9LVMtXD+ZrxSi2H6QNjpbrhsuuEBKS9xcMYevCviVNoFzpmSUWzn45Hw==} resolution: {integrity: sha512-YalNEaavld/kE444gOcsMKXdVVRGEe0SK77fAFcWYcqLg+a7xKnEet8bdfrEAJTfnMjj01rhgrIL10903w1a5Q==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
zod: ^3.25.76 || ^4.1.8 zod: ^3.25.76 || ^4.1.8
@@ -3380,9 +3380,6 @@ packages:
tailwind-merge@3.5.0: tailwind-merge@3.5.0:
resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==}
tailwindcss@4.2.1:
resolution: {integrity: sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==}
tailwindcss@4.2.2: tailwindcss@4.2.2:
resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==} resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==}
@@ -3514,32 +3511,32 @@ packages:
snapshots: snapshots:
'@ai-sdk/anthropic@3.0.62(zod@4.3.6)': '@ai-sdk/anthropic@3.0.63(zod@4.3.6)':
dependencies: dependencies:
'@ai-sdk/provider': 3.0.8 '@ai-sdk/provider': 3.0.8
'@ai-sdk/provider-utils': 4.0.20(zod@4.3.6) '@ai-sdk/provider-utils': 4.0.21(zod@4.3.6)
zod: 4.3.6 zod: 4.3.6
'@ai-sdk/gateway@3.0.75(zod@4.3.6)': '@ai-sdk/gateway@3.0.77(zod@4.3.6)':
dependencies: dependencies:
'@ai-sdk/provider': 3.0.8 '@ai-sdk/provider': 3.0.8
'@ai-sdk/provider-utils': 4.0.20(zod@4.3.6) '@ai-sdk/provider-utils': 4.0.21(zod@4.3.6)
'@vercel/oidc': 3.1.0 '@vercel/oidc': 3.1.0
zod: 4.3.6 zod: 4.3.6
'@ai-sdk/google@3.0.51(zod@4.3.6)': '@ai-sdk/google@3.0.52(zod@4.3.6)':
dependencies: dependencies:
'@ai-sdk/provider': 3.0.8 '@ai-sdk/provider': 3.0.8
'@ai-sdk/provider-utils': 4.0.20(zod@4.3.6) '@ai-sdk/provider-utils': 4.0.21(zod@4.3.6)
zod: 4.3.6 zod: 4.3.6
'@ai-sdk/openai@3.0.46(zod@4.3.6)': '@ai-sdk/openai@3.0.47(zod@4.3.6)':
dependencies: dependencies:
'@ai-sdk/provider': 3.0.8 '@ai-sdk/provider': 3.0.8
'@ai-sdk/provider-utils': 4.0.20(zod@4.3.6) '@ai-sdk/provider-utils': 4.0.21(zod@4.3.6)
zod: 4.3.6 zod: 4.3.6
'@ai-sdk/provider-utils@4.0.20(zod@4.3.6)': '@ai-sdk/provider-utils@4.0.21(zod@4.3.6)':
dependencies: dependencies:
'@ai-sdk/provider': 3.0.8 '@ai-sdk/provider': 3.0.8
'@standard-schema/spec': 1.1.0 '@standard-schema/spec': 1.1.0
@@ -3577,12 +3574,12 @@ snapshots:
nanostores: 1.1.1 nanostores: 1.1.1
zod: 4.3.6 zod: 4.3.6
'@better-auth/drizzle-adapter@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.18.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)))': '@better-auth/drizzle-adapter@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)))':
dependencies: dependencies:
'@better-auth/core': 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/core': 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1)
'@better-auth/utils': 0.3.1 '@better-auth/utils': 0.3.1
optionalDependencies: optionalDependencies:
drizzle-orm: 0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.18.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)) drizzle-orm: 0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))
'@better-auth/kysely-adapter@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(kysely@0.28.11)': '@better-auth/kysely-adapter@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(kysely@0.28.11)':
dependencies: dependencies:
@@ -3601,14 +3598,14 @@ snapshots:
'@better-auth/utils': 0.3.1 '@better-auth/utils': 0.3.1
mongodb: 7.1.0 mongodb: 7.1.0
'@better-auth/passkey@1.5.5(933ec2e58dee4f4ee115209e94e4bc6e)': '@better-auth/passkey@1.5.5(67982ffe784494dfa72893bff94217f8)':
dependencies: dependencies:
'@better-auth/core': 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/core': 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1)
'@better-auth/utils': 0.3.1 '@better-auth/utils': 0.3.1
'@better-fetch/fetch': 1.1.21 '@better-fetch/fetch': 1.1.21
'@simplewebauthn/browser': 13.2.2 '@simplewebauthn/browser': 13.2.2
'@simplewebauthn/server': 13.2.3 '@simplewebauthn/server': 13.2.3
better-auth: 1.5.5(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.18.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)))(mongodb@7.1.0)(mysql2@3.15.3)(next@16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.20.0)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) better-auth: 1.5.5(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)))(mongodb@7.1.0)(mysql2@3.15.3)(next@16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.20.0)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
better-call: 1.3.2(zod@4.3.6) better-call: 1.3.2(zod@4.3.6)
nanostores: 1.1.1 nanostores: 1.1.1
zod: 4.3.6 zod: 4.3.6
@@ -4146,9 +4143,9 @@ snapshots:
'@noble/hashes@2.0.1': {} '@noble/hashes@2.0.1': {}
'@openrouter/ai-sdk-provider@2.3.3(ai@6.0.129(zod@4.3.6))(zod@4.3.6)': '@openrouter/ai-sdk-provider@2.3.3(ai@6.0.134(zod@4.3.6))(zod@4.3.6)':
dependencies: dependencies:
ai: 6.0.129(zod@4.3.6) ai: 6.0.134(zod@4.3.6)
zod: 4.3.6 zod: 4.3.6
'@opentelemetry/api@1.9.0': {} '@opentelemetry/api@1.9.0': {}
@@ -5305,7 +5302,7 @@ snapshots:
'@types/pako@2.0.4': {} '@types/pako@2.0.4': {}
'@types/pg@8.18.0': '@types/pg@8.20.0':
dependencies: dependencies:
'@types/node': 25.5.0 '@types/node': 25.5.0
pg-protocol: 1.13.0 pg-protocol: 1.13.0
@@ -5347,11 +5344,11 @@ snapshots:
adler-32@1.3.1: {} adler-32@1.3.1: {}
ai@6.0.129(zod@4.3.6): ai@6.0.134(zod@4.3.6):
dependencies: dependencies:
'@ai-sdk/gateway': 3.0.75(zod@4.3.6) '@ai-sdk/gateway': 3.0.77(zod@4.3.6)
'@ai-sdk/provider': 3.0.8 '@ai-sdk/provider': 3.0.8
'@ai-sdk/provider-utils': 4.0.20(zod@4.3.6) '@ai-sdk/provider-utils': 4.0.21(zod@4.3.6)
'@opentelemetry/api': 1.9.0 '@opentelemetry/api': 1.9.0
zod: 4.3.6 zod: 4.3.6
@@ -5378,10 +5375,10 @@ snapshots:
baseline-browser-mapping@2.10.0: {} baseline-browser-mapping@2.10.0: {}
better-auth@1.5.5(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.18.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)))(mongodb@7.1.0)(mysql2@3.15.3)(next@16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.20.0)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): better-auth@1.5.5(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)))(mongodb@7.1.0)(mysql2@3.15.3)(next@16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.20.0)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies: dependencies:
'@better-auth/core': 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/core': 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1)
'@better-auth/drizzle-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.18.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))) '@better-auth/drizzle-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)))
'@better-auth/kysely-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(kysely@0.28.11) '@better-auth/kysely-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(kysely@0.28.11)
'@better-auth/memory-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1) '@better-auth/memory-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)
'@better-auth/mongo-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(mongodb@7.1.0) '@better-auth/mongo-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(mongodb@7.1.0)
@@ -5400,7 +5397,7 @@ snapshots:
optionalDependencies: optionalDependencies:
'@prisma/client': 7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3) '@prisma/client': 7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3)
drizzle-kit: 0.31.10 drizzle-kit: 0.31.10
drizzle-orm: 0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.18.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)) drizzle-orm: 0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))
mongodb: 7.1.0 mongodb: 7.1.0
mysql2: 3.15.3 mysql2: 3.15.3
next: 16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next: 16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -5607,12 +5604,12 @@ snapshots:
esbuild: 0.25.12 esbuild: 0.25.12
tsx: 4.21.0 tsx: 4.21.0
drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.18.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)): drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)):
optionalDependencies: optionalDependencies:
'@electric-sql/pglite': 0.3.15 '@electric-sql/pglite': 0.3.15
'@opentelemetry/api': 1.9.0 '@opentelemetry/api': 1.9.0
'@prisma/client': 7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3) '@prisma/client': 7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3)
'@types/pg': 8.18.0 '@types/pg': 8.20.0
kysely: 0.28.11 kysely: 0.28.11
mysql2: 3.15.3 mysql2: 3.15.3
pg: 8.20.0 pg: 8.20.0
@@ -6397,8 +6394,6 @@ snapshots:
tailwind-merge@3.5.0: {} tailwind-merge@3.5.0: {}
tailwindcss@4.2.1: {}
tailwindcss@4.2.2: {} tailwindcss@4.2.2: {}
tapable@2.3.0: {} tapable@2.3.0: {}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -1,6 +1,7 @@
import { InboxPage } from "@/features/inbox/components/inbox-page"; import { InboxPage } from "@/features/inbox/components/inbox-page";
import { import {
type ResolvedInboxSearchParams, type ResolvedInboxSearchParams,
resolveInboxApp,
resolveInboxPagination, resolveInboxPagination,
resolveInboxStatus, resolveInboxStatus,
} from "@/features/inbox/page-helpers"; } from "@/features/inbox/page-helpers";
@@ -8,6 +9,7 @@ import {
fetchAppLogoMap, fetchAppLogoMap,
fetchInboxDialogData, fetchInboxDialogData,
fetchInboxItemsPage, fetchInboxItemsPage,
fetchInboxSourceApps,
fetchInboxStatusCounts, fetchInboxStatusCounts,
} from "@/features/inbox/queries"; } from "@/features/inbox/queries";
import { getUserId } from "@/shared/lib/auth/server"; import { getUserId } from "@/shared/lib/auth/server";
@@ -32,21 +34,31 @@ export default async function Page({ searchParams }: PageProps) {
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);
const activeApp = resolveInboxApp(resolvedSearchParams);
const paginationInput = resolveInboxPagination(resolvedSearchParams); const paginationInput = resolveInboxPagination(resolvedSearchParams);
const [itemsPage, counts, dialogData, appLogoMap] = await Promise.all([ const [itemsPage, counts, sourceApps, dialogData, appLogoMap] =
fetchInboxItemsPage(userId, activeStatus, paginationInput), await Promise.all([
fetchInboxStatusCounts(userId), fetchInboxItemsPage(userId, activeStatus, {
activeStatus === "pending" ...paginationInput,
? fetchInboxDialogData(userId) sourceApp: activeApp,
: Promise.resolve(EMPTY_DIALOG_DATA), }),
fetchAppLogoMap(userId), fetchInboxStatusCounts(userId),
]); fetchInboxSourceApps(userId, activeStatus).catch(() => [] as string[]),
activeStatus === "pending"
? fetchInboxDialogData(userId)
: Promise.resolve(EMPTY_DIALOG_DATA),
fetchAppLogoMap(userId),
]);
const normalizedSourceApps = Array.isArray(sourceApps) ? sourceApps : [];
return ( return (
<main className="flex flex-col items-start gap-6"> <main className="flex flex-col items-start gap-6">
<InboxPage <InboxPage
activeStatus={activeStatus} activeStatus={activeStatus}
activeApp={activeApp}
sourceApps={normalizedSourceApps}
items={itemsPage.items} items={itemsPage.items}
counts={counts} counts={counts}
pagination={itemsPage.pagination} pagination={itemsPage.pagination}

View File

@@ -1,14 +1,25 @@
import { ImportPage } from "@/features/transactions/components/import/import-page"; import { ImportPage } from "@/features/transactions/components/import/import-page";
import {
buildOptionSets,
buildSluggedFilters,
} from "@/features/transactions/page-helpers";
import { fetchTransactionFilterSources } from "@/features/transactions/queries"; import { fetchTransactionFilterSources } from "@/features/transactions/queries";
import { buildOptionSets, buildSluggedFilters } from "@/features/transactions/page-helpers";
import { getUserId } from "@/shared/lib/auth/server"; import { getUserId } from "@/shared/lib/auth/server";
export default async function Page() { export default async function Page() {
const userId = await getUserId(); const userId = await getUserId();
const filterSources = await fetchTransactionFilterSources(userId); const filterSources = await fetchTransactionFilterSources(userId);
const sluggedFilters = buildSluggedFilters(filterSources); const sluggedFilters = buildSluggedFilters(filterSources);
const { payerOptions, accountOptions, cardOptions, categoryOptions, defaultPayerId } = const {
buildOptionSets({ ...sluggedFilters, payerRows: filterSources.payerRows }); payerOptions,
accountOptions,
cardOptions,
categoryOptions,
defaultPayerId,
} = buildOptionSets({
...sluggedFilters,
payerRows: filterSources.payerRows,
});
return ( return (
<main className="flex flex-col gap-6"> <main className="flex flex-col gap-6">

View File

@@ -1,8 +1,7 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { version as APP_VERSION } from "@/package.json";
import { db } from "@/shared/lib/db"; import { db } from "@/shared/lib/db";
const APP_VERSION = "1.0.0";
/** /**
* Health check endpoint para Docker, monitoring e OpenMonetis Companion * Health check endpoint para Docker, monitoring e OpenMonetis Companion
* GET /api/health * GET /api/health

View File

@@ -48,21 +48,20 @@ const accountBaseSchema = z.object({
.string({ message: "Selecione um logo." }) .string({ message: "Selecione um logo." })
.trim() .trim()
.min(1, "Selecione um logo."), .min(1, "Selecione um logo."),
initialBalance: z initialBalance: z.union([
.union([ z.number(),
z.number(), z
z .string()
.string() .trim()
.trim() .transform((value) =>
.transform((value) => value.length === 0 ? "0" : value.replace(",", "."),
value.length === 0 ? "0" : value.replace(",", "."), )
) .refine(
.refine( (value) => !Number.isNaN(Number.parseFloat(value)),
(value) => !Number.isNaN(Number.parseFloat(value)), "Informe um saldo inicial válido.",
"Informe um saldo inicial válido.", )
) .transform((value) => Number.parseFloat(value)),
.transform((value) => Number.parseFloat(value)), ]),
]),
excludeFromBalance: z excludeFromBalance: z
.union([z.boolean(), z.string()]) .union([z.boolean(), z.string()])
.transform((value) => value === true || value === "true"), .transform((value) => value === true || value === "true"),

View File

@@ -20,16 +20,15 @@ import {
CardTitle, CardTitle,
} from "@/shared/components/ui/card"; } from "@/shared/components/ui/card";
import { Checkbox } from "@/shared/components/ui/checkbox"; import { Checkbox } from "@/shared/components/ui/checkbox";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/shared/components/ui/tooltip";
import { resolveLogoSrc } from "@/shared/lib/logo"; import { resolveLogoSrc } from "@/shared/lib/logo";
import type { InboxItem } from "./types"; import type { InboxItem } from "./types";
// O timestamp vem do app Android em horário local mas salvo como UTC. const DEFAULT_INBOX_APP_LOGO = "/avatars/default_icon.png";
// Adicionamos o offset de Brasília para corrigir o cálculo de "há X tempo".
const BRASILIA_OFFSET_MS = 3 * 60 * 60 * 1000;
function adjustToBrasilia(date: Date): Date {
return new Date(date.getTime() + BRASILIA_OFFSET_MS);
}
function findMatchingLogo( function findMatchingLogo(
sourceAppName: string | null, sourceAppName: string | null,
@@ -78,17 +77,19 @@ export function InboxCard({
const matchedLogo = appLogoMap const matchedLogo = appLogoMap
? findMatchingLogo(item.sourceAppName, appLogoMap) ? findMatchingLogo(item.sourceAppName, appLogoMap)
: null; : null;
const displayLogo = matchedLogo ?? DEFAULT_INBOX_APP_LOGO;
const amount = item.parsedAmount ? parseFloat(item.parsedAmount) : null; const amount = item.parsedAmount ? parseFloat(item.parsedAmount) : null;
const rawDate = new Date(item.notificationTimestamp); const createdAtDate = new Date(item.createdAt);
const notificationDate = adjustToBrasilia(rawDate);
const timeAgo = formatDistanceToNow(notificationDate, { const timeAgo = formatDistanceToNow(createdAtDate, {
addSuffix: true, addSuffix: true,
locale: ptBR, locale: ptBR,
}); });
const fullDate = format(createdAtDate, "PPpp", { locale: ptBR });
const statusDate = const statusDate =
item.status === "processed" item.status === "processed"
? item.processedAt ? item.processedAt
@@ -107,21 +108,32 @@ export function InboxCard({
<CardHeader className="pt-4"> <CardHeader className="pt-4">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<CardTitle className="flex min-w-0 items-center gap-1.5 text-sm"> <CardTitle className="flex min-w-0 items-center gap-1.5 text-sm">
{matchedLogo && ( {onSelectToggle && (
<Image <Checkbox
src={matchedLogo} checked={!!selected}
alt="" onCheckedChange={() => onSelectToggle(item.id)}
width={24} aria-label="Selecionar item"
height={24} className="shrink-0"
className="shrink-0 rounded-full"
/> />
)} )}
<Image
src={displayLogo}
alt=""
width={32}
height={32}
className="shrink-0 rounded-full"
/>
<span className="truncate"> <span className="truncate">
{item.sourceAppName || item.sourceApp} {item.sourceAppName || item.sourceApp}
</span> </span>
<span className="shrink-0 text-xs font-normal text-muted-foreground"> <Tooltip>
{timeAgo} <TooltipTrigger asChild>
</span> <span className="shrink-0 cursor-default text-xs font-normal text-muted-foreground underline decoration-dotted underline-offset-2">
{timeAgo}
</span>
</TooltipTrigger>
<TooltipContent>{fullDate}</TooltipContent>
</Tooltip>
</CardTitle> </CardTitle>
{amount !== null && ( {amount !== null && (
<MoneyValues amount={amount} className="shrink-0 text-sm" /> <MoneyValues amount={amount} className="shrink-0 text-sm" />
@@ -174,13 +186,6 @@ export function InboxCard({
<RiDeleteBinLine className="size-4" /> <RiDeleteBinLine className="size-4" />
</Button> </Button>
)} )}
{onSelectToggle && (
<Checkbox
checked={!!selected}
onCheckedChange={() => onSelectToggle(item.id)}
aria-label="Selecionar item"
/>
)}
</div> </div>
</CardFooter> </CardFooter>
) : ( ) : (
@@ -213,13 +218,6 @@ export function InboxCard({
> >
<RiDeleteBinLine className="size-4" /> <RiDeleteBinLine className="size-4" />
</Button> </Button>
{onSelectToggle && (
<Checkbox
checked={!!selected}
onCheckedChange={() => onSelectToggle(item.id)}
aria-label="Selecionar item"
/>
)}
</CardFooter> </CardFooter>
)} )}
</Card> </Card>

View File

@@ -52,7 +52,14 @@ export function InboxDetailsDialog({
<div className="grid gap-2 text-sm"> <div className="grid gap-2 text-sm">
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-muted-foreground">App</span> <span className="text-muted-foreground">App</span>
<span>{item.sourceAppName || item.sourceApp}</span> <div className="flex flex-col items-end gap-0.5">
<span>{item.sourceAppName || item.sourceApp}</span>
{item.sourceAppName && (
<span className="font-mono text-xs text-muted-foreground">
{item.sourceApp}
</span>
)}
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -109,6 +116,11 @@ export function InboxDetailsDialog({
</div> </div>
<DialogFooter> <DialogFooter>
<DialogClose asChild>
<Button type="button" variant="outline">
Fechar
</Button>
</DialogClose>
{isPending && onProcess && ( {isPending && onProcess && (
<Button <Button
type="button" type="button"
@@ -120,11 +132,6 @@ export function InboxDetailsDialog({
Processar Processar
</Button> </Button>
)} )}
<DialogClose asChild>
<Button type="button" variant="outline">
Fechar
</Button>
</DialogClose>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@@ -6,8 +6,12 @@ import {
RiArrowRightDoubleLine, RiArrowRightDoubleLine,
RiArrowRightSLine, RiArrowRightSLine,
RiAtLine, RiAtLine,
RiCalendarEventLine,
RiDeleteBinLine, RiDeleteBinLine,
} from "@remixicon/react"; } from "@remixicon/react";
import { format } from "date-fns";
import { ptBR } from "date-fns/locale";
import Image from "next/image";
import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useEffect, useMemo, useState, useTransition } from "react"; import { useEffect, useMemo, useState, useTransition } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -42,6 +46,7 @@ import {
TabsList, TabsList,
TabsTrigger, TabsTrigger,
} from "@/shared/components/ui/tabs"; } from "@/shared/components/ui/tabs";
import { resolveLogoSrc } from "@/shared/lib/logo";
import { InboxCard } from "./inbox-card"; import { InboxCard } from "./inbox-card";
import { InboxDetailsDialog } from "./inbox-details-dialog"; import { InboxDetailsDialog } from "./inbox-details-dialog";
import type { import type {
@@ -52,8 +57,71 @@ import type {
SelectOption, SelectOption,
} from "./types"; } from "./types";
const BRASILIA_OFFSET_MS = 3 * 60 * 60 * 1000;
const DEFAULT_INBOX_APP_LOGO = "/avatars/default_icon.png";
function getDateKey(date: Date): string {
const adjusted = new Date(date.getTime() + BRASILIA_OFFSET_MS);
return adjusted.toISOString().slice(0, 10);
}
function getGroupLabel(dateKey: string): string {
const now = new Date();
const todayKey = getDateKey(now);
const yesterdayKey = getDateKey(
new Date(now.getTime() - 24 * 60 * 60 * 1000),
);
if (dateKey === todayKey) return "Hoje";
if (dateKey === yesterdayKey) return "Ontem";
const [year, month, day] = dateKey.split("-").map(Number);
return format(new Date(year, month - 1, day), "d 'de' MMMM", {
locale: ptBR,
});
}
function groupItemsByDay(
items: InboxItem[],
): { label: string; items: InboxItem[] }[] {
const groups = new Map<string, InboxItem[]>();
for (const item of items) {
const key = getDateKey(new Date(item.notificationTimestamp));
const group = groups.get(key);
if (group) {
group.push(item);
} else {
groups.set(key, [item]);
}
}
const sortedKeys = [...groups.keys()].sort((a, b) => b.localeCompare(a));
return sortedKeys.map((key) => ({
label: getGroupLabel(key),
items: groups.get(key) ?? [],
}));
}
function findMatchingLogo(
sourceAppName: string | null,
appLogoMap: Record<string, string>,
): string | null {
if (!sourceAppName) return null;
const appName = sourceAppName.toLowerCase();
if (appLogoMap[appName]) return resolveLogoSrc(appLogoMap[appName]);
for (const [name, logo] of Object.entries(appLogoMap)) {
if (name.includes(appName) || appName.includes(name)) {
return resolveLogoSrc(logo);
}
}
return null;
}
interface InboxPageProps { interface InboxPageProps {
activeStatus: InboxStatus; activeStatus: InboxStatus;
activeApp: string | null;
sourceApps: string[];
items: InboxItem[]; items: InboxItem[];
counts: InboxStatusCounts; counts: InboxStatusCounts;
pagination: InboxPaginationState; pagination: InboxPaginationState;
@@ -69,6 +137,8 @@ interface InboxPageProps {
export function InboxPage({ export function InboxPage({
activeStatus, activeStatus,
activeApp,
sourceApps = [],
items, items,
counts, counts,
pagination, pagination,
@@ -111,6 +181,38 @@ export function InboxPage({
const [selectionBulkStatus, setSelectionBulkStatus] = const [selectionBulkStatus, setSelectionBulkStatus] =
useState<InboxStatus>("pending"); useState<InboxStatus>("pending");
const normalizedSourceApps = useMemo(() => {
if (!Array.isArray(sourceApps)) {
return [];
}
const uniqueApps = new Set<string>();
for (const app of sourceApps) {
if (typeof app !== "string") {
continue;
}
const trimmedApp = app.trim();
if (!trimmedApp) {
continue;
}
uniqueApps.add(trimmedApp);
}
return [...uniqueApps].sort((left, right) =>
left.localeCompare(right, "pt-BR"),
);
}, [sourceApps]);
const appFilterOptions =
activeApp && !normalizedSourceApps.includes(activeApp)
? [activeApp, ...normalizedSourceApps]
: normalizedSourceApps;
const getAppLogo = (appName: string | null) =>
findMatchingLogo(appName, appLogoMap) ?? DEFAULT_INBOX_APP_LOGO;
const handleProcessOpenChange = (open: boolean) => { const handleProcessOpenChange = (open: boolean) => {
setProcessOpen(open); setProcessOpen(open);
if (!open) { if (!open) {
@@ -239,7 +341,6 @@ export function InboxPage({
setSelectedIds([]); setSelectedIds([]);
return; return;
} }
setSelectedIds(items.map((item) => item.id)); setSelectedIds(items.map((item) => item.id));
}; };
@@ -276,8 +377,42 @@ export function InboxPage({
}); });
}; };
const handleAppChange = (nextApp: string) => {
const nextParams = new URLSearchParams(searchParams.toString());
if (nextApp === "all") {
nextParams.delete("app");
} else {
nextParams.set("app", nextApp);
}
nextParams.delete("page");
startTransition(() => {
const target = nextParams.toString()
? `${pathname}?${nextParams.toString()}`
: pathname;
router.replace(target, { scroll: false });
});
};
const handleTabChange = (nextStatus: string) => { const handleTabChange = (nextStatus: string) => {
updateUrl(nextStatus as InboxStatus, 1, pagination.pageSize); const nextParams = new URLSearchParams(searchParams.toString());
nextParams.delete("app");
if (nextStatus === "pending") {
nextParams.delete("status");
} else {
nextParams.set("status", nextStatus);
}
nextParams.delete("page");
if (pagination.pageSize === INBOX_DEFAULT_PAGE_SIZE) {
nextParams.delete("pageSize");
} else {
nextParams.set("pageSize", pagination.pageSize.toString());
}
startTransition(() => {
const target = nextParams.toString()
? `${pathname}?${nextParams.toString()}`
: pathname;
router.replace(target, { scroll: false });
});
}; };
const handleSelectionBulkRequest = (status: InboxStatus) => { const handleSelectionBulkRequest = (status: InboxStatus) => {
@@ -401,32 +536,105 @@ export function InboxPage({
</Card> </Card>
); );
const renderGrid = (list: InboxItem[], readonly?: boolean) => const renderGroupedGrid = (list: InboxItem[], readonly?: boolean) => {
list.length === 0 ? ( if (list.length === 0) {
renderEmptyState( if (activeApp) {
return renderEmptyState("Nenhuma notificação deste app");
}
return renderEmptyState(
readonly readonly
? "Nenhuma notificação nesta aba" ? "Nenhuma notificação nesta aba"
: "Nenhum pré-lançamento pendente", : "Nenhum pré-lançamento pendente",
) );
) : ( }
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{list.map((item) => ( const groups = groupItemsByDay(list);
<InboxCard
key={item.id} return (
item={item} <div className="space-y-6">
readonly={readonly} {groups.map((group) => (
appLogoMap={appLogoMap} <div key={group.label}>
onProcess={readonly ? undefined : handleProcessRequest} <div className="mb-3 flex items-center gap-1 text-muted-foreground">
onDiscard={readonly ? undefined : handleDiscardRequest} <RiCalendarEventLine className="size-3.5 shrink-0" />
onViewDetails={readonly ? undefined : handleDetailsRequest} <p className="text-sm font-medium">{group.label}</p>
onDelete={readonly ? handleDeleteRequest : undefined} </div>
onRestoreToPending={readonly ? handleRestoreRequest : undefined} <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
selected={selectedIds.includes(item.id)} {group.items.map((item) => (
onSelectToggle={toggleSelection} <InboxCard
/> key={item.id}
item={item}
readonly={readonly}
appLogoMap={appLogoMap}
onProcess={readonly ? undefined : handleProcessRequest}
onDiscard={readonly ? undefined : handleDiscardRequest}
onViewDetails={readonly ? undefined : handleDetailsRequest}
onDelete={readonly ? handleDeleteRequest : undefined}
onRestoreToPending={
readonly ? handleRestoreRequest : undefined
}
selected={selectedIds.includes(item.id)}
onSelectToggle={toggleSelection}
/>
))}
</div>
</div>
))} ))}
</div> </div>
); );
};
const renderAppFilter = () => {
if (appFilterOptions.length === 0) {
return null;
}
return (
<Select value={activeApp ?? "all"} onValueChange={handleAppChange}>
<SelectTrigger className="w-[190px]">
<SelectValue>
<span className="flex min-w-0 items-center gap-2">
<Image
src={activeApp ? getAppLogo(activeApp) : DEFAULT_INBOX_APP_LOGO}
alt=""
width={20}
height={20}
className="shrink-0 rounded-full"
/>
<span className="truncate">{activeApp ?? "Todos"}</span>
</span>
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="all">
<span className="flex items-center gap-2">
<Image
src={DEFAULT_INBOX_APP_LOGO}
alt=""
width={20}
height={20}
className="shrink-0 rounded-full"
/>
<span>Todos</span>
</span>
</SelectItem>
{appFilterOptions.map((app) => (
<SelectItem key={app} value={app}>
<span className="flex min-w-0 items-center gap-2">
<Image
src={getAppLogo(app)}
alt=""
width={20}
height={20}
className="shrink-0 rounded-full"
/>
<span className="truncate">{app}</span>
</span>
</SelectItem>
))}
</SelectContent>
</Select>
);
};
return ( return (
<> <>
@@ -463,80 +671,110 @@ export function InboxPage({
</TabsList> </TabsList>
<TabsContent value="pending" className="mt-4"> <TabsContent value="pending" className="mt-4">
{activeStatus === "pending" && items.length > 0 && ( {activeStatus === "pending" &&
<div className="mb-4 flex items-center justify-end gap-2"> (appFilterOptions.length > 0 || items.length > 0) && (
<Button variant="outline" size="sm" onClick={toggleSelectAll}> <div className="mb-4 flex flex-wrap items-center gap-2">
{allSelected ? "Cancelar seleção" : "Selecionar página"} {renderAppFilter()}
</Button> {items.length > 0 ? (
{selectedIds.length > 0 && ( <div className="ml-auto flex items-center gap-2">
<Button <Button
variant="destructive" variant="outline"
size="sm" size="sm"
onClick={() => handleSelectionBulkRequest("pending")} onClick={toggleSelectAll}
> >
<RiDeleteBinLine className="mr-1.5 size-4" /> {allSelected ? "Cancelar seleção" : "Selecionar página"}
Descartar selecionados ({selectedIds.length}) </Button>
</Button> {selectedIds.length > 0 && (
)} <Button
</div> variant="destructive"
)} size="sm"
{activeStatus === "pending" ? renderGrid(items, false) : null} onClick={() => handleSelectionBulkRequest("pending")}
>
<RiDeleteBinLine className="mr-1.5 size-4" />
Descartar selecionados ({selectedIds.length})
</Button>
)}
</div>
) : null}
</div>
)}
{activeStatus === "pending" ? renderGroupedGrid(items, false) : null}
</TabsContent> </TabsContent>
<TabsContent value="processed" className="mt-4"> <TabsContent value="processed" className="mt-4">
{activeStatus === "processed" && items.length > 0 && ( {activeStatus === "processed" &&
<div className="mb-4 flex items-center justify-end gap-2"> (appFilterOptions.length > 0 || items.length > 0) && (
<Button variant="outline" size="sm" onClick={toggleSelectAll}> <div className="mb-4 flex flex-wrap items-center gap-2">
{allSelected ? "Cancelar seleção" : "Selecionar página"} {renderAppFilter()}
</Button> {items.length > 0 ? (
{selectedIds.length > 0 && ( <div className="ml-auto flex items-center gap-2">
<Button <Button
variant="destructive" variant="outline"
size="sm" size="sm"
onClick={() => handleSelectionBulkRequest("processed")} onClick={toggleSelectAll}
> >
<RiDeleteBinLine className="mr-1.5 size-4" /> {allSelected ? "Cancelar seleção" : "Selecionar página"}
Excluir selecionados ({selectedIds.length}) </Button>
</Button> {selectedIds.length > 0 && (
)} <Button
<Button variant="destructive"
variant="outline" size="sm"
size="sm" onClick={() => handleSelectionBulkRequest("processed")}
onClick={() => handleBulkDeleteRequest("processed")} >
> <RiDeleteBinLine className="mr-1.5 size-4" />
<RiDeleteBinLine className="mr-1.5 size-4" /> Excluir selecionados ({selectedIds.length})
Limpar processados </Button>
</Button> )}
</div> <Button
)} variant="outline"
{activeStatus === "processed" ? renderGrid(items, true) : null} size="sm"
onClick={() => handleBulkDeleteRequest("processed")}
>
<RiDeleteBinLine className="mr-1.5 size-4" />
Limpar processados
</Button>
</div>
) : null}
</div>
)}
{activeStatus === "processed" ? renderGroupedGrid(items, true) : null}
</TabsContent> </TabsContent>
<TabsContent value="discarded" className="mt-4"> <TabsContent value="discarded" className="mt-4">
{activeStatus === "discarded" && items.length > 0 && ( {activeStatus === "discarded" &&
<div className="mb-4 flex items-center justify-end gap-2"> (appFilterOptions.length > 0 || items.length > 0) && (
<Button variant="outline" size="sm" onClick={toggleSelectAll}> <div className="mb-4 flex flex-wrap items-center gap-2">
{allSelected ? "Cancelar seleção" : "Selecionar página"} {renderAppFilter()}
</Button> {items.length > 0 ? (
{selectedIds.length > 0 && ( <div className="ml-auto flex items-center gap-2">
<Button <Button
variant="destructive" variant="outline"
size="sm" size="sm"
onClick={() => handleSelectionBulkRequest("discarded")} onClick={toggleSelectAll}
> >
<RiDeleteBinLine className="mr-1.5 size-4" /> {allSelected ? "Cancelar seleção" : "Selecionar página"}
Excluir selecionados ({selectedIds.length}) </Button>
</Button> {selectedIds.length > 0 && (
)} <Button
<Button variant="destructive"
variant="outline" size="sm"
size="sm" onClick={() => handleSelectionBulkRequest("discarded")}
onClick={() => handleBulkDeleteRequest("discarded")} >
> <RiDeleteBinLine className="mr-1.5 size-4" />
<RiDeleteBinLine className="mr-1.5 size-4" /> Excluir selecionados ({selectedIds.length})
Limpar descartados </Button>
</Button> )}
</div> <Button
)} variant="outline"
{activeStatus === "discarded" ? renderGrid(items, true) : null} size="sm"
onClick={() => handleBulkDeleteRequest("discarded")}
>
<RiDeleteBinLine className="mr-1.5 size-4" />
Limpar descartados
</Button>
</div>
) : null}
</div>
)}
{activeStatus === "discarded" ? renderGroupedGrid(items, true) : null}
</TabsContent> </TabsContent>
</Tabs> </Tabs>

View File

@@ -31,6 +31,10 @@ export const resolveInboxStatus = (
: "pending"; : "pending";
}; };
export const resolveInboxApp = (
params: ResolvedInboxSearchParams,
): string | null => getSingleParam(params, "app");
export const resolveInboxPagination = ( export const resolveInboxPagination = (
params: ResolvedInboxSearchParams, params: ResolvedInboxSearchParams,
): Pick<InboxPaginationState, "page" | "pageSize"> => { ): Pick<InboxPaginationState, "page" | "pageSize"> => {

View File

@@ -39,18 +39,26 @@ export async function fetchInboxItemsPage(
{ {
page, page,
pageSize, pageSize,
sourceApp,
}: { }: {
page: number; page: number;
pageSize: number; pageSize: number;
sourceApp?: string | null;
}, },
): Promise<{ ): Promise<{
items: InboxItem[]; items: InboxItem[];
pagination: InboxPaginationState; pagination: InboxPaginationState;
}> { }> {
const where = and(
eq(inboxItems.userId, userId),
eq(inboxItems.status, status),
sourceApp ? eq(inboxItems.sourceAppName, sourceApp) : undefined,
);
const [countRow] = await db const [countRow] = await db
.select({ total: count() }) .select({ total: count() })
.from(inboxItems) .from(inboxItems)
.where(and(eq(inboxItems.userId, userId), eq(inboxItems.status, status))); .where(where);
const totalItems = Number(countRow?.total ?? 0); const totalItems = Number(countRow?.total ?? 0);
const totalPages = Math.max(Math.ceil(totalItems / pageSize), 1); const totalPages = Math.max(Math.ceil(totalItems / pageSize), 1);
@@ -60,7 +68,7 @@ export async function fetchInboxItemsPage(
const items = await db const items = await db
.select() .select()
.from(inboxItems) .from(inboxItems)
.where(and(eq(inboxItems.userId, userId), eq(inboxItems.status, status))) .where(where)
.orderBy(desc(inboxItems.notificationTimestamp), desc(inboxItems.createdAt)) .orderBy(desc(inboxItems.notificationTimestamp), desc(inboxItems.createdAt))
.limit(pageSize) .limit(pageSize)
.offset(offset); .offset(offset);
@@ -76,6 +84,22 @@ export async function fetchInboxItemsPage(
}; };
} }
export async function fetchInboxSourceApps(
userId: string,
status: InboxStatus,
): Promise<string[]> {
const rows = await db
.select({ name: inboxItems.sourceAppName })
.from(inboxItems)
.where(and(eq(inboxItems.userId, userId), eq(inboxItems.status, status)));
const seen = new Set<string>();
for (const row of rows) {
if (row.name) seen.add(row.name);
}
return [...seen].sort();
}
export async function fetchInboxStatusCounts( export async function fetchInboxStatusCounts(
userId: string, userId: string,
): Promise<InboxStatusCounts> { ): Promise<InboxStatusCounts> {

View File

@@ -6,7 +6,6 @@ import { normalizeDescriptionKey } from "@/features/transactions/lib/import-util
import { getUserId } from "@/shared/lib/auth/server"; import { getUserId } from "@/shared/lib/auth/server";
import { db } from "@/shared/lib/db"; import { db } from "@/shared/lib/db";
// Retorna um map de descriptionKey → categoryId para as descrições fornecidas // Retorna um map de descriptionKey → categoryId para as descrições fornecidas
export async function fetchCategoryMappings( export async function fetchCategoryMappings(
descriptions: string[], descriptions: string[],
@@ -53,7 +52,10 @@ export async function saveCategoryMappings(
.insert(importCategoryMappings) .insert(importCategoryMappings)
.values(toUpsert) .values(toUpsert)
.onConflictDoUpdate({ .onConflictDoUpdate({
target: [importCategoryMappings.userId, importCategoryMappings.descriptionKey], target: [
importCategoryMappings.userId,
importCategoryMappings.descriptionKey,
],
set: { set: {
categoryId: sql`excluded.category_id`, categoryId: sql`excluded.category_id`,
updatedAt: sql`excluded.updated_at`, updatedAt: sql`excluded.updated_at`,

View File

@@ -29,7 +29,11 @@ const importSchema = z.object({
accountId: uuidSchema("FinancialAccount").nullable().optional(), accountId: uuidSchema("FinancialAccount").nullable().optional(),
cardId: uuidSchema("Cartão").nullable().optional(), cardId: uuidSchema("Cartão").nullable().optional(),
paymentMethod: z.string().min(1), paymentMethod: z.string().min(1),
invoicePeriod: z.string().regex(/^\d{4}-\d{2}$/, "Período inválido.").nullable().optional(), invoicePeriod: z
.string()
.regex(/^\d{4}-\d{2}$/, "Período inválido.")
.nullable()
.optional(),
}); });
export type ImportRow = z.infer<typeof importRowSchema>; export type ImportRow = z.infer<typeof importRowSchema>;
@@ -51,10 +55,7 @@ export async function checkDuplicateFitIds(
.select({ ofxFitId: transactions.ofxFitId }) .select({ ofxFitId: transactions.ofxFitId })
.from(transactions) .from(transactions)
.where( .where(
and( and(eq(transactions.userId, userId), inArray(transactions.ofxFitId, ids)),
eq(transactions.userId, userId),
inArray(transactions.ofxFitId, ids),
),
); );
return rows.map((r) => r.ofxFitId).filter((id): id is string => id !== null); return rows.map((r) => r.ofxFitId).filter((id): id is string => id !== null);
@@ -67,10 +68,14 @@ export async function importTransactionsAction(
const parsed = importSchema.safeParse(input); const parsed = importSchema.safeParse(input);
if (!parsed.success) { if (!parsed.success) {
return { success: false, error: parsed.error.issues[0]?.message ?? "Dados inválidos." }; return {
success: false,
error: parsed.error.issues[0]?.message ?? "Dados inválidos.",
};
} }
const { rows, payerId, accountId, cardId, paymentMethod, invoicePeriod } = parsed.data; const { rows, payerId, accountId, cardId, paymentMethod, invoicePeriod } =
parsed.data;
// Valida ownership // Valida ownership
const [payerOk, accountOk, cardOk] = await Promise.all([ const [payerOk, accountOk, cardOk] = await Promise.all([
@@ -94,14 +99,19 @@ export async function importTransactionsAction(
const records = rows.map((row) => { const records = rows.map((row) => {
const purchaseDate = parseLocalDateString(row.date); const purchaseDate = parseLocalDateString(row.date);
const period = invoicePeriod ?? `${purchaseDate.getFullYear()}-${String(purchaseDate.getMonth() + 1).padStart(2, "0")}`; const period =
invoicePeriod ??
`${purchaseDate.getFullYear()}-${String(purchaseDate.getMonth() + 1).padStart(2, "0")}`;
return { return {
name: row.description, name: row.description,
transactionType: row.transactionType === "income" ? "Receita" : "Despesa", transactionType: row.transactionType === "income" ? "Receita" : "Despesa",
condition: "À vista" as const, condition: "À vista" as const,
paymentMethod, paymentMethod,
amount: (row.transactionType === "expense" ? -row.amount : row.amount).toFixed(2), amount: (row.transactionType === "expense"
? -row.amount
: row.amount
).toFixed(2),
purchaseDate, purchaseDate,
period, period,
isSettled, isSettled,
@@ -143,10 +153,7 @@ export async function deleteTransactionByFitId(
await db await db
.delete(transactions) .delete(transactions)
.where( .where(
and( and(eq(transactions.userId, userId), eq(transactions.ofxFitId, fitId)),
eq(transactions.userId, userId),
eq(transactions.ofxFitId, fitId),
),
); );
await revalidateForEntity("transactions", userId); await revalidateForEntity("transactions", userId);

View File

@@ -33,7 +33,8 @@ export function decodeAccountCard(value: string): {
id: string; id: string;
} | null { } | null {
if (value.startsWith("card:")) return { type: "card", id: value.slice(5) }; if (value.startsWith("card:")) return { type: "card", id: value.slice(5) };
if (value.startsWith("account:")) return { type: "account", id: value.slice(8) }; if (value.startsWith("account:"))
return { type: "account", id: value.slice(8) };
return null; return null;
} }
@@ -65,7 +66,9 @@ export function GlobalFields({
onBulkCategoryChange, onBulkCategoryChange,
}: GlobalFieldsProps) { }: GlobalFieldsProps) {
const isCard = accountCardValue?.startsWith("card:") ?? false; const isCard = accountCardValue?.startsWith("card:") ?? false;
const expenseCategories = categoryOptions.filter((o) => o.group === "despesa"); const expenseCategories = categoryOptions.filter(
(o) => o.group === "despesa",
);
const incomeCategories = categoryOptions.filter((o) => o.group === "receita"); const incomeCategories = categoryOptions.filter((o) => o.group === "receita");
return ( return (
@@ -131,7 +134,10 @@ export function GlobalFields({
<SelectContent> <SelectContent>
{payerOptions.map((opt) => ( {payerOptions.map((opt) => (
<SelectItem key={opt.value} value={opt.value}> <SelectItem key={opt.value} value={opt.value}>
<PayerSelectContent label={opt.label} avatarUrl={opt.avatarUrl} /> <PayerSelectContent
label={opt.label}
avatarUrl={opt.avatarUrl}
/>
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
@@ -150,7 +156,10 @@ export function GlobalFields({
<SelectLabel>Despesa</SelectLabel> <SelectLabel>Despesa</SelectLabel>
{expenseCategories.map((opt) => ( {expenseCategories.map((opt) => (
<SelectItem key={opt.value} value={opt.value}> <SelectItem key={opt.value} value={opt.value}>
<CategorySelectContent label={opt.label} icon={opt.icon} /> <CategorySelectContent
label={opt.label}
icon={opt.icon}
/>
</SelectItem> </SelectItem>
))} ))}
</SelectGroup> </SelectGroup>
@@ -163,7 +172,10 @@ export function GlobalFields({
<SelectLabel>Receita</SelectLabel> <SelectLabel>Receita</SelectLabel>
{incomeCategories.map((opt) => ( {incomeCategories.map((opt) => (
<SelectItem key={opt.value} value={opt.value}> <SelectItem key={opt.value} value={opt.value}>
<CategorySelectContent label={opt.label} icon={opt.icon} /> <CategorySelectContent
label={opt.label}
icon={opt.icon}
/>
</SelectItem> </SelectItem>
))} ))}
</SelectGroup> </SelectGroup>
@@ -172,17 +184,17 @@ export function GlobalFields({
</Select> </Select>
</div> </div>
{isCard && ( {isCard && (
<div className="flex min-w-44 flex-col gap-1.5"> <div className="flex min-w-44 flex-col gap-1.5">
<Label>Fatura</Label> <Label>Fatura</Label>
<PeriodPicker <PeriodPicker
value={invoicePeriod ?? ""} value={invoicePeriod ?? ""}
onChange={(v) => onInvoicePeriodChange(v || null)} onChange={(v) => onInvoicePeriodChange(v || null)}
placeholder="Selecionar fatura…" placeholder="Selecionar fatura…"
/> />
</div> </div>
)} )}
</div>
</div> </div>
</div>
); );
} }

View File

@@ -1,13 +1,18 @@
"use client"; "use client";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useCallback, useEffect, useMemo, useState, useTransition } from "react"; import {
useCallback,
useEffect,
useMemo,
useState,
useTransition,
} from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { import {
fetchCategoryMappings, fetchCategoryMappings,
saveCategoryMappings, saveCategoryMappings,
} from "@/features/transactions/actions/category-memory-action"; } from "@/features/transactions/actions/category-memory-action";
import { normalizeDescriptionKey } from "@/features/transactions/lib/import-utils";
import { import {
checkDuplicateFitIds, checkDuplicateFitIds,
deleteTransactionByFitId, deleteTransactionByFitId,
@@ -27,6 +32,7 @@ import {
} from "@/features/transactions/components/import/review-table"; } from "@/features/transactions/components/import/review-table";
import { UploadZone } from "@/features/transactions/components/import/upload-zone"; import { UploadZone } from "@/features/transactions/components/import/upload-zone";
import type { SelectOption } from "@/features/transactions/components/types"; import type { SelectOption } from "@/features/transactions/components/types";
import { normalizeDescriptionKey } from "@/features/transactions/lib/import-utils";
import { Button } from "@/shared/components/ui/button"; import { Button } from "@/shared/components/ui/button";
import { import {
Card, Card,
@@ -82,7 +88,8 @@ export function ImportPage({
...t, ...t,
isDuplicate: t.externalId ? duplicates.has(t.externalId) : false, isDuplicate: t.externalId ? duplicates.has(t.externalId) : false,
selected: t.externalId ? !duplicates.has(t.externalId) : true, selected: t.externalId ? !duplicates.has(t.externalId) : true,
categoryId: categoryMappings[normalizeDescriptionKey(t.description)] ?? null, categoryId:
categoryMappings[normalizeDescriptionKey(t.description)] ?? null,
})), })),
); );
} finally { } finally {
@@ -167,7 +174,9 @@ export function ImportPage({
const handleImport = () => { const handleImport = () => {
if (!statement || !canImport) return; if (!statement || !canImport) return;
const decoded = decodeAccountCard(accountCardValue!); const decoded = accountCardValue
? decodeAccountCard(accountCardValue)
: null;
const cardId = decoded?.type === "card" ? decoded.id : null; const cardId = decoded?.type === "card" ? decoded.id : null;
const accountId = decoded?.type === "account" ? decoded.id : null; const accountId = decoded?.type === "account" ? decoded.id : null;
const paymentMethod = const paymentMethod =
@@ -197,7 +206,10 @@ export function ImportPage({
// Salva mapeamentos description → category (fire-and-forget) // Salva mapeamentos description → category (fire-and-forget)
saveCategoryMappings( saveCategoryMappings(
selectedRows.map((r) => ({ description: r.description, categoryId: r.categoryId })), selectedRows.map((r) => ({
description: r.description,
categoryId: r.categoryId,
})),
); );
const { importBatchId } = result; const { importBatchId } = result;
@@ -236,7 +248,8 @@ export function ImportPage({
<div> <div>
<CardTitle>Importar extrato</CardTitle> <CardTitle>Importar extrato</CardTitle>
<CardDescription> <CardDescription>
Importe transações a partir de um arquivo .ofx ou planilha .xlsx exportado pelo seu banco. Importe transações a partir de um arquivo .ofx ou planilha .xlsx
exportado pelo seu banco.
</CardDescription> </CardDescription>
</div> </div>
<ImportSteps current={currentStep} /> <ImportSteps current={currentStep} />

View File

@@ -34,7 +34,9 @@ export function ImportSteps({ current }: ImportStepsProps) {
isCompleted && isCompleted &&
"border-primary bg-primary text-primary-foreground", "border-primary bg-primary text-primary-foreground",
isActive && "border-primary text-primary", isActive && "border-primary text-primary",
!isCompleted && !isActive && "border-muted-foreground/30 text-muted-foreground", !isCompleted &&
!isActive &&
"border-muted-foreground/30 text-muted-foreground",
)} )}
> >
{isCompleted ? ( {isCompleted ? (

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { useRef } from "react";
import { useVirtualizer } from "@tanstack/react-virtual"; import { useVirtualizer } from "@tanstack/react-virtual";
import { useRef } from "react";
import { CategorySelectContent } from "@/features/transactions/components/select-items"; import { CategorySelectContent } from "@/features/transactions/components/select-items";
import type { SelectOption } from "@/features/transactions/components/types"; import type { SelectOption } from "@/features/transactions/components/types";
import MoneyValues from "@/shared/components/money-values"; import MoneyValues from "@/shared/components/money-values";
@@ -91,9 +91,7 @@ export function ReviewTable({
onCheckedChange={(v) => onToggleAll(!!v)} onCheckedChange={(v) => onToggleAll(!!v)}
aria-label="Selecionar todas" aria-label="Selecionar todas"
data-state={ data-state={
!allSelected && someSelected !allSelected && someSelected ? "indeterminate" : undefined
? "indeterminate"
: undefined
} }
/> />
</TableHead> </TableHead>
@@ -114,7 +112,10 @@ export function ReviewTable({
</TableRow> </TableRow>
)} )}
{virtualRows.map((virtualRow) => { {virtualRows.map((virtualRow) => {
const row = rows[virtualRow.index]!; const row = rows[virtualRow.index];
if (!row) {
return null;
}
const index = virtualRow.index; const index = virtualRow.index;
return ( return (
<TableRow <TableRow
@@ -199,9 +200,7 @@ export function ReviewTable({
<TableCell> <TableCell>
<TransactionTypeBadge <TransactionTypeBadge
kind={ kind={
row.transactionType === "income" row.transactionType === "income" ? "Receita" : "Despesa"
? "Receita"
: "Despesa"
} }
/> />
</TableCell> </TableCell>

View File

@@ -37,7 +37,9 @@ export function UploadZone({ onParsed }: UploadZoneProps) {
} }
onParsed(statement); onParsed(statement);
} catch { } catch {
setError("Não foi possível ler o arquivo. Verifique se é um OFX válido."); setError(
"Não foi possível ler o arquivo. Verifique se é um OFX válido.",
);
} }
}; };
reader.readAsText(file, "windows-1252"); reader.readAsText(file, "windows-1252");
@@ -119,11 +121,7 @@ export function UploadZone({ onParsed }: UploadZoneProps) {
/> />
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
{error ? ( {error ? <p className="text-destructive text-sm">{error}</p> : <span />}
<p className="text-destructive text-sm">{error}</p>
) : (
<span />
)}
<button <button
type="button" type="button"
onClick={handleDownloadTemplate} onClick={handleDownloadTemplate}

View File

@@ -13,9 +13,9 @@ import {
RiCheckLine, RiCheckLine,
RiDeleteBin5Line, RiDeleteBin5Line,
RiFileCopyLine, RiFileCopyLine,
RiFileExcel2Line,
RiFileList2Line, RiFileList2Line,
RiFlashlightFill, RiFlashlightFill,
RiFileExcel2Line,
RiGroupLine, RiGroupLine,
RiHistoryLine, RiHistoryLine,
RiMoreFill, RiMoreFill,

View File

@@ -1,4 +1,4 @@
import type { ImportStatement, ImportedTransaction } from "./types"; import type { ImportedTransaction, ImportStatement } from "./types";
// Extrai o valor de uma tag leaf do OFX SGML: <TAG>valor // Extrai o valor de uma tag leaf do OFX SGML: <TAG>valor
function getField(block: string, tag: string): string | null { function getField(block: string, tag: string): string | null {

View File

@@ -1,5 +1,8 @@
import * as XLSX from "xlsx"; import * as XLSX from "xlsx";
import type { ImportStatement, ImportedTransaction } from "@/shared/lib/import/types"; import type {
ImportedTransaction,
ImportStatement,
} from "@/shared/lib/import/types";
function parseDateValue(value: unknown): string | null { function parseDateValue(value: unknown): string | null {
if (value == null || value === "") return null; if (value == null || value === "") return null;