chore: remover seções vazias de mudanças de código
Este commit remove seções vazias de mudanças de código do arquivo de mudanças. Isso ajuda a manter o histórico de mudanças mais limpo e organizado, facilitando a leitura e a compreensão das alterações realizadas no projeto.
This commit is contained in:
56
LICENSE
Normal file
56
LICENSE
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
|
||||||
|
|
||||||
|
Copyright (c) 2024-2025 Felipe Coutinho
|
||||||
|
|
||||||
|
Esta licença permite que outras pessoas remixem, adaptem e criem a partir do seu
|
||||||
|
trabalho para fins não comerciais, desde que atribuam o devido crédito e que licenciem
|
||||||
|
as novas criações sob termos idênticos.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
VOCÊ É LIVRE PARA:
|
||||||
|
|
||||||
|
• Compartilhar — copiar e redistribuir o material em qualquer suporte ou formato
|
||||||
|
• Adaptar — remixar, transformar, e criar a partir do material
|
||||||
|
|
||||||
|
O licenciante não pode revogar estes direitos desde que você respeite os termos da licença.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
DE ACORDO COM OS TERMOS SEGUINTES:
|
||||||
|
|
||||||
|
• Atribuição — Você deve dar o crédito apropriado, prover um link para a licença e indicar
|
||||||
|
se mudanças foram feitas. Você deve fazê-lo em qualquer circunstância razoável, mas de
|
||||||
|
maneira alguma que sugira ao licenciante a apoiar você ou o seu uso.
|
||||||
|
|
||||||
|
• NãoComercial — Você não pode usar o material para fins comerciais. Isso inclui:
|
||||||
|
- Vender o software ou serviços baseados nele
|
||||||
|
- Usar em produtos/serviços comerciais
|
||||||
|
- Oferecer como SaaS (Software as a Service) pago
|
||||||
|
- Qualquer uso que gere receita direta ou indireta
|
||||||
|
|
||||||
|
• CompartilhaIgual — Se você remixar, transformar, ou criar a partir do material, tem de
|
||||||
|
distribuir as suas contribuições sob a mesma licença que o original.
|
||||||
|
|
||||||
|
• Sem restrições adicionais — Você não pode aplicar termos jurídicos ou medidas de caráter
|
||||||
|
tecnológico que restrinjam legalmente outros de fazerem algo que a licença permita.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
AVISOS:
|
||||||
|
|
||||||
|
Você não tem de cumprir com os termos da licença relativamente a elementos do material
|
||||||
|
que estejam no domínio público ou cuja utilização seja permitida por uma exceção ou
|
||||||
|
limitação que seja aplicável.
|
||||||
|
|
||||||
|
Não são dadas quaisquer garantias. A licença pode não lhe dar todas as autorizações
|
||||||
|
necessárias para o uso pretendido. Por exemplo, outros direitos, tais como direitos de
|
||||||
|
imagem, de privacidade ou direitos morais, podem limitar o uso do material.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
TEXTO LEGAL COMPLETO:
|
||||||
|
https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.pt
|
||||||
|
|
||||||
|
RESUMO LEGÍVEL:
|
||||||
|
https://creativecommons.org/licenses/by-nc-sa/4.0/deed.pt
|
||||||
65
README.md
65
README.md
@@ -12,11 +12,13 @@
|
|||||||
[](https://www.typescriptlang.org/)
|
[](https://www.typescriptlang.org/)
|
||||||
[](https://www.postgresql.org/)
|
[](https://www.postgresql.org/)
|
||||||
[](https://www.docker.com/)
|
[](https://www.docker.com/)
|
||||||
|
[](LICENSE)
|
||||||
|
[](https://github.com/sponsors/felipegcoutinho)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="./public/dashboard-preview.png" alt="Dashboard Preview" width="800" />
|
<img src="./public/dashboard-preview-light.png" alt="Dashboard Preview" width="800" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -36,6 +38,7 @@
|
|||||||
- [Banco de Dados](#-banco-de-dados)
|
- [Banco de Dados](#-banco-de-dados)
|
||||||
- [Arquitetura](#-arquitetura)
|
- [Arquitetura](#-arquitetura)
|
||||||
- [Contribuindo](#-contribuindo)
|
- [Contribuindo](#-contribuindo)
|
||||||
|
- [Apoie o Projeto](#-apoie-o-projeto)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -45,6 +48,8 @@
|
|||||||
|
|
||||||
A ideia é simples: ter um lugar onde consigo ver todas as minhas contas, cartões, gastos e receitas de forma clara. Se isso for útil pra você também, fique à vontade para usar e contribuir.
|
A ideia é simples: ter um lugar onde consigo ver todas as minhas contas, cartões, gastos e receitas de forma clara. Se isso for útil pra você também, fique à vontade para usar e contribuir.
|
||||||
|
|
||||||
|
> 💡 **Licença Não-Comercial:** Este projeto é gratuito para uso pessoal, mas não pode ser usado comercialmente. Veja mais detalhes na seção [Licença](#-licença).
|
||||||
|
|
||||||
### ⚠️ Avisos importantes
|
### ⚠️ Avisos importantes
|
||||||
|
|
||||||
**1. Não há versão hospedada online**
|
**1. Não há versão hospedada online**
|
||||||
@@ -835,9 +840,59 @@ Contribuições são muito bem-vindas!
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 💖 Apoie o Projeto
|
||||||
|
|
||||||
|
Se o **Opensheets** está sendo útil para você e você quer apoiar o desenvolvimento contínuo do projeto, considere se tornar um sponsor!
|
||||||
|
|
||||||
|
[](https://github.com/sponsors/felipegcoutinho)
|
||||||
|
|
||||||
|
### Por que apoiar?
|
||||||
|
|
||||||
|
- 🚀 **Desenvolvimento contínuo** - Novas features e melhorias regulares
|
||||||
|
- 🐛 **Correções de bugs** - Manutenção ativa e suporte
|
||||||
|
- 📚 **Documentação** - Guias e tutoriais detalhados
|
||||||
|
- 💡 **Novas ideias** - Implementação de sugestões da comunidade
|
||||||
|
|
||||||
|
### Outras formas de contribuir
|
||||||
|
|
||||||
|
Além do suporte financeiro, você pode contribuir:
|
||||||
|
|
||||||
|
- ⭐ Dando uma **estrela** no repositório
|
||||||
|
- 🐛 Reportando **bugs** e sugerindo melhorias
|
||||||
|
- 📖 Melhorando a **documentação**
|
||||||
|
- 💻 Submetendo **pull requests**
|
||||||
|
- 💬 Compartilhando o projeto com outras pessoas
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 📄 Licença
|
## 📄 Licença
|
||||||
|
|
||||||
Este projeto é open source e está disponível sob a [Licença MIT](LICENSE).
|
Este projeto está licenciado sob a **Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International** (CC BY-NC-SA 4.0).
|
||||||
|
|
||||||
|
[](https://creativecommons.org/licenses/by-nc-sa/4.0/)
|
||||||
|
|
||||||
|
### ✅ Você PODE:
|
||||||
|
|
||||||
|
- **Usar** o projeto para fins pessoais
|
||||||
|
- **Modificar** o código-fonte
|
||||||
|
- **Distribuir** e compartilhar o projeto
|
||||||
|
- **Fazer fork** e criar versões modificadas
|
||||||
|
|
||||||
|
### ❌ Você NÃO PODE:
|
||||||
|
|
||||||
|
- **Uso comercial** - Ganhar dinheiro com o projeto (vender, SaaS, consultoria baseada nele)
|
||||||
|
- **Remover créditos** - Você deve manter a atribuição ao autor original
|
||||||
|
- **Mudar a licença** - Suas modificações devem usar a mesma licença
|
||||||
|
|
||||||
|
### 📋 Requisitos:
|
||||||
|
|
||||||
|
- Dar **crédito** ao autor original (Felipe Coutinho)
|
||||||
|
- Indicar se **modificações** foram feitas
|
||||||
|
- Distribuir sob a **mesma licença** (CC BY-NC-SA 4.0)
|
||||||
|
|
||||||
|
**Resumo:** Use livremente para projetos pessoais, contribua, modifique - mas não ganhe dinheiro com isso.
|
||||||
|
|
||||||
|
Para o texto legal completo, consulte o arquivo [LICENSE](LICENSE) ou visite [creativecommons.org](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.pt).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -861,7 +916,11 @@ Este projeto é open source e está disponível sob a [Licença MIT](LICENSE).
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
**⭐ Se este projeto foi útil, considere dar uma estrela!**
|
**⭐ Se este projeto foi útil pra você:**
|
||||||
|
|
||||||
|
- Dê uma estrela no repositório
|
||||||
|
- [Apoie o projeto como sponsor](https://github.com/sponsors/felipegcoutinho)
|
||||||
|
- Compartilhe com outras pessoas
|
||||||
|
|
||||||
Desenvolvido com ❤️ para a comunidade open source
|
Desenvolvido com ❤️ para a comunidade open source
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
|
import { getRecentEstablishmentsAction } from "@/app/(dashboard)/lancamentos/actions";
|
||||||
import { cartoes, lancamentos } from "@/db/schema";
|
import { cartoes, lancamentos } from "@/db/schema";
|
||||||
|
import { db } from "@/lib/db";
|
||||||
import {
|
import {
|
||||||
buildOptionSets,
|
buildOptionSets,
|
||||||
buildSluggedFilters,
|
buildSluggedFilters,
|
||||||
fetchLancamentoFilterSources,
|
fetchLancamentoFilterSources,
|
||||||
mapLancamentosData,
|
mapLancamentosData,
|
||||||
} from "@/lib/lancamentos/page-helpers";
|
} from "@/lib/lancamentos/page-helpers";
|
||||||
import { db } from "@/lib/db";
|
|
||||||
import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants";
|
import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants";
|
||||||
import { and, eq, gte, lte, ne, or } from "drizzle-orm";
|
import { and, eq, gte, lte, ne, or } from "drizzle-orm";
|
||||||
import { getRecentEstablishmentsAction } from "@/app/(dashboard)/lancamentos/actions";
|
|
||||||
|
|
||||||
import type { CalendarData, CalendarEvent } from "@/components/calendario/types";
|
import type {
|
||||||
|
CalendarData,
|
||||||
|
CalendarEvent,
|
||||||
|
} from "@/components/calendario/types";
|
||||||
|
|
||||||
const PAYMENT_METHOD_BOLETO = "Boleto";
|
const PAYMENT_METHOD_BOLETO = "Boleto";
|
||||||
const TRANSACTION_TYPE_TRANSFERENCIA = "Transferência";
|
const TRANSACTION_TYPE_TRANSFERENCIA = "Transferência";
|
||||||
@@ -98,7 +101,11 @@ export const fetchCalendarData = async ({
|
|||||||
|
|
||||||
const cardTotals = new Map<string, number>();
|
const cardTotals = new Map<string, number>();
|
||||||
for (const item of lancamentosData) {
|
for (const item of lancamentosData) {
|
||||||
if (!item.cartaoId || item.period !== period || item.pagadorRole !== PAGADOR_ROLE_ADMIN) {
|
if (
|
||||||
|
!item.cartaoId ||
|
||||||
|
item.period !== period ||
|
||||||
|
item.pagadorRole !== PAGADOR_ROLE_ADMIN
|
||||||
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const amount = Math.abs(item.amount ?? 0);
|
const amount = Math.abs(item.amount ?? 0);
|
||||||
|
|||||||
@@ -10,11 +10,7 @@ export default async function HistoricoCategoriasPage() {
|
|||||||
const data = await fetchCategoryHistory(user.id, currentPeriod);
|
const data = await fetchCategoryHistory(user.id, currentPeriod);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col gap-6">
|
<main>
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Acompanhe o histórico de desempenho das suas categorias ao longo de 9
|
|
||||||
meses.
|
|
||||||
</p>
|
|
||||||
<CategoryHistoryWidget data={data} />
|
<CategoryHistoryWidget data={data} />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export default function RootLayout({
|
|||||||
<PageDescription
|
<PageDescription
|
||||||
icon={<RiPriceTag3Line />}
|
icon={<RiPriceTag3Line />}
|
||||||
title="Categorias"
|
title="Categorias"
|
||||||
subtitle="Gerencie suas categorias de despesas e receitas. Acompanhe o desempenho financeiro por categoria e faça ajustes conforme necessário."
|
subtitle="Gerencie suas categorias de despesas e receitas acompanhando o histórico de desempenho dos últimos 9 meses, permitindo ajustes financeiros precisos conforme necessário."
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { contas, lancamentos } from "@/db/schema";
|
import { contas, lancamentos, pagadores, categorias, cartoes } from "@/db/schema";
|
||||||
import {
|
import {
|
||||||
INITIAL_BALANCE_CONDITION,
|
INITIAL_BALANCE_CONDITION,
|
||||||
INITIAL_BALANCE_NOTE,
|
INITIAL_BALANCE_NOTE,
|
||||||
@@ -31,6 +31,78 @@ import { and, asc, desc, eq, gte, inArray, sql } from "drizzle-orm";
|
|||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Authorization Validation Functions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
async function validatePagadorOwnership(
|
||||||
|
userId: string,
|
||||||
|
pagadorId: string | null | undefined
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (!pagadorId) return true; // Se não tem pagadorId, não precisa validar
|
||||||
|
|
||||||
|
const pagador = await db.query.pagadores.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(pagadores.id, pagadorId),
|
||||||
|
eq(pagadores.userId, userId)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
return !!pagador;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateCategoriaOwnership(
|
||||||
|
userId: string,
|
||||||
|
categoriaId: string | null | undefined
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (!categoriaId) return true;
|
||||||
|
|
||||||
|
const categoria = await db.query.categorias.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(categorias.id, categoriaId),
|
||||||
|
eq(categorias.userId, userId)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
return !!categoria;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateContaOwnership(
|
||||||
|
userId: string,
|
||||||
|
contaId: string | null | undefined
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (!contaId) return true;
|
||||||
|
|
||||||
|
const conta = await db.query.contas.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(contas.id, contaId),
|
||||||
|
eq(contas.userId, userId)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
return !!conta;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateCartaoOwnership(
|
||||||
|
userId: string,
|
||||||
|
cartaoId: string | null | undefined
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (!cartaoId) return true;
|
||||||
|
|
||||||
|
const cartao = await db.query.cartoes.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(cartoes.id, cartaoId),
|
||||||
|
eq(cartoes.userId, userId)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
return !!cartao;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Utility Functions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
const resolvePeriod = (purchaseDate: string, period?: string | null) => {
|
const resolvePeriod = (purchaseDate: string, period?: string | null) => {
|
||||||
if (period && /^\d{4}-\d{2}$/.test(period)) {
|
if (period && /^\d{4}-\d{2}$/.test(period)) {
|
||||||
return period;
|
return period;
|
||||||
@@ -472,6 +544,42 @@ export async function createLancamentoAction(
|
|||||||
const user = await getUser();
|
const user = await getUser();
|
||||||
const data = createSchema.parse(input);
|
const data = createSchema.parse(input);
|
||||||
|
|
||||||
|
// Validar propriedade dos recursos referenciados
|
||||||
|
if (data.pagadorId) {
|
||||||
|
const isValid = await validatePagadorOwnership(user.id, data.pagadorId);
|
||||||
|
if (!isValid) {
|
||||||
|
return { success: false, error: "Pagador não encontrado ou sem permissão." };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.secondaryPagadorId) {
|
||||||
|
const isValid = await validatePagadorOwnership(user.id, data.secondaryPagadorId);
|
||||||
|
if (!isValid) {
|
||||||
|
return { success: false, error: "Pagador secundário não encontrado ou sem permissão." };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.categoriaId) {
|
||||||
|
const isValid = await validateCategoriaOwnership(user.id, data.categoriaId);
|
||||||
|
if (!isValid) {
|
||||||
|
return { success: false, error: "Categoria não encontrada." };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.contaId) {
|
||||||
|
const isValid = await validateContaOwnership(user.id, data.contaId);
|
||||||
|
if (!isValid) {
|
||||||
|
return { success: false, error: "Conta não encontrada." };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.cartaoId) {
|
||||||
|
const isValid = await validateCartaoOwnership(user.id, data.cartaoId);
|
||||||
|
if (!isValid) {
|
||||||
|
return { success: false, error: "Cartão não encontrado." };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const period = resolvePeriod(data.purchaseDate, data.period);
|
const period = resolvePeriod(data.purchaseDate, data.period);
|
||||||
const purchaseDate = parseLocalDateString(data.purchaseDate);
|
const purchaseDate = parseLocalDateString(data.purchaseDate);
|
||||||
const dueDate = data.dueDate ? parseLocalDateString(data.dueDate) : null;
|
const dueDate = data.dueDate ? parseLocalDateString(data.dueDate) : null;
|
||||||
@@ -556,6 +664,42 @@ export async function updateLancamentoAction(
|
|||||||
const user = await getUser();
|
const user = await getUser();
|
||||||
const data = updateSchema.parse(input);
|
const data = updateSchema.parse(input);
|
||||||
|
|
||||||
|
// Validar propriedade dos recursos referenciados
|
||||||
|
if (data.pagadorId) {
|
||||||
|
const isValid = await validatePagadorOwnership(user.id, data.pagadorId);
|
||||||
|
if (!isValid) {
|
||||||
|
return { success: false, error: "Pagador não encontrado ou sem permissão." };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.secondaryPagadorId) {
|
||||||
|
const isValid = await validatePagadorOwnership(user.id, data.secondaryPagadorId);
|
||||||
|
if (!isValid) {
|
||||||
|
return { success: false, error: "Pagador secundário não encontrado ou sem permissão." };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.categoriaId) {
|
||||||
|
const isValid = await validateCategoriaOwnership(user.id, data.categoriaId);
|
||||||
|
if (!isValid) {
|
||||||
|
return { success: false, error: "Categoria não encontrada." };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.contaId) {
|
||||||
|
const isValid = await validateContaOwnership(user.id, data.contaId);
|
||||||
|
if (!isValid) {
|
||||||
|
return { success: false, error: "Conta não encontrada." };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.cartaoId) {
|
||||||
|
const isValid = await validateCartaoOwnership(user.id, data.cartaoId);
|
||||||
|
if (!isValid) {
|
||||||
|
return { success: false, error: "Cartão não encontrado." };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const existing = await db.query.lancamentos.findFirst({
|
const existing = await db.query.lancamentos.findFirst({
|
||||||
columns: {
|
columns: {
|
||||||
id: true,
|
id: true,
|
||||||
@@ -1124,12 +1268,12 @@ const massAddTransactionSchema = z.object({
|
|||||||
.number({ message: "Informe o valor da transação." })
|
.number({ message: "Informe o valor da transação." })
|
||||||
.min(0, "Informe um valor maior ou igual a zero."),
|
.min(0, "Informe um valor maior ou igual a zero."),
|
||||||
categoriaId: uuidSchema("Categoria").nullable().optional(),
|
categoriaId: uuidSchema("Categoria").nullable().optional(),
|
||||||
|
pagadorId: uuidSchema("Pagador").nullable().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const massAddSchema = z.object({
|
const massAddSchema = z.object({
|
||||||
fixedFields: z.object({
|
fixedFields: z.object({
|
||||||
transactionType: z.enum(LANCAMENTO_TRANSACTION_TYPES).optional(),
|
transactionType: z.enum(LANCAMENTO_TRANSACTION_TYPES).optional(),
|
||||||
pagadorId: uuidSchema("Pagador").nullable().optional(),
|
|
||||||
paymentMethod: z.enum(LANCAMENTO_PAYMENT_METHODS).optional(),
|
paymentMethod: z.enum(LANCAMENTO_PAYMENT_METHODS).optional(),
|
||||||
condition: z.enum(LANCAMENTO_CONDITIONS).optional(),
|
condition: z.enum(LANCAMENTO_CONDITIONS).optional(),
|
||||||
period: z
|
period: z
|
||||||
@@ -1156,6 +1300,46 @@ export async function createMassLancamentosAction(
|
|||||||
const user = await getUser();
|
const user = await getUser();
|
||||||
const data = massAddSchema.parse(input);
|
const data = massAddSchema.parse(input);
|
||||||
|
|
||||||
|
// Validar campos fixos
|
||||||
|
if (data.fixedFields.contaId) {
|
||||||
|
const isValid = await validateContaOwnership(user.id, data.fixedFields.contaId);
|
||||||
|
if (!isValid) {
|
||||||
|
return { success: false, error: "Conta não encontrada." };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.fixedFields.cartaoId) {
|
||||||
|
const isValid = await validateCartaoOwnership(user.id, data.fixedFields.cartaoId);
|
||||||
|
if (!isValid) {
|
||||||
|
return { success: false, error: "Cartão não encontrado." };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar cada transação individual
|
||||||
|
for (let i = 0; i < data.transactions.length; i++) {
|
||||||
|
const transaction = data.transactions[i];
|
||||||
|
|
||||||
|
if (transaction.pagadorId) {
|
||||||
|
const isValid = await validatePagadorOwnership(user.id, transaction.pagadorId);
|
||||||
|
if (!isValid) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Pagador não encontrado na transação ${i + 1}.`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transaction.categoriaId) {
|
||||||
|
const isValid = await validateCategoriaOwnership(user.id, transaction.categoriaId);
|
||||||
|
if (!isValid) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Categoria não encontrada na transação ${i + 1}.`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Default values for non-fixed fields
|
// Default values for non-fixed fields
|
||||||
const defaultTransactionType = LANCAMENTO_TRANSACTION_TYPES[0];
|
const defaultTransactionType = LANCAMENTO_TRANSACTION_TYPES[0];
|
||||||
const defaultCondition = LANCAMENTO_CONDITIONS[0];
|
const defaultCondition = LANCAMENTO_CONDITIONS[0];
|
||||||
@@ -1181,7 +1365,7 @@ export async function createMassLancamentosAction(
|
|||||||
const condition = data.fixedFields.condition ?? defaultCondition;
|
const condition = data.fixedFields.condition ?? defaultCondition;
|
||||||
const paymentMethod =
|
const paymentMethod =
|
||||||
data.fixedFields.paymentMethod ?? defaultPaymentMethod;
|
data.fixedFields.paymentMethod ?? defaultPaymentMethod;
|
||||||
const pagadorId = data.fixedFields.pagadorId ?? null;
|
const pagadorId = transaction.pagadorId ?? null;
|
||||||
const contaId =
|
const contaId =
|
||||||
paymentMethod === "Cartão de crédito"
|
paymentMethod === "Cartão de crédito"
|
||||||
? null
|
? null
|
||||||
|
|||||||
@@ -159,11 +159,19 @@ export default async function Page() {
|
|||||||
<div className="container">
|
<div className="container">
|
||||||
<div className="mx-auto max-w-6xl">
|
<div className="mx-auto max-w-6xl">
|
||||||
<Image
|
<Image
|
||||||
src="/dashboard-preview.png"
|
src="/dashboard-preview-light.png"
|
||||||
alt="opensheets Dashboard Preview"
|
alt="opensheets Dashboard Preview"
|
||||||
width={1920}
|
width={1920}
|
||||||
height={1080}
|
height={1080}
|
||||||
className="w-full h-auto"
|
className="w-full h-auto dark:hidden"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
<Image
|
||||||
|
src="/dashboard-preview-dark.png"
|
||||||
|
alt="opensheets Dashboard Preview"
|
||||||
|
width={1920}
|
||||||
|
height={1080}
|
||||||
|
className="w-full h-auto hidden dark:block"
|
||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -90,11 +90,11 @@
|
|||||||
.dark {
|
.dark {
|
||||||
/* Base surfaces - true dark with minimal saturation */
|
/* Base surfaces - true dark with minimal saturation */
|
||||||
--background: oklch(18.674% 0.00002 271.152);
|
--background: oklch(18.674% 0.00002 271.152);
|
||||||
--foreground: oklch(85.505% 0.02038 100.68);
|
--foreground: oklch(92.189% 0.0186 103.516);
|
||||||
--card: oklch(24.039% 0.00151 16.27);
|
--card: oklch(24.039% 0.00151 16.27);
|
||||||
--card-foreground: oklch(85.505% 0.02038 100.68);
|
--card-foreground: oklch(92.189% 0.0186 103.516);
|
||||||
--popover: oklch(24.039% 0.00151 16.27);
|
--popover: oklch(24.039% 0.00151 16.27);
|
||||||
--popover-foreground: oklch(85.196% 0.0204 100.682);
|
--popover-foreground: oklch(92.189% 0.0186 103.516);
|
||||||
|
|
||||||
/* Primary - vibrant terracotta stands out on dark */
|
/* Primary - vibrant terracotta stands out on dark */
|
||||||
--primary: oklch(69.18% 0.18855 38.353);
|
--primary: oklch(69.18% 0.18855 38.353);
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
|
|
||||||
/* Secondary - elevated surface */
|
/* Secondary - elevated surface */
|
||||||
--secondary: oklch(22% 0.004 285);
|
--secondary: oklch(22% 0.004 285);
|
||||||
--secondary-foreground: oklch(85.196% 0.0204 100.682);
|
--secondary-foreground: oklch(92.189% 0.0186 103.516);
|
||||||
|
|
||||||
/* Muted - subtle surface variant */
|
/* Muted - subtle surface variant */
|
||||||
--muted: oklch(33.674% 0.00531 91.552);
|
--muted: oklch(33.674% 0.00531 91.552);
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
|
|
||||||
/* Accent - subtle highlight */
|
/* Accent - subtle highlight */
|
||||||
--accent: oklch(26.893% 0.00391 84.539);
|
--accent: oklch(26.893% 0.00391 84.539);
|
||||||
--accent-foreground: oklch(85.196% 0.0204 100.682);
|
--accent-foreground: oklch(92.189% 0.0186 103.516);
|
||||||
|
|
||||||
/* Destructive - accessible red for dark */
|
/* Destructive - accessible red for dark */
|
||||||
--destructive: oklch(62% 0.2 28);
|
--destructive: oklch(62% 0.2 28);
|
||||||
@@ -131,11 +131,11 @@
|
|||||||
|
|
||||||
/* Sidebar - slight separation from main */
|
/* Sidebar - slight separation from main */
|
||||||
--sidebar: oklch(24.039% 0.00151 16.27);
|
--sidebar: oklch(24.039% 0.00151 16.27);
|
||||||
--sidebar-foreground: oklch(85.196% 0.0204 100.682);
|
--sidebar-foreground: oklch(92.189% 0.0186 103.516);
|
||||||
--sidebar-primary: oklch(69.18% 0.18855 38.353);
|
--sidebar-primary: oklch(69.18% 0.18855 38.353);
|
||||||
--sidebar-primary-foreground: oklch(12.897% 0.00619 87.19);
|
--sidebar-primary-foreground: oklch(12.897% 0.00619 87.19);
|
||||||
--sidebar-accent: oklch(32.242% 0.00447 67.486);
|
--sidebar-accent: oklch(32.242% 0.00447 67.486);
|
||||||
--sidebar-accent-foreground: oklch(85.196% 0.0204 100.682);
|
--sidebar-accent-foreground: oklch(92.189% 0.0186 103.516);
|
||||||
--sidebar-border: oklch(26% 0.004 285);
|
--sidebar-border: oklch(26% 0.004 285);
|
||||||
--sidebar-ring: oklch(69.18% 0.18855 38.353);
|
--sidebar-ring: oklch(69.18% 0.18855 38.353);
|
||||||
|
|
||||||
@@ -162,11 +162,11 @@
|
|||||||
|
|
||||||
/* Special components */
|
/* Special components */
|
||||||
--month-picker: oklch(24.039% 0.00151 16.27);
|
--month-picker: oklch(24.039% 0.00151 16.27);
|
||||||
--month-picker-foreground: oklch(85.196% 0.0204 100.682);
|
--month-picker-foreground: oklch(92.189% 0.0186 103.516);
|
||||||
--dark: oklch(85.196% 0.0204 100.682);
|
--dark: oklch(92.189% 0.0186 103.516);
|
||||||
--dark-foreground: oklch(18.711% 0.00427 84.566);
|
--dark-foreground: oklch(18.711% 0.00427 84.566);
|
||||||
--welcome-banner: oklch(24.039% 0.00151 16.27);
|
--welcome-banner: oklch(24.039% 0.00151 16.27);
|
||||||
--welcome-banner-foreground: oklch(85.196% 0.0204 100.682);
|
--welcome-banner-foreground: oklch(92.189% 0.0186 103.516);
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { ThemeProvider } from "@/components/theme-provider";
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { main_font } from "@/public/fonts/font_index";
|
import { main_font } from "@/public/fonts/font_index";
|
||||||
import type { Metadata } from "next";
|
|
||||||
import "./globals.css";
|
|
||||||
import { Analytics } from "@vercel/analytics/next";
|
import { Analytics } from "@vercel/analytics/next";
|
||||||
import { SpeedInsights } from "@vercel/speed-insights/next";
|
import { SpeedInsights } from "@vercel/speed-insights/next";
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
import "./globals.css";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Opensheets",
|
title: "Opensheets",
|
||||||
@@ -22,7 +22,7 @@ export default function RootLayout({
|
|||||||
<meta name="apple-mobile-web-app-title" content="Opensheets" />
|
<meta name="apple-mobile-web-app-title" content="Opensheets" />
|
||||||
</head>
|
</head>
|
||||||
<body
|
<body
|
||||||
className={`${main_font.className} antialiased`}
|
className={`${main_font.className} antialiased `}
|
||||||
suppressHydrationWarning
|
suppressHydrationWarning
|
||||||
>
|
>
|
||||||
<ThemeProvider attribute="class" defaultTheme="light">
|
<ThemeProvider attribute="class" defaultTheme="light">
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils/ui";
|
|
||||||
|
|
||||||
import type { CalendarEvent } from "@/components/calendario/types";
|
|
||||||
import { EVENT_TYPE_STYLES } from "@/components/calendario/day-cell";
|
import { EVENT_TYPE_STYLES } from "@/components/calendario/day-cell";
|
||||||
|
import type { CalendarEvent } from "@/components/calendario/types";
|
||||||
|
import { cn } from "@/lib/utils/ui";
|
||||||
|
|
||||||
const LEGEND_ITEMS: Array<{
|
const LEGEND_ITEMS: Array<{
|
||||||
type: CalendarEvent["type"];
|
type: CalendarEvent["type"];
|
||||||
@@ -16,17 +15,12 @@ const LEGEND_ITEMS: Array<{
|
|||||||
|
|
||||||
export function CalendarLegend() {
|
export function CalendarLegend() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap gap-3 rounded-lg border border-border/60 bg-muted/20 px-3 py-2 text-[11px] font-medium text-muted-foreground">
|
<div className="flex flex-wrap gap-3 rounded-sm border border-border/60 bg-muted/20 p-2 text-xs font-medium text-muted-foreground">
|
||||||
{LEGEND_ITEMS.map((item) => {
|
{LEGEND_ITEMS.map((item) => {
|
||||||
const style = EVENT_TYPE_STYLES[item.type];
|
const style = EVENT_TYPE_STYLES[item.type];
|
||||||
return (
|
return (
|
||||||
<span key={item.type} className="flex items-center gap-2">
|
<span key={item.type} className="flex items-center gap-2">
|
||||||
<span
|
<span className={cn("size-3 rounded-full", style.dot)} />
|
||||||
className={cn(
|
|
||||||
"size-2.5 rounded-full border border-black/10 dark:border-white/20",
|
|
||||||
style.dot
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{item.label}
|
{item.label}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import type { CalendarDay, CalendarEvent } from "@/components/calendario/types";
|
||||||
|
import { currencyFormatter } from "@/lib/lancamentos/formatting-helpers";
|
||||||
import { cn } from "@/lib/utils/ui";
|
import { cn } from "@/lib/utils/ui";
|
||||||
import { RiAddLine } from "@remixicon/react";
|
import { RiAddLine } from "@remixicon/react";
|
||||||
import type { KeyboardEvent, MouseEvent } from "react";
|
import type { KeyboardEvent, MouseEvent } from "react";
|
||||||
import type { CalendarDay, CalendarEvent } from "@/components/calendario/types";
|
|
||||||
import { currencyFormatter } from "@/lib/lancamentos/formatting-helpers";
|
|
||||||
|
|
||||||
type DayCellProps = {
|
type DayCellProps = {
|
||||||
day: CalendarDay;
|
day: CalendarDay;
|
||||||
@@ -17,17 +17,18 @@ export const EVENT_TYPE_STYLES: Record<
|
|||||||
{ wrapper: string; dot: string; accent?: string }
|
{ wrapper: string; dot: string; accent?: string }
|
||||||
> = {
|
> = {
|
||||||
lancamento: {
|
lancamento: {
|
||||||
wrapper: "bg-cyan-600 text-cyan-50 dark:bg-cyan-500/30 dark:text-cyan-200",
|
wrapper:
|
||||||
dot: "bg-cyan-600",
|
"bg-orange-100 text-orange-600 dark:bg-orange-800 dark:text-orange-50",
|
||||||
|
dot: "bg-orange-600",
|
||||||
},
|
},
|
||||||
boleto: {
|
boleto: {
|
||||||
wrapper: "bg-red-600 text-red-50 dark:bg-red-500/30 dark:text-red-200",
|
wrapper:
|
||||||
dot: "bg-red-600",
|
"bg-emerald-100 text-emerald-600 dark:bg-emerald-800 dark:text-emerald-50",
|
||||||
|
dot: "bg-emerald-600",
|
||||||
},
|
},
|
||||||
cartao: {
|
cartao: {
|
||||||
wrapper:
|
wrapper: "bg-blue-100 text-blue-600 dark:bg-blue-800 dark:text-blue-50",
|
||||||
"bg-violet-600 text-violet-50 dark:bg-violet-500/30 dark:text-violet-200",
|
dot: "bg-blue-600",
|
||||||
dot: "bg-violet-600",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -82,11 +83,12 @@ const DayEventPreview = ({ event }: { event: CalendarEvent }) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full items-center justify-between gap-2 rounded p-1 text-xs leading-tight",
|
"flex w-full items-center justify-between gap-2 rounded p-1 text-xs",
|
||||||
style.wrapper
|
style.wrapper
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex min-w-0">
|
<div className="flex min-w-0 items-center gap-1">
|
||||||
|
<span className={cn("size-1.5 rounded-full", style.dot)} />
|
||||||
<span className="truncate">{label}</span>
|
<span className="truncate">{label}</span>
|
||||||
</div>
|
</div>
|
||||||
{complement ? (
|
{complement ? (
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export function CategoryDetailHeader({
|
|||||||
<Card className="px-4">
|
<Card className="px-4">
|
||||||
<div className="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
|
<div className="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<span className="flex size-12 items-center justify-center rounded-xl bg-muted border text-primary">
|
<span className="flex size-12 items-center justify-center rounded-xl bg-muted">
|
||||||
{IconComponent ? (
|
{IconComponent ? (
|
||||||
<IconComponent className="size-6" aria-hidden />
|
<IconComponent className="size-6" aria-hidden />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import type { ChangelogEntry } from "@/lib/changelog/data";
|
|||||||
import {
|
import {
|
||||||
getCategoryLabel,
|
getCategoryLabel,
|
||||||
groupEntriesByCategory,
|
groupEntriesByCategory,
|
||||||
|
parseSafariCompatibleDate,
|
||||||
} from "@/lib/changelog/utils";
|
} from "@/lib/changelog/utils";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { RiMegaphoneLine } from "@remixicon/react";
|
import { RiMegaphoneLine } from "@remixicon/react";
|
||||||
@@ -115,7 +116,7 @@ export function ChangelogNotification({
|
|||||||
{entry.title}
|
{entry.title}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{formatDistanceToNow(new Date(entry.date), {
|
{formatDistanceToNow(parseSafariCompatibleDate(entry.date), {
|
||||||
addSuffix: true,
|
addSuffix: true,
|
||||||
locale: ptBR,
|
locale: ptBR,
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ export function CategoryHistoryWidget({ data }: CategoryHistoryWidgetProps) {
|
|||||||
<CardContent className="space-y-2.5">
|
<CardContent className="space-y-2.5">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{selectedCategoryDetails.length > 0 && (
|
{selectedCategoryDetails.length > 0 && (
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4 mb-4">
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{selectedCategoryDetails.map((category) => {
|
{selectedCategoryDetails.map((category) => {
|
||||||
if (!category) return null;
|
if (!category) return null;
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export function InstallmentAnalysisPage({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{/* Card de resumo principal */}
|
{/* Card de resumo principal */}
|
||||||
<Card className="border-primary/20 bg-primary/5">
|
<Card className="border-none bg-primary/15">
|
||||||
<CardContent className="flex flex-col items-start justify-center gap-2 py-2">
|
<CardContent className="flex flex-col items-start justify-center gap-2 py-2">
|
||||||
<p className="text-sm font-medium text-muted-foreground">
|
<p className="text-sm font-medium text-muted-foreground">
|
||||||
Se você pagar tudo que está selecionado:
|
Se você pagar tudo que está selecionado:
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import type { DashboardCardMetrics } from "@/lib/dashboard/metrics";
|
import type { DashboardCardMetrics } from "@/lib/dashboard/metrics";
|
||||||
|
import { title_font } from "@/public/fonts/font_index";
|
||||||
import {
|
import {
|
||||||
RiArrowDownLine,
|
RiArrowDownLine,
|
||||||
RiArrowUpLine,
|
RiArrowUpLine,
|
||||||
@@ -15,7 +16,6 @@ import {
|
|||||||
RiSubtractLine,
|
RiSubtractLine,
|
||||||
} from "@remixicon/react";
|
} from "@remixicon/react";
|
||||||
import MoneyValues from "../money-values";
|
import MoneyValues from "../money-values";
|
||||||
import { title_font } from "@/public/fonts/font_index";
|
|
||||||
|
|
||||||
type SectionCardsProps = {
|
type SectionCardsProps = {
|
||||||
metrics: DashboardCardMetrics;
|
metrics: DashboardCardMetrics;
|
||||||
@@ -73,7 +73,7 @@ export function SectionCards({ metrics }: SectionCardsProps) {
|
|||||||
<Card key={label} className="@container/card gap-2">
|
<Card key={label} className="@container/card gap-2">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-1">
|
<CardTitle className="flex items-center gap-1">
|
||||||
<Icon className="size-4" />
|
<Icon className="size-4 text-primary" />
|
||||||
{label}
|
{label}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<MoneyValues className="text-2xl" amount={metric.current} />
|
<MoneyValues className="text-2xl" amount={metric.current} />
|
||||||
@@ -84,9 +84,9 @@ export function SectionCards({ metrics }: SectionCardsProps) {
|
|||||||
</Badge>
|
</Badge>
|
||||||
</CardAction>
|
</CardAction>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardFooter className="flex-col items-start gap-1.5 text-sm">
|
<CardFooter className="flex-col items-start gap-2 text-sm">
|
||||||
<div className="line-clamp-1 flex gap-2 text-sm">
|
<div className="line-clamp-1 flex gap-2 text-xs">
|
||||||
Mês anterior:
|
Mês anterior
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted-foreground">
|
<div className="text-muted-foreground">
|
||||||
<MoneyValues amount={metric.previous} />
|
<MoneyValues amount={metric.previous} />
|
||||||
|
|||||||
@@ -1,25 +1,30 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
deleteSavedInsightsAction,
|
||||||
generateInsightsAction,
|
generateInsightsAction,
|
||||||
loadSavedInsightsAction,
|
loadSavedInsightsAction,
|
||||||
saveInsightsAction,
|
saveInsightsAction,
|
||||||
deleteSavedInsightsAction,
|
|
||||||
} from "@/app/(dashboard)/insights/actions";
|
} from "@/app/(dashboard)/insights/actions";
|
||||||
import { DEFAULT_MODEL } from "@/app/(dashboard)/insights/data";
|
import { DEFAULT_MODEL } from "@/app/(dashboard)/insights/data";
|
||||||
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
||||||
import type { InsightsResponse } from "@/lib/schemas/insights";
|
import type { InsightsResponse } from "@/lib/schemas/insights";
|
||||||
import { RiDeleteBinLine, RiSaveLine, RiSparklingLine, RiAlertLine } from "@remixicon/react";
|
import {
|
||||||
|
RiAlertLine,
|
||||||
|
RiDeleteBinLine,
|
||||||
|
RiSaveLine,
|
||||||
|
RiSparklingLine,
|
||||||
|
} from "@remixicon/react";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import { ptBR } from "date-fns/locale";
|
||||||
import { useEffect, useState, useTransition } from "react";
|
import { useEffect, useState, useTransition } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { EmptyState } from "../empty-state";
|
import { EmptyState } from "../empty-state";
|
||||||
import { InsightsGrid } from "./insights-grid";
|
import { InsightsGrid } from "./insights-grid";
|
||||||
import { ModelSelector } from "./model-selector";
|
import { ModelSelector } from "./model-selector";
|
||||||
import { format } from "date-fns";
|
|
||||||
import { ptBR } from "date-fns/locale";
|
|
||||||
|
|
||||||
interface InsightsPageProps {
|
interface InsightsPageProps {
|
||||||
period: string;
|
period: string;
|
||||||
@@ -129,11 +134,14 @@ export function InsightsPage({ period, onAnalyze }: InsightsPageProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
{/* Privacy Warning */}
|
{/* Privacy Warning */}
|
||||||
<Alert>
|
<Alert className="border-none">
|
||||||
<RiAlertLine className="size-4" />
|
<RiAlertLine className="size-4" />
|
||||||
<AlertDescription className="text-sm">
|
<AlertDescription className="text-sm">
|
||||||
<strong>Aviso de privacidade:</strong> Ao gerar insights, seus dados financeiros serão enviados para o provedor de IA selecionado
|
<strong>Aviso de privacidade:</strong> Ao gerar insights, seus dados
|
||||||
(Anthropic, OpenAI, Google ou OpenRouter) para processamento. Certifique-se de que você confia no provedor escolhido antes de prosseguir.
|
financeiros serão enviados para o provedor de IA selecionado
|
||||||
|
(Anthropic, OpenAI, Google ou OpenRouter) para processamento.
|
||||||
|
Certifique-se de que você confia no provedor escolhido antes de
|
||||||
|
prosseguir.
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import { createMonthOptions } from "@/lib/utils/period";
|
|||||||
import { RiAddLine, RiDeleteBinLine } from "@remixicon/react";
|
import { RiAddLine, RiDeleteBinLine } from "@remixicon/react";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { EstabelecimentoInput } from "../shared/estabelecimento-input";
|
|
||||||
import type { SelectOption } from "../../types";
|
import type { SelectOption } from "../../types";
|
||||||
import {
|
import {
|
||||||
CategoriaSelectContent,
|
CategoriaSelectContent,
|
||||||
@@ -40,6 +39,7 @@ import {
|
|||||||
PaymentMethodSelectContent,
|
PaymentMethodSelectContent,
|
||||||
TransactionTypeSelectContent,
|
TransactionTypeSelectContent,
|
||||||
} from "../select-items";
|
} from "../select-items";
|
||||||
|
import { EstabelecimentoInput } from "../shared/estabelecimento-input";
|
||||||
|
|
||||||
interface MassAddDialogProps {
|
interface MassAddDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -57,7 +57,6 @@ interface MassAddDialogProps {
|
|||||||
export interface MassAddFormData {
|
export interface MassAddFormData {
|
||||||
fixedFields: {
|
fixedFields: {
|
||||||
transactionType?: string;
|
transactionType?: string;
|
||||||
pagadorId?: string;
|
|
||||||
paymentMethod?: string;
|
paymentMethod?: string;
|
||||||
condition?: string;
|
condition?: string;
|
||||||
period?: string;
|
period?: string;
|
||||||
@@ -69,6 +68,7 @@ export interface MassAddFormData {
|
|||||||
name: string;
|
name: string;
|
||||||
amount: string;
|
amount: string;
|
||||||
categoriaId?: string;
|
categoriaId?: string;
|
||||||
|
pagadorId?: string;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,6 +78,7 @@ interface TransactionRow {
|
|||||||
name: string;
|
name: string;
|
||||||
amount: string;
|
amount: string;
|
||||||
categoriaId: string | undefined;
|
categoriaId: string | undefined;
|
||||||
|
pagadorId: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MassAddDialog({
|
export function MassAddDialog({
|
||||||
@@ -96,9 +97,6 @@ export function MassAddDialog({
|
|||||||
|
|
||||||
// Fixed fields state (sempre ativos, sem checkboxes)
|
// Fixed fields state (sempre ativos, sem checkboxes)
|
||||||
const [transactionType, setTransactionType] = useState<string>("Despesa");
|
const [transactionType, setTransactionType] = useState<string>("Despesa");
|
||||||
const [pagadorId, setPagadorId] = useState<string | undefined>(
|
|
||||||
defaultPagadorId ?? undefined
|
|
||||||
);
|
|
||||||
const [paymentMethod, setPaymentMethod] = useState<string>(
|
const [paymentMethod, setPaymentMethod] = useState<string>(
|
||||||
LANCAMENTO_PAYMENT_METHODS[0]
|
LANCAMENTO_PAYMENT_METHODS[0]
|
||||||
);
|
);
|
||||||
@@ -115,6 +113,7 @@ export function MassAddDialog({
|
|||||||
name: "",
|
name: "",
|
||||||
amount: "",
|
amount: "",
|
||||||
categoriaId: undefined,
|
categoriaId: undefined,
|
||||||
|
pagadorId: defaultPagadorId ?? undefined,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -141,6 +140,7 @@ export function MassAddDialog({
|
|||||||
name: "",
|
name: "",
|
||||||
amount: "",
|
amount: "",
|
||||||
categoriaId: undefined,
|
categoriaId: undefined,
|
||||||
|
pagadorId: defaultPagadorId ?? undefined,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
@@ -191,7 +191,6 @@ export function MassAddDialog({
|
|||||||
const formData: MassAddFormData = {
|
const formData: MassAddFormData = {
|
||||||
fixedFields: {
|
fixedFields: {
|
||||||
transactionType,
|
transactionType,
|
||||||
pagadorId,
|
|
||||||
paymentMethod,
|
paymentMethod,
|
||||||
condition,
|
condition,
|
||||||
period,
|
period,
|
||||||
@@ -203,6 +202,7 @@ export function MassAddDialog({
|
|||||||
name: t.name.trim(),
|
name: t.name.trim(),
|
||||||
amount: t.amount.trim(),
|
amount: t.amount.trim(),
|
||||||
categoriaId: t.categoriaId,
|
categoriaId: t.categoriaId,
|
||||||
|
pagadorId: t.pagadorId,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -212,7 +212,6 @@ export function MassAddDialog({
|
|||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
// Reset form
|
// Reset form
|
||||||
setTransactionType("Despesa");
|
setTransactionType("Despesa");
|
||||||
setPagadorId(defaultPagadorId ?? undefined);
|
|
||||||
setPaymentMethod(LANCAMENTO_PAYMENT_METHODS[0]);
|
setPaymentMethod(LANCAMENTO_PAYMENT_METHODS[0]);
|
||||||
setCondition("À vista");
|
setCondition("À vista");
|
||||||
setPeriod(selectedPeriod);
|
setPeriod(selectedPeriod);
|
||||||
@@ -225,6 +224,7 @@ export function MassAddDialog({
|
|||||||
name: "",
|
name: "",
|
||||||
amount: "",
|
amount: "",
|
||||||
categoriaId: undefined,
|
categoriaId: undefined,
|
||||||
|
pagadorId: defaultPagadorId ?? undefined,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
@@ -236,7 +236,7 @@ export function MassAddDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="sm:max-w-2xl max-h-[90vh] overflow-y-auto p-6 sm:px-8">
|
<DialogContent className="sm:max-w-3xl max-h-[90vh] overflow-y-auto p-6 sm:px-8">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Adicionar múltiplos lançamentos</DialogTitle>
|
<DialogTitle>Adicionar múltiplos lançamentos</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
@@ -248,7 +248,7 @@ export function MassAddDialog({
|
|||||||
{/* Fixed Fields Section */}
|
{/* Fixed Fields Section */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-sm font-semibold">Valores Padrão</h3>
|
<h3 className="text-sm font-semibold">Valores Padrão</h3>
|
||||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-5">
|
||||||
{/* Transaction Type */}
|
{/* Transaction Type */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="transaction-type">Tipo de Transação</Label>
|
<Label htmlFor="transaction-type">Tipo de Transação</Label>
|
||||||
@@ -274,39 +274,6 @@ export function MassAddDialog({
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pagador */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="pagador">Pagador</Label>
|
|
||||||
<Select value={pagadorId} onValueChange={setPagadorId}>
|
|
||||||
<SelectTrigger id="pagador" className="w-full">
|
|
||||||
<SelectValue placeholder="Selecione o pagador">
|
|
||||||
{pagadorId &&
|
|
||||||
(() => {
|
|
||||||
const selectedOption = pagadorOptions.find(
|
|
||||||
(opt) => opt.value === pagadorId
|
|
||||||
);
|
|
||||||
return selectedOption ? (
|
|
||||||
<PagadorSelectContent
|
|
||||||
label={selectedOption.label}
|
|
||||||
avatarUrl={selectedOption.avatarUrl}
|
|
||||||
/>
|
|
||||||
) : null;
|
|
||||||
})()}
|
|
||||||
</SelectValue>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{pagadorOptions.map((option) => (
|
|
||||||
<SelectItem key={option.value} value={option.value}>
|
|
||||||
<PagadorSelectContent
|
|
||||||
label={option.label}
|
|
||||||
avatarUrl={option.avatarUrl}
|
|
||||||
/>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Payment Method */}
|
{/* Payment Method */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="payment-method">Forma de Pagamento</Label>
|
<Label htmlFor="payment-method">Forma de Pagamento</Label>
|
||||||
@@ -489,6 +456,7 @@ export function MassAddDialog({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
placeholder="Data"
|
placeholder="Data"
|
||||||
|
className="w-32 truncate"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -528,6 +496,52 @@ export function MassAddDialog({
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full">
|
||||||
|
<Label
|
||||||
|
htmlFor={`pagador-${transaction.id}`}
|
||||||
|
className="sr-only"
|
||||||
|
>
|
||||||
|
Pagador {index + 1}
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={transaction.pagadorId}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
updateTransaction(transaction.id, "pagadorId", value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id={`pagador-${transaction.id}`}
|
||||||
|
className="w-32 truncate"
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Pagador">
|
||||||
|
{transaction.pagadorId &&
|
||||||
|
(() => {
|
||||||
|
const selectedOption = pagadorOptions.find(
|
||||||
|
(opt) => opt.value === transaction.pagadorId
|
||||||
|
);
|
||||||
|
return selectedOption ? (
|
||||||
|
<PagadorSelectContent
|
||||||
|
label={selectedOption.label}
|
||||||
|
avatarUrl={selectedOption.avatarUrl}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
})()}
|
||||||
|
</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{pagadorOptions.map((option) => (
|
||||||
|
<SelectItem key={option.value} value={option.value}>
|
||||||
|
<PagadorSelectContent
|
||||||
|
label={option.label}
|
||||||
|
avatarUrl={option.avatarUrl}
|
||||||
|
/>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Label
|
<Label
|
||||||
htmlFor={`categoria-${transaction.id}`}
|
htmlFor={`categoria-${transaction.id}`}
|
||||||
@@ -547,7 +561,7 @@ export function MassAddDialog({
|
|||||||
>
|
>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
id={`categoria-${transaction.id}`}
|
id={`categoria-${transaction.id}`}
|
||||||
className="w-42 truncate"
|
className="w-32 truncate"
|
||||||
>
|
>
|
||||||
<SelectValue placeholder="Categoria" />
|
<SelectValue placeholder="Categoria" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|||||||
@@ -712,6 +712,8 @@ export function LancamentosTable({
|
|||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{onMassAdd ? (
|
{onMassAdd ? (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
onClick={onMassAdd}
|
onClick={onMassAdd}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -723,6 +725,11 @@ export function LancamentosTable({
|
|||||||
Adicionar múltiplos lançamentos
|
Adicionar múltiplos lançamentos
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Adicionar múltiplos lançamentos</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export function Logo({ variant = "full", className }: LogoProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex items-center", className)}>
|
<div className={cn("flex items-center py-4", className)}>
|
||||||
<Image
|
<Image
|
||||||
src="/logo_small.png"
|
src="/logo_small.png"
|
||||||
alt="Opensheets"
|
alt="Opensheets"
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ function MoneyValues({ amount, className }: Props) {
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
money_font.className,
|
money_font.className,
|
||||||
"inline-flex items-baseline font-medium transition-all duration-200",
|
"inline-flex items-baseline transition-all duration-200 tracking-tighter",
|
||||||
privacyMode &&
|
privacyMode &&
|
||||||
"blur-[6px] select-none hover:blur-none focus-within:blur-none",
|
"blur-[6px] select-none hover:blur-none focus-within:blur-none",
|
||||||
className
|
className
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export default function MonthPicker() {
|
|||||||
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div
|
<div
|
||||||
className="mx-1 space-x-1 capitalize font-medium"
|
className="mx-1 space-x-1 capitalize font-bold"
|
||||||
aria-current={!isDifferentFromCurrent ? "date" : undefined}
|
aria-current={!isDifferentFromCurrent ? "date" : undefined}
|
||||||
aria-label={`Período selecionado: ${currentMonthLabel} de ${currentYear}`}
|
aria-label={`Período selecionado: ${currentMonthLabel} de ${currentYear}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export function AppSidebar({
|
|||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
asChild
|
asChild
|
||||||
className="data-[slot=sidebar-menu-button]:px-1.5! hover:bg-transparent"
|
className="data-[slot=sidebar-menu-button]:px-1.5! hover:bg-transparent active:bg-transparent pt-4 justify-center hover:scale-105 transition-all duration-200"
|
||||||
>
|
>
|
||||||
<a href="/dashboard">
|
<a href="/dashboard">
|
||||||
<LogoContent />
|
<LogoContent />
|
||||||
|
|||||||
@@ -48,6 +48,14 @@ type NavSection = {
|
|||||||
items: NavItem[];
|
items: NavItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MONTH_PERIOD_PARAM = "periodo";
|
||||||
|
|
||||||
|
const PERIOD_AWARE_PATHS = new Set([
|
||||||
|
"/dashboard",
|
||||||
|
"/lancamentos",
|
||||||
|
"/orcamentos",
|
||||||
|
]);
|
||||||
|
|
||||||
export function NavMain({ sections }: { sections: NavSection[] }) {
|
export function NavMain({ sections }: { sections: NavSection[] }) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
@@ -167,7 +175,7 @@ export function NavMain({ sections }: { sections: NavSection[] }) {
|
|||||||
src={avatarSrc}
|
src={avatarSrc}
|
||||||
alt={`Avatar de ${subItem.title}`}
|
alt={`Avatar de ${subItem.title}`}
|
||||||
/>
|
/>
|
||||||
<AvatarFallback className="text-[10px] font-medium uppercase">
|
<AvatarFallback className="text-xs font-medium uppercase">
|
||||||
{initial}
|
{initial}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
@@ -196,11 +204,3 @@ export function NavMain({ sections }: { sections: NavSection[] }) {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const MONTH_PERIOD_PARAM = "periodo";
|
|
||||||
|
|
||||||
const PERIOD_AWARE_PATHS = new Set([
|
|
||||||
"/dashboard",
|
|
||||||
"/lancamentos",
|
|
||||||
"/orcamentos",
|
|
||||||
]);
|
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import { title_font } from "@/public/fonts/font_index";
|
||||||
import { RiExpandDiagonalLine } from "@remixicon/react";
|
import { RiExpandDiagonalLine } from "@remixicon/react";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import { title_font } from "@/public/fonts/font_index";
|
|
||||||
|
|
||||||
const OVERFLOW_THRESHOLD_PX = 16;
|
const OVERFLOW_THRESHOLD_PX = 16;
|
||||||
const OVERFLOW_CHECK_DEBOUNCE_MS = 100;
|
const OVERFLOW_CHECK_DEBOUNCE_MS = 100;
|
||||||
@@ -82,7 +82,7 @@ export default function WidgetCard({
|
|||||||
<CardTitle
|
<CardTitle
|
||||||
className={`${title_font.className} flex items-center gap-1`}
|
className={`${title_font.className} flex items-center gap-1`}
|
||||||
>
|
>
|
||||||
{icon}
|
<span className="text-primary">{icon}</span>
|
||||||
{title}
|
{title}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-muted-foreground text-sm capitalize mt-1">
|
<CardDescription className="text-muted-foreground text-sm capitalize mt-1">
|
||||||
|
|||||||
@@ -49,6 +49,14 @@ function getNameFromGoogleProfile(profile: GoogleProfile): string {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export const auth = betterAuth({
|
export const auth = betterAuth({
|
||||||
|
// Base URL configuration
|
||||||
|
baseURL: process.env.BETTER_AUTH_URL || "http://localhost:3000",
|
||||||
|
|
||||||
|
// Trust host configuration for production environments
|
||||||
|
trustedOrigins: process.env.BETTER_AUTH_URL
|
||||||
|
? [process.env.BETTER_AUTH_URL]
|
||||||
|
: [],
|
||||||
|
|
||||||
// Email/Password authentication
|
// Email/Password authentication
|
||||||
emailAndPassword: {
|
emailAndPassword: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -62,6 +70,26 @@ export const auth = betterAuth({
|
|||||||
camelCase: true,
|
camelCase: true,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// Session configuration - Safari compatibility
|
||||||
|
session: {
|
||||||
|
cookieCache: {
|
||||||
|
enabled: true,
|
||||||
|
maxAge: 60 * 5, // 5 minutes
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Advanced configuration for Safari compatibility
|
||||||
|
advanced: {
|
||||||
|
cookieOptions: {
|
||||||
|
sameSite: "lax", // Safari compatible
|
||||||
|
secure: process.env.NODE_ENV === "production", // HTTPS in production only
|
||||||
|
httpOnly: true,
|
||||||
|
},
|
||||||
|
crossSubDomainCookies: {
|
||||||
|
enabled: false, // Disable for better Safari compatibility
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
// Google OAuth (se configurado)
|
// Google OAuth (se configurado)
|
||||||
socialProviders:
|
socialProviders:
|
||||||
googleClientId && googleClientSecret
|
googleClientId && googleClientSecret
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
import type { ChangelogEntry } from "./data";
|
import type { ChangelogEntry } from "./data";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converte uma string de data para um formato compatível com Safari.
|
||||||
|
* Safari não aceita "YYYY-MM-DD HH:mm:ss ±HHMM", requer "YYYY-MM-DDTHH:mm:ss±HHMM"
|
||||||
|
*
|
||||||
|
* @param dateString - String de data no formato "YYYY-MM-DD HH:mm:ss ±HHMM"
|
||||||
|
* @returns Date object válido
|
||||||
|
*/
|
||||||
|
export function parseSafariCompatibleDate(dateString: string): Date {
|
||||||
|
// Substitui o espaço entre data e hora por "T" (formato ISO 8601)
|
||||||
|
// Exemplo: "2025-12-09 17:26:08 +0000" → "2025-12-09T17:26:08+0000"
|
||||||
|
const isoString = dateString.replace(/(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})\s+/, "$1T$2");
|
||||||
|
return new Date(isoString);
|
||||||
|
}
|
||||||
|
|
||||||
export function getCategoryLabel(category: string): string {
|
export function getCategoryLabel(category: string): string {
|
||||||
const labels: Record<string, string> = {
|
const labels: Record<string, string> = {
|
||||||
feature: "Novidades",
|
feature: "Novidades",
|
||||||
|
|||||||
@@ -19,6 +19,28 @@ const nextConfig: NextConfig = {
|
|||||||
devIndicators: {
|
devIndicators: {
|
||||||
position: "bottom-right",
|
position: "bottom-right",
|
||||||
},
|
},
|
||||||
|
// Headers for Safari compatibility
|
||||||
|
async headers() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: "/:path*",
|
||||||
|
headers: [
|
||||||
|
{
|
||||||
|
key: "X-DNS-Prefetch-Control",
|
||||||
|
value: "on",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Strict-Transport-Security",
|
||||||
|
value: "max-age=31536000; includeSubDomains",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "X-Content-Type-Options",
|
||||||
|
value: "nosniff",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
36
package.json
36
package.json
@@ -28,10 +28,10 @@
|
|||||||
"docker:rebuild": "docker compose up --build --force-recreate"
|
"docker:rebuild": "docker compose up --build --force-recreate"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "^2.0.54",
|
"@ai-sdk/anthropic": "^2.0.56",
|
||||||
"@ai-sdk/google": "^2.0.45",
|
"@ai-sdk/google": "^2.0.46",
|
||||||
"@ai-sdk/openai": "^2.0.80",
|
"@ai-sdk/openai": "^2.0.86",
|
||||||
"@openrouter/ai-sdk-provider": "^1.5.2",
|
"@openrouter/ai-sdk-provider": "^1.5.3",
|
||||||
"@radix-ui/react-alert-dialog": "1.1.15",
|
"@radix-ui/react-alert-dialog": "1.1.15",
|
||||||
"@radix-ui/react-avatar": "1.1.11",
|
"@radix-ui/react-avatar": "1.1.11",
|
||||||
"@radix-ui/react-checkbox": "1.3.3",
|
"@radix-ui/react-checkbox": "1.3.3",
|
||||||
@@ -56,42 +56,42 @@
|
|||||||
"@tanstack/react-table": "8.21.3",
|
"@tanstack/react-table": "8.21.3",
|
||||||
"@vercel/analytics": "^1.6.1",
|
"@vercel/analytics": "^1.6.1",
|
||||||
"@vercel/speed-insights": "^1.3.1",
|
"@vercel/speed-insights": "^1.3.1",
|
||||||
"ai": "^5.0.108",
|
"ai": "^5.0.113",
|
||||||
"babel-plugin-react-compiler": "^1.0.0",
|
"babel-plugin-react-compiler": "^1.0.0",
|
||||||
"better-auth": "1.4.6",
|
"better-auth": "1.4.7",
|
||||||
"class-variance-authority": "0.7.1",
|
"class-variance-authority": "0.7.1",
|
||||||
"clsx": "2.1.1",
|
"clsx": "2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"drizzle-orm": "0.45.1",
|
"drizzle-orm": "0.45.1",
|
||||||
"motion": "^12.23.26",
|
"motion": "^12.23.26",
|
||||||
"next": "16.0.8",
|
"next": "16.0.10",
|
||||||
"next-themes": "0.4.6",
|
"next-themes": "0.4.6",
|
||||||
"pg": "8.16.3",
|
"pg": "8.16.3",
|
||||||
"react": "19.2.1",
|
"react": "19.2.3",
|
||||||
"react-day-picker": "^9.12.0",
|
"react-day-picker": "^9.12.0",
|
||||||
"react-dom": "19.2.1",
|
"react-dom": "19.2.3",
|
||||||
"recharts": "3.5.1",
|
"recharts": "3.6.0",
|
||||||
"resend": "^6.6.0",
|
"resend": "^6.6.0",
|
||||||
"sonner": "2.0.7",
|
"sonner": "2.0.7",
|
||||||
"tailwind-merge": "3.4.0",
|
"tailwind-merge": "3.4.0",
|
||||||
"vaul": "1.1.2",
|
"vaul": "1.1.2",
|
||||||
"zod": "4.1.13"
|
"zod": "4.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "4.1.17",
|
"@tailwindcss/postcss": "4.1.18",
|
||||||
"@types/d3-array": "^3.2.2",
|
"@types/d3-array": "^3.2.2",
|
||||||
"@types/node": "24.10.2",
|
"@types/node": "25.0.2",
|
||||||
"@types/pg": "^8.15.6",
|
"@types/pg": "^8.16.0",
|
||||||
"@types/react": "19.2.7",
|
"@types/react": "19.2.7",
|
||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "19.2.3",
|
||||||
"baseline-browser-mapping": "^2.9.6",
|
"baseline-browser-mapping": "^2.9.7",
|
||||||
"depcheck": "^1.4.7",
|
"depcheck": "^1.4.7",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"drizzle-kit": "0.31.8",
|
"drizzle-kit": "0.31.8",
|
||||||
"eslint": "9.39.1",
|
"eslint": "9.39.2",
|
||||||
"eslint-config-next": "16.0.8",
|
"eslint-config-next": "16.0.10",
|
||||||
"tailwindcss": "4.1.17",
|
"tailwindcss": "4.1.18",
|
||||||
"tsx": "4.21.0",
|
"tsx": "4.21.0",
|
||||||
"typescript": "5.9.3"
|
"typescript": "5.9.3"
|
||||||
}
|
}
|
||||||
|
|||||||
1478
pnpm-lock.yaml
generated
1478
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,31 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"generatedAt": "2025-12-10T16:45:04.592Z",
|
"generatedAt": "2025-12-13T13:52:07.921Z",
|
||||||
"entries": [
|
"entries": [
|
||||||
|
{
|
||||||
|
"id": "0767636eed5085a211e08de52577beed658f05cf",
|
||||||
|
"type": "feat",
|
||||||
|
"title": "ajustar layout e estilos",
|
||||||
|
"date": "2025-12-11 17:43:33 +0000",
|
||||||
|
"icon": "✨",
|
||||||
|
"category": "feature"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0744991edd717748fce24e148907eafd7222c64e",
|
||||||
|
"type": "chore",
|
||||||
|
"title": "remove unused code and clean up imports",
|
||||||
|
"date": "2025-12-10 16:53:19 +0000",
|
||||||
|
"icon": "🔧",
|
||||||
|
"category": "chore"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "b767bd959955854e54b41270e9c220fe0b546947",
|
||||||
|
"type": "feat",
|
||||||
|
"title": "adicionar widgets de despesas e receitas com gráfico - Adiciona o widget de despesas por categoria com gráfico. - Adiciona o widget de receitas por categoria com gráfico. - Atualiza a configuração dos widgets para incluir novos componentes. - Ajusta estilos e tamanhos de elementos nos widgets existentes.",
|
||||||
|
"date": "2025-12-10 16:51:45 +0000",
|
||||||
|
"icon": "✨",
|
||||||
|
"category": "feature"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "89765d4373b820a3e7c8e4fa40479dd2673558b0",
|
"id": "89765d4373b820a3e7c8e4fa40479dd2673558b0",
|
||||||
"type": "chore",
|
"type": "chore",
|
||||||
@@ -137,30 +161,6 @@
|
|||||||
"date": "2025-11-23 12:26:05 -0300",
|
"date": "2025-11-23 12:26:05 -0300",
|
||||||
"icon": "✨",
|
"icon": "✨",
|
||||||
"category": "feature"
|
"category": "feature"
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "3ce8541a5699317c747c629e1c0e07d579458633",
|
|
||||||
"type": "fix",
|
|
||||||
"title": "corrige a grafia de \"OpenSheets\" para \"Opensheets\"",
|
|
||||||
"date": "2025-11-22 20:29:25 -0300",
|
|
||||||
"icon": "🐛",
|
|
||||||
"category": "bugfix"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "ac24961e4b97bfb58a52e1b95f3d9696fe1e5d86",
|
|
||||||
"type": "refactor",
|
|
||||||
"title": "substitui '•' por '-' em textos de exibição",
|
|
||||||
"date": "2025-11-22 12:58:57 -0300",
|
|
||||||
"icon": "♻️",
|
|
||||||
"category": "refactor"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "8c5313119dafaf3a33ab4bffeeb40d7f0278eb08",
|
|
||||||
"type": "feat",
|
|
||||||
"title": "atualiza fontes e altera avatar SVG",
|
|
||||||
"date": "2025-11-22 12:49:56 -0300",
|
|
||||||
"icon": "✨",
|
|
||||||
"category": "feature"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
BIN
public/dashboard-preview-dark.png
Normal file
BIN
public/dashboard-preview-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1021 KiB |
BIN
public/dashboard-preview-light.png
Normal file
BIN
public/dashboard-preview-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 869 KiB |
BIN
public/fonts/LaranjinhaDisplayPro_Bd.woff2
Normal file
BIN
public/fonts/LaranjinhaDisplayPro_Bd.woff2
Normal file
Binary file not shown.
BIN
public/fonts/LaranjinhaTextPro_Rg.woff2
Normal file
BIN
public/fonts/LaranjinhaTextPro_Rg.woff2
Normal file
Binary file not shown.
@@ -1,17 +1,23 @@
|
|||||||
import { Barlow, Inter } from "next/font/google";
|
import localFont from "next/font/local";
|
||||||
|
|
||||||
const inter = Inter({
|
const laranjinha = localFont({
|
||||||
subsets: ["latin"],
|
src: [
|
||||||
weight: ["500", "600", "700"],
|
{
|
||||||
|
path: "./LaranjinhaTextPro_Rg.woff2",
|
||||||
|
weight: "400",
|
||||||
|
style: "normal",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "./LaranjinhaDisplayPro_Bd.woff2",
|
||||||
|
weight: "700",
|
||||||
|
style: "normal",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
display: "swap",
|
||||||
});
|
});
|
||||||
|
|
||||||
const barlow = Barlow({
|
const main_font = laranjinha;
|
||||||
subsets: ["latin"],
|
const money_font = laranjinha;
|
||||||
weight: "500",
|
const title_font = laranjinha;
|
||||||
});
|
|
||||||
|
|
||||||
const main_font = inter;
|
|
||||||
const money_font = barlow;
|
|
||||||
const title_font = inter;
|
|
||||||
|
|
||||||
export { main_font, money_font, title_font };
|
export { main_font, money_font, title_font };
|
||||||
|
|||||||
Reference in New Issue
Block a user