8 Commits

Author SHA1 Message Date
Felipe Coutinho
1df2ba787d chore: bump versão para 2.5.2 e atualizar changelog
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 15:42:51 +00:00
Felipe Coutinho
e5d9b66cca feat(dashboard): complementar texto de recorrências com "mensais"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 15:42:48 +00:00
Felipe Coutinho
37edb1b76d feat(notes): substituir ícone de tarefa pendente por RiSubtractLine em contextos read-only
No card e no modal de detalhes de anotações, onde não há interação
de marcação, tarefas não concluídas exibem RiSubtractLine em vez
do quadrado com borda. Locais interativos mantêm o comportamento atual.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 15:42:44 +00:00
Felipe Coutinho
6288f5f8d4 feat(budgets, cards): progress bar em cor destructive quando limite excedido
Adiciona prop indicatorClassName ao componente Progress. Orçamentos
estourados e cartões com 100% do limite utilizado exibem a barra
com indicador e fundo na cor destructive.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 15:42:39 +00:00
Felipe Coutinho
57ac326c2a feat(transactions): filtro de contas por tipo dinheiro e sinal + em transferências recebidas
Ao selecionar "Dinheiro" como forma de pagamento, exibe apenas contas
do tipo "Dinheiro". Transferências recebidas (amount > 0) passam a
exibir sinal + mantendo a cor azul.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 15:42:35 +00:00
Felipe Coutinho
dccc18b1c1 fix(transfers): corrigir forma de pagamento de pix para transferência bancária
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 15:42:31 +00:00
Felipe Coutinho
0cb01a1d4c feat(accounts): adicionar tipos de conta dinheiro e outros com ícones no seletor
Adiciona "Dinheiro" (issue #50) e "Outros" à lista de tipos de conta.
Implementa AccountTypeSelectContent com ícones distintos por tipo via
getAccountTypeIcon em shared/utils/icons.tsx.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 15:42:27 +00:00
Felipe Coutinho
51652da4f8 fix(invoices): exibir ícone de anexo na fatura do cartão (2.5.1)
fetchCardTransactions não preenchia hasAttachments, então o ícone não
aparecia em /cards/[cardId]/invoice. Agora delega para
fetchTransactionsWithRelations, que já calcula o flag via EXISTS.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 01:44:08 +00:00
18 changed files with 103 additions and 29 deletions

View File

@@ -5,6 +5,29 @@ Todas as mudanças notáveis deste projeto serão documentadas neste arquivo.
O formato é baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/),
e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR/).
## [2.5.2] - 2026-05-04
Esta versão traz melhorias visuais e de usabilidade em contas, lançamentos, orçamentos, cartões e anotações: novos tipos de conta, ícones no seletor, feedback visual de limite excedido nas progress bars e refinamentos nos ícones de tarefas em anotações.
### Adicionado
- Novos tipos de conta `"Dinheiro"` e `"Outros"` na lista padrão do diálogo de contas (issue #50).
- Ícones por tipo de conta no seletor (Conta Corrente, Poupança, Carteira Digital, Investimento, Pré-Pago, Dinheiro, Outros).
- Filtro automático: ao selecionar `"Dinheiro"` como forma de pagamento em lançamentos, o select de conta exibe apenas contas do tipo `"Dinheiro"`.
- Sinal `+` no valor de transferências recebidas na tabela de lançamentos (mantém cor azul).
### Alterado
- Forma de pagamento de novas transferências entre contas alterada de `"Pix"` para `"Transferência bancária"`.
- Progress bar de orçamentos excedidos agora exibe indicador e fundo na cor `destructive`.
- Progress bar de cartões com 100% do limite utilizado agora exibe indicador e fundo na cor `destructive`.
- Ícone de tarefa não concluída no card e no modal de detalhes de anotações substituído por `RiSubtractLine` (locais sem interação de marcação).
## [2.5.1] - 2026-05-04
Versão de correção pontual focada na exibição do indicador de anexo nas tabelas de lançamentos da fatura do cartão. Em `/cards/[cardId]/invoice`, lançamentos com anexos não mostravam o ícone porque o fetcher dedicado da fatura não calculava o flag `hasAttachments`. A primeira tentativa de adicionar o EXISTS via `extras` na query relacional gerou SQL inválido (Drizzle re-aliasava `transactionAttachments.transactionId` para o alias da tabela externa). A correção definitiva troca o fetcher pela função compartilhada `fetchTransactionsWithRelations` de `features/transactions`, que já implementa o EXISTS corretamente via `select`.
### Corrigido
- Ícone de anexo voltou a aparecer na tabela de lançamentos da fatura do cartão (`/cards/[cardId]/invoice`). `fetchCardTransactions` em `features/invoices/queries.ts` agora delega para `fetchTransactionsWithRelations`, garantindo que o flag `hasAttachments` seja preenchido com a mesma EXISTS subquery usada no restante do app.
## [2.5.0] - 2026-05-01
Esta versão melhora o fechamento de faturas, a correção de lançamentos já registrados e a conferência de saldos contra o extrato do banco. O novo **ajuste de fatura** fecha a conta entre o total calculado pelo sistema e o valor real cobrado pelo banco, sem exigir que o usuário reabra lançamentos individuais. A mesma ideia foi estendida para **contas correntes**: na página do extrato, ao lado de "Saldo ao final do período", o usuário informa o saldo real e o sistema cria (ou atualiza) um lançamento de ajuste no período visualizado. Também entra o fluxo de **reembolso** para despesas à vista: pelo menu de ações do lançamento, o usuário informa a data do reembolso e o sistema cria uma receita espelhada no extrato ou na fatura correta. O widget de boletos do dashboard ganhou paridade com o widget de faturas — confirmação de pagamento agora pede conta de origem e data antes de quitar o boleto. Por fim, o **limite do cartão** passou a ser obrigatório e o sistema bloqueia despesas em cartão que ultrapassem o limite disponível, retornando uma mensagem com o valor exato disponível. As operações mantêm rastro no lançamento gerado e respeitam a proteção de faturas já pagas.

View File

@@ -8,7 +8,7 @@
> **⚠️ Não há versão online hospedada.** Você precisa clonar o repositório e rodar localmente ou no seu próprio servidor.
[![Version](https://img.shields.io/badge/version-2.5.0-blue?style=flat-square)](CHANGELOG.md)
[![Version](https://img.shields.io/badge/version-2.5.2-blue?style=flat-square)](CHANGELOG.md)
[![Next.js](https://img.shields.io/badge/Next.js-black?style=flat-square&logo=next.js)](https://nextjs.org/)
[![TypeScript](https://img.shields.io/badge/TypeScript-blue?style=flat-square&logo=typescript)](https://www.typescriptlang.org/)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-blue?style=flat-square&logo=postgresql)](https://www.postgresql.org/)

View File

@@ -1,6 +1,6 @@
{
"name": "openmonetis",
"version": "2.5.0",
"version": "2.5.2",
"private": true,
"packageManager": "pnpm@10.33.0",
"scripts": {

BIN
public/logos/dinheiro.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

@@ -37,7 +37,9 @@ const DEFAULT_ACCOUNT_TYPES = [
"Conta Poupança",
"Carteira Digital",
"Conta Investimento",
"Dinheiro",
"Pré-Pago | VR/VA",
"Outros",
] as const;
const DEFAULT_ACCOUNT_STATUS = ["Ativa", "Inativa"] as const;

View File

@@ -12,7 +12,10 @@ import {
SelectValue,
} from "@/shared/components/ui/select";
import { Textarea } from "@/shared/components/ui/textarea";
import { StatusSelectContent } from "./account-select-items";
import {
AccountTypeSelectContent,
StatusSelectContent,
} from "./account-select-items";
import type { AccountFormValues } from "./types";
@@ -54,12 +57,16 @@ export function AccountFormFields({
onValueChange={(value) => onChange("accountType", value)}
>
<SelectTrigger id="account-type" className="w-full">
<SelectValue placeholder="Selecione o tipo" />
<SelectValue placeholder="Selecione o tipo">
{values.accountType && (
<AccountTypeSelectContent label={values.accountType} />
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
{accountTypes.map((type) => (
<SelectItem key={type} value={type}>
{type}
<AccountTypeSelectContent label={type} />
</SelectItem>
))}
</SelectContent>

View File

@@ -1,6 +1,18 @@
"use client";
import StatusDot from "@/shared/components/status-dot";
import { getAccountTypeIcon } from "@/shared/utils/icons";
export function AccountTypeSelectContent({ label }: { label: string }) {
const icon = getAccountTypeIcon(label);
return (
<span className="flex items-center gap-2">
{icon}
<span>{label}</span>
</span>
);
}
export function StatusSelectContent({ label }: { label: string }) {
const isActive = label === "Ativa";

View File

@@ -90,6 +90,7 @@ export function BudgetCard({ budget, onEdit, onRemove }: BudgetCardProps) {
<Progress
value={usagePercent}
className={cn("h-2.5", exceeded && "bg-destructive/20!")}
indicatorClassName={cn(exceeded && "bg-destructive")}
aria-label={`${usagePercent.toFixed(1)}% do orçamento utilizado`}
/>
<span className="text-xs text-muted-foreground">

View File

@@ -69,6 +69,7 @@ export function CardItem({
const usagePercent =
limit > 0 ? Math.min(Math.max((used / limit) * 100, 0), 100) : 0;
const exceeded = usagePercent >= 100;
const logoPath = resolveLogoSrc(logo);
const brandAsset = resolveCardBrandAsset(brand);
@@ -194,7 +195,8 @@ export function CardItem({
<div className="flex flex-col gap-2">
<Progress
value={usagePercent}
className="h-2.5"
className={cn("h-2.5", exceeded && "bg-destructive/20!")}
indicatorClassName={cn(exceeded && "bg-destructive")}
aria-label={`${usagePercent.toFixed(0)}% do limite utilizado`}
/>
<span className="text-xs text-muted-foreground">

View File

@@ -13,7 +13,7 @@ const formatOccurrences = (value: number | null) => {
return "Recorrência contínua";
}
return `${value} recorrências`;
return `${value} recorrências mensais`;
};
export function RecurringExpensesWidget({

View File

@@ -1,5 +1,6 @@
import { and, desc, eq, type SQL, sum } from "drizzle-orm";
import { and, eq, type SQL, sum } from "drizzle-orm";
import { cards, invoices, transactions } from "@/db/schema";
import { fetchTransactionsWithRelations } from "@/features/transactions/queries";
import { buildInvoicePaymentNote } from "@/shared/lib/accounts/constants";
import { db } from "@/shared/lib/db";
import {
@@ -104,14 +105,5 @@ export async function fetchInvoiceData(
}
export async function fetchCardTransactions(filters: SQL[]) {
return db.query.transactions.findMany({
where: and(...filters),
with: {
payer: true,
financialAccount: true,
card: true,
category: true,
},
orderBy: desc(transactions.purchaseDate),
});
return fetchTransactionsWithRelations({ filters });
}

View File

@@ -7,6 +7,7 @@ import {
RiFileList2Line,
RiInboxUnarchiveLine,
RiPencilLine,
RiSubtractLine,
} from "@remixicon/react";
import {
buildNoteDisplayTitle,
@@ -101,7 +102,7 @@ export function NoteCard({
{task.completed ? (
<RiCheckLine className="h-4 w-4 text-success" />
) : (
<div className="h-4 w-4 rounded-sm border border-input" />
<RiSubtractLine className="h-4 w-4 text-muted-foreground" />
)}
</div>
<span

View File

@@ -1,6 +1,6 @@
"use client";
import { RiCheckLine } from "@remixicon/react";
import { RiCheckLine, RiSubtractLine } from "@remixicon/react";
import {
buildNoteDisplayTitle,
formatNoteCreatedAtLong,
@@ -69,7 +69,7 @@ export function NoteDetailsDialog({
{task.completed ? (
<RiCheckLine className="h-4 w-4 text-success" />
) : (
<div className="h-4 w-4 rounded-sm border border-input" />
<RiSubtractLine className="h-4 w-4 text-muted-foreground" />
)}
</div>
<span

View File

@@ -93,7 +93,9 @@ export function PaymentMethodSection({
? accountOptions.filter(
(option) => option.accountType === "Pré-Pago | VR/VA",
)
: accountOptions;
: formState.paymentMethod === "Dinheiro"
? accountOptions.filter((option) => option.accountType === "Dinheiro")
: accountOptions;
const hasSecondaryColumn = isCartaoSelected || showContaSelect;

View File

@@ -348,10 +348,12 @@ function buildColumns({
cell: ({ row }) => {
const isReceita = row.original.transactionType === "Receita";
const isTransfer = row.original.transactionType === "Transferência";
const isIncomingTransfer =
isTransfer && Number(row.original.amount) > 0;
return (
<MoneyValues
amount={row.original.amount}
showPositiveSign={isReceita}
showPositiveSign={isReceita || isIncomingTransfer}
className={cn(
"whitespace-nowrap",
isReceita ? "text-success" : "text-foreground",

View File

@@ -7,9 +7,12 @@ import { cn } from "@/shared/utils/ui";
function Progress({
className,
indicatorClassName,
value,
...props
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
}: React.ComponentProps<typeof ProgressPrimitive.Root> & {
indicatorClassName?: string;
}) {
return (
<ProgressPrimitive.Root
data-slot="progress"
@@ -21,7 +24,10 @@ function Progress({
>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="bg-primary h-full w-full flex-1 transition-all"
className={cn(
"bg-primary h-full w-full flex-1 transition-all",
indicatorClassName,
)}
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>

View File

@@ -1,5 +1,5 @@
export const TRANSFER_CATEGORY_NAME = "Transferência interna";
export const TRANSFER_ESTABLISHMENT_SAIDA = "Saída - Transf. entre contas";
export const TRANSFER_ESTABLISHMENT_ENTRADA = "Entrada - Transf. entre contas";
export const TRANSFER_PAYMENT_METHOD = "Pix";
export const TRANSFER_PAYMENT_METHOD = "Transferência bancária";
export const TRANSFER_CONDITION = "À vista";

View File

@@ -36,13 +36,37 @@ export const getConditionIcon = (condition: string): ReactNode => {
return registry[key] ?? null;
};
export const getAccountTypeIcon = (accountType: string): ReactNode => {
const key = normalizeKey(accountType);
const registry: Record<string, ReactNode> = {
contacorrente: <RemixIcons.RiBankLine className={ICON_CLASS} aria-hidden />,
contapoupanca: (
<RemixIcons.RiSafe2Line className={ICON_CLASS} aria-hidden />
),
carteiradigital: (
<RemixIcons.RiWalletLine className={ICON_CLASS} aria-hidden />
),
containvestimento: (
<RemixIcons.RiFundsLine className={ICON_CLASS} aria-hidden />
),
prepagovrva: <RemixIcons.RiCouponLine className={ICON_CLASS} aria-hidden />,
dinheiro: <RemixIcons.RiCashLine className={ICON_CLASS} aria-hidden />,
outros: <RemixIcons.RiMoreFill className={ICON_CLASS} aria-hidden />,
};
return (
registry[key] ?? (
<RemixIcons.RiBankLine className={ICON_CLASS} aria-hidden />
)
);
};
export const getPaymentMethodIcon = (paymentMethod: string): ReactNode => {
const key = normalizeKey(paymentMethod);
const registry: Record<string, ReactNode> = {
dinheiro: (
<RemixIcons.RiMoneyDollarCircleLine className={ICON_CLASS} aria-hidden />
),
dinheiro: <RemixIcons.RiCashLine className={ICON_CLASS} aria-hidden />,
pix: <RemixIcons.RiPixLine className={ICON_CLASS} aria-hidden />,
boleto: <RemixIcons.RiBarcodeLine className={ICON_CLASS} aria-hidden />,
credito: (