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.postgresql.org/)
|
||||
[](https://www.docker.com/)
|
||||
[](LICENSE)
|
||||
[](https://github.com/sponsors/felipegcoutinho)
|
||||
|
||||
---
|
||||
|
||||
<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>
|
||||
|
||||
---
|
||||
@@ -36,6 +38,7 @@
|
||||
- [Banco de Dados](#-banco-de-dados)
|
||||
- [Arquitetura](#-arquitetura)
|
||||
- [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.
|
||||
|
||||
> 💡 **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
|
||||
|
||||
**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
|
||||
|
||||
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">
|
||||
|
||||
**⭐ 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
|
||||
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { getRecentEstablishmentsAction } from "@/app/(dashboard)/lancamentos/actions";
|
||||
import { cartoes, lancamentos } from "@/db/schema";
|
||||
import { db } from "@/lib/db";
|
||||
import {
|
||||
buildOptionSets,
|
||||
buildSluggedFilters,
|
||||
fetchLancamentoFilterSources,
|
||||
mapLancamentosData,
|
||||
} from "@/lib/lancamentos/page-helpers";
|
||||
import { db } from "@/lib/db";
|
||||
import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants";
|
||||
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 TRANSACTION_TYPE_TRANSFERENCIA = "Transferência";
|
||||
@@ -98,7 +101,11 @@ export const fetchCalendarData = async ({
|
||||
|
||||
const cardTotals = new Map<string, number>();
|
||||
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;
|
||||
}
|
||||
const amount = Math.abs(item.amount ?? 0);
|
||||
|
||||
@@ -10,11 +10,7 @@ export default async function HistoricoCategoriasPage() {
|
||||
const data = await fetchCategoryHistory(user.id, currentPeriod);
|
||||
|
||||
return (
|
||||
<main className="flex flex-col gap-6">
|
||||
<p className="text-muted-foreground">
|
||||
Acompanhe o histórico de desempenho das suas categorias ao longo de 9
|
||||
meses.
|
||||
</p>
|
||||
<main>
|
||||
<CategoryHistoryWidget data={data} />
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@ export default function RootLayout({
|
||||
<PageDescription
|
||||
icon={<RiPriceTag3Line />}
|
||||
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}
|
||||
</section>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { contas, lancamentos } from "@/db/schema";
|
||||
import { contas, lancamentos, pagadores, categorias, cartoes } from "@/db/schema";
|
||||
import {
|
||||
INITIAL_BALANCE_CONDITION,
|
||||
INITIAL_BALANCE_NOTE,
|
||||
@@ -31,6 +31,78 @@ import { and, asc, desc, eq, gte, inArray, sql } from "drizzle-orm";
|
||||
import { randomUUID } from "node:crypto";
|
||||
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) => {
|
||||
if (period && /^\d{4}-\d{2}$/.test(period)) {
|
||||
return period;
|
||||
@@ -472,6 +544,42 @@ export async function createLancamentoAction(
|
||||
const user = await getUser();
|
||||
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 purchaseDate = parseLocalDateString(data.purchaseDate);
|
||||
const dueDate = data.dueDate ? parseLocalDateString(data.dueDate) : null;
|
||||
@@ -556,6 +664,42 @@ export async function updateLancamentoAction(
|
||||
const user = await getUser();
|
||||
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({
|
||||
columns: {
|
||||
id: true,
|
||||
@@ -1124,12 +1268,12 @@ const massAddTransactionSchema = z.object({
|
||||
.number({ message: "Informe o valor da transação." })
|
||||
.min(0, "Informe um valor maior ou igual a zero."),
|
||||
categoriaId: uuidSchema("Categoria").nullable().optional(),
|
||||
pagadorId: uuidSchema("Pagador").nullable().optional(),
|
||||
});
|
||||
|
||||
const massAddSchema = z.object({
|
||||
fixedFields: z.object({
|
||||
transactionType: z.enum(LANCAMENTO_TRANSACTION_TYPES).optional(),
|
||||
pagadorId: uuidSchema("Pagador").nullable().optional(),
|
||||
paymentMethod: z.enum(LANCAMENTO_PAYMENT_METHODS).optional(),
|
||||
condition: z.enum(LANCAMENTO_CONDITIONS).optional(),
|
||||
period: z
|
||||
@@ -1156,6 +1300,46 @@ export async function createMassLancamentosAction(
|
||||
const user = await getUser();
|
||||
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
|
||||
const defaultTransactionType = LANCAMENTO_TRANSACTION_TYPES[0];
|
||||
const defaultCondition = LANCAMENTO_CONDITIONS[0];
|
||||
@@ -1181,7 +1365,7 @@ export async function createMassLancamentosAction(
|
||||
const condition = data.fixedFields.condition ?? defaultCondition;
|
||||
const paymentMethod =
|
||||
data.fixedFields.paymentMethod ?? defaultPaymentMethod;
|
||||
const pagadorId = data.fixedFields.pagadorId ?? null;
|
||||
const pagadorId = transaction.pagadorId ?? null;
|
||||
const contaId =
|
||||
paymentMethod === "Cartão de crédito"
|
||||
? null
|
||||
|
||||
@@ -159,11 +159,19 @@ export default async function Page() {
|
||||
<div className="container">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<Image
|
||||
src="/dashboard-preview.png"
|
||||
src="/dashboard-preview-light.png"
|
||||
alt="opensheets Dashboard Preview"
|
||||
width={1920}
|
||||
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
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -90,11 +90,11 @@
|
||||
.dark {
|
||||
/* Base surfaces - true dark with minimal saturation */
|
||||
--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-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-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: oklch(69.18% 0.18855 38.353);
|
||||
@@ -102,7 +102,7 @@
|
||||
|
||||
/* Secondary - elevated surface */
|
||||
--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: oklch(33.674% 0.00531 91.552);
|
||||
@@ -110,7 +110,7 @@
|
||||
|
||||
/* Accent - subtle highlight */
|
||||
--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: oklch(62% 0.2 28);
|
||||
@@ -131,11 +131,11 @@
|
||||
|
||||
/* Sidebar - slight separation from main */
|
||||
--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-foreground: oklch(12.897% 0.00619 87.19);
|
||||
--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-ring: oklch(69.18% 0.18855 38.353);
|
||||
|
||||
@@ -162,11 +162,11 @@
|
||||
|
||||
/* Special components */
|
||||
--month-picker: oklch(24.039% 0.00151 16.27);
|
||||
--month-picker-foreground: oklch(85.196% 0.0204 100.682);
|
||||
--dark: oklch(85.196% 0.0204 100.682);
|
||||
--month-picker-foreground: oklch(92.189% 0.0186 103.516);
|
||||
--dark: oklch(92.189% 0.0186 103.516);
|
||||
--dark-foreground: oklch(18.711% 0.00427 84.566);
|
||||
--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 {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { main_font } from "@/public/fonts/font_index";
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
import { Analytics } from "@vercel/analytics/next";
|
||||
import { SpeedInsights } from "@vercel/speed-insights/next";
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Opensheets",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"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 type { CalendarEvent } from "@/components/calendario/types";
|
||||
import { cn } from "@/lib/utils/ui";
|
||||
|
||||
const LEGEND_ITEMS: Array<{
|
||||
type: CalendarEvent["type"];
|
||||
@@ -16,17 +15,12 @@ const LEGEND_ITEMS: Array<{
|
||||
|
||||
export function CalendarLegend() {
|
||||
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) => {
|
||||
const style = EVENT_TYPE_STYLES[item.type];
|
||||
return (
|
||||
<span key={item.type} className="flex items-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"size-2.5 rounded-full border border-black/10 dark:border-white/20",
|
||||
style.dot
|
||||
)}
|
||||
/>
|
||||
<span className={cn("size-3 rounded-full", style.dot)} />
|
||||
{item.label}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import type { CalendarDay, CalendarEvent } from "@/components/calendario/types";
|
||||
import { currencyFormatter } from "@/lib/lancamentos/formatting-helpers";
|
||||
import { cn } from "@/lib/utils/ui";
|
||||
import { RiAddLine } from "@remixicon/react";
|
||||
import type { KeyboardEvent, MouseEvent } from "react";
|
||||
import type { CalendarDay, CalendarEvent } from "@/components/calendario/types";
|
||||
import { currencyFormatter } from "@/lib/lancamentos/formatting-helpers";
|
||||
|
||||
type DayCellProps = {
|
||||
day: CalendarDay;
|
||||
@@ -17,17 +17,18 @@ export const EVENT_TYPE_STYLES: Record<
|
||||
{ wrapper: string; dot: string; accent?: string }
|
||||
> = {
|
||||
lancamento: {
|
||||
wrapper: "bg-cyan-600 text-cyan-50 dark:bg-cyan-500/30 dark:text-cyan-200",
|
||||
dot: "bg-cyan-600",
|
||||
wrapper:
|
||||
"bg-orange-100 text-orange-600 dark:bg-orange-800 dark:text-orange-50",
|
||||
dot: "bg-orange-600",
|
||||
},
|
||||
boleto: {
|
||||
wrapper: "bg-red-600 text-red-50 dark:bg-red-500/30 dark:text-red-200",
|
||||
dot: "bg-red-600",
|
||||
wrapper:
|
||||
"bg-emerald-100 text-emerald-600 dark:bg-emerald-800 dark:text-emerald-50",
|
||||
dot: "bg-emerald-600",
|
||||
},
|
||||
cartao: {
|
||||
wrapper:
|
||||
"bg-violet-600 text-violet-50 dark:bg-violet-500/30 dark:text-violet-200",
|
||||
dot: "bg-violet-600",
|
||||
wrapper: "bg-blue-100 text-blue-600 dark:bg-blue-800 dark:text-blue-50",
|
||||
dot: "bg-blue-600",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -82,11 +83,12 @@ const DayEventPreview = ({ event }: { event: CalendarEvent }) => {
|
||||
return (
|
||||
<div
|
||||
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
|
||||
)}
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
{complement ? (
|
||||
|
||||
@@ -87,7 +87,7 @@ export function CategoryDetailHeader({
|
||||
<Card className="px-4">
|
||||
<div className="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
|
||||
<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 className="size-6" aria-hidden />
|
||||
) : (
|
||||
|
||||
@@ -19,6 +19,7 @@ import type { ChangelogEntry } from "@/lib/changelog/data";
|
||||
import {
|
||||
getCategoryLabel,
|
||||
groupEntriesByCategory,
|
||||
parseSafariCompatibleDate,
|
||||
} from "@/lib/changelog/utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { RiMegaphoneLine } from "@remixicon/react";
|
||||
@@ -115,7 +116,7 @@ export function ChangelogNotification({
|
||||
{entry.title}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatDistanceToNow(new Date(entry.date), {
|
||||
{formatDistanceToNow(parseSafariCompatibleDate(entry.date), {
|
||||
addSuffix: true,
|
||||
locale: ptBR,
|
||||
})}
|
||||
|
||||
@@ -205,7 +205,7 @@ export function CategoryHistoryWidget({ data }: CategoryHistoryWidgetProps) {
|
||||
<CardContent className="space-y-2.5">
|
||||
<div className="space-y-2">
|
||||
{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">
|
||||
{selectedCategoryDetails.map((category) => {
|
||||
if (!category) return null;
|
||||
|
||||
@@ -130,7 +130,7 @@ export function InstallmentAnalysisPage({
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* 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">
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
Se você pagar tudo que está selecionado:
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import type { DashboardCardMetrics } from "@/lib/dashboard/metrics";
|
||||
import { title_font } from "@/public/fonts/font_index";
|
||||
import {
|
||||
RiArrowDownLine,
|
||||
RiArrowUpLine,
|
||||
@@ -15,7 +16,6 @@ import {
|
||||
RiSubtractLine,
|
||||
} from "@remixicon/react";
|
||||
import MoneyValues from "../money-values";
|
||||
import { title_font } from "@/public/fonts/font_index";
|
||||
|
||||
type SectionCardsProps = {
|
||||
metrics: DashboardCardMetrics;
|
||||
@@ -73,7 +73,7 @@ export function SectionCards({ metrics }: SectionCardsProps) {
|
||||
<Card key={label} className="@container/card gap-2">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-1">
|
||||
<Icon className="size-4" />
|
||||
<Icon className="size-4 text-primary" />
|
||||
{label}
|
||||
</CardTitle>
|
||||
<MoneyValues className="text-2xl" amount={metric.current} />
|
||||
@@ -84,9 +84,9 @@ export function SectionCards({ metrics }: SectionCardsProps) {
|
||||
</Badge>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex-col items-start gap-1.5 text-sm">
|
||||
<div className="line-clamp-1 flex gap-2 text-sm">
|
||||
Mês anterior:
|
||||
<CardFooter className="flex-col items-start gap-2 text-sm">
|
||||
<div className="line-clamp-1 flex gap-2 text-xs">
|
||||
Mês anterior
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
<MoneyValues amount={metric.previous} />
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
deleteSavedInsightsAction,
|
||||
generateInsightsAction,
|
||||
loadSavedInsightsAction,
|
||||
saveInsightsAction,
|
||||
deleteSavedInsightsAction,
|
||||
} from "@/app/(dashboard)/insights/actions";
|
||||
import { DEFAULT_MODEL } from "@/app/(dashboard)/insights/data";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
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 { toast } from "sonner";
|
||||
import { EmptyState } from "../empty-state";
|
||||
import { InsightsGrid } from "./insights-grid";
|
||||
import { ModelSelector } from "./model-selector";
|
||||
import { format } from "date-fns";
|
||||
import { ptBR } from "date-fns/locale";
|
||||
|
||||
interface InsightsPageProps {
|
||||
period: string;
|
||||
@@ -129,11 +134,14 @@ export function InsightsPage({ period, onAnalyze }: InsightsPageProps) {
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Privacy Warning */}
|
||||
<Alert>
|
||||
<Alert className="border-none">
|
||||
<RiAlertLine className="size-4" />
|
||||
<AlertDescription className="text-sm">
|
||||
<strong>Aviso de privacidade:</strong> Ao gerar insights, seus dados 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.
|
||||
<strong>Aviso de privacidade:</strong> Ao gerar insights, seus dados
|
||||
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>
|
||||
</Alert>
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ import { createMonthOptions } from "@/lib/utils/period";
|
||||
import { RiAddLine, RiDeleteBinLine } from "@remixicon/react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { EstabelecimentoInput } from "../shared/estabelecimento-input";
|
||||
import type { SelectOption } from "../../types";
|
||||
import {
|
||||
CategoriaSelectContent,
|
||||
@@ -40,6 +39,7 @@ import {
|
||||
PaymentMethodSelectContent,
|
||||
TransactionTypeSelectContent,
|
||||
} from "../select-items";
|
||||
import { EstabelecimentoInput } from "../shared/estabelecimento-input";
|
||||
|
||||
interface MassAddDialogProps {
|
||||
open: boolean;
|
||||
@@ -57,7 +57,6 @@ interface MassAddDialogProps {
|
||||
export interface MassAddFormData {
|
||||
fixedFields: {
|
||||
transactionType?: string;
|
||||
pagadorId?: string;
|
||||
paymentMethod?: string;
|
||||
condition?: string;
|
||||
period?: string;
|
||||
@@ -69,6 +68,7 @@ export interface MassAddFormData {
|
||||
name: string;
|
||||
amount: string;
|
||||
categoriaId?: string;
|
||||
pagadorId?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ interface TransactionRow {
|
||||
name: string;
|
||||
amount: string;
|
||||
categoriaId: string | undefined;
|
||||
pagadorId: string | undefined;
|
||||
}
|
||||
|
||||
export function MassAddDialog({
|
||||
@@ -96,9 +97,6 @@ export function MassAddDialog({
|
||||
|
||||
// Fixed fields state (sempre ativos, sem checkboxes)
|
||||
const [transactionType, setTransactionType] = useState<string>("Despesa");
|
||||
const [pagadorId, setPagadorId] = useState<string | undefined>(
|
||||
defaultPagadorId ?? undefined
|
||||
);
|
||||
const [paymentMethod, setPaymentMethod] = useState<string>(
|
||||
LANCAMENTO_PAYMENT_METHODS[0]
|
||||
);
|
||||
@@ -115,6 +113,7 @@ export function MassAddDialog({
|
||||
name: "",
|
||||
amount: "",
|
||||
categoriaId: undefined,
|
||||
pagadorId: defaultPagadorId ?? undefined,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -141,6 +140,7 @@ export function MassAddDialog({
|
||||
name: "",
|
||||
amount: "",
|
||||
categoriaId: undefined,
|
||||
pagadorId: defaultPagadorId ?? undefined,
|
||||
},
|
||||
]);
|
||||
};
|
||||
@@ -191,7 +191,6 @@ export function MassAddDialog({
|
||||
const formData: MassAddFormData = {
|
||||
fixedFields: {
|
||||
transactionType,
|
||||
pagadorId,
|
||||
paymentMethod,
|
||||
condition,
|
||||
period,
|
||||
@@ -203,6 +202,7 @@ export function MassAddDialog({
|
||||
name: t.name.trim(),
|
||||
amount: t.amount.trim(),
|
||||
categoriaId: t.categoriaId,
|
||||
pagadorId: t.pagadorId,
|
||||
})),
|
||||
};
|
||||
|
||||
@@ -212,7 +212,6 @@ export function MassAddDialog({
|
||||
onOpenChange(false);
|
||||
// Reset form
|
||||
setTransactionType("Despesa");
|
||||
setPagadorId(defaultPagadorId ?? undefined);
|
||||
setPaymentMethod(LANCAMENTO_PAYMENT_METHODS[0]);
|
||||
setCondition("À vista");
|
||||
setPeriod(selectedPeriod);
|
||||
@@ -225,6 +224,7 @@ export function MassAddDialog({
|
||||
name: "",
|
||||
amount: "",
|
||||
categoriaId: undefined,
|
||||
pagadorId: defaultPagadorId ?? undefined,
|
||||
},
|
||||
]);
|
||||
} catch (_error) {
|
||||
@@ -236,7 +236,7 @@ export function MassAddDialog({
|
||||
|
||||
return (
|
||||
<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>
|
||||
<DialogTitle>Adicionar múltiplos lançamentos</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -248,7 +248,7 @@ export function MassAddDialog({
|
||||
{/* Fixed Fields Section */}
|
||||
<div className="space-y-4">
|
||||
<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 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="transaction-type">Tipo de Transação</Label>
|
||||
@@ -274,39 +274,6 @@ export function MassAddDialog({
|
||||
</Select>
|
||||
</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 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="payment-method">Forma de Pagamento</Label>
|
||||
@@ -489,6 +456,7 @@ export function MassAddDialog({
|
||||
)
|
||||
}
|
||||
placeholder="Data"
|
||||
className="w-32 truncate"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -528,6 +496,52 @@ export function MassAddDialog({
|
||||
required
|
||||
/>
|
||||
</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">
|
||||
<Label
|
||||
htmlFor={`categoria-${transaction.id}`}
|
||||
@@ -547,7 +561,7 @@ export function MassAddDialog({
|
||||
>
|
||||
<SelectTrigger
|
||||
id={`categoria-${transaction.id}`}
|
||||
className="w-42 truncate"
|
||||
className="w-32 truncate"
|
||||
>
|
||||
<SelectValue placeholder="Categoria" />
|
||||
</SelectTrigger>
|
||||
|
||||
@@ -712,6 +712,8 @@ export function LancamentosTable({
|
||||
</Button>
|
||||
) : null}
|
||||
{onMassAdd ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
onClick={onMassAdd}
|
||||
variant="outline"
|
||||
@@ -723,6 +725,11 @@ export function LancamentosTable({
|
||||
Adicionar múltiplos lançamentos
|
||||
</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Adicionar múltiplos lançamentos</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -21,7 +21,7 @@ export function Logo({ variant = "full", className }: LogoProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center", className)}>
|
||||
<div className={cn("flex items-center py-4", className)}>
|
||||
<Image
|
||||
src="/logo_small.png"
|
||||
alt="Opensheets"
|
||||
|
||||
@@ -22,7 +22,7 @@ function MoneyValues({ amount, className }: Props) {
|
||||
<span
|
||||
className={cn(
|
||||
money_font.className,
|
||||
"inline-flex items-baseline font-medium transition-all duration-200",
|
||||
"inline-flex items-baseline transition-all duration-200 tracking-tighter",
|
||||
privacyMode &&
|
||||
"blur-[6px] select-none hover:blur-none focus-within:blur-none",
|
||||
className
|
||||
|
||||
@@ -89,7 +89,7 @@ export default function MonthPicker() {
|
||||
|
||||
<div className="flex items-center">
|
||||
<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-label={`Período selecionado: ${currentMonthLabel} de ${currentYear}`}
|
||||
>
|
||||
|
||||
@@ -51,7 +51,7 @@ export function AppSidebar({
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
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">
|
||||
<LogoContent />
|
||||
|
||||
@@ -48,6 +48,14 @@ type NavSection = {
|
||||
items: NavItem[];
|
||||
};
|
||||
|
||||
const MONTH_PERIOD_PARAM = "periodo";
|
||||
|
||||
const PERIOD_AWARE_PATHS = new Set([
|
||||
"/dashboard",
|
||||
"/lancamentos",
|
||||
"/orcamentos",
|
||||
]);
|
||||
|
||||
export function NavMain({ sections }: { sections: NavSection[] }) {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
@@ -167,7 +175,7 @@ export function NavMain({ sections }: { sections: NavSection[] }) {
|
||||
src={avatarSrc}
|
||||
alt={`Avatar de ${subItem.title}`}
|
||||
/>
|
||||
<AvatarFallback className="text-[10px] font-medium uppercase">
|
||||
<AvatarFallback className="text-xs font-medium uppercase">
|
||||
{initial}
|
||||
</AvatarFallback>
|
||||
</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,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { title_font } from "@/public/fonts/font_index";
|
||||
import { RiExpandDiagonalLine } from "@remixicon/react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Button } from "./ui/button";
|
||||
import { title_font } from "@/public/fonts/font_index";
|
||||
|
||||
const OVERFLOW_THRESHOLD_PX = 16;
|
||||
const OVERFLOW_CHECK_DEBOUNCE_MS = 100;
|
||||
@@ -82,7 +82,7 @@ export default function WidgetCard({
|
||||
<CardTitle
|
||||
className={`${title_font.className} flex items-center gap-1`}
|
||||
>
|
||||
{icon}
|
||||
<span className="text-primary">{icon}</span>
|
||||
{title}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-muted-foreground text-sm capitalize mt-1">
|
||||
|
||||
@@ -49,6 +49,14 @@ function getNameFromGoogleProfile(profile: GoogleProfile): string {
|
||||
// ============================================================================
|
||||
|
||||
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
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
@@ -62,6 +70,26 @@ export const auth = betterAuth({
|
||||
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)
|
||||
socialProviders:
|
||||
googleClientId && googleClientSecret
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
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 {
|
||||
const labels: Record<string, string> = {
|
||||
feature: "Novidades",
|
||||
|
||||
@@ -19,6 +19,28 @@ const nextConfig: NextConfig = {
|
||||
devIndicators: {
|
||||
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;
|
||||
|
||||
36
package.json
36
package.json
@@ -28,10 +28,10 @@
|
||||
"docker:rebuild": "docker compose up --build --force-recreate"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^2.0.54",
|
||||
"@ai-sdk/google": "^2.0.45",
|
||||
"@ai-sdk/openai": "^2.0.80",
|
||||
"@openrouter/ai-sdk-provider": "^1.5.2",
|
||||
"@ai-sdk/anthropic": "^2.0.56",
|
||||
"@ai-sdk/google": "^2.0.46",
|
||||
"@ai-sdk/openai": "^2.0.86",
|
||||
"@openrouter/ai-sdk-provider": "^1.5.3",
|
||||
"@radix-ui/react-alert-dialog": "1.1.15",
|
||||
"@radix-ui/react-avatar": "1.1.11",
|
||||
"@radix-ui/react-checkbox": "1.3.3",
|
||||
@@ -56,42 +56,42 @@
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"@vercel/analytics": "^1.6.1",
|
||||
"@vercel/speed-insights": "^1.3.1",
|
||||
"ai": "^5.0.108",
|
||||
"ai": "^5.0.113",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"better-auth": "1.4.6",
|
||||
"better-auth": "1.4.7",
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"drizzle-orm": "0.45.1",
|
||||
"motion": "^12.23.26",
|
||||
"next": "16.0.8",
|
||||
"next": "16.0.10",
|
||||
"next-themes": "0.4.6",
|
||||
"pg": "8.16.3",
|
||||
"react": "19.2.1",
|
||||
"react": "19.2.3",
|
||||
"react-day-picker": "^9.12.0",
|
||||
"react-dom": "19.2.1",
|
||||
"recharts": "3.5.1",
|
||||
"react-dom": "19.2.3",
|
||||
"recharts": "3.6.0",
|
||||
"resend": "^6.6.0",
|
||||
"sonner": "2.0.7",
|
||||
"tailwind-merge": "3.4.0",
|
||||
"vaul": "1.1.2",
|
||||
"zod": "4.1.13"
|
||||
"zod": "4.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "4.1.17",
|
||||
"@tailwindcss/postcss": "4.1.18",
|
||||
"@types/d3-array": "^3.2.2",
|
||||
"@types/node": "24.10.2",
|
||||
"@types/pg": "^8.15.6",
|
||||
"@types/node": "25.0.2",
|
||||
"@types/pg": "^8.16.0",
|
||||
"@types/react": "19.2.7",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"baseline-browser-mapping": "^2.9.6",
|
||||
"baseline-browser-mapping": "^2.9.7",
|
||||
"depcheck": "^1.4.7",
|
||||
"dotenv": "^17.2.3",
|
||||
"drizzle-kit": "0.31.8",
|
||||
"eslint": "9.39.1",
|
||||
"eslint-config-next": "16.0.8",
|
||||
"tailwindcss": "4.1.17",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-config-next": "16.0.10",
|
||||
"tailwindcss": "4.1.18",
|
||||
"tsx": "4.21.0",
|
||||
"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",
|
||||
"generatedAt": "2025-12-10T16:45:04.592Z",
|
||||
"generatedAt": "2025-12-13T13:52:07.921Z",
|
||||
"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",
|
||||
"type": "chore",
|
||||
@@ -137,30 +161,6 @@
|
||||
"date": "2025-11-23 12:26:05 -0300",
|
||||
"icon": "✨",
|
||||
"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({
|
||||
subsets: ["latin"],
|
||||
weight: ["500", "600", "700"],
|
||||
const laranjinha = localFont({
|
||||
src: [
|
||||
{
|
||||
path: "./LaranjinhaTextPro_Rg.woff2",
|
||||
weight: "400",
|
||||
style: "normal",
|
||||
},
|
||||
{
|
||||
path: "./LaranjinhaDisplayPro_Bd.woff2",
|
||||
weight: "700",
|
||||
style: "normal",
|
||||
},
|
||||
],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const barlow = Barlow({
|
||||
subsets: ["latin"],
|
||||
weight: "500",
|
||||
});
|
||||
|
||||
const main_font = inter;
|
||||
const money_font = barlow;
|
||||
const title_font = inter;
|
||||
const main_font = laranjinha;
|
||||
const money_font = laranjinha;
|
||||
const title_font = laranjinha;
|
||||
|
||||
export { main_font, money_font, title_font };
|
||||
|
||||
Reference in New Issue
Block a user