refactor(dashboard): reorganizar módulos em subdiretórios e nova arquitetura de widgets

Arquivos de queries, helpers e controllers dispersos na raiz de dashboard/
foram movidos para subdiretórios temáticos (bills/, invoices/, notes/,
notifications/, overview/, payments/, goals-progress/, categories/).
~25 widgets monolíticos obsoletos removidos em favor de nova arquitetura
baseada em widget-registry com components/widgets/. Novos componentes:
category-breakdown-chart/list, goals-progress-item, percentage-change-indicator.
Imports atualizados em fetch-dashboard-data e transaction-filters limpos.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Felipe Coutinho
2026-04-20 17:51:56 +00:00
parent 3e80d5995b
commit ba05985725
99 changed files with 784 additions and 2055 deletions

View File

@@ -3,8 +3,8 @@ import {
buildBillStatusLabel,
buildBillWidgetStatusLabel,
isBillOverdue,
} from "@/features/dashboard/bills-helpers";
import type { DashboardBill } from "@/features/dashboard/bills-queries";
} from "@/features/dashboard/bills/bills-helpers";
import type { DashboardBill } from "@/features/dashboard/bills/bills-queries";
import { EstablishmentLogo } from "@/shared/components/entity-avatar";
import MoneyValues from "@/shared/components/money-values";
import { Button } from "@/shared/components/ui/button";
@@ -82,8 +82,8 @@ export function BillListItem({ bill, onPay }: BillListItemProps) {
onClick={() => onPay(bill.id)}
>
{bill.isSettled ? (
<span className="flex items-center gap-1 text-success">
<RiCheckboxCircleFill className="size-4" /> Pago
<span className="flex items-center gap-0.5 text-success">
<RiCheckboxCircleFill className="size-3.5" /> Pago
</span>
) : overdue ? (
<span className="overdue-blink">

View File

@@ -8,8 +8,8 @@ import {
type BillDialogState,
formatBillDateLabel,
getBillStatusBadgeVariant,
} from "@/features/dashboard/bills-helpers";
import type { DashboardBill } from "@/features/dashboard/bills-queries";
} from "@/features/dashboard/bills/bills-helpers";
import type { DashboardBill } from "@/features/dashboard/bills/bills-queries";
import MoneyValues from "@/shared/components/money-values";
import { PaymentSuccess } from "@/shared/components/payment-success";
import { Badge } from "@/shared/components/ui/badge";

View File

@@ -1,5 +1,5 @@
import { RiBarcodeFill } from "@remixicon/react";
import type { DashboardBill } from "@/features/dashboard/bills-queries";
import type { DashboardBill } from "@/features/dashboard/bills/bills-queries";
import { WidgetEmptyState } from "@/shared/components/widget-empty-state";
import { BillListItem } from "./bill-list-item";

View File

@@ -1,5 +1,5 @@
import type { BillDialogState } from "@/features/dashboard/bills-helpers";
import type { DashboardBill } from "@/features/dashboard/bills-queries";
import type { BillDialogState } from "@/features/dashboard/bills/bills-helpers";
import type { DashboardBill } from "@/features/dashboard/bills/bills-queries";
import { BillPaymentDialog } from "./bill-payment-dialog";
import { BillsList } from "./bills-list";

View File

@@ -0,0 +1,161 @@
"use client";
import { useMemo } from "react";
import { Pie, PieChart, Tooltip } from "recharts";
import type { DashboardCategoryBreakdownItem } from "@/features/dashboard/categories/category-breakdown-helpers";
import { type ChartConfig, ChartContainer } from "@/shared/components/ui/chart";
import { formatCurrency } from "@/shared/utils/currency";
import { formatPercentage as formatPercentageValue } from "@/shared/utils/percentage";
const CATEGORY_BREAKDOWN_COLORS = [
"var(--chart-1)",
"var(--chart-2)",
"var(--chart-3)",
"var(--chart-4)",
"var(--chart-5)",
"var(--chart-1)",
"var(--chart-2)",
];
const formatPercentage = (value: number, digits: number) =>
formatPercentageValue(value, {
minimumFractionDigits: digits,
maximumFractionDigits: digits,
absolute: true,
});
type CategoryBreakdownChartProps = {
categories: DashboardCategoryBreakdownItem[];
percentageDigits: number;
};
export function CategoryBreakdownChart({
categories,
percentageDigits,
}: CategoryBreakdownChartProps) {
const chartConfig = useMemo(() => {
const nextConfig: ChartConfig = {};
const topCategories = categories.slice(0, 7);
topCategories.forEach((category, index) => {
nextConfig[category.categoryId] = {
label: category.categoryName,
color:
CATEGORY_BREAKDOWN_COLORS[index % CATEGORY_BREAKDOWN_COLORS.length],
};
});
if (categories.length > 7) {
nextConfig.outros = { label: "Outros", color: "var(--chart-6)" };
}
return nextConfig;
}, [categories]);
const chartData = useMemo(() => {
if (categories.length <= 7) {
return categories.map((category) => ({
category: category.categoryId,
name: category.categoryName,
value: category.currentAmount,
percentage: category.percentageOfTotal,
fill: chartConfig[category.categoryId]?.color,
}));
}
const topCategories = categories.slice(0, 7);
const otherCategories = categories.slice(7);
const otherTotal = otherCategories.reduce(
(sum, c) => sum + c.currentAmount,
0,
);
const otherPercentage = otherCategories.reduce(
(sum, c) => sum + c.percentageOfTotal,
0,
);
const groupedData = topCategories.map((category) => ({
category: category.categoryId,
name: category.categoryName,
value: category.currentAmount,
percentage: category.percentageOfTotal,
fill: chartConfig[category.categoryId]?.color,
}));
if (otherCategories.length > 0) {
groupedData.push({
category: "outros",
name: "Outros",
value: otherTotal,
percentage: otherPercentage,
fill: chartConfig.outros?.color,
});
}
return groupedData;
}, [categories, chartConfig]);
return (
<div className="flex items-center gap-4">
<ChartContainer config={chartConfig} className="h-[280px] flex-1">
<PieChart>
<Pie
data={chartData}
cx="50%"
cy="50%"
labelLine={false}
label={({ payload }) =>
formatPercentage(
(payload as { percentage?: number } | undefined)?.percentage ??
0,
percentageDigits,
)
}
outerRadius={75}
dataKey="value"
nameKey="category"
/>
<Tooltip
content={({ active, payload }) => {
if (!active || !payload?.length) return null;
const entry = payload[0]?.payload;
if (!entry) return null;
return (
<div className="rounded-lg border bg-background p-2 shadow-sm">
<div className="grid gap-2">
<div className="flex flex-col">
<span className="text-xs uppercase text-muted-foreground">
{entry.name}
</span>
<span className="font-medium text-foreground">
{formatCurrency(entry.value)}
</span>
<span className="text-xs text-muted-foreground">
{formatPercentage(entry.percentage, percentageDigits)}{" "}
do total
</span>
</div>
</div>
</div>
);
}}
/>
</PieChart>
</ChartContainer>
<div className="min-w-[140px] flex flex-col gap-2">
{chartData.map((entry, index) => (
<div key={`legend-${index}`} className="flex items-center gap-2">
<div
className="size-3 shrink-0 rounded-sm"
style={{ backgroundColor: entry.fill }}
/>
<span className="truncate text-xs text-muted-foreground">
{entry.name}
</span>
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,129 @@
import { RiExternalLinkLine, RiWallet3Line } from "@remixicon/react";
import Link from "next/link";
import type { DashboardCategoryBreakdownItem } from "@/features/dashboard/categories/category-breakdown-helpers";
import { PercentageChangeIndicator } from "@/features/dashboard/components/percentage-change-indicator";
import { CategoryIconBadge } from "@/shared/components/entity-avatar";
import MoneyValues from "@/shared/components/money-values";
import { formatCurrency } from "@/shared/utils/currency";
import { formatPercentage as formatPercentageValue } from "@/shared/utils/percentage";
type CategoryBreakdownListItemConfig = {
shareLabel: string;
percentageDigits: number;
positiveTrend: "up" | "down";
includeBudgetAmount: boolean;
};
type CategoryBreakdownListItemProps = {
category: DashboardCategoryBreakdownItem;
periodParam: string;
config: CategoryBreakdownListItemConfig;
};
const formatPercentage = (value: number, digits: number) =>
formatPercentageValue(value, {
minimumFractionDigits: digits,
maximumFractionDigits: digits,
absolute: true,
});
export function CategoryBreakdownListItem({
category,
periodParam,
config,
}: CategoryBreakdownListItemProps) {
const hasBudget = category.budgetAmount !== null;
const budgetExceeded =
hasBudget &&
category.budgetUsedPercentage !== null &&
category.budgetUsedPercentage > 100;
const exceededAmount =
budgetExceeded && category.budgetAmount
? category.currentAmount - category.budgetAmount
: 0;
return (
<div>
<div className="flex items-center justify-between gap-3 transition-all duration-300 py-2">
<div className="flex min-w-0 flex-1 items-center gap-2">
<CategoryIconBadge
icon={category.categoryIcon}
name={category.categoryName}
/>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<Link
href={`/categories/${category.categoryId}?periodo=${periodParam}`}
className="flex max-w-full items-center gap-1 text-sm font-medium text-foreground underline-offset-2 hover:underline"
>
<span className="truncate">{category.categoryName}</span>
<RiExternalLinkLine
className="size-3 shrink-0 text-muted-foreground"
aria-hidden
/>
</Link>
</div>
<div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-muted-foreground">
<span>
{formatPercentage(
category.percentageOfTotal,
config.percentageDigits,
)}{" "}
da {config.shareLabel}
</span>
{hasBudget && category.budgetUsedPercentage !== null ? (
<>
<span aria-hidden>·</span>
<span
className={`flex items-center gap-1 ${budgetExceeded ? "text-destructive" : "text-info"}`}
>
<RiWallet3Line className="size-3 shrink-0" />
{budgetExceeded ? (
<>
excedeu{" "}
<span className="font-medium">
{formatCurrency(exceededAmount)}
</span>
</>
) : (
<>
{formatPercentage(
category.budgetUsedPercentage,
config.percentageDigits,
)}{" "}
do limite
{config.includeBudgetAmount &&
category.budgetAmount !== null
? ` ${formatCurrency(category.budgetAmount)}`
: ""}
</>
)}
</span>
</>
) : null}
</div>
</div>
</div>
<div className="flex shrink-0 flex-col items-end gap-0.5">
<MoneyValues
className="text-foreground font-medium"
amount={category.currentAmount}
/>
<PercentageChangeIndicator
value={category.percentageChange}
label={
category.percentageChange !== null
? formatPercentage(
category.percentageChange,
config.percentageDigits,
)
: undefined
}
positiveTrend={config.positiveTrend}
/>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,34 @@
import type { DashboardCategoryBreakdownItem } from "@/features/dashboard/categories/category-breakdown-helpers";
import { CategoryBreakdownListItem } from "./category-breakdown-list-item";
type CategoryBreakdownListConfig = {
shareLabel: string;
percentageDigits: number;
positiveTrend: "up" | "down";
includeBudgetAmount: boolean;
};
type CategoryBreakdownListProps = {
categories: DashboardCategoryBreakdownItem[];
periodParam: string;
config: CategoryBreakdownListConfig;
};
export function CategoryBreakdownList({
categories,
periodParam,
config,
}: CategoryBreakdownListProps) {
return (
<div>
{categories.map((category) => (
<CategoryBreakdownListItem
key={category.categoryId}
category={category}
periodParam={periodParam}
config={config}
/>
))}
</div>
);
}

View File

@@ -1,21 +1,12 @@
"use client";
import {
RiArrowDownSFill,
RiArrowUpSFill,
RiExternalLinkLine,
RiListUnordered,
RiPieChart2Line,
RiPieChartLine,
RiWallet3Line,
} from "@remixicon/react";
import Link from "next/link";
import { useMemo, useState } from "react";
import { Pie, PieChart, Tooltip } from "recharts";
import type { DashboardCategoryBreakdownData } from "@/features/dashboard/categories/category-breakdown";
import { CategoryIconBadge } from "@/shared/components/entity-avatar";
import MoneyValues from "@/shared/components/money-values";
import { type ChartConfig, ChartContainer } from "@/shared/components/ui/chart";
import { useState } from "react";
import type { DashboardCategoryBreakdownData } from "@/features/dashboard/categories/category-breakdown-helpers";
import {
Tabs,
TabsContent,
@@ -23,9 +14,9 @@ import {
TabsTrigger,
} from "@/shared/components/ui/tabs";
import { WidgetEmptyState } from "@/shared/components/widget-empty-state";
import { formatCurrency } from "@/shared/utils/currency";
import { formatPercentage as formatPercentageValue } from "@/shared/utils/percentage";
import { formatPeriodForUrl } from "@/shared/utils/period";
import { CategoryBreakdownChart } from "./category-breakdown-chart";
import { CategoryBreakdownList } from "./category-breakdown-list";
type CategoryBreakdownVariant = "income" | "expense";
@@ -35,16 +26,6 @@ type CategoryBreakdownWidgetViewProps = {
variant: CategoryBreakdownVariant;
};
const CATEGORY_BREAKDOWN_COLORS = [
"var(--chart-1)",
"var(--chart-2)",
"var(--chart-3)",
"var(--chart-4)",
"var(--chart-5)",
"var(--chart-1)",
"var(--chart-2)",
];
const VARIANT_CONFIG = {
income: {
emptyTitle: "Nenhuma receita encontrada",
@@ -52,10 +33,7 @@ const VARIANT_CONFIG = {
"Quando houver receitas registradas, elas aparecerão aqui.",
shareLabel: "receita total",
percentageDigits: 1,
changeClassName: {
increase: "text-success",
decrease: "text-destructive",
},
positiveTrend: "up",
includeBudgetAmount: true,
},
expense: {
@@ -64,21 +42,11 @@ const VARIANT_CONFIG = {
"Quando houver despesas registradas, elas aparecerão aqui.",
shareLabel: "despesa total",
percentageDigits: 0,
changeClassName: {
increase: "text-destructive",
decrease: "text-success",
},
positiveTrend: "down",
includeBudgetAmount: false,
},
} as const;
const formatPercentage = (value: number, digits: number) =>
formatPercentageValue(value, {
minimumFractionDigits: digits,
maximumFractionDigits: digits,
absolute: true,
});
export function CategoryBreakdownWidgetView({
data,
period,
@@ -88,78 +56,6 @@ export function CategoryBreakdownWidgetView({
const periodParam = formatPeriodForUrl(period);
const config = VARIANT_CONFIG[variant];
const chartConfig = useMemo(() => {
const nextConfig: ChartConfig = {};
if (data.categories.length <= 7) {
data.categories.forEach((category, index) => {
nextConfig[category.categoryId] = {
label: category.categoryName,
color:
CATEGORY_BREAKDOWN_COLORS[index % CATEGORY_BREAKDOWN_COLORS.length],
};
});
} else {
const topCategories = data.categories.slice(0, 7);
topCategories.forEach((category, index) => {
nextConfig[category.categoryId] = {
label: category.categoryName,
color:
CATEGORY_BREAKDOWN_COLORS[index % CATEGORY_BREAKDOWN_COLORS.length],
};
});
nextConfig.outros = {
label: "Outros",
color: "var(--chart-6)",
};
}
return nextConfig;
}, [data.categories]);
const chartData = useMemo(() => {
if (data.categories.length <= 7) {
return data.categories.map((category) => ({
category: category.categoryId,
name: category.categoryName,
value: category.currentAmount,
percentage: category.percentageOfTotal,
fill: chartConfig[category.categoryId]?.color,
}));
}
const topCategories = data.categories.slice(0, 7);
const otherCategories = data.categories.slice(7);
const otherTotal = otherCategories.reduce(
(sum, category) => sum + category.currentAmount,
0,
);
const otherPercentage = otherCategories.reduce(
(sum, category) => sum + category.percentageOfTotal,
0,
);
const groupedData = topCategories.map((category) => ({
category: category.categoryId,
name: category.categoryName,
value: category.currentAmount,
percentage: category.percentageOfTotal,
fill: chartConfig[category.categoryId]?.color,
}));
if (otherCategories.length > 0) {
groupedData.push({
category: "outros",
name: "Outros",
value: otherTotal,
percentage: otherPercentage,
fill: chartConfig.outros?.color,
});
}
return groupedData;
}, [data.categories, chartConfig]);
if (data.categories.length === 0) {
return (
<WidgetEmptyState
@@ -178,11 +74,17 @@ export function CategoryBreakdownWidgetView({
>
<div className="flex items-center justify-between">
<TabsList className="grid grid-cols-2">
<TabsTrigger value="list" className="text-xs">
<TabsTrigger
value="list"
className="text-xs data-[state=active]:bg-transparent"
>
<RiListUnordered className="mr-1 size-3.5" />
Lista
</TabsTrigger>
<TabsTrigger value="chart" className="text-xs">
<TabsTrigger
value="chart"
className="text-xs data-[state=active]:bg-transparent"
>
<RiPieChart2Line className="mr-1 size-3.5" />
Gráfico
</TabsTrigger>
@@ -190,195 +92,18 @@ export function CategoryBreakdownWidgetView({
</div>
<TabsContent value="list" className="mt-0">
<div>
{data.categories.map((category, index) => {
const hasIncrease =
category.percentageChange !== null &&
category.percentageChange > 0;
const hasDecrease =
category.percentageChange !== null &&
category.percentageChange < 0;
const hasBudget = category.budgetAmount !== null;
const budgetExceeded =
hasBudget &&
category.budgetUsedPercentage !== null &&
category.budgetUsedPercentage > 100;
const exceededAmount =
budgetExceeded && category.budgetAmount
? category.currentAmount - category.budgetAmount
: 0;
const changeClassName = hasIncrease
? config.changeClassName.increase
: hasDecrease
? config.changeClassName.decrease
: "text-muted-foreground";
return (
<div key={category.categoryId}>
<div className="flex items-center justify-between gap-3 transition-all duration-300 py-2">
<div className="flex min-w-0 flex-1 items-center gap-2">
<CategoryIconBadge
icon={category.categoryIcon}
name={category.categoryName}
/>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<Link
href={`/categories/${category.categoryId}?periodo=${periodParam}`}
className="flex max-w-full items-center gap-1 text-sm font-medium text-foreground underline-offset-2 hover:underline"
>
<span className="truncate">
{category.categoryName}
</span>
<RiExternalLinkLine
className="size-3 shrink-0 text-muted-foreground"
aria-hidden
/>
</Link>
</div>
<div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-muted-foreground">
<span>
{formatPercentage(
category.percentageOfTotal,
config.percentageDigits,
)}{" "}
da {config.shareLabel}
</span>
{hasBudget && category.budgetUsedPercentage !== null ? (
<>
<span aria-hidden>·</span>
<span
className={`flex items-center gap-1 ${budgetExceeded ? "text-destructive" : "text-info"}`}
>
<RiWallet3Line className="size-3 shrink-0" />
{budgetExceeded ? (
<>
excedeu{" "}
<span className="font-medium">
{formatCurrency(exceededAmount)}
</span>
</>
) : (
<>
{formatPercentage(
category.budgetUsedPercentage,
config.percentageDigits,
)}{" "}
do limite
{config.includeBudgetAmount &&
category.budgetAmount !== null
? ` ${formatCurrency(category.budgetAmount)}`
: ""}
</>
)}
</span>
</>
) : null}
</div>
</div>
</div>
<div className="flex shrink-0 flex-col items-end gap-0.5">
<MoneyValues
className="text-foreground font-medium"
amount={category.currentAmount}
/>
{category.percentageChange !== null ? (
<span
className={`flex items-center gap-0.5 text-xs font-medium ${changeClassName}`}
>
{hasIncrease ? (
<RiArrowUpSFill className="size-3" />
) : null}
{hasDecrease ? (
<RiArrowDownSFill className="size-3" />
) : null}
{formatPercentage(
category.percentageChange,
config.percentageDigits,
)}
</span>
) : null}
</div>
</div>
</div>
);
})}
</div>
<CategoryBreakdownList
categories={data.categories}
periodParam={periodParam}
config={config}
/>
</TabsContent>
<TabsContent value="chart" className="mt-0">
<div className="flex items-center gap-4">
<ChartContainer config={chartConfig} className="h-[280px] flex-1">
<PieChart>
<Pie
data={chartData}
cx="50%"
cy="50%"
labelLine={false}
label={({ payload }) =>
formatPercentage(
(payload as { percentage?: number } | undefined)
?.percentage ?? 0,
config.percentageDigits,
)
}
outerRadius={75}
dataKey="value"
nameKey="category"
/>
<Tooltip
content={({ active, payload }) => {
if (!active || !payload?.length) {
return null;
}
const entry = payload[0]?.payload;
if (!entry) {
return null;
}
return (
<div className="rounded-lg border bg-background p-2 shadow-sm">
<div className="grid gap-2">
<div className="flex flex-col">
<span className="text-xs uppercase text-muted-foreground">
{entry.name}
</span>
<span className="font-medium text-foreground">
{formatCurrency(entry.value)}
</span>
<span className="text-xs text-muted-foreground">
{formatPercentage(
entry.percentage,
config.percentageDigits,
)}{" "}
do total
</span>
</div>
</div>
</div>
);
}}
/>
</PieChart>
</ChartContainer>
<div className="min-w-[140px] flex flex-col gap-2">
{chartData.map((entry, index) => (
<div key={`legend-${index}`} className="flex items-center gap-2">
<div
className="size-3 shrink-0 rounded-sm"
style={{ backgroundColor: entry.fill }}
/>
<span className="truncate text-xs text-muted-foreground">
{entry.name}
</span>
</div>
))}
</div>
</div>
<CategoryBreakdownChart
categories={data.categories}
percentageDigits={config.percentageDigits}
/>
</TabsContent>
</Tabs>
);

View File

@@ -25,19 +25,19 @@ import {
} from "@remixicon/react";
import { useMemo, useState, useTransition } from "react";
import { toast } from "sonner";
import { SortableWidget } from "@/features/dashboard/components/sortable-widget";
import { WidgetSettingsDialog } from "@/features/dashboard/components/widget-settings-dialog";
import { SortableWidget } from "@/features/dashboard/components/widgets/sortable-widget";
import { WidgetSettingsDialog } from "@/features/dashboard/components/widgets/widget-settings-dialog";
import type { DashboardData } from "@/features/dashboard/fetch-dashboard-data";
import {
resetWidgetPreferences,
updateWidgetPreferences,
type WidgetPreferences,
} from "@/features/dashboard/widgets/actions";
} from "@/features/dashboard/widget-registry/widget-actions";
import {
type DashboardWidgetQuickActionOptions,
type WidgetConfig,
widgetsConfig,
} from "@/features/dashboard/widgets/widgets-config";
} from "@/features/dashboard/widget-registry/widget-config";
import { NoteDialog } from "@/features/notes/components/note-dialog";
import { TransactionDialog } from "@/features/transactions/components/dialogs/transaction-dialog/transaction-dialog";
import { ExpandableWidgetCard } from "@/shared/components/expandable-widget-card";

View File

@@ -1,14 +1,12 @@
import {
RiArrowDownLine,
RiArrowDownSFill,
RiArrowUpLine,
RiArrowUpSFill,
RiCalendarCheckLine,
RiScalesLine,
RiSubtractLine,
RiArrowLeftRightLine,
RiArrowRightDownLine,
RiArrowRightUpLine,
RiCalendar2Line,
} from "@remixicon/react";
import { MetricsCardInfoButton } from "@/features/dashboard/components/metrics-card-info-button";
import type { DashboardCardMetrics } from "@/features/dashboard/dashboard-metrics-queries";
import { PercentageChangeIndicator } from "@/features/dashboard/components/percentage-change-indicator";
import type { DashboardCardMetrics } from "@/features/dashboard/overview/dashboard-metrics-queries";
import MoneyValues from "@/shared/components/money-values";
import {
Card,
@@ -34,7 +32,7 @@ const CARDS = [
label: "Receitas",
subtitle: "Entradas do período",
key: "receitas",
icon: RiArrowDownLine,
icon: RiArrowRightDownLine,
invertTrend: false,
iconClass: "text-success",
helpTitle: "Como calculamos receitas",
@@ -50,7 +48,7 @@ const CARDS = [
label: "Despesas",
subtitle: "Saídas do período",
key: "despesas",
icon: RiArrowUpLine,
icon: RiArrowRightUpLine,
invertTrend: true,
iconClass: "text-destructive",
helpTitle: "Como calculamos despesas",
@@ -66,7 +64,7 @@ const CARDS = [
label: "Balanço",
subtitle: "Receitas, despesas e ajustes entre contas",
key: "balanco",
icon: RiScalesLine,
icon: RiArrowLeftRightLine,
invertTrend: false,
iconClass: "text-warning",
helpTitle: "Como calculamos o balanço",
@@ -81,7 +79,7 @@ const CARDS = [
label: "Previsto",
subtitle: "Saldo acumulado projetado",
key: "previsto",
icon: RiCalendarCheckLine,
icon: RiCalendar2Line,
invertTrend: false,
iconClass: "text-cyan-600",
helpTitle: "Como calculamos o previsto",
@@ -94,12 +92,6 @@ const CARDS = [
},
] as const;
const TREND_ICONS = {
up: RiArrowUpSFill,
down: RiArrowDownSFill,
flat: RiSubtractLine,
} as const;
const getTrend = (current: number, previous: number): Trend => {
const diff = current - previous;
if (diff > TREND_THRESHOLD) return "up";
@@ -126,12 +118,6 @@ const getPercentChange = (current: number, previous: number): string => {
});
};
const getTrendBadgeClass = (trend: Trend, invertTrend: boolean): string => {
if (trend === "flat") return "text-muted-foreground";
const isPositive = invertTrend ? trend === "down" : trend === "up";
return isPositive ? "text-success" : "text-destructive";
};
export function DashboardMetricsCards({ metrics }: DashboardMetricsCardsProps) {
return (
<div className="grid grid-cols-1 gap-3 @xl/main:grid-cols-2 @5xl/main:grid-cols-4">
@@ -148,8 +134,6 @@ export function DashboardMetricsCards({ metrics }: DashboardMetricsCardsProps) {
}) => {
const metric = metrics[key];
const trend = getTrend(metric.current, metric.previous);
const TrendIcon = TREND_ICONS[trend];
const trendBadgeClass = getTrendBadgeClass(trend, invertTrend);
const percentChange = getPercentChange(
metric.current,
metric.previous,
@@ -157,23 +141,19 @@ export function DashboardMetricsCards({ metrics }: DashboardMetricsCardsProps) {
return (
<Card key={label} className="gap-2 overflow-hidden">
<CardHeader>
<div className="flex items-start justify-between">
<div>
<CardTitle className="flex items-center gap-1.5 ">
<Icon className={cn("size-4", iconClass)} aria-hidden />
{label}
<MetricsCardInfoButton
label={label}
helpTitle={helpTitle}
helpLines={helpLines}
/>
</CardTitle>
<CardDescription className="mt-1.5 tracking-tight">
{subtitle}
</CardDescription>
</div>
</div>
<CardHeader className="gap-1">
<CardTitle className="flex items-center gap-1">
<Icon className={cn("size-4", iconClass)} aria-hidden />
{label}
<MetricsCardInfoButton
label={label}
helpTitle={helpTitle}
helpLines={helpLines}
/>
</CardTitle>
<CardDescription className="mt-1 tracking-tight">
{subtitle}
</CardDescription>
<Separator className="mt-1" />
</CardHeader>
@@ -183,15 +163,14 @@ export function DashboardMetricsCards({ metrics }: DashboardMetricsCardsProps) {
className="text-2xl leading-none font-medium"
amount={metric.current}
/>
<div
className={cn(
"inline-flex items-center gap-1 text-xs font-medium",
trendBadgeClass,
)}
>
<TrendIcon className="size-3.5" aria-hidden />
<span>{percentChange}</span>
</div>
<PercentageChangeIndicator
trend={trend}
label={percentChange}
positiveTrend={invertTrend ? "down" : "up"}
showFlatIcon
className="gap-1"
iconClassName="size-3.5"
/>
</div>
<div className="text-xs text-muted-foreground">

View File

@@ -1,4 +1,4 @@
import { formatCurrentDate, getGreeting } from "./welcome-widget";
import { formatCurrentDate, getGreeting } from "@/features/dashboard/widget-registry/welcome-widget";
type DashboardWelcomeProps = {
name?: string | null;
@@ -10,13 +10,11 @@ export function DashboardWelcome({ name }: DashboardWelcomeProps) {
const greeting = getGreeting();
return (
<section className="py-4">
<div>
<h1 className="text-xl tracking-tight">
{greeting}, {displayName}
</h1>
<h2 className="mt-1 text-sm text-muted-foreground">{formattedDate}</h2>
</div>
<section className="py-4 space-y-1">
<h1 className="text-xl tracking-tight">
<span className="text-muted-foreground">{greeting},</span> {displayName}
</h1>
<h2 className="text-sm text-muted-foreground">{formattedDate}</h2>
</section>
);
}

View File

@@ -1,9 +1,10 @@
import { RiPencilLine } from "@remixicon/react";
import { PercentageChangeIndicator } from "@/features/dashboard/components/percentage-change-indicator";
import {
clampGoalProgress,
formatGoalProgressPercentage,
} from "@/features/dashboard/goals-progress-helpers";
import type { GoalProgressItem as GoalProgressItemData } from "@/features/dashboard/goals-progress-queries";
} from "@/features/dashboard/goals-progress/goals-progress-helpers";
import type { GoalProgressItem as GoalProgressItemData } from "@/features/dashboard/goals-progress/goals-progress-queries";
import { CategoryIconBadge } from "@/shared/components/entity-avatar";
import MoneyValues from "@/shared/components/money-values";
import { Button } from "@/shared/components/ui/button";
@@ -22,12 +23,6 @@ export function GoalProgressItem({
}: GoalProgressItemProps) {
const progressValue = clampGoalProgress(item.usedPercentage, 0, 100);
const percentageDelta = item.usedPercentage - 100;
const deltaColor =
percentageDelta > 0
? "text-destructive"
: percentageDelta < 0
? "text-success"
: "text-muted-foreground";
const isExceeded = item.status === "exceeded";
return (
@@ -47,9 +42,12 @@ export function GoalProgressItem({
<MoneyValues className="font-medium" amount={item.spentAmount} />{" "}
de{" "}
<MoneyValues className="font-medium" amount={item.budgetAmount} />
<span className={`ml-1.5 font-medium ${deltaColor}`}>
{formatGoalProgressPercentage(percentageDelta, true)}
</span>
<PercentageChangeIndicator
value={percentageDelta}
label={formatGoalProgressPercentage(percentageDelta, true)}
positiveTrend="down"
className="ml-1.5 align-middle"
/>
</p>
</div>
</div>

View File

@@ -1,7 +1,7 @@
import { RiFundsLine } from "@remixicon/react";
import type { GoalProgressItem } from "@/features/dashboard/goals-progress-queries";
import type { GoalProgressItem } from "@/features/dashboard/goals-progress/goals-progress-queries";
import { WidgetEmptyState } from "@/shared/components/widget-empty-state";
import { GoalProgressItem as GoalProgressListItem } from "./goal-progress-item";
import { GoalProgressItem as GoalProgressListItem } from "./goals-progress-item";
type GoalsProgressListProps = {
items: GoalProgressItem[];

View File

@@ -5,7 +5,7 @@ import type {
import type {
GoalProgressItem,
GoalsProgressData,
} from "@/features/dashboard/goals-progress-queries";
} from "@/features/dashboard/goals-progress/goals-progress-queries";
import { GoalsProgressList } from "./goals-progress-list";
import { GoalsProgressWidgetDialogs } from "./goals-progress-widget-dialogs";

View File

@@ -130,7 +130,7 @@ export function InstallmentAnalysisPage({
return (
<div className="flex flex-col gap-4">
{/* Card de resumo principal */}
<Card className="border-none bg-primary/15">
<Card className="border-none bg-primary/10 dark:bg-primary/10">
<CardContent className="flex flex-col items-start justify-center gap-2 py-2">
<p className="text-sm text-muted-foreground">
Se você pagar tudo que está selecionado:

View File

@@ -1,6 +1,6 @@
import Image from "next/image";
import type { InstallmentExpense } from "@/features/dashboard/expenses/installment-expenses-queries";
import { buildInstallmentExpenseDisplay } from "@/features/dashboard/installment-expenses-helpers";
import { buildInstallmentExpenseDisplay } from "@/features/dashboard/expenses/installment-expenses-helpers";
import { EstablishmentLogo } from "@/shared/components/entity-avatar";
import MoneyValues from "@/shared/components/money-values";
import { Progress } from "@/shared/components/ui/progress";

View File

@@ -1,5 +1,6 @@
import { RiCheckboxCircleFill, RiExternalLinkLine } from "@remixicon/react";
import Link from "next/link";
import { PercentageChangeIndicator } from "@/features/dashboard/components/percentage-change-indicator";
import {
buildInvoiceDetailsHref,
buildInvoiceInitials,
@@ -8,8 +9,8 @@ import {
getInvoiceShareLabel,
parseInvoiceDueDate,
parseInvoiceWidgetDueDate,
} from "@/features/dashboard/invoices-helpers";
import type { DashboardInvoice } from "@/features/dashboard/invoices-queries";
} from "@/features/dashboard/invoices/invoices-helpers";
import type { DashboardInvoice } from "@/features/dashboard/invoices/invoices-queries";
import MoneyValues from "@/shared/components/money-values";
import {
Avatar,
@@ -83,7 +84,7 @@ export function InvoiceListItem({ invoice, onPay }: InvoiceListItemProps) {
{hasBreakdown ? (
<HoverCard openDelay={150}>
<HoverCardTrigger asChild>{linkNode}</HoverCardTrigger>
<HoverCardContent align="start" className="w-72 space-y-3">
<HoverCardContent align="start" className="w-80 space-y-3">
<p className="text-xs text-muted-foreground">
Distribuição por pagador
</p>
@@ -115,11 +116,14 @@ export function InvoiceListItem({ invoice, onPay }: InvoiceListItemProps) {
)}
</p>
</div>
<div className="text-sm font-medium text-foreground">
<div className="flex shrink-0 flex-col items-end gap-0.5 text-sm font-medium text-foreground">
<MoneyValues
className="font-medium"
amount={share.amount}
/>
<PercentageChangeIndicator
value={share.percentageChange}
/>
</div>
</li>
))}
@@ -179,8 +183,8 @@ export function InvoiceListItem({ invoice, onPay }: InvoiceListItemProps) {
onClick={() => onPay(invoice.id)}
>
{isPaid ? (
<span className="flex items-center gap-1 text-success">
<RiCheckboxCircleFill className="size-4" /> Pago
<span className="flex items-center gap-0.5 text-success">
<RiCheckboxCircleFill className="size-3.5" /> Pago
</span>
) : isOverdue ? (
<span className="overdue-blink">

View File

@@ -2,7 +2,7 @@ import Image from "next/image";
import {
buildInvoiceInitials,
type InvoiceLogoTone,
} from "@/features/dashboard/invoices-helpers";
} from "@/features/dashboard/invoices/invoices-helpers";
import { resolveLogoSrc } from "@/shared/lib/logo";
import { cn } from "@/shared/utils/ui";

View File

@@ -9,8 +9,8 @@ import {
getInvoiceStatusBadgeVariant,
type InvoiceDialogState,
parseInvoiceDueDate,
} from "@/features/dashboard/invoices-helpers";
import type { DashboardInvoice } from "@/features/dashboard/invoices-queries";
} from "@/features/dashboard/invoices/invoices-helpers";
import type { DashboardInvoice } from "@/features/dashboard/invoices/invoices-queries";
import MoneyValues from "@/shared/components/money-values";
import { PaymentSuccess } from "@/shared/components/payment-success";
import { Badge } from "@/shared/components/ui/badge";

View File

@@ -1,5 +1,5 @@
import { RiBillLine } from "@remixicon/react";
import type { DashboardInvoice } from "@/features/dashboard/invoices-queries";
import type { DashboardInvoice } from "@/features/dashboard/invoices/invoices-queries";
import { WidgetEmptyState } from "@/shared/components/widget-empty-state";
import { InvoiceListItem } from "./invoice-list-item";

View File

@@ -1,5 +1,5 @@
import type { InvoiceDialogState } from "@/features/dashboard/invoices-helpers";
import type { DashboardInvoice } from "@/features/dashboard/invoices-queries";
import type { InvoiceDialogState } from "@/features/dashboard/invoices/invoices-helpers";
import type { DashboardInvoice } from "@/features/dashboard/invoices/invoices-queries";
import { InvoicePaymentDialog } from "./invoice-payment-dialog";
import { InvoicesList } from "./invoices-list";

View File

@@ -29,14 +29,12 @@ export function NoteListItem({
{displayTitle}
</p>
<div className="mt-1 flex items-center gap-2">
<Badge variant="outline" className="h-5 px-1.5 text-[10px]">
<Badge variant="outline" className="h-5 px-1.5 text-xs">
{getNoteTasksSummary(note)}
</Badge>
{createdAtLabel ? (
<p className="truncate text-xs text-muted-foreground">
{createdAtLabel}
</p>
) : null}
<p className="truncate text-xs text-muted-foreground">
{createdAtLabel}
</p>
</div>
</div>

View File

@@ -4,7 +4,7 @@ import type { ReactNode } from "react";
import {
formatPaymentBreakdownPercentage,
formatPaymentBreakdownTransactionsLabel,
} from "@/features/dashboard/payment-breakdown-formatters";
} from "@/features/dashboard/payments/payment-breakdown-formatters";
import MoneyValues from "@/shared/components/money-values";
import { Progress } from "@/shared/components/ui/progress";
import {

View File

@@ -1,5 +1,5 @@
import { RiMoneyDollarCircleLine, RiSlideshowLine } from "@remixicon/react";
import type { PaymentOverviewTab } from "@/features/dashboard/payment-overview-tabs";
import type { PaymentOverviewTab } from "@/features/dashboard/payments/payment-overview-tabs";
import type { PaymentConditionsData } from "@/features/dashboard/payments/payment-conditions-queries";
import type { PaymentMethodsData } from "@/features/dashboard/payments/payment-methods-queries";
import {
@@ -31,11 +31,17 @@ export function PaymentOverviewWidgetView({
return (
<Tabs value={activeTab} onValueChange={onTabChange} className="w-full">
<TabsList className="grid grid-cols-2">
<TabsTrigger value="conditions" className="text-xs">
<TabsTrigger
value="conditions"
className="text-xs data-[state=active]:bg-transparent"
>
<RiSlideshowLine className="mr-1 size-3.5" />
Condições
</TabsTrigger>
<TabsTrigger value="methods" className="text-xs">
<TabsTrigger
value="methods"
className="text-xs data-[state=active]:bg-transparent"
>
<RiMoneyDollarCircleLine className="mr-1 size-3.5" />
Formas
</TabsTrigger>

View File

@@ -0,0 +1,71 @@
import {
RiArrowDownSFill,
RiArrowUpSFill,
RiSubtractLine,
} from "@remixicon/react";
import { formatPercentage } from "@/shared/utils/percentage";
import { cn } from "@/shared/utils/ui";
export type PercentageChangeTrend = "up" | "down" | "flat";
type PercentageChangeIndicatorProps = {
value?: number | null;
label?: string;
trend?: PercentageChangeTrend;
positiveTrend?: Exclude<PercentageChangeTrend, "flat">;
showFlatIcon?: boolean;
className?: string;
iconClassName?: string;
};
export function PercentageChangeIndicator({
value,
label,
trend,
positiveTrend = "down",
showFlatIcon = false,
className,
iconClassName,
}: PercentageChangeIndicatorProps) {
const hasNumericValue = typeof value === "number" && Number.isFinite(value);
const resolvedTrend =
trend ??
(hasNumericValue
? value > 0
? "up"
: value < 0
? "down"
: "flat"
: "flat");
const resolvedLabel =
label ?? (hasNumericValue ? formatPercentage(value) : null);
if (!resolvedLabel) {
return null;
}
return (
<span
className={cn(
"inline-flex items-center gap-0.5 text-xs font-medium",
resolvedTrend === "flat"
? "text-muted-foreground"
: resolvedTrend === positiveTrend
? "text-success"
: "text-destructive",
className,
)}
>
{resolvedTrend === "up" ? (
<RiArrowUpSFill className={cn("size-3", iconClassName)} />
) : null}
{resolvedTrend === "down" ? (
<RiArrowDownSFill className={cn("size-3", iconClassName)} />
) : null}
{resolvedTrend === "flat" && showFlatIcon ? (
<RiSubtractLine className={cn("size-3", iconClassName)} />
) : null}
{resolvedLabel}
</span>
);
}

View File

@@ -1,9 +0,0 @@
import {
formatBusinessCurrentDate,
getBusinessGreeting,
} from "@/shared/utils/date";
export const formatCurrentDate = (date = new Date()) =>
formatBusinessCurrentDate(date);
export const getGreeting = (date = new Date()) => getBusinessGreeting(date);

View File

@@ -1,8 +1,8 @@
"use client";
import type { DashboardBill } from "@/features/dashboard/bills-queries";
import { useBillWidgetController } from "@/features/dashboard/use-bill-widget-controller";
import { BillsWidgetView } from "./bills/bills-widget-view";
import type { DashboardBill } from "@/features/dashboard/bills/bills-queries";
import { useBillWidgetController } from "@/features/dashboard/bills/use-bill-widget-controller";
import { BillsWidgetView } from "../bills/bills-widget-view";
type BillWidgetProps = {
bills?: DashboardBill[];

View File

@@ -1,15 +1,12 @@
"use client";
import {
RiArrowDownSFill,
RiArrowUpSFill,
RiLineChartLine,
} from "@remixicon/react";
import type { DashboardCategoryBreakdownItem } from "@/features/dashboard/categories/category-breakdown";
import { RiLineChartLine } from "@remixicon/react";
import type { DashboardCategoryBreakdownItem } from "@/features/dashboard/categories/category-breakdown-helpers";
import { PercentageChangeIndicator } from "@/features/dashboard/components/percentage-change-indicator";
import { CategoryIconBadge } from "@/shared/components/entity-avatar";
import MoneyValues from "@/shared/components/money-values";
import { WidgetEmptyState } from "@/shared/components/widget-empty-state";
import { cn } from "@/shared/utils/ui";
import { formatPercentage } from "@/shared/utils/percentage";
type CategoryTrendsWidgetProps = {
categories: DashboardCategoryBreakdownItem[];
@@ -40,7 +37,6 @@ export function CategoryTrendsWidget({
<ul className="flex flex-col space-y-1">
{trending.map((category) => {
const change = category.percentageChange ?? 0;
const isUp = change > 0;
return (
<li key={category.categoryId}>
@@ -62,19 +58,17 @@ export function CategoryTrendsWidget({
/>
</p>
</div>
<span
className={cn(
"inline-flex shrink-0 items-center gap-0.5 font-semibold text-sm",
isUp ? " text-destructive" : " text-success",
)}
>
{isUp ? (
<RiArrowUpSFill className="size-3.5" />
) : (
<RiArrowDownSFill className="size-3.5" />
)}
{Math.abs(change).toFixed(0)}%
</span>
<PercentageChangeIndicator
value={change}
label={formatPercentage(change, {
absolute: true,
minimumFractionDigits: 0,
maximumFractionDigits: 0,
})}
positiveTrend="down"
className="shrink-0 text-sm font-semibold"
iconClassName="size-3.5"
/>
</div>
</li>
);

View File

@@ -1,7 +1,7 @@
"use client";
import type { ExpensesByCategoryData } from "@/features/dashboard/categories/expenses-by-category-queries";
import { CategoryBreakdownWidgetView } from "./category-breakdown/category-breakdown-widget-view";
import { CategoryBreakdownWidgetView } from "../category-breakdown/category-breakdown-widget-view";
type ExpensesByCategoryWidgetWithChartProps = {
data: ExpensesByCategoryData;

View File

@@ -1,8 +1,8 @@
"use client";
import type { GoalsProgressData } from "@/features/dashboard/goals-progress-queries";
import { useGoalsProgressWidgetController } from "@/features/dashboard/use-goals-progress-widget-controller";
import { GoalsProgressWidgetView } from "./goals-progress/goals-progress-widget-view";
import type { GoalsProgressData } from "@/features/dashboard/goals-progress/goals-progress-queries";
import { useGoalsProgressWidgetController } from "@/features/dashboard/goals-progress/use-goals-progress-widget-controller";
import { GoalsProgressWidgetView } from "../goals-progress/goals-progress-widget-view";
type GoalsProgressWidgetProps = {
data: GoalsProgressData;

View File

@@ -10,7 +10,7 @@ import { useRouter } from "next/navigation";
import { useMemo, useState } from "react";
import { toast } from "sonner";
import type { DashboardInboxSnapshot } from "@/features/dashboard/inbox-snapshot-queries";
import type { DashboardWidgetQuickActionOptions } from "@/features/dashboard/widgets/widgets-config";
import type { DashboardWidgetQuickActionOptions } from "@/features/dashboard/widget-registry/widget-config";
import {
discardInboxItemAction,
markInboxAsProcessedAction,
@@ -178,7 +178,7 @@ export function InboxWidget({
key={item.id}
className="flex items-center justify-between py-1.5"
>
<div className="flex min-w-0 flex-1 items-center gap-2 py-1">
<div className="flex flex-1 items-center gap-2">
<Image
src={displayLogo}
alt={item.sourceAppName ?? ""}
@@ -188,9 +188,11 @@ export function InboxWidget({
unoptimized
/>
<div className="min-w-0">
<p className="truncate text-sm font-medium text-foreground">
{displayName}
<div>
<p className="text-sm font-medium text-foreground">
{displayName.length > 30
? `${displayName.slice(0, 30)}...`
: displayName}
</p>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
{item.sourceAppName && <span>{item.sourceAppName}</span>}

View File

@@ -1,7 +1,7 @@
"use client";
import type { IncomeByCategoryData } from "@/features/dashboard/categories/income-by-category-queries";
import { CategoryBreakdownWidgetView } from "./category-breakdown/category-breakdown-widget-view";
import { CategoryBreakdownWidgetView } from "../category-breakdown/category-breakdown-widget-view";
type IncomeByCategoryWidgetWithChartProps = {
data: IncomeByCategoryData;

View File

@@ -2,7 +2,7 @@
import { RiLineChartLine } from "@remixicon/react";
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts";
import type { IncomeExpenseBalanceData } from "@/features/dashboard/income-expense-balance-queries";
import type { IncomeExpenseBalanceData } from "@/features/dashboard/overview/income-expense-balance-queries";
import { CardContent } from "@/shared/components/ui/card";
import {
type ChartConfig,
@@ -19,15 +19,15 @@ type IncomeExpenseBalanceWidgetProps = {
const chartConfig = {
receita: {
label: "Receita",
color: "var(--data-9)",
color: "var(--success)",
},
despesa: {
label: "Despesa",
color: "var(--data-1)",
color: "var(--destructive)",
},
balanco: {
label: "Balanço",
color: "var(--data-4)",
color: "var(--warning)",
},
} satisfies ChartConfig;

View File

@@ -1,5 +1,5 @@
import type { InstallmentExpensesData } from "@/features/dashboard/expenses/installment-expenses-queries";
import { InstallmentExpensesWidgetView } from "./installment-expenses/installment-expenses-widget-view";
import { InstallmentExpensesWidgetView } from "../installment-expenses/installment-expenses-widget-view";
type InstallmentExpensesWidgetProps = {
data: InstallmentExpensesData;

View File

@@ -1,8 +1,8 @@
"use client";
import type { DashboardInvoice } from "@/features/dashboard/invoices-queries";
import { useInvoicesWidgetController } from "@/features/dashboard/use-invoices-widget-controller";
import { InvoicesWidgetView } from "./invoices/invoices-widget-view";
import type { DashboardInvoice } from "@/features/dashboard/invoices/invoices-queries";
import { useInvoicesWidgetController } from "@/features/dashboard/invoices/use-invoices-widget-controller";
import { InvoicesWidgetView } from "../invoices/invoices-widget-view";
type InvoicesWidgetProps = {
invoices: DashboardInvoice[];

View File

@@ -11,7 +11,7 @@ import Link from "next/link";
import { useTransition } from "react";
import { toast } from "sonner";
import type { DashboardAccount } from "@/features/dashboard/accounts-queries";
import { updateMyAccountsWidgetPreference } from "@/features/dashboard/widgets/actions";
import { updateMyAccountsWidgetPreference } from "@/features/dashboard/widget-registry/widget-actions";
import MoneyValues from "@/shared/components/money-values";
import { Badge } from "@/shared/components/ui/badge";
import { Button } from "@/shared/components/ui/button";

View File

@@ -1,8 +1,8 @@
"use client";
import type { DashboardNote } from "@/features/dashboard/notes-queries";
import { useNotesWidgetController } from "@/features/dashboard/use-notes-widget-controller";
import { NotesWidgetView } from "./notes/notes-widget-view";
import type { DashboardNote } from "@/features/dashboard/notes/notes-queries";
import { useNotesWidgetController } from "@/features/dashboard/notes/use-notes-widget-controller";
import { NotesWidgetView } from "../notes/notes-widget-view";
type NotesWidgetProps = {
notes: DashboardNote[];

View File

@@ -1,13 +1,12 @@
"use client";
import {
RiArrowDownSFill,
RiArrowUpSFill,
RiExternalLinkLine,
RiGroupLine,
RiVerifiedBadgeFill,
} from "@remixicon/react";
import Link from "next/link";
import { PercentageChangeIndicator } from "@/features/dashboard/components/percentage-change-indicator";
import type { DashboardPagador } from "@/features/dashboard/payers-queries";
import MoneyValues from "@/shared/components/money-values";
import {
@@ -18,7 +17,6 @@ import {
import { WidgetEmptyState } from "@/shared/components/widget-empty-state";
import { getAvatarSrc } from "@/shared/lib/payers/utils";
import { buildInitials } from "@/shared/utils/initials";
import { formatPercentage } from "@/shared/utils/percentage";
type PayersWidgetProps = {
payers: DashboardPagador[];
@@ -87,25 +85,7 @@ export function PayersWidget({ payers }: PayersWidgetProps) {
className="font-medium"
amount={payer.totalExpenses}
/>
{percentageChange !== null && (
<span
className={`flex items-center gap-0.5 text-xs font-medium ${
percentageChange > 0
? "text-destructive"
: percentageChange < 0
? "text-success"
: "text-muted-foreground"
}`}
>
{percentageChange > 0 && (
<RiArrowUpSFill className="size-3" />
)}
{percentageChange < 0 && (
<RiArrowDownSFill className="size-3" />
)}
{formatPercentage(percentageChange)}
</span>
)}
<PercentageChangeIndicator value={percentageChange} />
</div>
</div>
);

View File

@@ -2,8 +2,8 @@
import type { PaymentConditionsData } from "@/features/dashboard/payments/payment-conditions-queries";
import type { PaymentMethodsData } from "@/features/dashboard/payments/payment-methods-queries";
import { usePaymentOverviewWidgetController } from "@/features/dashboard/use-payment-overview-widget-controller";
import { PaymentOverviewWidgetView } from "./payment-overview/payment-overview-widget-view";
import { usePaymentOverviewWidgetController } from "@/features/dashboard/payments/use-payment-overview-widget-controller";
import { PaymentOverviewWidgetView } from "../payment-overview/payment-overview-widget-view";
type PaymentOverviewWidgetProps = {
paymentConditionsData: PaymentConditionsData;

View File

@@ -1,7 +1,7 @@
"use client";
import type { PaymentStatusData } from "@/features/dashboard/payments/payment-status-queries";
import { PaymentStatusWidgetView } from "./payment-status/payment-status-widget-view";
import { PaymentStatusWidgetView } from "../payment-status/payment-status-widget-view";
type PaymentStatusWidgetProps = {
data: PaymentStatusData;

View File

@@ -2,7 +2,7 @@
import { RiArrowDownSFill, RiStore3Line } from "@remixicon/react";
import { useEffect, useMemo, useRef, useState } from "react";
import type { PurchasesByCategoryData } from "@/features/dashboard/purchases-by-category-queries";
import type { PurchasesByCategoryData } from "@/features/dashboard/categories/purchases-by-category-queries";
import { EstablishmentLogo } from "@/shared/components/entity-avatar";
import MoneyValues from "@/shared/components/money-values";
import {

View File

@@ -37,11 +37,17 @@ export function SpendingOverviewWidget({
className="w-full"
>
<TabsList className="grid grid-cols-2">
<TabsTrigger value="expenses" className="text-xs">
<TabsTrigger
value="expenses"
className="text-xs data-[state=active]:bg-transparent"
>
<RiArrowUpDoubleLine className="mr-1 size-3.5" />
Top gastos
</TabsTrigger>
<TabsTrigger value="establishments" className="text-xs">
<TabsTrigger
value="establishments"
className="text-xs data-[state=active]:bg-transparent"
>
<RiStore2Line className="mr-1 size-3.5" />
Estabelecimentos
</TabsTrigger>

View File

@@ -2,7 +2,7 @@
import { RiRefreshLine, RiSettings4Line } from "@remixicon/react";
import { useState } from "react";
import { widgetsConfig } from "@/features/dashboard/widgets/widgets-config";
import { widgetsConfig } from "@/features/dashboard/widget-registry/widget-config";
import { Button } from "@/shared/components/ui/button";
import {
Dialog,