Padroniza copias e badges da interface

This commit is contained in:
Felipe Coutinho
2026-03-14 18:36:02 +00:00
parent 1e8e6e0d3d
commit 62b94e6b1d
45 changed files with 184 additions and 200 deletions

View File

@@ -11,10 +11,10 @@ import {
import { fetchUserPreferences } from "@/features/settings/queries"; import { fetchUserPreferences } from "@/features/settings/queries";
import { TransactionsPage as LancamentosSection } from "@/features/transactions/components/page/transactions-page"; import { TransactionsPage as LancamentosSection } from "@/features/transactions/components/page/transactions-page";
import { import {
buildTransactionWhere,
buildOptionSets, buildOptionSets,
buildSluggedFilters, buildSluggedFilters,
buildSlugMaps, buildSlugMaps,
buildTransactionWhere,
extractTransactionSearchFilters, extractTransactionSearchFilters,
getSingleParam, getSingleParam,
mapTransactionsData, mapTransactionsData,

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { RiAddCircleLine, RiBankLine } from "@remixicon/react"; import { RiAddCircleFill, RiBankLine } from "@remixicon/react";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
@@ -176,7 +176,7 @@ export function AccountsPage({
logoOptions={logoOptions} logoOptions={logoOptions}
trigger={ trigger={
<Button className="w-full sm:w-auto"> <Button className="w-full sm:w-auto">
<RiAddCircleLine className="size-4" /> <RiAddCircleFill className="size-4" />
Nova conta Nova conta
</Button> </Button>
} }

View File

@@ -207,7 +207,7 @@ export function BudgetDialog({
) : ( ) : (
<form className="space-y-4" onSubmit={handleSubmit}> <form className="space-y-4" onSubmit={handleSubmit}>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="budget-category">Category</Label> <Label htmlFor="budget-category">Categoria</Label>
<Select <Select
value={formState.categoryId} value={formState.categoryId}
onValueChange={(value) => updateField("categoryId", value)} onValueChange={(value) => updateField("categoryId", value)}

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { RiAddCircleLine, RiFileCopyLine, RiFundsLine } from "@remixicon/react"; import { RiAddCircleFill, RiFileCopyLine, RiFundsLine } from "@remixicon/react";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { import {
@@ -115,7 +115,7 @@ export function BudgetsPage({
disabled={categories.length === 0} disabled={categories.length === 0}
className="w-full sm:w-auto" className="w-full sm:w-auto"
> >
<RiAddCircleLine className="size-4" /> <RiAddCircleFill className="size-4" />
Novo orçamento Novo orçamento
</Button> </Button>
} }

View File

@@ -1,11 +1,5 @@
import { and, asc, eq, inArray, isNull, or, sql, sum } from "drizzle-orm"; import { and, asc, eq, inArray, isNull, or, sql, sum } from "drizzle-orm";
import { import { budgets, categories, payers, transactions } from "@/db/schema";
type Budget,
budgets,
categories,
payers,
transactions,
} from "@/db/schema";
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/shared/lib/accounts/constants"; import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/shared/lib/accounts/constants";
import { db } from "@/shared/lib/db"; import { db } from "@/shared/lib/db";
import { PAYER_ROLE_ADMIN } from "@/shared/lib/payers/constants"; import { PAYER_ROLE_ADMIN } from "@/shared/lib/payers/constants";

View File

@@ -162,7 +162,7 @@ export function DayCell({ day, onSelect, onCreate }: DayCellProps) {
<button <button
type="button" type="button"
onClick={handleCreateClick} onClick={handleCreateClick}
className="flex size-6 items-center justify-center rounded-full border bg-muted text-muted-foreground transition-colors hover:bg-primary/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1" className="flex size-6 items-center justify-center rounded-full bg-muted text-muted-foreground transition-colors hover:bg-primary/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1"
aria-label={`Criar lançamento em ${day.date}`} aria-label={`Criar lançamento em ${day.date}`}
> >
<RiAddLine className="size-3.5" /> <RiAddLine className="size-3.5" />

View File

@@ -160,7 +160,7 @@ export function CardFormFields({
</div> </div>
<div className="flex flex-col gap-2 sm:col-span-2"> <div className="flex flex-col gap-2 sm:col-span-2">
<Label htmlFor="card-account">FinancialAccount vinculada</Label> <Label htmlFor="card-account">Conta vinculada</Label>
<Select <Select
value={values.accountId} value={values.accountId}
onValueChange={(value) => onChange("accountId", value)} onValueChange={(value) => onChange("accountId", value)}

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { RiAddCircleLine, RiBankCard2Line } from "@remixicon/react"; import { RiAddCircleFill, RiBankCard2Line } from "@remixicon/react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -164,7 +164,7 @@ export function CardsPage({
logoOptions={logoOptions} logoOptions={logoOptions}
trigger={ trigger={
<Button className="w-full sm:w-auto"> <Button className="w-full sm:w-auto">
<RiAddCircleLine className="size-4" /> <RiAddCircleFill className="size-4" />
Novo cartão Novo cartão
</Button> </Button>
} }

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { import {
RiAddCircleLine, RiAddCircleFill,
RiDeleteBin5Line, RiDeleteBin5Line,
RiExternalLinkLine, RiExternalLinkLine,
RiPencilLine, RiPencilLine,
@@ -127,7 +127,7 @@ export function CategoriesPage({ categories }: CategoriesPageProps) {
defaultType={activeType} defaultType={activeType}
trigger={ trigger={
<Button className="w-full sm:w-auto"> <Button className="w-full sm:w-auto">
<RiAddCircleLine className="size-4" /> <RiAddCircleFill className="size-4" />
Nova categoria Nova categoria
</Button> </Button>
} }

View File

@@ -1,5 +1,5 @@
import { RiArrowDownSFill, RiArrowUpSFill } from "@remixicon/react"; import { RiArrowDownSFill, RiArrowUpSFill } from "@remixicon/react";
import { TypeBadge } from "@/shared/components/type-badge"; import { TransactionTypeBadge } from "@/shared/components/transaction-type-badge";
import { Card } from "@/shared/components/ui/card"; import { Card } from "@/shared/components/ui/card";
import type { CategoryType } from "@/shared/lib/categories/constants"; import type { CategoryType } from "@/shared/lib/categories/constants";
import { currencyFormatter } from "@/shared/utils/currency"; import { currencyFormatter } from "@/shared/utils/currency";
@@ -85,7 +85,7 @@ export function CategoryDetailHeader({
{category.name} {category.name}
</h1> </h1>
<div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground"> <div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
<TypeBadge type={category.type} /> <TransactionTypeBadge kind={category.type} />
<span> <span>
{transactionCount}{" "} {transactionCount}{" "}
{transactionCount === 1 ? "lançamento" : "lançamentos"} no{" "} {transactionCount === 1 ? "lançamento" : "lançamentos"} no{" "}

View File

@@ -16,7 +16,7 @@ import {
sortableKeyboardCoordinates, sortableKeyboardCoordinates,
} from "@dnd-kit/sortable"; } from "@dnd-kit/sortable";
import { import {
RiAddCircleLine, RiAddCircleFill,
RiCheckLine, RiCheckLine,
RiCloseLine, RiCloseLine,
RiDragMove2Line, RiDragMove2Line,
@@ -198,10 +198,7 @@ export function DashboardGridEditable({
{/* Toolbar */} {/* Toolbar */}
<div className="flex flex-wrap items-center justify-between gap-2"> <div className="flex flex-wrap items-center justify-between gap-2">
{!isEditing ? ( {!isEditing ? (
<div className="flex w-full min-w-0 flex-col gap-1 px-1 sm:w-auto sm:flex-row sm:items-center sm:gap-2"> <div className="flex w-full min-w-0 flex-col gap-1 sm:w-auto sm:flex-row sm:items-center sm:gap-2">
<span className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
Ações rápidas
</span>
<div className="-mb-1 grid w-full grid-cols-3 gap-1 pb-1 sm:mb-0 sm:flex sm:w-auto sm:items-center sm:gap-2 sm:overflow-visible sm:pb-0"> <div className="-mb-1 grid w-full grid-cols-3 gap-1 pb-1 sm:mb-0 sm:flex sm:w-auto sm:items-center sm:gap-2 sm:overflow-visible sm:pb-0">
<TransactionDialog <TransactionDialog
mode="create" mode="create"
@@ -221,7 +218,7 @@ export function DashboardGridEditable({
className="h-12 w-full min-w-0 flex-col justify-center gap-0.5 px-1.5 text-sm whitespace-normal sm:h-8 sm:w-auto sm:flex-row sm:gap-2 sm:px-3 sm:whitespace-nowrap" className="h-12 w-full min-w-0 flex-col justify-center gap-0.5 px-1.5 text-sm whitespace-normal sm:h-8 sm:w-auto sm:flex-row sm:gap-2 sm:px-3 sm:whitespace-nowrap"
> >
<span className="flex items-center gap-0.5"> <span className="flex items-center gap-0.5">
<RiAddCircleLine className="size-3.5 shrink-0 text-success/80" /> <RiAddCircleFill className="size-3.5 shrink-0 text-success/80" />
</span> </span>
<span className="sm:hidden">Receita</span> <span className="sm:hidden">Receita</span>
<span className="hidden sm:inline">Nova receita</span> <span className="hidden sm:inline">Nova receita</span>
@@ -246,7 +243,7 @@ export function DashboardGridEditable({
className="h-12 w-full min-w-0 flex-col justify-center gap-0.5 px-1.5 text-sm whitespace-normal sm:h-8 sm:w-auto sm:flex-row sm:gap-2 sm:px-3 sm:whitespace-nowrap" className="h-12 w-full min-w-0 flex-col justify-center gap-0.5 px-1.5 text-sm whitespace-normal sm:h-8 sm:w-auto sm:flex-row sm:gap-2 sm:px-3 sm:whitespace-nowrap"
> >
<span className="flex items-center gap-0.5"> <span className="flex items-center gap-0.5">
<RiAddCircleLine className="size-3.5 shrink-0 text-destructive/80" /> <RiAddCircleFill className="size-3.5 shrink-0 text-destructive/80" />
</span> </span>
<span className="sm:hidden">Despesa</span> <span className="sm:hidden">Despesa</span>
<span className="hidden sm:inline">Nova despesa</span> <span className="hidden sm:inline">Nova despesa</span>

View File

@@ -113,10 +113,7 @@ export async function fetchDashboardPayers(
})) }))
.sort((a, b) => b.totalExpenses - a.totalExpenses); .sort((a, b) => b.totalExpenses - a.totalExpenses);
const totalExpenses = payerList.reduce( const totalExpenses = payerList.reduce((sum, p) => sum + p.totalExpenses, 0);
(sum, p) => sum + p.totalExpenses,
0,
);
return { return {
payers: payerList, payers: payerList,

View File

@@ -134,7 +134,7 @@ 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 className="border-none"> <Alert className="border-none bg-primary/15">
<RiAlertLine className="size-4" color="red" /> <RiAlertLine className="size-4" color="red" />
<AlertDescription className="text-sm text-card-foreground"> <AlertDescription className="text-sm text-card-foreground">
<strong>Aviso de privacidade:</strong> Ao gerar insights, seus dados <strong>Aviso de privacidade:</strong> Ao gerar insights, seus dados

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { RiAddLine, RiDeleteBinLine } from "@remixicon/react"; import { RiAddCircleFill, RiDeleteBinLine } from "@remixicon/react";
import { import {
type ReactNode, type ReactNode,
useEffect, useEffect,
@@ -329,7 +329,7 @@ export function NoteDialog({
disabled={isPending || !normalize(newTaskText)} disabled={isPending || !normalize(newTaskText)}
className="shrink-0" className="shrink-0"
> >
<RiAddLine className="h-4 w-4" /> <RiAddCircleFill className="h-4 w-4" />
</Button> </Button>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { RiAddCircleLine, RiTodoLine } from "@remixicon/react"; import { RiAddCircleFill, RiTodoLine } from "@remixicon/react";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { archiveNoteAction, deleteNoteAction } from "@/features/notes/actions"; import { archiveNoteAction, deleteNoteAction } from "@/features/notes/actions";
@@ -208,7 +208,7 @@ export function NotesPage({ notes, archivedNotes }: NotesPageProps) {
onOpenChange={handleCreateOpenChange} onOpenChange={handleCreateOpenChange}
trigger={ trigger={
<Button className="w-full sm:w-auto"> <Button className="w-full sm:w-auto">
<RiAddCircleLine className="size-4" /> <RiAddCircleFill className="size-4" />
Nova anotação Nova anotação
</Button> </Button>
} }

View File

@@ -201,7 +201,7 @@ export function PayerHeaderCard({
{summary.periodLabel} {summary.periodLabel}
</span>{" "} </span>{" "}
para{" "} para{" "}
<span className="font-medium text-foreground"> <span className="font-semibold text-foreground">
{payer.email} {payer.email}
</span> </span>
</DialogDescription> </DialogDescription>

View File

@@ -32,7 +32,7 @@ export function PagadorInfoCard({ payer }: PayerInfoCardProps) {
<Card className="border gap-4"> <Card className="border gap-4">
<CardHeader className="gap-1.5"> <CardHeader className="gap-1.5">
<CardTitle className="text-lg font-semibold"> <CardTitle className="text-lg font-semibold">
Detalhes do payer Detalhes do pagador
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
{showSensitiveDetails {showSensitiveDetails

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { RiAddCircleLine } from "@remixicon/react"; import { RiAddCircleFill } from "@remixicon/react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useMemo, useState, useTransition } from "react"; import { useMemo, useState, useTransition } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -126,7 +126,7 @@ export function PayersPage({ payers, avatarOptions }: PayersPageProps) {
avatarOptions={avatarOptions} avatarOptions={avatarOptions}
trigger={ trigger={
<Button className="w-full sm:w-auto"> <Button className="w-full sm:w-auto">
<RiAddCircleLine className="size-4" /> <RiAddCircleFill className="size-4" />
Novo pagador Novo pagador
</Button> </Button>
} }

View File

@@ -1,8 +1,8 @@
import type { payers } from "@/db/schema"; import type { payers } from "@/db/schema";
import type { import type {
AccountCardFilterOption, AccountCardFilterOption,
TransactionFilterOption,
SelectOption, SelectOption,
TransactionFilterOption,
TransactionItem, TransactionItem,
} from "@/features/transactions/components/types"; } from "@/features/transactions/components/types";
import type { buildOptionSets } from "@/features/transactions/page-helpers"; import type { buildOptionSets } from "@/features/transactions/page-helpers";
@@ -35,10 +35,7 @@ export function buildReadOnlyOptionSets(
if (item.accountId && !contaOptionsMap.has(item.accountId)) { if (item.accountId && !contaOptionsMap.has(item.accountId)) {
contaOptionsMap.set(item.accountId, { contaOptionsMap.set(item.accountId, {
value: item.accountId, value: item.accountId,
label: normalizeOptionLabel( label: normalizeOptionLabel(item.contaName, "Conta sem nome"),
item.contaName,
"FinancialAccount sem nome",
),
slug: item.accountId, slug: item.accountId,
}); });
} }

View File

@@ -24,7 +24,7 @@ export function CardCategoryBreakdown({ data }: CardCategoryBreakdownProps) {
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardTitle className="flex items-center gap-1.5 text-base"> <CardTitle className="flex items-center gap-1.5 text-base">
<RiPieChartLine className="size-4 text-primary" /> <RiPieChartLine className="size-4 text-primary" />
Gastos por Category Gastos por Categoria
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@@ -45,7 +45,7 @@ export function CardCategoryBreakdown({ data }: CardCategoryBreakdownProps) {
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardTitle className="flex items-center gap-1.5 text-base"> <CardTitle className="flex items-center gap-1.5 text-base">
<RiPieChartLine className="size-4 text-primary" /> <RiPieChartLine className="size-4 text-primary" />
Gastos por Category Gastos por Categoria
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>

View File

@@ -150,7 +150,7 @@ export function CategoryReportChart({ data }: CategoryReportChartProps) {
<Card className="pt-0"> <Card className="pt-0">
<CardHeader className="flex items-center gap-2 space-y-0 border-b py-5 sm:flex-row"> <CardHeader className="flex items-center gap-2 space-y-0 border-b py-5 sm:flex-row">
<div className="grid flex-1 gap-1"> <div className="grid flex-1 gap-1">
<CardTitle>Evolução por Category</CardTitle> <CardTitle>Evolução por Categoria</CardTitle>
<CardDescription>{periodLabel}</CardDescription> <CardDescription>{periodLabel}</CardDescription>
</div> </div>
<Select value={limit} onValueChange={setLimit}> <Select value={limit} onValueChange={setLimit}>

View File

@@ -54,7 +54,7 @@ export function CategoryReportExport({
// Build CSV content // Build CSV content
const headers = [ const headers = [
"Category", "Categoria",
...data.periods.map(formatPeriodLabel), ...data.periods.map(formatPeriodLabel),
"Total", "Total",
]; ];
@@ -129,7 +129,7 @@ export function CategoryReportExport({
// Build data array // Build data array
const headers = [ const headers = [
"Category", "Categoria",
...data.periods.map(formatPeriodLabel), ...data.periods.map(formatPeriodLabel),
"Total", "Total",
]; ];
@@ -249,7 +249,7 @@ export function CategoryReportExport({
// Build table data // Build table data
const headers = [ const headers = [
["Category", ...data.periods.map(formatPeriodLabel), "Total"], ["Categoria", ...data.periods.map(formatPeriodLabel), "Total"],
]; ];
const body: string[][] = []; const body: string[][] = [];

View File

@@ -129,7 +129,7 @@ export function CategoryReportFilters({
const selectedText = const selectedText =
selectedCategories.length === 0 selectedCategories.length === 0
? "Category" ? "Categoria"
: selectedCategories.length === categories.length : selectedCategories.length === categories.length
? "Todas" ? "Todas"
: selectedCategories.length === 1 : selectedCategories.length === 1

View File

@@ -65,7 +65,7 @@ export function CategoryTable({
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead className="w-[240px] min-w-[240px] font-bold"> <TableHead className="w-[240px] min-w-[240px] font-bold">
Category Categoria
</TableHead> </TableHead>
{periods.map((period) => ( {periods.map((period) => (
<TableHead <TableHead

View File

@@ -92,7 +92,7 @@ export function EstablishmentsList({
.map((cat, catIndex) => ( .map((cat, catIndex) => (
<Badge <Badge
key={catIndex} key={catIndex}
variant="secondary" variant="outline"
className="text-xs px-1.5 py-0 h-5" className="text-xs px-1.5 py-0 h-5"
> >
{cat.name} {cat.name}

View File

@@ -11,17 +11,15 @@ type HighlightsCardsProps = {
export function HighlightsCards({ summary }: HighlightsCardsProps) { export function HighlightsCards({ summary }: HighlightsCardsProps) {
return ( return (
<div className="grid gap-3 sm:grid-cols-2"> <div className="grid gap-3 sm:grid-cols-2">
<Card className="bg-linear-to-br from-violet-50 to-violet-50/50 dark:from-violet-950/20 dark:to-violet-950/10"> <Card className="">
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex items-center justify-center size-10 rounded-xl bg-violet-100 dark:bg-violet-900/40"> <div className="flex items-center justify-center size-10 rounded-md bg-primary">
<RiTrophyLine className="size-5 text-violet-600 dark:text-violet-400" /> <RiTrophyLine className="size-5" />
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="text-xs text-violet-700/80 dark:text-violet-400/80 font-medium"> <p className="text-xs font-bold">Mais Frequente</p>
Mais Frequente <p className="font-bold text-2xl truncate">
</p>
<p className="font-bold text-xl text-violet-900 dark:text-violet-100 truncate">
{summary.mostFrequent || "—"} {summary.mostFrequent || "—"}
</p> </p>
</div> </div>
@@ -29,17 +27,15 @@ export function HighlightsCards({ summary }: HighlightsCardsProps) {
</CardContent> </CardContent>
</Card> </Card>
<Card className="bg-linear-to-br from-red-50 to-rose-50/50 dark:from-red-950/20 dark:to-rose-950/10"> <Card className="">
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex items-center justify-center size-10 rounded-xl bg-red-100 dark:bg-red-900/40"> <div className="flex items-center justify-center size-10 rounded-md bg-primary">
<RiFireLine className="size-5 text-red-600 dark:text-red-400" /> <RiFireLine className="size-5" />
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="text-xs text-red-700/80 dark:text-red-400/80 font-medium"> <p className="text-xs font-bold">Maior Gasto Total</p>
Maior Gasto Total <p className="font-bold text-2xl truncate">
</p>
<p className="font-bold text-xl text-red-900 dark:text-red-100 truncate">
{summary.highestSpending || "—"} {summary.highestSpending || "—"}
</p> </p>
</div> </div>

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { import {
RiAddLine, RiAddCircleFill,
RiAlertLine, RiAlertLine,
RiCheckLine, RiCheckLine,
RiDeleteBinLine, RiDeleteBinLine,
@@ -154,7 +154,7 @@ export function ApiTokensForm({ tokens }: ApiTokensFormProps) {
> >
<DialogTrigger asChild> <DialogTrigger asChild>
<Button size="sm"> <Button size="sm">
<RiAddLine className="h-4 w-4 mr-1" /> <RiAddCircleFill className="h-4 w-4 mr-1" />
Novo Token Novo Token
</Button> </Button>
</DialogTrigger> </DialogTrigger>

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { import {
RiAddLine, RiAddCircleFill,
RiAlertLine, RiAlertLine,
RiDeleteBinLine, RiDeleteBinLine,
RiFingerprintLine, RiFingerprintLine,
@@ -210,7 +210,7 @@ export function PasskeysForm() {
> >
<DialogTrigger asChild> <DialogTrigger asChild>
<Button size="sm" disabled={isMutating || !passkeySupported}> <Button size="sm" disabled={isMutating || !passkeySupported}>
<RiAddLine className="h-4 w-4 mr-1" /> <RiAddCircleFill className="h-4 w-4 mr-1" />
Adicionar Adicionar
</Button> </Button>
</DialogTrigger> </DialogTrigger>

View File

@@ -269,7 +269,7 @@ export function AnticipateInstallmentsDialog({
</Field> </Field>
<Field className="gap-1"> <Field className="gap-1">
<FieldLabel htmlFor="anticipation-pagador">Payer</FieldLabel> <FieldLabel htmlFor="anticipation-pagador">Pagador</FieldLabel>
<FieldContent> <FieldContent>
<Select <Select
value={formState.payerId} value={formState.payerId}

View File

@@ -24,8 +24,8 @@ import {
SelectValue, SelectValue,
} from "@/shared/components/ui/select"; } from "@/shared/components/ui/select";
import { import {
CategorySelectContent,
AccountCardSelectContent, AccountCardSelectContent,
CategorySelectContent,
PayerSelectContent, PayerSelectContent,
} from "../select-items"; } from "../select-items";
import type { SelectOption, TransactionItem } from "../types"; import type { SelectOption, TransactionItem } from "../types";
@@ -203,7 +203,7 @@ export function BulkImportDialog({
<form className="space-y-4" onSubmit={handleSubmit}> <form className="space-y-4" onSubmit={handleSubmit}>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="pagador">Payer *</Label> <Label htmlFor="pagador">Pagador *</Label>
<Select value={payerId} onValueChange={setPagadorId}> <Select value={payerId} onValueChange={setPagadorId}>
<SelectTrigger id="pagador" className="w-full"> <SelectTrigger id="pagador" className="w-full">
<SelectValue placeholder="Selecione o pagador"> <SelectValue placeholder="Selecione o pagador">
@@ -235,7 +235,7 @@ export function BulkImportDialog({
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="categoria">Category *</Label> <Label htmlFor="categoria">Categoria *</Label>
<Select value={categoryId} onValueChange={setCategoriaId}> <Select value={categoryId} onValueChange={setCategoriaId}>
<SelectTrigger id="categoria" className="w-full"> <SelectTrigger id="categoria" className="w-full">
<SelectValue placeholder="Selecione a categoria"> <SelectValue placeholder="Selecione a categoria">
@@ -274,7 +274,7 @@ export function BulkImportDialog({
{hasNonCredit && ( {hasNonCredit && (
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="conta"> <Label htmlFor="conta">
FinancialAccount {hasCredit ? "(para não cartão)" : "*"} Conta {hasCredit ? "(para não cartão)" : "*"}
</Label> </Label>
<Select value={accountId} onValueChange={setContaId}> <Select value={accountId} onValueChange={setContaId}>
<SelectTrigger id="conta" className="w-full"> <SelectTrigger id="conta" className="w-full">

View File

@@ -44,8 +44,8 @@ import {
periodToDate, periodToDate,
} from "@/shared/utils/period"; } from "@/shared/utils/period";
import { import {
CategorySelectContent,
AccountCardSelectContent, AccountCardSelectContent,
CategorySelectContent,
PayerSelectContent, PayerSelectContent,
PaymentMethodSelectContent, PaymentMethodSelectContent,
TransactionTypeSelectContent, TransactionTypeSelectContent,
@@ -413,7 +413,7 @@ export function MassAddDialog({
{/* FinancialAccount (for non-credit-card methods) */} {/* FinancialAccount (for non-credit-card methods) */}
{!isCartaoSelected ? ( {!isCartaoSelected ? (
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="conta">FinancialAccount</Label> <Label htmlFor="conta">Conta</Label>
<Select value={accountId} onValueChange={setContaId}> <Select value={accountId} onValueChange={setContaId}>
<SelectTrigger id="conta" className="w-full"> <SelectTrigger id="conta" className="w-full">
<SelectValue placeholder="Selecione"> <SelectValue placeholder="Selecione">
@@ -534,7 +534,7 @@ export function MassAddDialog({
htmlFor={`pagador-${transaction.id}`} htmlFor={`pagador-${transaction.id}`}
className="sr-only" className="sr-only"
> >
Payer {index + 1} Pagador {index + 1}
</Label> </Label>
<Select <Select
value={transaction.payerId} value={transaction.payerId}
@@ -546,7 +546,7 @@ export function MassAddDialog({
id={`pagador-${transaction.id}`} id={`pagador-${transaction.id}`}
className="w-32 truncate" className="w-32 truncate"
> >
<SelectValue placeholder="Payer"> <SelectValue placeholder="Pagador">
{transaction.payerId && {transaction.payerId &&
(() => { (() => {
const selectedOption = payerOptions.find( const selectedOption = payerOptions.find(
@@ -579,7 +579,7 @@ export function MassAddDialog({
htmlFor={`categoria-${transaction.id}`} htmlFor={`categoria-${transaction.id}`}
className="sr-only" className="sr-only"
> >
Category {index + 1} Categoria {index + 1}
</Label> </Label>
<Select <Select
value={transaction.categoryId} value={transaction.categoryId}
@@ -591,7 +591,7 @@ export function MassAddDialog({
id={`categoria-${transaction.id}`} id={`categoria-${transaction.id}`}
className="w-32 truncate" className="w-32 truncate"
> >
<SelectValue placeholder="Category" /> <SelectValue placeholder="Categoria" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{groupedCategorias.map((group) => ( {groupedCategorias.map((group) => (

View File

@@ -5,9 +5,8 @@ import {
formatCondition, formatCondition,
formatDate, formatDate,
formatPeriod, formatPeriod,
getTransactionBadgeVariant,
} from "@/features/transactions/formatting-helpers"; } from "@/features/transactions/formatting-helpers";
import { Badge } from "@/shared/components/ui/badge"; import { TransactionTypeBadge } from "@/shared/components/transaction-type-badge";
import { Button } from "@/shared/components/ui/button"; import { Button } from "@/shared/components/ui/button";
import { import {
CardContent, CardContent,
@@ -93,12 +92,12 @@ export function TransactionDetailsDialog({
</li> </li>
<DetailRow <DetailRow
label={transaction.cartaoName ? "Cartão" : "FinancialAccount"} label={transaction.cartaoName ? "Cartão" : "Conta"}
value={transaction.cartaoName ?? transaction.contaName ?? "—"} value={transaction.cartaoName ?? transaction.contaName ?? "—"}
/> />
<DetailRow <DetailRow
label="Category" label="Categoria"
value={transaction.categoriaName ?? "—"} value={transaction.categoriaName ?? "—"}
/> />
@@ -106,19 +105,13 @@ export function TransactionDetailsDialog({
<span className="text-muted-foreground"> <span className="text-muted-foreground">
Tipo de Transação Tipo de Transação
</span> </span>
<span className="capitalize"> <TransactionTypeBadge
<Badge kind={
variant={getTransactionBadgeVariant( transaction.categoriaName === "Saldo inicial"
transaction.categoriaName === "Saldo inicial" ? "Saldo inicial"
? "Saldo inicial" : transaction.transactionType
: transaction.transactionType, }
)} />
>
{transaction.categoriaName === "Saldo inicial"
? "Saldo Inicial"
: transaction.transactionType}
</Badge>
</span>
</li> </li>
<DetailRow <DetailRow

View File

@@ -65,7 +65,7 @@ export function CategorySection({
showTransactionTypeField ? "md:w-1/2" : "md:w-full", showTransactionTypeField ? "md:w-1/2" : "md:w-full",
)} )}
> >
<Label htmlFor="categoria">Category</Label> <Label htmlFor="categoria">Categoria</Label>
<Select <Select
value={formState.categoryId} value={formState.categoryId}
onValueChange={(value) => onFieldChange("categoryId", value)} onValueChange={(value) => onFieldChange("categoryId", value)}

View File

@@ -36,7 +36,7 @@ export function PayerSection({
return ( return (
<div className="flex w-full flex-col gap-2 md:flex-row"> <div className="flex w-full flex-col gap-2 md:flex-row">
<div className="w-full space-y-1"> <div className="w-full space-y-1">
<Label htmlFor="payer">Payer</Label> <Label htmlFor="payer">Pagador</Label>
<div className="flex gap-2"> <div className="flex gap-2">
<Select <Select
value={formState.payerId} value={formState.payerId}

View File

@@ -194,7 +194,7 @@ export function PaymentMethodSection({
!isUpdateMode ? "md:w-1/2" : "md:w-full", !isUpdateMode ? "md:w-1/2" : "md:w-full",
)} )}
> >
<Label htmlFor="conta">FinancialAccount</Label> <Label htmlFor="conta">Conta</Label>
<Select <Select
value={formState.accountId} value={formState.accountId}
onValueChange={(value) => onFieldChange("accountId", value)} onValueChange={(value) => onFieldChange("accountId", value)}
@@ -308,7 +308,7 @@ export function PaymentMethodSection({
!isUpdateMode ? "md:w-1/2" : "md:w-full", !isUpdateMode ? "md:w-1/2" : "md:w-full",
)} )}
> >
<Label htmlFor="contaUpdate">FinancialAccount</Label> <Label htmlFor="contaUpdate">Conta</Label>
<Select <Select
value={formState.accountId} value={formState.accountId}
onValueChange={(value) => onFieldChange("accountId", value)} onValueChange={(value) => onFieldChange("accountId", value)}

View File

@@ -134,14 +134,14 @@ export function AnticipationCard({
{anticipation.payer && ( {anticipation.payer && (
<div> <div>
<dt className="text-muted-foreground">Payer</dt> <dt className="text-muted-foreground">Pagador</dt>
<dd className="mt-1 font-medium">{anticipation.payer.name}</dd> <dd className="mt-1 font-medium">{anticipation.payer.name}</dd>
</div> </div>
)} )}
{anticipation.category && ( {anticipation.category && (
<div> <div>
<dt className="text-muted-foreground">Category</dt> <dt className="text-muted-foreground">Categoria</dt>
<dd className="mt-1 font-medium">{anticipation.category.name}</dd> <dd className="mt-1 font-medium">{anticipation.category.name}</dd>
</div> </div>
)} )}

View File

@@ -368,7 +368,7 @@ export function TransactionsFilters({
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">Payer</label> <label className="text-sm font-medium">Pagador</label>
<Select <Select
value={getParamValue("payer")} value={getParamValue("payer")}
onValueChange={(value) => onValueChange={(value) =>
@@ -409,7 +409,7 @@ export function TransactionsFilters({
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">Category</label> <label className="text-sm font-medium">Categoria</label>
<Popover <Popover
open={categoryOpen} open={categoryOpen}
onOpenChange={setCategoryOpen} onOpenChange={setCategoryOpen}

View File

@@ -34,7 +34,7 @@ import { CategoryIcon } from "@/features/categories/components/category-icon";
import { DEFAULT_LANCAMENTOS_COLUMN_ORDER } from "@/features/transactions/column-order"; import { DEFAULT_LANCAMENTOS_COLUMN_ORDER } from "@/features/transactions/column-order";
import { EmptyState } from "@/shared/components/empty-state"; import { EmptyState } from "@/shared/components/empty-state";
import MoneyValues from "@/shared/components/money-values"; import MoneyValues from "@/shared/components/money-values";
import { TypeBadge } from "@/shared/components/type-badge"; import { TransactionTypeBadge } from "@/shared/components/transaction-type-badge";
import { import {
Avatar, Avatar,
AvatarFallback, AvatarFallback,
@@ -302,8 +302,8 @@ const buildColumns = ({
: row.original.transactionType; : row.original.transactionType;
return ( return (
<TypeBadge <TransactionTypeBadge
type={ kind={
type as "Despesa" | "Receita" | "Transferência" | "Saldo inicial" type as "Despesa" | "Receita" | "Transferência" | "Saldo inicial"
} }
/> />
@@ -458,7 +458,7 @@ const buildColumns = ({
<Tooltip> <Tooltip>
<TooltipTrigger asChild>{content}</TooltipTrigger> <TooltipTrigger asChild>{content}</TooltipTrigger>
<TooltipContent side="top"> <TooltipContent side="top">
{isCartao ? "Cartão" : "FinancialAccount"}: {label} {isCartao ? "Cartão" : "Conta"}: {label}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
); );
@@ -484,7 +484,7 @@ const buildColumns = ({
</Link> </Link>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top"> <TooltipContent side="top">
{isCartao ? "Cartão" : "FinancialAccount"}: {label} {isCartao ? "Cartão" : "Conta"}: {label}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
); );
@@ -607,7 +607,7 @@ const buildColumns = ({
row.original.userId !== currentUserId && ( row.original.userId !== currentUserId && (
<DropdownMenuItem onSelect={() => handleImport(row.original)}> <DropdownMenuItem onSelect={() => handleImport(row.original)}>
<RiFileCopyLine className="size-4" /> <RiFileCopyLine className="size-4" />
Importar para Minha FinancialAccount Importar para Minha Conta
</DropdownMenuItem> </DropdownMenuItem>
)} )}
{row.original.userId === currentUserId && ( {row.original.userId === currentUserId && (
@@ -866,14 +866,14 @@ export function TransactionsTable({
onClick={() => onCreate("Receita")} onClick={() => onCreate("Receita")}
className="w-full sm:w-auto" className="w-full sm:w-auto"
> >
<RiAddCircleLine className="size-4" /> <RiAddCircleFill className="size-4" />
Nova Receita Nova Receita
</Button> </Button>
<Button <Button
onClick={() => onCreate("Despesa")} onClick={() => onCreate("Despesa")}
className="w-full sm:w-auto" className="w-full sm:w-auto"
> >
<RiAddCircleLine className="size-4" /> <RiAddCircleFill className="size-4" />
Nova Despesa Nova Despesa
</Button> </Button>
</> </>
@@ -887,7 +887,7 @@ export function TransactionsTable({
size="icon" size="icon"
className="hidden size-9 sm:inline-flex" className="hidden size-9 sm:inline-flex"
> >
<RiAddCircleFill className="size-4" /> <RiAddCircleLine className="size-4" />
<span className="sr-only"> <span className="sr-only">
Adicionar múltiplos lançamentos Adicionar múltiplos lançamentos
</span> </span>

View File

@@ -72,21 +72,6 @@ export function formatCondition(value?: string | null): string {
return capitalize(value); return capitalize(value);
} }
/**
* Gets the badge variant for a transaction type
* @param type - Transaction type (Receita/Despesa)
* @returns Badge variant
*/
export function getTransactionBadgeVariant(
type?: string | null,
): "default" | "destructive" | "secondary" {
if (!type) return "secondary";
const normalized = type.toLowerCase();
return normalized === "receita" || normalized === "saldo inicial"
? "default"
: "destructive";
}
/** /**
* Formats currency value * Formats currency value
* @param value - Numeric value * @param value - Numeric value

View File

@@ -69,7 +69,7 @@ export function ExpandableWidgetCard({
<div className="pointer-events-none absolute inset-x-0 bottom-0 flex justify-center bg-linear-to-t from-card to-transparent pt-12 pb-6"> <div className="pointer-events-none absolute inset-x-0 bottom-0 flex justify-center bg-linear-to-t from-card to-transparent pt-12 pb-6">
<Button <Button
variant="secondary" variant="secondary"
className="pointer-events-auto rounded-full text-xs" className="pointer-events-auto rounded-full text-xs backdrop-blur-sm bg-primary/10"
onClick={() => setIsOpen(true)} onClick={() => setIsOpen(true)}
aria-label="Expandir para ver todo o conteúdo" aria-label="Expandir para ver todo o conteúdo"
> >

View File

@@ -0,0 +1,88 @@
import { Badge } from "@/shared/components/ui/badge";
import { cn } from "@/shared/utils/ui";
import StatusDot from "./status-dot";
type FinancialKind =
| "receita"
| "despesa"
| "Receita"
| "Despesa"
| "Transferência"
| "transferência"
| "Saldo inicial"
| "Saldo Inicial";
type FinancialKindKey =
| "receita"
| "despesa"
| "transferência"
| "saldo inicial";
type TransactionTypeBadgeProps = {
kind: FinancialKind | string;
className?: string;
};
type BadgeConfig = {
label: string;
className: string;
dotClassName: string;
};
const BADGE_CONFIG: Record<FinancialKindKey, BadgeConfig> = {
receita: {
label: "Receita",
className: "bg-success/10 text-success dark:bg-success/10",
dotClassName: "bg-success/80",
},
despesa: {
label: "Despesa",
className: "bg-destructive/10 text-destructive dark:bg-destructive/10",
dotClassName: "bg-destructive/80",
},
transferência: {
label: "Transferência",
className: "bg-info/10 text-info dark:bg-info/10",
dotClassName: "bg-info/80",
},
"saldo inicial": {
label: "Saldo Inicial",
className: "bg-success/10 text-success dark:bg-success/10",
dotClassName: "bg-success/80",
},
};
function normalizeKind(value: string): FinancialKindKey | null {
const normalizedValue = value.trim().toLowerCase();
return normalizedValue in BADGE_CONFIG
? (normalizedValue as FinancialKindKey)
: null;
}
export function TransactionTypeBadge({
kind,
className,
}: TransactionTypeBadgeProps) {
const normalizedKind = normalizeKind(kind);
const config = normalizedKind ? BADGE_CONFIG[normalizedKind] : null;
const label = config?.label ?? kind;
return (
<Badge
variant="outline"
data-kind={normalizedKind ?? "custom"}
className={cn(
"h-6 gap-1.5 rounded-full border-transparent px-2 py-0 text-xs font-medium shadow-none",
config?.className ??
"bg-muted/30 text-muted-foreground dark:bg-muted/20",
className,
)}
>
<StatusDot
color={config?.dotClassName ?? "bg-muted-foreground/60"}
className="size-1.5"
/>
<span>{label}</span>
</Badge>
);
}

View File

@@ -1,63 +0,0 @@
import { Badge } from "@/shared/components/ui/badge";
import { cn } from "@/shared/utils/ui";
import StatusDot from "./status-dot";
type TypeBadgeType =
| "receita"
| "despesa"
| "Receita"
| "Despesa"
| "Transferência"
| "transferência"
| "Saldo inicial"
| "Saldo Inicial";
interface TypeBadgeProps {
type: TypeBadgeType | string;
className?: string;
}
const TYPE_LABELS: Record<string, string> = {
receita: "Receita",
despesa: "Despesa",
Receita: "Receita",
Despesa: "Despesa",
Transferência: "Transferência",
transferência: "Transferência",
"Saldo inicial": "Saldo Inicial",
"Saldo Inicial": "Saldo Inicial",
};
export function TypeBadge({ type, className }: TypeBadgeProps) {
const normalizedType = type.toLowerCase();
const isReceita = normalizedType === "receita";
const isTransferencia = normalizedType === "transferência";
const isSaldoInicial = normalizedType === "saldo inicial";
const label = TYPE_LABELS[type] || type;
const colorClass = isTransferencia
? "text-info"
: isReceita || isSaldoInicial
? "text-success"
: "text-destructive";
const dotColor = isTransferencia
? "bg-info"
: isReceita || isSaldoInicial
? "bg-success"
: "bg-destructive";
return (
<Badge
variant={"outline"}
className={cn(
"flex items-center gap-1 px-2 text-xs",
colorClass,
className,
)}
>
<StatusDot color={dotColor} />
{label}
</Badge>
);
}

View File

@@ -117,7 +117,7 @@ function CommandGroup({
<CommandPrimitive.Group <CommandPrimitive.Group
data-slot="command-group" data-slot="command-group"
className={cn( className={cn(
"text-foreground **:[[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium", "text-popover-foreground **:[[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium",
className, className,
)} )}
{...props} {...props}

View File

@@ -41,7 +41,7 @@ export default function WidgetCard({
<span className="size-4">{icon}</span> <span className="size-4">{icon}</span>
{title} {title}
</CardTitle> </CardTitle>
<CardDescription className="text-muted-foreground text-sm lowercase mt-2"> <CardDescription className="text-muted-foreground text-sm lowercase mt-1.5 tracking-tight">
{subtitle} {subtitle}
</CardDescription> </CardDescription>
</div> </div>

View File

@@ -42,7 +42,7 @@ export const DEFAULT_CATEGORIES: DefaultCategory[] = [
{ name: "Outras receitas", type: "receita", icon: "RiMore2Line" }, { name: "Outras receitas", type: "receita", icon: "RiMore2Line" },
{ name: "Saldo inicial", type: "receita", icon: "RiWallet2Line" }, { name: "Saldo inicial", type: "receita", icon: "RiWallet2Line" },
// Category especial para transferências entre financialAccounts // Category especial para transferências entre Contas
{ {
name: "Transferência interna", name: "Transferência interna",
type: "receita", type: "receita",