diff --git a/src/features/dashboard/accounts-queries.ts b/src/features/dashboard/accounts-queries.ts
index 624be57..92488d7 100644
--- a/src/features/dashboard/accounts-queries.ts
+++ b/src/features/dashboard/accounts-queries.ts
@@ -26,7 +26,7 @@ export type DashboardAccount = {
excludeFromBalance: boolean;
};
-export type DashboardAccountsSnapshot = {
+type DashboardAccountsSnapshot = {
totalBalance: number;
accounts: DashboardAccount[];
};
diff --git a/src/features/dashboard/bills-queries.ts b/src/features/dashboard/bills-queries.ts
deleted file mode 100644
index cd7ecda..0000000
--- a/src/features/dashboard/bills-queries.ts
+++ /dev/null
@@ -1,166 +0,0 @@
-"use server";
-
-import { and, eq } from "drizzle-orm";
-import { transactions } from "@/db/schema";
-import { db } from "@/shared/lib/db";
-import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
-import {
- compareDateOnly,
- getBusinessDateString,
- isDateOnlyPast,
- toDateOnlyString,
-} from "@/shared/utils/date";
-import { safeToNumber as toNumber } from "@/shared/utils/number";
-
-const PAYMENT_METHOD_BOLETO = "Boleto";
-
-type RawDashboardBill = {
- id: string;
- name: string;
- amount: string | number | null;
- dueDate: string | Date | null;
- boletoPaymentDate: string | Date | null;
- isSettled: boolean | null;
-};
-
-export type DashboardBill = {
- id: string;
- name: string;
- amount: number;
- dueDate: string | null;
- boletoPaymentDate: string | null;
- isSettled: boolean;
-};
-
-export type DashboardBillsSnapshot = {
- bills: DashboardBill[];
- totalPendingAmount: number;
- pendingCount: number;
-};
-
-const compareDateOnlyAscWithNullsLast = (
- left: string | null,
- right: string | null,
-) => {
- if (!left && !right) return 0;
- if (!left) return 1;
- if (!right) return -1;
- return compareDateOnly(left, right);
-};
-
-const compareDateOnlyDescWithNullsLast = (
- left: string | null,
- right: string | null,
-) => {
- if (!left && !right) return 0;
- if (!left) return 1;
- if (!right) return -1;
- return compareDateOnly(right, left);
-};
-
-export async function fetchDashboardBills(
- userId: string,
- period: string,
-): Promise {
- const today = getBusinessDateString();
- const adminPayerId = await getAdminPayerId(userId);
- if (!adminPayerId) {
- return { bills: [], totalPendingAmount: 0, pendingCount: 0 };
- }
-
- const rows = await db
- .select({
- id: transactions.id,
- name: transactions.name,
- amount: transactions.amount,
- dueDate: transactions.dueDate,
- boletoPaymentDate: transactions.boletoPaymentDate,
- isSettled: transactions.isSettled,
- })
- .from(transactions)
- .where(
- and(
- eq(transactions.userId, userId),
- eq(transactions.period, period),
- eq(transactions.paymentMethod, PAYMENT_METHOD_BOLETO),
- eq(transactions.payerId, adminPayerId),
- ),
- );
-
- const bills = rows.map((row: RawDashboardBill): DashboardBill => {
- const amount = Math.abs(toNumber(row.amount));
- return {
- id: row.id,
- name: row.name,
- amount,
- dueDate: toDateOnlyString(row.dueDate),
- boletoPaymentDate: toDateOnlyString(row.boletoPaymentDate),
- isSettled: Boolean(row.isSettled),
- };
- });
-
- bills.sort((a, b) => {
- if (a.isSettled !== b.isSettled) {
- return a.isSettled ? 1 : -1;
- }
-
- if (!a.isSettled && !b.isSettled) {
- const aIsOverdue = a.dueDate ? isDateOnlyPast(a.dueDate, today) : false;
- const bIsOverdue = b.dueDate ? isDateOnlyPast(b.dueDate, today) : false;
-
- if (aIsOverdue !== bIsOverdue) {
- return aIsOverdue ? -1 : 1;
- }
-
- const dueDateDiff = compareDateOnlyAscWithNullsLast(a.dueDate, b.dueDate);
- if (dueDateDiff !== 0) {
- return dueDateDiff;
- }
-
- const amountDiff = b.amount - a.amount;
- if (amountDiff !== 0) {
- return amountDiff;
- }
- }
-
- if (a.isSettled && b.isSettled) {
- const paidAtDiff = compareDateOnlyDescWithNullsLast(
- a.boletoPaymentDate,
- b.boletoPaymentDate,
- );
- if (paidAtDiff !== 0) {
- return paidAtDiff;
- }
-
- const amountDiff = b.amount - a.amount;
- if (amountDiff !== 0) {
- return amountDiff;
- }
- }
-
- const nameDiff = a.name.localeCompare(b.name, "pt-BR", {
- sensitivity: "base",
- });
- if (nameDiff !== 0) {
- return nameDiff;
- }
-
- return a.id.localeCompare(b.id);
- });
-
- let totalPendingAmount = 0;
- let pendingCount = 0;
-
- for (const bill of bills) {
- if (!bill.isSettled) {
- totalPendingAmount += bill.amount;
- pendingCount += 1;
- }
- }
-
- return {
- bills,
- totalPendingAmount,
- pendingCount,
- };
-}
diff --git a/src/features/dashboard/bills-helpers.ts b/src/features/dashboard/bills/bills-helpers.ts
similarity index 89%
rename from src/features/dashboard/bills-helpers.ts
rename to src/features/dashboard/bills/bills-helpers.ts
index 7b6badf..99c2552 100644
--- a/src/features/dashboard/bills-helpers.ts
+++ b/src/features/dashboard/bills/bills-helpers.ts
@@ -1,5 +1,5 @@
-import type { DashboardBill } from "@/features/dashboard/bills-queries";
-import type { PaymentDialogState } from "@/features/dashboard/use-payment-dialog-controller";
+import type { DashboardBill } from "@/features/dashboard/bills/bills-queries";
+import type { PaymentDialogState } from "@/features/dashboard/payments/use-payment-dialog-controller";
import { getBusinessDateString, isDateOnlyPast } from "@/shared/utils/date";
import {
buildFinancialStatusLabel,
diff --git a/src/features/dashboard/bills/bills-queries.ts b/src/features/dashboard/bills/bills-queries.ts
new file mode 100644
index 0000000..cd2c1a9
--- /dev/null
+++ b/src/features/dashboard/bills/bills-queries.ts
@@ -0,0 +1,14 @@
+export type DashboardBill = {
+ id: string;
+ name: string;
+ amount: number;
+ dueDate: string | null;
+ boletoPaymentDate: string | null;
+ isSettled: boolean;
+};
+
+export type DashboardBillsSnapshot = {
+ bills: DashboardBill[];
+ totalPendingAmount: number;
+ pendingCount: number;
+};
diff --git a/src/features/dashboard/use-bill-widget-controller.ts b/src/features/dashboard/bills/use-bill-widget-controller.ts
similarity index 80%
rename from src/features/dashboard/use-bill-widget-controller.ts
rename to src/features/dashboard/bills/use-bill-widget-controller.ts
index da2e316..b68cf20 100644
--- a/src/features/dashboard/use-bill-widget-controller.ts
+++ b/src/features/dashboard/bills/use-bill-widget-controller.ts
@@ -4,17 +4,17 @@ import {
type BillDialogState,
getCurrentBillDateString,
markBillAsSettled,
-} 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 {
type PaymentDialogController,
usePaymentDialogController,
-} from "@/features/dashboard/use-payment-dialog-controller";
+} from "@/features/dashboard/payments/use-payment-dialog-controller";
import { toggleTransactionSettlementAction } from "@/features/transactions/actions";
const EMPTY_BILLS: DashboardBill[] = [];
-export type BillWidgetController = Omit<
+type BillWidgetController = Omit<
PaymentDialogController,
"selectedItem"
> & {
diff --git a/src/features/dashboard/categories/category-breakdown.ts b/src/features/dashboard/categories/category-breakdown-helpers.ts
similarity index 100%
rename from src/features/dashboard/categories/category-breakdown.ts
rename to src/features/dashboard/categories/category-breakdown-helpers.ts
diff --git a/src/features/dashboard/categories/category-history-queries.ts b/src/features/dashboard/categories/category-history-queries.ts
index a7b89d4..5b53e95 100644
--- a/src/features/dashboard/categories/category-history-queries.ts
+++ b/src/features/dashboard/categories/category-history-queries.ts
@@ -51,7 +51,7 @@ type UniqueCategory = {
icon: string | null;
};
-export async function fetchAllCategories(
+async function fetchAllCategories(
userId: string,
): Promise {
const result = await db
diff --git a/src/features/dashboard/category-overview-queries.ts b/src/features/dashboard/categories/category-overview-queries.ts
similarity index 97%
rename from src/features/dashboard/category-overview-queries.ts
rename to src/features/dashboard/categories/category-overview-queries.ts
index bfdcd9d..7bb96b5 100644
--- a/src/features/dashboard/category-overview-queries.ts
+++ b/src/features/dashboard/categories/category-overview-queries.ts
@@ -8,14 +8,14 @@ import {
import {
buildCategoryBreakdownData,
type DashboardCategoryBreakdownData,
-} from "@/features/dashboard/categories/category-breakdown";
+} from "@/features/dashboard/categories/category-breakdown-helpers";
import type { ExpensesByCategoryData } from "@/features/dashboard/categories/expenses-by-category-queries";
import type { IncomeByCategoryData } from "@/features/dashboard/categories/income-by-category-queries";
import type {
GoalProgressCategory,
GoalProgressItem,
GoalsProgressData,
-} from "@/features/dashboard/goals-progress-queries";
+} from "@/features/dashboard/goals-progress/goals-progress-queries";
import {
buildDashboardAdminFilters,
excludeAutoInvoiceEntries,
@@ -50,7 +50,7 @@ type BudgetSnapshotRow = {
amount: string | number | null;
};
-export type DashboardCategoryOverview = {
+type DashboardCategoryOverview = {
goalsProgressData: GoalsProgressData;
incomeByCategoryData: IncomeByCategoryData;
expensesByCategoryData: ExpensesByCategoryData;
diff --git a/src/features/dashboard/categories/expenses-by-category-queries.ts b/src/features/dashboard/categories/expenses-by-category-queries.ts
index 1a44527..4e0a08d 100644
--- a/src/features/dashboard/categories/expenses-by-category-queries.ts
+++ b/src/features/dashboard/categories/expenses-by-category-queries.ts
@@ -1,82 +1,3 @@
-import { and, eq, inArray, sql } from "drizzle-orm";
-import {
- budgets,
- categories,
- financialAccounts,
- transactions,
-} from "@/db/schema";
-import {
- buildCategoryBreakdownData,
- type DashboardCategoryBreakdownData,
- type DashboardCategoryBreakdownItem,
-} from "@/features/dashboard/categories/category-breakdown";
-import {
- buildDashboardAdminFilters,
- excludeAutoInvoiceEntries,
- excludeTransactionsFromExcludedAccounts,
-} from "@/features/dashboard/transaction-filters";
-import { db } from "@/shared/lib/db";
-import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
-import { getPreviousPeriod } from "@/shared/utils/period";
+import type { DashboardCategoryBreakdownData } from "@/features/dashboard/categories/category-breakdown-helpers";
-export type CategoryExpenseItem = DashboardCategoryBreakdownItem;
export type ExpensesByCategoryData = DashboardCategoryBreakdownData;
-
-export async function fetchExpensesByCategory(
- userId: string,
- period: string,
-): Promise {
- const previousPeriod = getPreviousPeriod(period);
-
- const adminPayerId = await getAdminPayerId(userId);
- if (!adminPayerId) {
- return { categories: [], currentTotal: 0, previousTotal: 0 };
- }
-
- // Single query: GROUP BY categoryId + period for both current and previous periods
- const [rows, budgetRows] = await Promise.all([
- db
- .select({
- categoryId: categories.id,
- categoryName: categories.name,
- categoryIcon: categories.icon,
- period: transactions.period,
- total: sql`coalesce(sum(${transactions.amount}), 0)`,
- })
- .from(transactions)
- .innerJoin(categories, eq(transactions.categoryId, categories.id))
- .leftJoin(
- financialAccounts,
- eq(transactions.accountId, financialAccounts.id),
- )
- .where(
- and(
- ...buildDashboardAdminFilters({ userId, adminPayerId }),
- inArray(transactions.period, [period, previousPeriod]),
- eq(transactions.transactionType, "Despesa"),
- eq(categories.type, "despesa"),
- excludeAutoInvoiceEntries(),
- excludeTransactionsFromExcludedAccounts(),
- ),
- )
- .groupBy(
- categories.id,
- categories.name,
- categories.icon,
- transactions.period,
- ),
- db
- .select({
- categoryId: budgets.categoryId,
- amount: budgets.amount,
- })
- .from(budgets)
- .where(and(eq(budgets.userId, userId), eq(budgets.period, period))),
- ]);
-
- return buildCategoryBreakdownData({
- rows,
- budgetRows,
- period,
- });
-}
diff --git a/src/features/dashboard/categories/income-by-category-queries.ts b/src/features/dashboard/categories/income-by-category-queries.ts
index d59d343..4892bb3 100644
--- a/src/features/dashboard/categories/income-by-category-queries.ts
+++ b/src/features/dashboard/categories/income-by-category-queries.ts
@@ -1,84 +1,3 @@
-import { and, eq, inArray, sql } from "drizzle-orm";
-import {
- budgets,
- categories,
- financialAccounts,
- transactions,
-} from "@/db/schema";
-import {
- buildCategoryBreakdownData,
- type DashboardCategoryBreakdownData,
- type DashboardCategoryBreakdownItem,
-} from "@/features/dashboard/categories/category-breakdown";
-import {
- buildDashboardAdminFilters,
- excludeAutoInvoiceEntries,
- excludeInitialBalanceWhenConfigured,
- excludeTransactionsFromExcludedAccounts,
-} from "@/features/dashboard/transaction-filters";
-import { db } from "@/shared/lib/db";
-import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
-import { getPreviousPeriod } from "@/shared/utils/period";
+import type { DashboardCategoryBreakdownData } from "@/features/dashboard/categories/category-breakdown-helpers";
-export type CategoryIncomeItem = DashboardCategoryBreakdownItem;
export type IncomeByCategoryData = DashboardCategoryBreakdownData;
-
-export async function fetchIncomeByCategory(
- userId: string,
- period: string,
-): Promise {
- const previousPeriod = getPreviousPeriod(period);
-
- const adminPayerId = await getAdminPayerId(userId);
- if (!adminPayerId) {
- return { categories: [], currentTotal: 0, previousTotal: 0 };
- }
-
- // Single query: GROUP BY categoryId + period for both current and previous periods
- const [rows, budgetRows] = await Promise.all([
- db
- .select({
- categoryId: categories.id,
- categoryName: categories.name,
- categoryIcon: categories.icon,
- period: transactions.period,
- total: sql`coalesce(sum(${transactions.amount}), 0)`,
- })
- .from(transactions)
- .innerJoin(categories, eq(transactions.categoryId, categories.id))
- .leftJoin(
- financialAccounts,
- eq(transactions.accountId, financialAccounts.id),
- )
- .where(
- and(
- ...buildDashboardAdminFilters({ userId, adminPayerId }),
- inArray(transactions.period, [period, previousPeriod]),
- eq(transactions.transactionType, "Receita"),
- eq(categories.type, "receita"),
- excludeAutoInvoiceEntries(),
- excludeInitialBalanceWhenConfigured(),
- excludeTransactionsFromExcludedAccounts(),
- ),
- )
- .groupBy(
- categories.id,
- categories.name,
- categories.icon,
- transactions.period,
- ),
- db
- .select({
- categoryId: budgets.categoryId,
- amount: budgets.amount,
- })
- .from(budgets)
- .where(and(eq(budgets.userId, userId), eq(budgets.period, period))),
- ]);
-
- return buildCategoryBreakdownData({
- rows,
- budgetRows,
- period,
- });
-}
diff --git a/src/features/dashboard/categories/purchases-by-category-queries.ts b/src/features/dashboard/categories/purchases-by-category-queries.ts
new file mode 100644
index 0000000..986abf0
--- /dev/null
+++ b/src/features/dashboard/categories/purchases-by-category-queries.ts
@@ -0,0 +1,18 @@
+export type CategoryOption = {
+ id: string;
+ name: string;
+ type: string;
+};
+
+export type CategoryTransaction = {
+ id: string;
+ name: string;
+ amount: number;
+ purchaseDate: Date;
+ logo: string | null;
+};
+
+export type PurchasesByCategoryData = {
+ categories: CategoryOption[];
+ transactionsByCategory: Record;
+};
diff --git a/src/features/dashboard/components/bills/bill-list-item.tsx b/src/features/dashboard/components/bills/bill-list-item.tsx
index 0dd6eff..e991712 100644
--- a/src/features/dashboard/components/bills/bill-list-item.tsx
+++ b/src/features/dashboard/components/bills/bill-list-item.tsx
@@ -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 ? (
-
- Pago
+
+ Pago
) : overdue ? (
diff --git a/src/features/dashboard/components/bills/bill-payment-dialog.tsx b/src/features/dashboard/components/bills/bill-payment-dialog.tsx
index d9872ae..80d5ffa 100644
--- a/src/features/dashboard/components/bills/bill-payment-dialog.tsx
+++ b/src/features/dashboard/components/bills/bill-payment-dialog.tsx
@@ -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";
diff --git a/src/features/dashboard/components/bills/bills-list.tsx b/src/features/dashboard/components/bills/bills-list.tsx
index 7e9ebe4..ebd4b2d 100644
--- a/src/features/dashboard/components/bills/bills-list.tsx
+++ b/src/features/dashboard/components/bills/bills-list.tsx
@@ -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";
diff --git a/src/features/dashboard/components/bills/bills-widget-view.tsx b/src/features/dashboard/components/bills/bills-widget-view.tsx
index c001f6d..4db437e 100644
--- a/src/features/dashboard/components/bills/bills-widget-view.tsx
+++ b/src/features/dashboard/components/bills/bills-widget-view.tsx
@@ -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";
diff --git a/src/features/dashboard/components/category-breakdown/category-breakdown-chart.tsx b/src/features/dashboard/components/category-breakdown/category-breakdown-chart.tsx
new file mode 100644
index 0000000..e505f5f
--- /dev/null
+++ b/src/features/dashboard/components/category-breakdown/category-breakdown-chart.tsx
@@ -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 (
+
+
+
+
+ formatPercentage(
+ (payload as { percentage?: number } | undefined)?.percentage ??
+ 0,
+ percentageDigits,
+ )
+ }
+ outerRadius={75}
+ dataKey="value"
+ nameKey="category"
+ />
+ {
+ if (!active || !payload?.length) return null;
+ const entry = payload[0]?.payload;
+ if (!entry) return null;
+ return (
+
+
+
+
+ {entry.name}
+
+
+ {formatCurrency(entry.value)}
+
+
+ {formatPercentage(entry.percentage, percentageDigits)}{" "}
+ do total
+
+
+
+
+ );
+ }}
+ />
+
+
+
+
+ {chartData.map((entry, index) => (
+
+ ))}
+
+
+ );
+}
diff --git a/src/features/dashboard/components/category-breakdown/category-breakdown-list-item.tsx b/src/features/dashboard/components/category-breakdown/category-breakdown-list-item.tsx
new file mode 100644
index 0000000..103acfa
--- /dev/null
+++ b/src/features/dashboard/components/category-breakdown/category-breakdown-list-item.tsx
@@ -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 (
+
+
+
+
+
+
+
+ {category.categoryName}
+
+
+
+
+
+ {formatPercentage(
+ category.percentageOfTotal,
+ config.percentageDigits,
+ )}{" "}
+ da {config.shareLabel}
+
+ {hasBudget && category.budgetUsedPercentage !== null ? (
+ <>
+ ·
+
+
+ {budgetExceeded ? (
+ <>
+ excedeu{" "}
+
+ {formatCurrency(exceededAmount)}
+
+ >
+ ) : (
+ <>
+ {formatPercentage(
+ category.budgetUsedPercentage,
+ config.percentageDigits,
+ )}{" "}
+ do limite
+ {config.includeBudgetAmount &&
+ category.budgetAmount !== null
+ ? ` ${formatCurrency(category.budgetAmount)}`
+ : ""}
+ >
+ )}
+
+ >
+ ) : null}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/features/dashboard/components/category-breakdown/category-breakdown-list.tsx b/src/features/dashboard/components/category-breakdown/category-breakdown-list.tsx
new file mode 100644
index 0000000..f1d22f4
--- /dev/null
+++ b/src/features/dashboard/components/category-breakdown/category-breakdown-list.tsx
@@ -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 (
+
+ {categories.map((category) => (
+
+ ))}
+
+ );
+}
diff --git a/src/features/dashboard/components/category-breakdown/category-breakdown-widget-view.tsx b/src/features/dashboard/components/category-breakdown/category-breakdown-widget-view.tsx
index 1c234f3..87a94a5 100644
--- a/src/features/dashboard/components/category-breakdown/category-breakdown-widget-view.tsx
+++ b/src/features/dashboard/components/category-breakdown/category-breakdown-widget-view.tsx
@@ -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 (
-
+
Lista
-
+
Gráfico
@@ -190,195 +92,18 @@ export function CategoryBreakdownWidgetView({
-
- {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 (
-
-
-
-
-
-
-
-
-
- {category.categoryName}
-
-
-
-
-
-
- {formatPercentage(
- category.percentageOfTotal,
- config.percentageDigits,
- )}{" "}
- da {config.shareLabel}
-
- {hasBudget && category.budgetUsedPercentage !== null ? (
- <>
- ·
-
-
- {budgetExceeded ? (
- <>
- excedeu{" "}
-
- {formatCurrency(exceededAmount)}
-
- >
- ) : (
- <>
- {formatPercentage(
- category.budgetUsedPercentage,
- config.percentageDigits,
- )}{" "}
- do limite
- {config.includeBudgetAmount &&
- category.budgetAmount !== null
- ? ` ${formatCurrency(category.budgetAmount)}`
- : ""}
- >
- )}
-
- >
- ) : null}
-
-
-
-
-
-
- {category.percentageChange !== null ? (
-
- {hasIncrease ? (
-
- ) : null}
- {hasDecrease ? (
-
- ) : null}
- {formatPercentage(
- category.percentageChange,
- config.percentageDigits,
- )}
-
- ) : null}
-
-
-
- );
- })}
-
+
-
-
-
-
- formatPercentage(
- (payload as { percentage?: number } | undefined)
- ?.percentage ?? 0,
- config.percentageDigits,
- )
- }
- outerRadius={75}
- dataKey="value"
- nameKey="category"
- />
- {
- if (!active || !payload?.length) {
- return null;
- }
-
- const entry = payload[0]?.payload;
- if (!entry) {
- return null;
- }
-
- return (
-
-
-
-
- {entry.name}
-
-
- {formatCurrency(entry.value)}
-
-
- {formatPercentage(
- entry.percentage,
- config.percentageDigits,
- )}{" "}
- do total
-
-
-
-
- );
- }}
- />
-
-
-
-
- {chartData.map((entry, index) => (
-
- ))}
-
-
+
);
diff --git a/src/features/dashboard/components/dashboard-grid-editable.tsx b/src/features/dashboard/components/dashboard-grid-editable.tsx
index a6b768d..3beec5a 100644
--- a/src/features/dashboard/components/dashboard-grid-editable.tsx
+++ b/src/features/dashboard/components/dashboard-grid-editable.tsx
@@ -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";
diff --git a/src/features/dashboard/components/dashboard-metrics-cards.tsx b/src/features/dashboard/components/dashboard-metrics-cards.tsx
index ac80de0..0eaa89b 100644
--- a/src/features/dashboard/components/dashboard-metrics-cards.tsx
+++ b/src/features/dashboard/components/dashboard-metrics-cards.tsx
@@ -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 (
@@ -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 (
-
-
-
-
-
- {label}
-
-
-
- {subtitle}
-
-
-
+
+
+
+ {label}
+
+
+
+ {subtitle}
+
@@ -183,15 +163,14 @@ export function DashboardMetricsCards({ metrics }: DashboardMetricsCardsProps) {
className="text-2xl leading-none font-medium"
amount={metric.current}
/>
-
-
- {percentChange}
-
+
diff --git a/src/features/dashboard/components/dashboard-welcome.tsx b/src/features/dashboard/components/dashboard-welcome.tsx
index 3bea253..eff5650 100644
--- a/src/features/dashboard/components/dashboard-welcome.tsx
+++ b/src/features/dashboard/components/dashboard-welcome.tsx
@@ -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 (
-
-
-
- {greeting}, {displayName}
-
- {formattedDate}
-
+
+
+ {greeting}, {displayName}
+
+ {formattedDate}
);
}
diff --git a/src/features/dashboard/components/goals-progress/goal-progress-item.tsx b/src/features/dashboard/components/goals-progress/goals-progress-item.tsx
similarity index 82%
rename from src/features/dashboard/components/goals-progress/goal-progress-item.tsx
rename to src/features/dashboard/components/goals-progress/goals-progress-item.tsx
index 9a7c2e0..fe487cc 100644
--- a/src/features/dashboard/components/goals-progress/goal-progress-item.tsx
+++ b/src/features/dashboard/components/goals-progress/goals-progress-item.tsx
@@ -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({
{" "}
de{" "}
-
- {formatGoalProgressPercentage(percentageDelta, true)}
-
+
diff --git a/src/features/dashboard/components/goals-progress/goals-progress-list.tsx b/src/features/dashboard/components/goals-progress/goals-progress-list.tsx
index 3cacbe6..d97af68 100644
--- a/src/features/dashboard/components/goals-progress/goals-progress-list.tsx
+++ b/src/features/dashboard/components/goals-progress/goals-progress-list.tsx
@@ -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[];
diff --git a/src/features/dashboard/components/goals-progress/goals-progress-widget-view.tsx b/src/features/dashboard/components/goals-progress/goals-progress-widget-view.tsx
index 59bd676..59910f2 100644
--- a/src/features/dashboard/components/goals-progress/goals-progress-widget-view.tsx
+++ b/src/features/dashboard/components/goals-progress/goals-progress-widget-view.tsx
@@ -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";
diff --git a/src/features/dashboard/components/installment-analysis/installment-analysis-page.tsx b/src/features/dashboard/components/installment-analysis/installment-analysis-page.tsx
index f60aebe..2a5fceb 100644
--- a/src/features/dashboard/components/installment-analysis/installment-analysis-page.tsx
+++ b/src/features/dashboard/components/installment-analysis/installment-analysis-page.tsx
@@ -130,7 +130,7 @@ export function InstallmentAnalysisPage({
return (
{/* Card de resumo principal */}
-
+
Se você pagar tudo que está selecionado:
diff --git a/src/features/dashboard/components/installment-expenses/installment-expense-list-item.tsx b/src/features/dashboard/components/installment-expenses/installment-expense-list-item.tsx
index 22ddd1b..11f2b54 100644
--- a/src/features/dashboard/components/installment-expenses/installment-expense-list-item.tsx
+++ b/src/features/dashboard/components/installment-expenses/installment-expense-list-item.tsx
@@ -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";
diff --git a/src/features/dashboard/components/invoices/invoice-list-item.tsx b/src/features/dashboard/components/invoices/invoice-list-item.tsx
index fcb0672..2a33135 100644
--- a/src/features/dashboard/components/invoices/invoice-list-item.tsx
+++ b/src/features/dashboard/components/invoices/invoice-list-item.tsx
@@ -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 ? (
{linkNode}
-
+
Distribuição por pagador
@@ -115,11 +116,14 @@ export function InvoiceListItem({ invoice, onPay }: InvoiceListItemProps) {
)}
-
+
))}
@@ -179,8 +183,8 @@ export function InvoiceListItem({ invoice, onPay }: InvoiceListItemProps) {
onClick={() => onPay(invoice.id)}
>
{isPaid ? (
-
- Pago
+
+ Pago
) : isOverdue ? (
diff --git a/src/features/dashboard/components/invoices/invoice-logo.tsx b/src/features/dashboard/components/invoices/invoice-logo.tsx
index 26c2fd2..e7bcc16 100644
--- a/src/features/dashboard/components/invoices/invoice-logo.tsx
+++ b/src/features/dashboard/components/invoices/invoice-logo.tsx
@@ -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";
diff --git a/src/features/dashboard/components/invoices/invoice-payment-dialog.tsx b/src/features/dashboard/components/invoices/invoice-payment-dialog.tsx
index a86ea47..8c0286c 100644
--- a/src/features/dashboard/components/invoices/invoice-payment-dialog.tsx
+++ b/src/features/dashboard/components/invoices/invoice-payment-dialog.tsx
@@ -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";
diff --git a/src/features/dashboard/components/invoices/invoices-list.tsx b/src/features/dashboard/components/invoices/invoices-list.tsx
index 5b24bbb..c9ce3d5 100644
--- a/src/features/dashboard/components/invoices/invoices-list.tsx
+++ b/src/features/dashboard/components/invoices/invoices-list.tsx
@@ -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";
diff --git a/src/features/dashboard/components/invoices/invoices-widget-view.tsx b/src/features/dashboard/components/invoices/invoices-widget-view.tsx
index 88ab909..77f910c 100644
--- a/src/features/dashboard/components/invoices/invoices-widget-view.tsx
+++ b/src/features/dashboard/components/invoices/invoices-widget-view.tsx
@@ -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";
diff --git a/src/features/dashboard/components/notes/note-list-item.tsx b/src/features/dashboard/components/notes/note-list-item.tsx
index 07a7c5b..a81a341 100644
--- a/src/features/dashboard/components/notes/note-list-item.tsx
+++ b/src/features/dashboard/components/notes/note-list-item.tsx
@@ -29,14 +29,12 @@ export function NoteListItem({
{displayTitle}
-
+
{getNoteTasksSummary(note)}
- {createdAtLabel ? (
-
- {createdAtLabel}
-
- ) : null}
+
+ {createdAtLabel}
+
diff --git a/src/features/dashboard/components/payment-overview/payment-breakdown-list-item.tsx b/src/features/dashboard/components/payment-overview/payment-breakdown-list-item.tsx
index c645fe4..27b641d 100644
--- a/src/features/dashboard/components/payment-overview/payment-breakdown-list-item.tsx
+++ b/src/features/dashboard/components/payment-overview/payment-breakdown-list-item.tsx
@@ -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 {
diff --git a/src/features/dashboard/components/payment-overview/payment-overview-widget-view.tsx b/src/features/dashboard/components/payment-overview/payment-overview-widget-view.tsx
index e32245f..d4a9b38 100644
--- a/src/features/dashboard/components/payment-overview/payment-overview-widget-view.tsx
+++ b/src/features/dashboard/components/payment-overview/payment-overview-widget-view.tsx
@@ -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 (
-
+
Condições
-
+
Formas
diff --git a/src/features/dashboard/components/percentage-change-indicator.tsx b/src/features/dashboard/components/percentage-change-indicator.tsx
new file mode 100644
index 0000000..182dff5
--- /dev/null
+++ b/src/features/dashboard/components/percentage-change-indicator.tsx
@@ -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;
+ 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 (
+
+ {resolvedTrend === "up" ? (
+
+ ) : null}
+ {resolvedTrend === "down" ? (
+
+ ) : null}
+ {resolvedTrend === "flat" && showFlatIcon ? (
+
+ ) : null}
+ {resolvedLabel}
+
+ );
+}
diff --git a/src/features/dashboard/components/attachments-widget.tsx b/src/features/dashboard/components/widgets/attachments-widget.tsx
similarity index 100%
rename from src/features/dashboard/components/attachments-widget.tsx
rename to src/features/dashboard/components/widgets/attachments-widget.tsx
diff --git a/src/features/dashboard/components/bill-widget.tsx b/src/features/dashboard/components/widgets/bill-widget.tsx
similarity index 71%
rename from src/features/dashboard/components/bill-widget.tsx
rename to src/features/dashboard/components/widgets/bill-widget.tsx
index 4ef2bb9..e935c54 100644
--- a/src/features/dashboard/components/bill-widget.tsx
+++ b/src/features/dashboard/components/widgets/bill-widget.tsx
@@ -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[];
diff --git a/src/features/dashboard/components/category-history-widget.tsx b/src/features/dashboard/components/widgets/category-history-widget.tsx
similarity index 100%
rename from src/features/dashboard/components/category-history-widget.tsx
rename to src/features/dashboard/components/widgets/category-history-widget.tsx
diff --git a/src/features/dashboard/components/category-trends-widget.tsx b/src/features/dashboard/components/widgets/category-trends-widget.tsx
similarity index 75%
rename from src/features/dashboard/components/category-trends-widget.tsx
rename to src/features/dashboard/components/widgets/category-trends-widget.tsx
index 28feb43..46e91e9 100644
--- a/src/features/dashboard/components/category-trends-widget.tsx
+++ b/src/features/dashboard/components/widgets/category-trends-widget.tsx
@@ -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({
{trending.map((category) => {
const change = category.percentageChange ?? 0;
- const isUp = change > 0;
return (
-
@@ -62,19 +58,17 @@ export function CategoryTrendsWidget({
/>
-
- {isUp ? (
-
- ) : (
-
- )}
- {Math.abs(change).toFixed(0)}%
-
+
);
diff --git a/src/features/dashboard/components/expenses-by-category-widget-with-chart.tsx b/src/features/dashboard/components/widgets/expenses-by-category-widget-with-chart.tsx
similarity index 81%
rename from src/features/dashboard/components/expenses-by-category-widget-with-chart.tsx
rename to src/features/dashboard/components/widgets/expenses-by-category-widget-with-chart.tsx
index 5473fad..6147170 100644
--- a/src/features/dashboard/components/expenses-by-category-widget-with-chart.tsx
+++ b/src/features/dashboard/components/widgets/expenses-by-category-widget-with-chart.tsx
@@ -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;
diff --git a/src/features/dashboard/components/goals-progress-widget.tsx b/src/features/dashboard/components/widgets/goals-progress-widget.tsx
similarity index 78%
rename from src/features/dashboard/components/goals-progress-widget.tsx
rename to src/features/dashboard/components/widgets/goals-progress-widget.tsx
index 4aaa7b8..2593329 100644
--- a/src/features/dashboard/components/goals-progress-widget.tsx
+++ b/src/features/dashboard/components/widgets/goals-progress-widget.tsx
@@ -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;
diff --git a/src/features/dashboard/components/inbox-widget.tsx b/src/features/dashboard/components/widgets/inbox-widget.tsx
similarity index 96%
rename from src/features/dashboard/components/inbox-widget.tsx
rename to src/features/dashboard/components/widgets/inbox-widget.tsx
index d87a0a9..1e02d92 100644
--- a/src/features/dashboard/components/inbox-widget.tsx
+++ b/src/features/dashboard/components/widgets/inbox-widget.tsx
@@ -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"
>
-
+
-
-
- {displayName}
+
+
+ {displayName.length > 30
+ ? `${displayName.slice(0, 30)}...`
+ : displayName}
{item.sourceAppName &&
{item.sourceAppName}}
diff --git a/src/features/dashboard/components/income-by-category-widget-with-chart.tsx b/src/features/dashboard/components/widgets/income-by-category-widget-with-chart.tsx
similarity index 80%
rename from src/features/dashboard/components/income-by-category-widget-with-chart.tsx
rename to src/features/dashboard/components/widgets/income-by-category-widget-with-chart.tsx
index 522c096..19a92ee 100644
--- a/src/features/dashboard/components/income-by-category-widget-with-chart.tsx
+++ b/src/features/dashboard/components/widgets/income-by-category-widget-with-chart.tsx
@@ -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;
diff --git a/src/features/dashboard/components/income-expense-balance-widget.tsx b/src/features/dashboard/components/widgets/income-expense-balance-widget.tsx
similarity index 97%
rename from src/features/dashboard/components/income-expense-balance-widget.tsx
rename to src/features/dashboard/components/widgets/income-expense-balance-widget.tsx
index d10cbd8..c3888aa 100644
--- a/src/features/dashboard/components/income-expense-balance-widget.tsx
+++ b/src/features/dashboard/components/widgets/income-expense-balance-widget.tsx
@@ -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;
diff --git a/src/features/dashboard/components/installment-expenses-widget.tsx b/src/features/dashboard/components/widgets/installment-expenses-widget.tsx
similarity index 75%
rename from src/features/dashboard/components/installment-expenses-widget.tsx
rename to src/features/dashboard/components/widgets/installment-expenses-widget.tsx
index 260f5c5..820b926 100644
--- a/src/features/dashboard/components/installment-expenses-widget.tsx
+++ b/src/features/dashboard/components/widgets/installment-expenses-widget.tsx
@@ -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;
diff --git a/src/features/dashboard/components/invoices-widget.tsx b/src/features/dashboard/components/widgets/invoices-widget.tsx
similarity index 84%
rename from src/features/dashboard/components/invoices-widget.tsx
rename to src/features/dashboard/components/widgets/invoices-widget.tsx
index ecf673b..1481144 100644
--- a/src/features/dashboard/components/invoices-widget.tsx
+++ b/src/features/dashboard/components/widgets/invoices-widget.tsx
@@ -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[];
diff --git a/src/features/dashboard/components/my-accounts-widget.tsx b/src/features/dashboard/components/widgets/my-accounts-widget.tsx
similarity index 99%
rename from src/features/dashboard/components/my-accounts-widget.tsx
rename to src/features/dashboard/components/widgets/my-accounts-widget.tsx
index 4eff4c5..3327936 100644
--- a/src/features/dashboard/components/my-accounts-widget.tsx
+++ b/src/features/dashboard/components/widgets/my-accounts-widget.tsx
@@ -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";
diff --git a/src/features/dashboard/components/notes-widget.tsx b/src/features/dashboard/components/widgets/notes-widget.tsx
similarity index 73%
rename from src/features/dashboard/components/notes-widget.tsx
rename to src/features/dashboard/components/widgets/notes-widget.tsx
index 2ccfe73..50b0223 100644
--- a/src/features/dashboard/components/notes-widget.tsx
+++ b/src/features/dashboard/components/widgets/notes-widget.tsx
@@ -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[];
diff --git a/src/features/dashboard/components/payers-widget.tsx b/src/features/dashboard/components/widgets/payers-widget.tsx
similarity index 80%
rename from src/features/dashboard/components/payers-widget.tsx
rename to src/features/dashboard/components/widgets/payers-widget.tsx
index fae2e57..ad1ca29 100644
--- a/src/features/dashboard/components/payers-widget.tsx
+++ b/src/features/dashboard/components/widgets/payers-widget.tsx
@@ -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 && (
-
0
- ? "text-destructive"
- : percentageChange < 0
- ? "text-success"
- : "text-muted-foreground"
- }`}
- >
- {percentageChange > 0 && (
-
- )}
- {percentageChange < 0 && (
-
- )}
- {formatPercentage(percentageChange)}
-
- )}
+
);
diff --git a/src/features/dashboard/components/payment-overview-widget.tsx b/src/features/dashboard/components/widgets/payment-overview-widget.tsx
similarity index 85%
rename from src/features/dashboard/components/payment-overview-widget.tsx
rename to src/features/dashboard/components/widgets/payment-overview-widget.tsx
index 4a0bb7b..275342d 100644
--- a/src/features/dashboard/components/payment-overview-widget.tsx
+++ b/src/features/dashboard/components/widgets/payment-overview-widget.tsx
@@ -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;
diff --git a/src/features/dashboard/components/payment-status-widget.tsx b/src/features/dashboard/components/widgets/payment-status-widget.tsx
similarity index 77%
rename from src/features/dashboard/components/payment-status-widget.tsx
rename to src/features/dashboard/components/widgets/payment-status-widget.tsx
index a7993da..c09b1d7 100644
--- a/src/features/dashboard/components/payment-status-widget.tsx
+++ b/src/features/dashboard/components/widgets/payment-status-widget.tsx
@@ -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;
diff --git a/src/features/dashboard/components/purchases-by-category-widget.tsx b/src/features/dashboard/components/widgets/purchases-by-category-widget.tsx
similarity index 99%
rename from src/features/dashboard/components/purchases-by-category-widget.tsx
rename to src/features/dashboard/components/widgets/purchases-by-category-widget.tsx
index c2f53d4..2804081 100644
--- a/src/features/dashboard/components/purchases-by-category-widget.tsx
+++ b/src/features/dashboard/components/widgets/purchases-by-category-widget.tsx
@@ -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 {
diff --git a/src/features/dashboard/components/recurring-expenses-widget.tsx b/src/features/dashboard/components/widgets/recurring-expenses-widget.tsx
similarity index 100%
rename from src/features/dashboard/components/recurring-expenses-widget.tsx
rename to src/features/dashboard/components/widgets/recurring-expenses-widget.tsx
diff --git a/src/features/dashboard/components/sortable-widget.tsx b/src/features/dashboard/components/widgets/sortable-widget.tsx
similarity index 100%
rename from src/features/dashboard/components/sortable-widget.tsx
rename to src/features/dashboard/components/widgets/sortable-widget.tsx
diff --git a/src/features/dashboard/components/spending-overview-widget.tsx b/src/features/dashboard/components/widgets/spending-overview-widget.tsx
similarity index 88%
rename from src/features/dashboard/components/spending-overview-widget.tsx
rename to src/features/dashboard/components/widgets/spending-overview-widget.tsx
index 22f34c3..a29b882 100644
--- a/src/features/dashboard/components/spending-overview-widget.tsx
+++ b/src/features/dashboard/components/widgets/spending-overview-widget.tsx
@@ -37,11 +37,17 @@ export function SpendingOverviewWidget({
className="w-full"
>
-
+
Top gastos
-
+
Estabelecimentos
diff --git a/src/features/dashboard/components/top-establishments-widget.tsx b/src/features/dashboard/components/widgets/top-establishments-widget.tsx
similarity index 100%
rename from src/features/dashboard/components/top-establishments-widget.tsx
rename to src/features/dashboard/components/widgets/top-establishments-widget.tsx
diff --git a/src/features/dashboard/components/top-expenses-widget.tsx b/src/features/dashboard/components/widgets/top-expenses-widget.tsx
similarity index 100%
rename from src/features/dashboard/components/top-expenses-widget.tsx
rename to src/features/dashboard/components/widgets/top-expenses-widget.tsx
diff --git a/src/features/dashboard/components/widget-settings-dialog.tsx b/src/features/dashboard/components/widgets/widget-settings-dialog.tsx
similarity index 96%
rename from src/features/dashboard/components/widget-settings-dialog.tsx
rename to src/features/dashboard/components/widgets/widget-settings-dialog.tsx
index c8a0f82..3e08720 100644
--- a/src/features/dashboard/components/widget-settings-dialog.tsx
+++ b/src/features/dashboard/components/widgets/widget-settings-dialog.tsx
@@ -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,
diff --git a/src/features/dashboard/dashboard-metrics-queries.ts b/src/features/dashboard/dashboard-metrics-queries.ts
deleted file mode 100644
index b721e34..0000000
--- a/src/features/dashboard/dashboard-metrics-queries.ts
+++ /dev/null
@@ -1,189 +0,0 @@
-import { and, asc, eq, gte, inArray, lte, sum } from "drizzle-orm";
-import { financialAccounts, transactions } from "@/db/schema";
-import {
- buildDashboardAdminFilters,
- excludeAutoInvoiceEntries,
- excludeInitialBalanceWhenConfigured,
- excludeTransactionsFromExcludedAccounts,
-} from "@/features/dashboard/transaction-filters";
-import { db } from "@/shared/lib/db";
-import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
-import { safeToNumber } from "@/shared/utils/number";
-import {
- addMonthsToPeriod,
- buildPeriodRange,
- comparePeriods,
- getPreviousPeriod,
-} from "@/shared/utils/period";
-
-const RECEITA = "Receita";
-const DESPESA = "Despesa";
-const TRANSFERENCIA = "Transferência";
-
-type MetricPair = {
- current: number;
- previous: number;
-};
-
-export type DashboardCardMetrics = {
- period: string;
- previousPeriod: string;
- receitas: MetricPair;
- despesas: MetricPair;
- balanco: MetricPair;
- previsto: MetricPair;
-};
-
-type PeriodTotals = {
- receitas: number;
- despesas: number;
- transferAdjustment: number;
- balanco: number;
-};
-
-const createEmptyTotals = (): PeriodTotals => ({
- receitas: 0,
- despesas: 0,
- transferAdjustment: 0,
- balanco: 0,
-});
-
-const ensurePeriodTotals = (
- store: Map,
- period: string,
-): PeriodTotals => {
- if (!store.has(period)) {
- store.set(period, createEmptyTotals());
- }
- const totals = store.get(period);
- // This should always exist since we just set it above
- if (!totals) {
- const emptyTotals = createEmptyTotals();
- store.set(period, emptyTotals);
- return emptyTotals;
- }
- return totals;
-};
-
-// Re-export for backward compatibility
-
-export async function fetchDashboardCardMetrics(
- userId: string,
- period: string,
-): Promise {
- const previousPeriod = getPreviousPeriod(period);
-
- const adminPayerId = await getAdminPayerId(userId);
- if (!adminPayerId) {
- return {
- period,
- previousPeriod,
- receitas: { current: 0, previous: 0 },
- despesas: { current: 0, previous: 0 },
- balanco: { current: 0, previous: 0 },
- previsto: { current: 0, previous: 0 },
- };
- }
-
- // Limitar scan histórico a 24 meses para evitar scans progressivamente mais lentos
- const startPeriod = addMonthsToPeriod(period, -24);
-
- const rows = await db
- .select({
- period: transactions.period,
- transactionType: transactions.transactionType,
- totalAmount: sum(transactions.amount).as("total"),
- accountExcludeFromBalance: financialAccounts.excludeFromBalance,
- })
- .from(transactions)
- .leftJoin(
- financialAccounts,
- eq(transactions.accountId, financialAccounts.id),
- )
- .where(
- and(
- ...buildDashboardAdminFilters({ userId, adminPayerId }),
- gte(transactions.period, startPeriod),
- lte(transactions.period, period),
- inArray(transactions.transactionType, [
- RECEITA,
- DESPESA,
- TRANSFERENCIA,
- ]),
- excludeAutoInvoiceEntries(),
- excludeInitialBalanceWhenConfigured(),
- excludeTransactionsFromExcludedAccounts(),
- ),
- )
- .groupBy(
- transactions.period,
- transactions.transactionType,
- financialAccounts.excludeFromBalance,
- )
- .orderBy(asc(transactions.period), asc(transactions.transactionType));
-
- const periodTotals = new Map();
-
- for (const row of rows) {
- if (!row.period) continue;
- const totals = ensurePeriodTotals(periodTotals, row.period);
- const total = safeToNumber(row.totalAmount);
- if (row.transactionType === RECEITA) {
- totals.receitas += total;
- } else if (row.transactionType === DESPESA) {
- totals.despesas += Math.abs(total);
- } else if (
- row.transactionType === TRANSFERENCIA &&
- row.accountExcludeFromBalance === false
- ) {
- totals.transferAdjustment += total;
- }
- }
-
- ensurePeriodTotals(periodTotals, period);
- ensurePeriodTotals(periodTotals, previousPeriod);
-
- const earliestPeriod =
- periodTotals.size > 0 ? Array.from(periodTotals.keys()).sort()[0] : period;
-
- const startRangePeriod =
- comparePeriods(earliestPeriod, previousPeriod) <= 0
- ? earliestPeriod
- : previousPeriod;
-
- const periodRange = buildPeriodRange(startRangePeriod, period);
- const forecastByPeriod = new Map();
- let runningForecast = 0;
-
- for (const key of periodRange) {
- const totals = ensurePeriodTotals(periodTotals, key);
- totals.balanco =
- totals.receitas - totals.despesas + totals.transferAdjustment;
- runningForecast += totals.balanco;
- forecastByPeriod.set(key, runningForecast);
- }
-
- const currentTotals = ensurePeriodTotals(periodTotals, period);
- const previousTotals = ensurePeriodTotals(periodTotals, previousPeriod);
-
- return {
- period,
- previousPeriod,
- receitas: {
- current: currentTotals.receitas,
- previous: previousTotals.receitas,
- },
- despesas: {
- current: currentTotals.despesas,
- previous: previousTotals.despesas,
- },
- balanco: {
- current: currentTotals.balanco,
- previous: previousTotals.balanco,
- },
- previsto: {
- current: forecastByPeriod.get(period) ?? runningForecast,
- previous: forecastByPeriod.get(previousPeriod) ?? 0,
- },
- };
-}
diff --git a/src/features/dashboard/installment-expenses-helpers.ts b/src/features/dashboard/expenses/installment-expenses-helpers.ts
similarity index 100%
rename from src/features/dashboard/installment-expenses-helpers.ts
rename to src/features/dashboard/expenses/installment-expenses-helpers.ts
diff --git a/src/features/dashboard/expenses/installment-expenses-queries.ts b/src/features/dashboard/expenses/installment-expenses-queries.ts
index f239215..c52c7c0 100644
--- a/src/features/dashboard/expenses/installment-expenses-queries.ts
+++ b/src/features/dashboard/expenses/installment-expenses-queries.ts
@@ -1,13 +1,3 @@
-import { and, desc, eq, isNull, or, sql } from "drizzle-orm";
-import { transactions } from "@/db/schema";
-import {
- ACCOUNT_AUTO_INVOICE_NOTE_PREFIX,
- INITIAL_BALANCE_NOTE,
-} from "@/shared/lib/accounts/constants";
-import { db } from "@/shared/lib/db";
-import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
-import { safeToNumber as toNumber } from "@/shared/utils/number";
-
export type InstallmentExpense = {
id: string;
name: string;
@@ -23,78 +13,3 @@ export type InstallmentExpense = {
export type InstallmentExpensesData = {
expenses: InstallmentExpense[];
};
-
-export async function fetchInstallmentExpenses(
- userId: string,
- period: string,
-): Promise {
- const adminPayerId = await getAdminPayerId(userId);
- if (!adminPayerId) {
- return { expenses: [] };
- }
-
- const rows = await db
- .select({
- id: transactions.id,
- name: transactions.name,
- amount: transactions.amount,
- paymentMethod: transactions.paymentMethod,
- currentInstallment: transactions.currentInstallment,
- installmentCount: transactions.installmentCount,
- dueDate: transactions.dueDate,
- purchaseDate: transactions.purchaseDate,
- period: transactions.period,
- })
- .from(transactions)
- .where(
- and(
- eq(transactions.userId, userId),
- eq(transactions.period, period),
- eq(transactions.transactionType, "Despesa"),
- eq(transactions.condition, "Parcelado"),
- eq(transactions.isAnticipated, false),
- eq(transactions.payerId, adminPayerId),
- or(
- isNull(transactions.note),
- and(
- sql`${transactions.note} != ${INITIAL_BALANCE_NOTE}`,
- sql`${transactions.note} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`,
- ),
- ),
- ),
- )
- .orderBy(desc(transactions.purchaseDate), desc(transactions.createdAt));
-
- type InstallmentExpenseRow = (typeof rows)[number];
-
- const expenses = rows
- .map(
- (row: InstallmentExpenseRow): InstallmentExpense => ({
- id: row.id,
- name: row.name,
- amount: Math.abs(toNumber(row.amount)),
- paymentMethod: row.paymentMethod,
- currentInstallment: row.currentInstallment,
- installmentCount: row.installmentCount,
- dueDate: row.dueDate ?? null,
- purchaseDate: row.purchaseDate,
- period: row.period,
- }),
- )
- .sort((a: InstallmentExpense, b: InstallmentExpense) => {
- // Calcula parcelas restantes para cada item
- const remainingA =
- a.installmentCount && a.currentInstallment
- ? a.installmentCount - a.currentInstallment
- : 0;
- const remainingB =
- b.installmentCount && b.currentInstallment
- ? b.installmentCount - b.currentInstallment
- : 0;
-
- // Ordena do menor número de parcelas restantes para o maior
- return remainingA - remainingB;
- });
-
- return { expenses };
-}
diff --git a/src/features/dashboard/expenses/recurring-expenses-queries.ts b/src/features/dashboard/expenses/recurring-expenses-queries.ts
index 2cfe5cf..71c4e9b 100644
--- a/src/features/dashboard/expenses/recurring-expenses-queries.ts
+++ b/src/features/dashboard/expenses/recurring-expenses-queries.ts
@@ -1,13 +1,3 @@
-import { and, desc, eq, isNull, or, sql } from "drizzle-orm";
-import { transactions } from "@/db/schema";
-import {
- ACCOUNT_AUTO_INVOICE_NOTE_PREFIX,
- INITIAL_BALANCE_NOTE,
-} from "@/shared/lib/accounts/constants";
-import { db } from "@/shared/lib/db";
-import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
-import { safeToNumber as toNumber } from "@/shared/utils/number";
-
export type RecurringExpense = {
id: string;
name: string;
@@ -19,54 +9,3 @@ export type RecurringExpense = {
export type RecurringExpensesData = {
expenses: RecurringExpense[];
};
-
-export async function fetchRecurringExpenses(
- userId: string,
- period: string,
-): Promise {
- const adminPayerId = await getAdminPayerId(userId);
- if (!adminPayerId) {
- return { expenses: [] };
- }
-
- const results = await db
- .select({
- id: transactions.id,
- name: transactions.name,
- amount: transactions.amount,
- paymentMethod: transactions.paymentMethod,
- recurrenceCount: transactions.recurrenceCount,
- })
- .from(transactions)
- .where(
- and(
- eq(transactions.userId, userId),
- eq(transactions.period, period),
- eq(transactions.transactionType, "Despesa"),
- eq(transactions.condition, "Recorrente"),
- eq(transactions.payerId, adminPayerId),
- or(
- isNull(transactions.note),
- and(
- sql`${transactions.note} != ${INITIAL_BALANCE_NOTE}`,
- sql`${transactions.note} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`,
- ),
- ),
- ),
- )
- .orderBy(desc(transactions.purchaseDate), desc(transactions.createdAt));
-
- const expenses = results.map(
- (row): RecurringExpense => ({
- id: row.id,
- name: row.name,
- amount: Math.abs(toNumber(row.amount)),
- paymentMethod: row.paymentMethod,
- recurrenceCount: row.recurrenceCount,
- }),
- );
-
- return {
- expenses,
- };
-}
diff --git a/src/features/dashboard/expenses/top-expenses-queries.ts b/src/features/dashboard/expenses/top-expenses-queries.ts
index 59edd76..f6f0b7b 100644
--- a/src/features/dashboard/expenses/top-expenses-queries.ts
+++ b/src/features/dashboard/expenses/top-expenses-queries.ts
@@ -1,13 +1,3 @@
-import { and, asc, eq } from "drizzle-orm";
-import { cards, financialAccounts, transactions } from "@/db/schema";
-import {
- buildDashboardAdminPeriodFilters,
- excludeAutoGeneratedEntryNotes,
-} from "@/features/dashboard/transaction-filters";
-import { db } from "@/shared/lib/db";
-import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
-import { safeToNumber as toNumber } from "@/shared/utils/number";
-
export type TopExpense = {
id: string;
name: string;
@@ -20,66 +10,3 @@ export type TopExpense = {
export type TopExpensesData = {
expenses: TopExpense[];
};
-
-export async function fetchTopExpenses(
- userId: string,
- period: string,
- cardOnly: boolean = false,
-): Promise {
- const adminPayerId = await getAdminPayerId(userId);
- if (!adminPayerId) {
- return { expenses: [] };
- }
-
- const conditions = [
- ...buildDashboardAdminPeriodFilters({
- userId,
- period,
- adminPayerId,
- }),
- eq(transactions.transactionType, "Despesa"),
- excludeAutoGeneratedEntryNotes(),
- ];
-
- // Se cardOnly for true, filtra apenas pagamentos com cartão
- if (cardOnly) {
- conditions.push(eq(transactions.paymentMethod, "Cartão de Crédito"));
- }
-
- const results = await db
- .select({
- id: transactions.id,
- name: transactions.name,
- amount: transactions.amount,
- purchaseDate: transactions.purchaseDate,
- paymentMethod: transactions.paymentMethod,
- cardId: transactions.cardId,
- accountId: transactions.accountId,
- cardLogo: cards.logo,
- accountLogo: financialAccounts.logo,
- })
- .from(transactions)
- .leftJoin(cards, eq(transactions.cardId, cards.id))
- .leftJoin(
- financialAccounts,
- eq(transactions.accountId, financialAccounts.id),
- )
- .where(and(...conditions))
- .orderBy(asc(transactions.amount))
- .limit(10);
-
- const expenses = results.map(
- (row: (typeof results)[number]): TopExpense => ({
- id: row.id,
- name: row.name,
- amount: Math.abs(toNumber(row.amount)),
- purchaseDate: row.purchaseDate,
- paymentMethod: row.paymentMethod,
- logo: row.cardLogo ?? row.accountLogo ?? null,
- }),
- );
-
- return {
- expenses,
- };
-}
diff --git a/src/features/dashboard/fetch-dashboard-data.ts b/src/features/dashboard/fetch-dashboard-data.ts
index f091634..081a0d4 100644
--- a/src/features/dashboard/fetch-dashboard-data.ts
+++ b/src/features/dashboard/fetch-dashboard-data.ts
@@ -1,13 +1,13 @@
import { cacheLife, cacheTag } from "next/cache";
import { fetchAttachmentsForPeriod } from "@/features/attachments/queries";
import { fetchDashboardAccounts } from "./accounts-queries";
-import { fetchDashboardCategoryOverview } from "./category-overview-queries";
-import { fetchDashboardCurrentPeriodOverview } from "./current-period-overview-queries";
+import { fetchDashboardCategoryOverview } from "./categories/category-overview-queries";
import { fetchDashboardInboxSnapshot } from "./inbox-snapshot-queries";
-import { fetchDashboardInvoices } from "./invoices-queries";
-import { fetchDashboardNotes } from "./notes-queries";
+import { fetchDashboardInvoices } from "./invoices/invoices-queries";
+import { fetchDashboardNotes } from "./notes/notes-queries";
+import { fetchDashboardCurrentPeriodOverview } from "./overview/current-period-overview-queries";
+import { fetchDashboardPeriodOverview } from "./overview/period-overview-queries";
import { fetchDashboardPayers } from "./payers-queries";
-import { fetchDashboardPeriodOverview } from "./period-overview-queries";
async function fetchDashboardDataInternal(userId: string, period: string) {
const [
diff --git a/src/features/dashboard/goals-progress-queries.ts b/src/features/dashboard/goals-progress-queries.ts
deleted file mode 100644
index d739b32..0000000
--- a/src/features/dashboard/goals-progress-queries.ts
+++ /dev/null
@@ -1,147 +0,0 @@
-import { and, eq, ne, sql } from "drizzle-orm";
-import { budgets, categories, transactions } from "@/db/schema";
-import { db } from "@/shared/lib/db";
-import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
-import { safeToNumber as toNumber } from "@/shared/utils/number";
-
-const BUDGET_CRITICAL_THRESHOLD = 80;
-
-export type GoalProgressStatus = "on-track" | "critical" | "exceeded";
-
-export type GoalProgressItem = {
- id: string;
- categoryId: string | null;
- categoryName: string;
- categoryIcon: string | null;
- period: string;
- createdAt: string;
- budgetAmount: number;
- spentAmount: number;
- usedPercentage: number;
- status: GoalProgressStatus;
-};
-
-export type GoalProgressCategory = {
- id: string;
- name: string;
- icon: string | null;
-};
-
-export type GoalsProgressData = {
- items: GoalProgressItem[];
- categories: GoalProgressCategory[];
- totalBudgets: number;
- exceededCount: number;
- criticalCount: number;
-};
-
-const resolveStatus = (usedPercentage: number): GoalProgressStatus => {
- if (usedPercentage >= 100) {
- return "exceeded";
- }
- if (usedPercentage >= BUDGET_CRITICAL_THRESHOLD) {
- return "critical";
- }
- return "on-track";
-};
-
-export async function fetchGoalsProgressData(
- userId: string,
- period: string,
-): Promise {
- const adminPayerId = await getAdminPayerId(userId);
-
- if (!adminPayerId) {
- return {
- items: [],
- categories: [],
- totalBudgets: 0,
- exceededCount: 0,
- criticalCount: 0,
- };
- }
-
- const [rows, categoryRows] = await Promise.all([
- db
- .select({
- orcamentoId: budgets.id,
- categoryId: categories.id,
- categoryName: categories.name,
- categoryIcon: categories.icon,
- period: budgets.period,
- createdAt: budgets.createdAt,
- budgetAmount: budgets.amount,
- spentAmount: sql`COALESCE(SUM(ABS(${transactions.amount})), 0)`,
- })
- .from(budgets)
- .innerJoin(categories, eq(budgets.categoryId, categories.id))
- .leftJoin(
- transactions,
- and(
- eq(transactions.categoryId, budgets.categoryId),
- eq(transactions.userId, budgets.userId),
- eq(transactions.period, budgets.period),
- eq(transactions.payerId, adminPayerId),
- eq(transactions.transactionType, "Despesa"),
- ne(transactions.condition, "cancelado"),
- ),
- )
- .where(and(eq(budgets.userId, userId), eq(budgets.period, period)))
- .groupBy(
- budgets.id,
- categories.id,
- categories.name,
- categories.icon,
- budgets.period,
- budgets.createdAt,
- budgets.amount,
- ),
- db.query.categories.findMany({
- where: and(eq(categories.userId, userId), eq(categories.type, "despesa")),
- orderBy: (category, { asc }) => [asc(category.name)],
- }),
- ]);
-
- const categoryList: GoalProgressCategory[] = categoryRows.map((category) => ({
- id: category.id,
- name: category.name,
- icon: category.icon,
- }));
-
- const items: GoalProgressItem[] = rows
- .map((row) => {
- const budgetAmount = toNumber(row.budgetAmount);
- const spentAmount = toNumber(row.spentAmount);
- const usedPercentage =
- budgetAmount > 0 ? (spentAmount / budgetAmount) * 100 : 0;
-
- return {
- id: row.orcamentoId,
- categoryId: row.categoryId,
- categoryName: row.categoryName,
- categoryIcon: row.categoryIcon,
- period: row.period,
- createdAt: row.createdAt.toISOString(),
- budgetAmount,
- spentAmount,
- usedPercentage,
- status: resolveStatus(usedPercentage),
- };
- })
- .sort((a, b) => b.usedPercentage - a.usedPercentage);
-
- const exceededCount = items.filter(
- (item) => item.status === "exceeded",
- ).length;
- const criticalCount = items.filter(
- (item) => item.status === "critical",
- ).length;
-
- return {
- items,
- categories: categoryList,
- totalBudgets: items.length,
- exceededCount,
- criticalCount,
- };
-}
diff --git a/src/features/dashboard/goals-progress-helpers.ts b/src/features/dashboard/goals-progress/goals-progress-helpers.ts
similarity index 94%
rename from src/features/dashboard/goals-progress-helpers.ts
rename to src/features/dashboard/goals-progress/goals-progress-helpers.ts
index 9ced368..e51ee1a 100644
--- a/src/features/dashboard/goals-progress-helpers.ts
+++ b/src/features/dashboard/goals-progress/goals-progress-helpers.ts
@@ -6,7 +6,7 @@ import type {
GoalProgressCategory,
GoalProgressItem,
GoalProgressStatus,
-} from "@/features/dashboard/goals-progress-queries";
+} from "@/features/dashboard/goals-progress/goals-progress-queries";
import { formatPercentage } from "@/shared/utils/percentage";
export const clampGoalProgress = (value: number, min: number, max: number) =>
diff --git a/src/features/dashboard/goals-progress/goals-progress-queries.ts b/src/features/dashboard/goals-progress/goals-progress-queries.ts
new file mode 100644
index 0000000..d440bed
--- /dev/null
+++ b/src/features/dashboard/goals-progress/goals-progress-queries.ts
@@ -0,0 +1,28 @@
+export type GoalProgressStatus = "on-track" | "critical" | "exceeded";
+
+export type GoalProgressItem = {
+ id: string;
+ categoryId: string | null;
+ categoryName: string;
+ categoryIcon: string | null;
+ period: string;
+ createdAt: string;
+ budgetAmount: number;
+ spentAmount: number;
+ usedPercentage: number;
+ status: GoalProgressStatus;
+};
+
+export type GoalProgressCategory = {
+ id: string;
+ name: string;
+ icon: string | null;
+};
+
+export type GoalsProgressData = {
+ items: GoalProgressItem[];
+ categories: GoalProgressCategory[];
+ totalBudgets: number;
+ exceededCount: number;
+ criticalCount: number;
+};
diff --git a/src/features/dashboard/use-goals-progress-widget-controller.ts b/src/features/dashboard/goals-progress/use-goals-progress-widget-controller.ts
similarity index 87%
rename from src/features/dashboard/use-goals-progress-widget-controller.ts
rename to src/features/dashboard/goals-progress/use-goals-progress-widget-controller.ts
index 63f68be..e865d14 100644
--- a/src/features/dashboard/use-goals-progress-widget-controller.ts
+++ b/src/features/dashboard/goals-progress/use-goals-progress-widget-controller.ts
@@ -8,13 +8,13 @@ import type {
import {
mapGoalProgressCategoriesToBudgetCategories,
mapGoalProgressItemToBudget,
-} from "@/features/dashboard/goals-progress-helpers";
+} from "@/features/dashboard/goals-progress/goals-progress-helpers";
import type {
GoalProgressItem,
GoalsProgressData,
-} from "@/features/dashboard/goals-progress-queries";
+} from "@/features/dashboard/goals-progress/goals-progress-queries";
-export type GoalsProgressWidgetController = {
+type GoalsProgressWidgetController = {
selectedBudget: Budget | null;
editOpen: boolean;
categories: BudgetCategory[];
diff --git a/src/features/dashboard/income-expense-balance-queries.ts b/src/features/dashboard/income-expense-balance-queries.ts
deleted file mode 100644
index 8a24576..0000000
--- a/src/features/dashboard/income-expense-balance-queries.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-import { and, eq, inArray, sql } from "drizzle-orm";
-import { financialAccounts, transactions } from "@/db/schema";
-import {
- buildDashboardAdminFilters,
- excludeAutoInvoiceEntries,
- excludeInitialBalanceWhenConfigured,
- excludeTransactionsFromExcludedAccounts,
-} from "@/features/dashboard/transaction-filters";
-import { db } from "@/shared/lib/db";
-import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
-import { safeToNumber as toNumber } from "@/shared/utils/number";
-import {
- buildPeriodWindow,
- formatPeriodMonthShort,
- getCurrentPeriod,
-} from "@/shared/utils/period";
-
-export type MonthData = {
- month: string;
- monthLabel: string;
- income: number;
- expense: number;
- balance: number;
-};
-
-export type IncomeExpenseBalanceData = {
- months: MonthData[];
-};
-
-const generateLast6Months = (currentPeriod: string): string[] => {
- try {
- return buildPeriodWindow(currentPeriod, 6);
- } catch {
- return buildPeriodWindow(getCurrentPeriod(), 6);
- }
-};
-
-export async function fetchIncomeExpenseBalance(
- userId: string,
- currentPeriod: string,
-): Promise {
- const adminPayerId = await getAdminPayerId(userId);
- if (!adminPayerId) {
- return { months: [] };
- }
-
- const periods = generateLast6Months(currentPeriod);
-
- // Single query: GROUP BY period + transactionType instead of 12 separate queries
- const rows = await db
- .select({
- period: transactions.period,
- transactionType: transactions.transactionType,
- total: sql`coalesce(sum(${transactions.amount}), 0)`,
- accountExcludeFromBalance: financialAccounts.excludeFromBalance,
- })
- .from(transactions)
- .leftJoin(
- financialAccounts,
- eq(transactions.accountId, financialAccounts.id),
- )
- .where(
- and(
- ...buildDashboardAdminFilters({ userId, adminPayerId }),
- inArray(transactions.period, periods),
- inArray(transactions.transactionType, [
- "Receita",
- "Despesa",
- "Transferência",
- ]),
- excludeAutoInvoiceEntries(),
- excludeInitialBalanceWhenConfigured(),
- excludeTransactionsFromExcludedAccounts(),
- ),
- )
- .groupBy(
- transactions.period,
- transactions.transactionType,
- financialAccounts.excludeFromBalance,
- );
-
- // Build lookup from query results
- const dataMap = new Map<
- string,
- { income: number; expense: number; transferAdjustment: number }
- >();
- for (const row of rows) {
- if (!row.period) continue;
- const entry = dataMap.get(row.period) ?? {
- income: 0,
- expense: 0,
- transferAdjustment: 0,
- };
- const total = toNumber(row.total);
- if (row.transactionType === "Receita") {
- entry.income += Math.abs(total);
- } else if (row.transactionType === "Despesa") {
- entry.expense += Math.abs(total);
- } else if (
- row.transactionType === "Transferência" &&
- row.accountExcludeFromBalance === false
- ) {
- entry.transferAdjustment += total;
- }
- dataMap.set(row.period, entry);
- }
-
- // Build result array preserving period order
- const months = periods.map((period) => {
- const entry = dataMap.get(period) ?? {
- income: 0,
- expense: 0,
- transferAdjustment: 0,
- };
-
- return {
- month: period,
- monthLabel: formatPeriodMonthShort(period).toLowerCase(),
- income: entry.income,
- expense: entry.expense,
- balance: entry.income - entry.expense + entry.transferAdjustment,
- };
- });
-
- return { months };
-}
diff --git a/src/features/dashboard/invoices-helpers.ts b/src/features/dashboard/invoices/invoices-helpers.ts
similarity index 96%
rename from src/features/dashboard/invoices-helpers.ts
rename to src/features/dashboard/invoices/invoices-helpers.ts
index 317654d..5420724 100644
--- a/src/features/dashboard/invoices-helpers.ts
+++ b/src/features/dashboard/invoices/invoices-helpers.ts
@@ -1,5 +1,5 @@
-import type { DashboardInvoice } from "@/features/dashboard/invoices-queries";
-import type { PaymentDialogState } from "@/features/dashboard/use-payment-dialog-controller";
+import type { DashboardInvoice } from "@/features/dashboard/invoices/invoices-queries";
+import type { PaymentDialogState } from "@/features/dashboard/payments/use-payment-dialog-controller";
import {
INVOICE_PAYMENT_STATUS,
type InvoicePaymentStatus,
diff --git a/src/features/dashboard/invoices-queries.ts b/src/features/dashboard/invoices/invoices-queries.ts
similarity index 83%
rename from src/features/dashboard/invoices-queries.ts
rename to src/features/dashboard/invoices/invoices-queries.ts
index 3a7f7e5..6582ab9 100644
--- a/src/features/dashboard/invoices-queries.ts
+++ b/src/features/dashboard/invoices/invoices-queries.ts
@@ -1,4 +1,4 @@
-import { and, eq, ilike, isNotNull, sql } from "drizzle-orm";
+import { and, eq, ilike, inArray, isNotNull, sql } from "drizzle-orm";
import { cards, invoices, payers, transactions } from "@/db/schema";
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/shared/lib/accounts/constants";
import { db } from "@/shared/lib/db";
@@ -14,7 +14,9 @@ import {
isDateOnlyPast,
toDateOnlyString,
} from "@/shared/utils/date";
+import { calculatePercentageChange } from "@/shared/utils/math";
import { safeToNumber as toNumber } from "@/shared/utils/number";
+import { getPreviousPeriod } from "@/shared/utils/period";
type RawDashboardInvoice = {
invoiceId: string | null;
@@ -45,6 +47,7 @@ export type InvoicePagadorBreakdown = {
pagadorName: string;
pagadorAvatar: string | null;
amount: number;
+ percentageChange: number | null;
};
export type DashboardInvoice = {
@@ -62,7 +65,7 @@ export type DashboardInvoice = {
pagadorBreakdown: InvoicePagadorBreakdown[];
};
-export type DashboardInvoicesSnapshot = {
+type DashboardInvoicesSnapshot = {
invoices: DashboardInvoice[];
totalPending: number;
};
@@ -99,6 +102,7 @@ export async function fetchDashboardInvoices(
period: string,
): Promise {
const today = getBusinessDateString();
+ const previousPeriod = getPreviousPeriod(period);
const paymentRows = await db
.select({
note: transactions.note,
@@ -203,7 +207,7 @@ export async function fetchDashboardInvoices(
.where(
and(
eq(transactions.userId, userId),
- eq(transactions.period, period),
+ inArray(transactions.period, [period, previousPeriod]),
isNotNull(transactions.cardId),
),
)
@@ -216,23 +220,74 @@ export async function fetchDashboardInvoices(
),
])) as [RawDashboardInvoice[], RawInvoiceBreakdownRow[]];
- const breakdownMap = new Map();
+ const groupedBreakdown = new Map<
+ string,
+ {
+ cardId: string;
+ payerId: string | null;
+ pagadorName: string;
+ pagadorAvatar: string | null;
+ currentAmount: number;
+ previousAmount: number;
+ }
+ >();
+
for (const row of breakdownRows) {
if (!row.cardId) {
continue;
}
+
const resolvedPeriod = row.period ?? period;
const amount = Math.abs(toNumber(row.amount));
if (amount <= 0) {
continue;
}
- const key = `${row.cardId}:${resolvedPeriod}`;
+
+ const payerId = row.payerId ?? null;
+ const pagadorName = row.pagadorName?.trim() || "Sem pagador";
+ const pagadorAvatar = row.pagadorAvatar ?? null;
+ const payerKey = payerId ?? "__without-payer__";
+ const key = `${row.cardId}:${payerKey}`;
+ const current = groupedBreakdown.get(key) ?? {
+ cardId: row.cardId,
+ payerId,
+ pagadorName,
+ pagadorAvatar,
+ currentAmount: 0,
+ previousAmount: 0,
+ };
+
+ if (resolvedPeriod === period) {
+ current.payerId = payerId;
+ current.pagadorName = pagadorName;
+ current.pagadorAvatar = pagadorAvatar;
+ current.currentAmount = amount;
+ }
+
+ if (resolvedPeriod === previousPeriod) {
+ current.previousAmount = amount;
+ }
+
+ groupedBreakdown.set(key, current);
+ }
+
+ const breakdownMap = new Map();
+ for (const share of groupedBreakdown.values()) {
+ if (share.currentAmount <= 0) {
+ continue;
+ }
+
+ const key = `${share.cardId}:${period}`;
const current = breakdownMap.get(key) ?? [];
current.push({
- payerId: row.payerId ?? null,
- pagadorName: row.pagadorName?.trim() || "Sem pagador",
- pagadorAvatar: row.pagadorAvatar ?? null,
- amount,
+ payerId: share.payerId,
+ pagadorName: share.pagadorName,
+ pagadorAvatar: share.pagadorAvatar,
+ amount: share.currentAmount,
+ percentageChange: calculatePercentageChange(
+ share.currentAmount,
+ share.previousAmount,
+ ),
});
breakdownMap.set(key, current);
}
diff --git a/src/features/dashboard/use-invoices-widget-controller.ts b/src/features/dashboard/invoices/use-invoices-widget-controller.ts
similarity index 86%
rename from src/features/dashboard/use-invoices-widget-controller.ts
rename to src/features/dashboard/invoices/use-invoices-widget-controller.ts
index fe00137..feaceb3 100644
--- a/src/features/dashboard/use-invoices-widget-controller.ts
+++ b/src/features/dashboard/invoices/use-invoices-widget-controller.ts
@@ -5,16 +5,16 @@ import {
type InvoiceDialogState,
isInvoicePaid,
markInvoiceAsPaid,
-} 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 {
type PaymentDialogController,
usePaymentDialogController,
-} from "@/features/dashboard/use-payment-dialog-controller";
+} from "@/features/dashboard/payments/use-payment-dialog-controller";
import { updateInvoicePaymentStatusAction } from "@/features/invoices/actions";
import { INVOICE_PAYMENT_STATUS } from "@/shared/lib/invoices";
-export type InvoicesWidgetController = Omit<
+type InvoicesWidgetController = Omit<
PaymentDialogController,
"selectedItem"
> & {
diff --git a/src/features/dashboard/navbar-queries.ts b/src/features/dashboard/navbar-queries.ts
index 0296c38..0726af5 100644
--- a/src/features/dashboard/navbar-queries.ts
+++ b/src/features/dashboard/navbar-queries.ts
@@ -8,9 +8,9 @@ import { getBusinessDateString } from "@/shared/utils/date";
import {
type DashboardNotificationsSnapshot,
fetchDashboardNotifications,
-} from "./notifications-queries";
+} from "./notifications/notifications-queries";
-export type DashboardNavbarData = {
+type DashboardNavbarData = {
pagadorAvatarUrl: string | null;
preLancamentosCount: number;
notificationsSnapshot: DashboardNotificationsSnapshot;
diff --git a/src/features/dashboard/notes-mappers.ts b/src/features/dashboard/notes/notes-mappers.ts
similarity index 83%
rename from src/features/dashboard/notes-mappers.ts
rename to src/features/dashboard/notes/notes-mappers.ts
index a9a178b..a7b740f 100644
--- a/src/features/dashboard/notes-mappers.ts
+++ b/src/features/dashboard/notes/notes-mappers.ts
@@ -1,4 +1,4 @@
-import type { DashboardNote } from "@/features/dashboard/notes-queries";
+import type { DashboardNote } from "@/features/dashboard/notes/notes-queries";
import type { Note } from "@/features/notes/components/types";
export const mapDashboardNoteToNote = (note: DashboardNote): Note => ({
diff --git a/src/features/dashboard/notes-queries.ts b/src/features/dashboard/notes/notes-queries.ts
similarity index 100%
rename from src/features/dashboard/notes-queries.ts
rename to src/features/dashboard/notes/notes-queries.ts
diff --git a/src/features/dashboard/use-notes-widget-controller.ts b/src/features/dashboard/notes/use-notes-widget-controller.ts
similarity index 91%
rename from src/features/dashboard/use-notes-widget-controller.ts
rename to src/features/dashboard/notes/use-notes-widget-controller.ts
index b89d023..c9060a4 100644
--- a/src/features/dashboard/use-notes-widget-controller.ts
+++ b/src/features/dashboard/notes/use-notes-widget-controller.ts
@@ -1,11 +1,11 @@
"use client";
import { useMemo, useState } from "react";
-import { mapDashboardNotesToNotes } from "@/features/dashboard/notes-mappers";
-import type { DashboardNote } from "@/features/dashboard/notes-queries";
+import { mapDashboardNotesToNotes } from "@/features/dashboard/notes/notes-mappers";
+import type { DashboardNote } from "@/features/dashboard/notes/notes-queries";
import type { Note } from "@/features/notes/components/types";
-export type NotesWidgetController = {
+type NotesWidgetController = {
mappedNotes: Note[];
noteToEdit: Note | null;
isEditOpen: boolean;
diff --git a/src/features/dashboard/notifications-actions.ts b/src/features/dashboard/notifications/notifications-actions.ts
similarity index 100%
rename from src/features/dashboard/notifications-actions.ts
rename to src/features/dashboard/notifications/notifications-actions.ts
diff --git a/src/features/dashboard/notifications-queries.ts b/src/features/dashboard/notifications/notifications-queries.ts
similarity index 99%
rename from src/features/dashboard/notifications-queries.ts
rename to src/features/dashboard/notifications/notifications-queries.ts
index 5ee2f81..ceca14d 100644
--- a/src/features/dashboard/notifications-queries.ts
+++ b/src/features/dashboard/notifications/notifications-queries.ts
@@ -7,7 +7,7 @@ import {
invoices,
transactions,
} from "@/db/schema";
-import { buildInvoiceDetailsHref } from "@/features/dashboard/invoices-helpers";
+import { buildInvoiceDetailsHref } from "@/features/dashboard/invoices/invoices-helpers";
import { db } from "@/shared/lib/db";
import { INVOICE_PAYMENT_STATUS } from "@/shared/lib/invoices";
import { isNotificationStatesTableMissing } from "@/shared/lib/notifications/is-table-missing";
diff --git a/src/features/dashboard/current-period-overview-queries.ts b/src/features/dashboard/overview/current-period-overview-queries.ts
similarity index 99%
rename from src/features/dashboard/current-period-overview-queries.ts
rename to src/features/dashboard/overview/current-period-overview-queries.ts
index a784945..25d22ee 100644
--- a/src/features/dashboard/current-period-overview-queries.ts
+++ b/src/features/dashboard/overview/current-period-overview-queries.ts
@@ -5,7 +5,7 @@ import {
financialAccounts,
transactions,
} from "@/db/schema";
-import type { DashboardBillsSnapshot } from "@/features/dashboard/bills-queries";
+import type { DashboardBillsSnapshot } from "@/features/dashboard/bills/bills-queries";
import type { InstallmentExpensesData } from "@/features/dashboard/expenses/installment-expenses-queries";
import type { RecurringExpensesData } from "@/features/dashboard/expenses/recurring-expenses-queries";
import type {
@@ -15,7 +15,7 @@ import type {
import type { PaymentConditionsData } from "@/features/dashboard/payments/payment-conditions-queries";
import type { PaymentMethodsData } from "@/features/dashboard/payments/payment-methods-queries";
import type { PaymentStatusData } from "@/features/dashboard/payments/payment-status-queries";
-import type { PurchasesByCategoryData } from "@/features/dashboard/purchases-by-category-queries";
+import type { PurchasesByCategoryData } from "@/features/dashboard/categories/purchases-by-category-queries";
import type { TopEstablishmentsData } from "@/features/dashboard/top-establishments-queries";
import { excludeTransactionsFromExcludedAccounts } from "@/features/dashboard/transaction-filters";
import {
@@ -67,7 +67,7 @@ type CategoryOption = PurchasesByCategoryData["categories"][number];
type CategoryTransaction =
PurchasesByCategoryData["transactionsByCategory"][string][number];
-export type DashboardCurrentPeriodOverview = {
+type DashboardCurrentPeriodOverview = {
billsSnapshot: DashboardBillsSnapshot;
paymentStatusData: PaymentStatusData;
paymentConditionsData: PaymentConditionsData;
diff --git a/src/features/dashboard/overview/dashboard-metrics-queries.ts b/src/features/dashboard/overview/dashboard-metrics-queries.ts
new file mode 100644
index 0000000..67645ea
--- /dev/null
+++ b/src/features/dashboard/overview/dashboard-metrics-queries.ts
@@ -0,0 +1,13 @@
+type MetricPair = {
+ current: number;
+ previous: number;
+};
+
+export type DashboardCardMetrics = {
+ period: string;
+ previousPeriod: string;
+ receitas: MetricPair;
+ despesas: MetricPair;
+ balanco: MetricPair;
+ previsto: MetricPair;
+};
diff --git a/src/features/dashboard/overview/income-expense-balance-queries.ts b/src/features/dashboard/overview/income-expense-balance-queries.ts
new file mode 100644
index 0000000..1c1e0e8
--- /dev/null
+++ b/src/features/dashboard/overview/income-expense-balance-queries.ts
@@ -0,0 +1,11 @@
+export type MonthData = {
+ month: string;
+ monthLabel: string;
+ income: number;
+ expense: number;
+ balance: number;
+};
+
+export type IncomeExpenseBalanceData = {
+ months: MonthData[];
+};
diff --git a/src/features/dashboard/period-overview-queries.ts b/src/features/dashboard/overview/period-overview-queries.ts
similarity index 97%
rename from src/features/dashboard/period-overview-queries.ts
rename to src/features/dashboard/overview/period-overview-queries.ts
index cb712e2..8056f92 100644
--- a/src/features/dashboard/period-overview-queries.ts
+++ b/src/features/dashboard/overview/period-overview-queries.ts
@@ -1,10 +1,10 @@
import { and, asc, eq, gte, inArray, lte, sum } from "drizzle-orm";
import { financialAccounts, transactions } from "@/db/schema";
-import type { DashboardCardMetrics } from "@/features/dashboard/dashboard-metrics-queries";
+import type { DashboardCardMetrics } from "@/features/dashboard/overview/dashboard-metrics-queries";
import type {
IncomeExpenseBalanceData,
MonthData,
-} from "@/features/dashboard/income-expense-balance-queries";
+} from "@/features/dashboard/overview/income-expense-balance-queries";
import {
buildDashboardAdminFilters,
excludeAutoInvoiceEntries,
@@ -42,7 +42,7 @@ type PeriodSummaryRow = {
accountExcludeFromBalance: boolean | null;
};
-export type DashboardPeriodOverview = {
+type DashboardPeriodOverview = {
metrics: DashboardCardMetrics;
incomeExpenseBalanceData: IncomeExpenseBalanceData;
};
diff --git a/src/features/dashboard/page-data-queries.ts b/src/features/dashboard/page-data-queries.ts
index c82a205..552c88b 100644
--- a/src/features/dashboard/page-data-queries.ts
+++ b/src/features/dashboard/page-data-queries.ts
@@ -10,7 +10,7 @@ import {
fetchTransactionFilterSources,
} from "@/features/transactions/queries";
-export type DashboardQuickActionOptions = {
+type DashboardQuickActionOptions = {
payerOptions: ReturnType["payerOptions"];
splitPayerOptions: ReturnType["splitPayerOptions"];
defaultPayerId: string | null;
diff --git a/src/features/dashboard/payers-queries.ts b/src/features/dashboard/payers-queries.ts
index b2fe4f5..b7b54bf 100644
--- a/src/features/dashboard/payers-queries.ts
+++ b/src/features/dashboard/payers-queries.ts
@@ -19,7 +19,7 @@ export type DashboardPagador = {
isAdmin: boolean;
};
-export type DashboardPayersSnapshot = {
+type DashboardPayersSnapshot = {
payers: DashboardPagador[];
totalExpenses: number;
};
diff --git a/src/features/dashboard/payment-breakdown-formatters.ts b/src/features/dashboard/payments/payment-breakdown-formatters.ts
similarity index 100%
rename from src/features/dashboard/payment-breakdown-formatters.ts
rename to src/features/dashboard/payments/payment-breakdown-formatters.ts
diff --git a/src/features/dashboard/payments/payment-conditions-queries.ts b/src/features/dashboard/payments/payment-conditions-queries.ts
index 68b9189..c226282 100644
--- a/src/features/dashboard/payments/payment-conditions-queries.ts
+++ b/src/features/dashboard/payments/payment-conditions-queries.ts
@@ -1,13 +1,3 @@
-import { and, eq, sql } from "drizzle-orm";
-import { transactions } from "@/db/schema";
-import {
- buildDashboardAdminPeriodFilters,
- excludeAutoGeneratedEntryNotes,
-} from "@/features/dashboard/transaction-filters";
-import { db } from "@/shared/lib/db";
-import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
-import { safeToNumber as toNumber } from "@/shared/utils/number";
-
export type PaymentConditionSummary = {
condition: string;
amount: number;
@@ -18,68 +8,3 @@ export type PaymentConditionSummary = {
export type PaymentConditionsData = {
conditions: PaymentConditionSummary[];
};
-
-export async function fetchPaymentConditions(
- userId: string,
- period: string,
-): Promise {
- const adminPayerId = await getAdminPayerId(userId);
- if (!adminPayerId) {
- return { conditions: [] };
- }
-
- const rows = await db
- .select({
- condition: transactions.condition,
- totalAmount: sql`coalesce(sum(${transactions.amount}), 0)`,
- transactions: sql`count(${transactions.id})`,
- })
- .from(transactions)
- .where(
- and(
- ...buildDashboardAdminPeriodFilters({
- userId,
- period,
- adminPayerId,
- }),
- eq(transactions.transactionType, "Despesa"),
- excludeAutoGeneratedEntryNotes(),
- ),
- )
- .groupBy(transactions.condition);
-
- const summaries = rows.map((row: (typeof rows)[number]) => {
- const totalAmount = Math.abs(toNumber(row.totalAmount));
- const transactions = Number(row.transactions ?? 0);
-
- return {
- condition: row.condition,
- amount: totalAmount,
- transactions,
- };
- });
-
- const overallTotal = summaries.reduce(
- (acc: number, item: (typeof summaries)[number]) => acc + item.amount,
- 0,
- );
-
- const conditions = summaries
- .map((item: (typeof summaries)[number]) => ({
- condition: item.condition,
- amount: item.amount,
- transactions: item.transactions,
- percentage:
- overallTotal > 0
- ? Number(((item.amount / overallTotal) * 100).toFixed(2))
- : 0,
- }))
- .sort(
- (a: (typeof summaries)[number], b: (typeof summaries)[number]) =>
- b.amount - a.amount,
- );
-
- return {
- conditions,
- };
-}
diff --git a/src/features/dashboard/payments/payment-methods-queries.ts b/src/features/dashboard/payments/payment-methods-queries.ts
index 98713ef..94a0dc3 100644
--- a/src/features/dashboard/payments/payment-methods-queries.ts
+++ b/src/features/dashboard/payments/payment-methods-queries.ts
@@ -1,13 +1,3 @@
-import { and, eq, sql } from "drizzle-orm";
-import { transactions } from "@/db/schema";
-import {
- buildDashboardAdminPeriodFilters,
- excludeAutoGeneratedEntryNotes,
-} from "@/features/dashboard/transaction-filters";
-import { db } from "@/shared/lib/db";
-import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
-import { safeToNumber as toNumber } from "@/shared/utils/number";
-
export type PaymentMethodSummary = {
paymentMethod: string;
amount: number;
@@ -18,68 +8,3 @@ export type PaymentMethodSummary = {
export type PaymentMethodsData = {
methods: PaymentMethodSummary[];
};
-
-export async function fetchPaymentMethods(
- userId: string,
- period: string,
-): Promise {
- const adminPayerId = await getAdminPayerId(userId);
- if (!adminPayerId) {
- return { methods: [] };
- }
-
- const rows = await db
- .select({
- paymentMethod: transactions.paymentMethod,
- totalAmount: sql`coalesce(sum(${transactions.amount}), 0)`,
- transactions: sql`count(${transactions.id})`,
- })
- .from(transactions)
- .where(
- and(
- ...buildDashboardAdminPeriodFilters({
- userId,
- period,
- adminPayerId,
- }),
- eq(transactions.transactionType, "Despesa"),
- excludeAutoGeneratedEntryNotes(),
- ),
- )
- .groupBy(transactions.paymentMethod);
-
- const summaries = rows.map((row: (typeof rows)[number]) => {
- const amount = Math.abs(toNumber(row.totalAmount));
- const transactions = Number(row.transactions ?? 0);
-
- return {
- paymentMethod: row.paymentMethod,
- amount,
- transactions,
- };
- });
-
- const overallTotal = summaries.reduce(
- (acc: number, item: (typeof summaries)[number]) => acc + item.amount,
- 0,
- );
-
- const methods = summaries
- .map((item: (typeof summaries)[number]) => ({
- paymentMethod: item.paymentMethod,
- amount: item.amount,
- transactions: item.transactions,
- percentage:
- overallTotal > 0
- ? Number(((item.amount / overallTotal) * 100).toFixed(2))
- : 0,
- }))
- .sort(
- (a: (typeof summaries)[number], b: (typeof summaries)[number]) =>
- b.amount - a.amount,
- );
-
- return {
- methods,
- };
-}
diff --git a/src/features/dashboard/payment-overview-tabs.ts b/src/features/dashboard/payments/payment-overview-tabs.ts
similarity index 100%
rename from src/features/dashboard/payment-overview-tabs.ts
rename to src/features/dashboard/payments/payment-overview-tabs.ts
diff --git a/src/features/dashboard/payments/payment-status-queries.ts b/src/features/dashboard/payments/payment-status-queries.ts
index 3efbaa7..f0afa04 100644
--- a/src/features/dashboard/payments/payment-status-queries.ts
+++ b/src/features/dashboard/payments/payment-status-queries.ts
@@ -1,15 +1,3 @@
-import { and, eq, inArray, sql } from "drizzle-orm";
-import { financialAccounts, transactions } from "@/db/schema";
-import {
- buildDashboardAdminPeriodFilters,
- excludeAutoInvoiceEntries,
- excludeInitialBalanceWhenConfigured,
- excludeTransactionsFromExcludedAccounts,
-} from "@/features/dashboard/transaction-filters";
-import { db } from "@/shared/lib/db";
-import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
-import { safeToNumber as toNumber } from "@/shared/utils/number";
-
export type PaymentStatusCategory = {
total: number;
confirmed: number;
@@ -20,76 +8,3 @@ export type PaymentStatusData = {
income: PaymentStatusCategory;
expenses: PaymentStatusCategory;
};
-
-const emptyCategory = (): PaymentStatusCategory => ({
- total: 0,
- confirmed: 0,
- pending: 0,
-});
-
-export async function fetchPaymentStatus(
- userId: string,
- period: string,
-): Promise {
- const adminPayerId = await getAdminPayerId(userId);
- if (!adminPayerId) {
- return { income: emptyCategory(), expenses: emptyCategory() };
- }
-
- // Single query: GROUP BY transactionType instead of 2 separate queries
- const rows = await db
- .select({
- transactionType: transactions.transactionType,
- confirmed: sql`
- coalesce(
- sum(case when ${transactions.isSettled} = true then ${transactions.amount} else 0 end),
- 0
- )
- `,
- pending: sql`
- coalesce(
- sum(case when ${transactions.isSettled} = false or ${transactions.isSettled} is null then ${transactions.amount} else 0 end),
- 0
- )
- `,
- })
- .from(transactions)
- .leftJoin(
- financialAccounts,
- eq(transactions.accountId, financialAccounts.id),
- )
- .where(
- and(
- ...buildDashboardAdminPeriodFilters({
- userId,
- period,
- adminPayerId,
- }),
- inArray(transactions.transactionType, ["Receita", "Despesa"]),
- excludeAutoInvoiceEntries(),
- excludeInitialBalanceWhenConfigured(),
- excludeTransactionsFromExcludedAccounts(),
- ),
- )
- .groupBy(transactions.transactionType);
-
- const result = { income: emptyCategory(), expenses: emptyCategory() };
-
- for (const row of rows) {
- const confirmed = toNumber(row.confirmed);
- const pending = toNumber(row.pending);
- const category = {
- total: confirmed + pending,
- confirmed,
- pending,
- };
-
- if (row.transactionType === "Receita") {
- result.income = category;
- } else if (row.transactionType === "Despesa") {
- result.expenses = category;
- }
- }
-
- return result;
-}
diff --git a/src/features/dashboard/use-payment-dialog-controller.ts b/src/features/dashboard/payments/use-payment-dialog-controller.ts
similarity index 100%
rename from src/features/dashboard/use-payment-dialog-controller.ts
rename to src/features/dashboard/payments/use-payment-dialog-controller.ts
diff --git a/src/features/dashboard/use-payment-overview-widget-controller.ts b/src/features/dashboard/payments/use-payment-overview-widget-controller.ts
similarity index 84%
rename from src/features/dashboard/use-payment-overview-widget-controller.ts
rename to src/features/dashboard/payments/use-payment-overview-widget-controller.ts
index 583559f..b5e8c1a 100644
--- a/src/features/dashboard/use-payment-overview-widget-controller.ts
+++ b/src/features/dashboard/payments/use-payment-overview-widget-controller.ts
@@ -5,9 +5,9 @@ import {
DEFAULT_PAYMENT_OVERVIEW_TAB,
type PaymentOverviewTab,
parsePaymentOverviewTab,
-} from "@/features/dashboard/payment-overview-tabs";
+} from "@/features/dashboard/payments/payment-overview-tabs";
-export type PaymentOverviewWidgetController = {
+type PaymentOverviewWidgetController = {
activeTab: PaymentOverviewTab;
handleTabChange: (value: string) => void;
};
diff --git a/src/features/dashboard/preferences-queries.ts b/src/features/dashboard/preferences-queries.ts
index 2c8ae84..aa0f0a7 100644
--- a/src/features/dashboard/preferences-queries.ts
+++ b/src/features/dashboard/preferences-queries.ts
@@ -1,9 +1,9 @@
import { eq } from "drizzle-orm";
import { cacheLife, cacheTag } from "next/cache";
-import type { WidgetPreferences } from "@/features/dashboard/widgets/actions";
+import type { WidgetPreferences } from "@/features/dashboard/widget-registry/widget-actions";
import { db, schema } from "@/shared/lib/db";
-export interface UserDashboardPreferences {
+interface UserDashboardPreferences {
dashboardWidgets: WidgetPreferences | null;
}
diff --git a/src/features/dashboard/purchases-by-category-queries.ts b/src/features/dashboard/purchases-by-category-queries.ts
deleted file mode 100644
index 3c4ddb8..0000000
--- a/src/features/dashboard/purchases-by-category-queries.ts
+++ /dev/null
@@ -1,145 +0,0 @@
-import { and, desc, eq, inArray } from "drizzle-orm";
-import {
- cards,
- categories,
- financialAccounts,
- transactions,
-} from "@/db/schema";
-import {
- buildDashboardAdminPeriodFilters,
- excludeAutoGeneratedEntryNotes,
-} from "@/features/dashboard/transaction-filters";
-import { db } from "@/shared/lib/db";
-import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
-import { safeToNumber as toNumber } from "@/shared/utils/number";
-
-export type CategoryOption = {
- id: string;
- name: string;
- type: string;
-};
-
-export type CategoryTransaction = {
- id: string;
- name: string;
- amount: number;
- purchaseDate: Date;
- logo: string | null;
-};
-
-export type PurchasesByCategoryData = {
- categories: CategoryOption[];
- transactionsByCategory: Record;
-};
-
-const shouldIncludeTransaction = (name: string) => {
- const normalized = name.trim().toLowerCase();
-
- if (normalized === "saldo inicial") {
- return false;
- }
-
- if (normalized.includes("fatura")) {
- return false;
- }
-
- return true;
-};
-
-export async function fetchPurchasesByCategory(
- userId: string,
- period: string,
-): Promise {
- const adminPayerId = await getAdminPayerId(userId);
- if (!adminPayerId) {
- return { categories: [], transactionsByCategory: {} };
- }
-
- const transactionsRows = await db
- .select({
- id: transactions.id,
- name: transactions.name,
- amount: transactions.amount,
- purchaseDate: transactions.purchaseDate,
- categoryId: transactions.categoryId,
- categoryName: categories.name,
- categoryType: categories.type,
- cardLogo: cards.logo,
- accountLogo: financialAccounts.logo,
- })
- .from(transactions)
- .innerJoin(categories, eq(transactions.categoryId, categories.id))
- .leftJoin(cards, eq(transactions.cardId, cards.id))
- .leftJoin(
- financialAccounts,
- eq(transactions.accountId, financialAccounts.id),
- )
- .where(
- and(
- ...buildDashboardAdminPeriodFilters({
- userId,
- period,
- adminPayerId,
- }),
- inArray(categories.type, ["despesa", "receita"]),
- excludeAutoGeneratedEntryNotes(),
- ),
- )
- .orderBy(desc(transactions.purchaseDate));
-
- const transactionsByCategory: Record = {};
- const categoriesMap = new Map();
-
- for (const row of transactionsRows) {
- const categoryId = row.categoryId;
-
- if (!categoryId) {
- continue;
- }
-
- if (!shouldIncludeTransaction(row.name)) {
- continue;
- }
-
- // Adiciona a categoria ao mapa se ainda não existir
- if (!categoriesMap.has(categoryId)) {
- categoriesMap.set(categoryId, {
- id: categoryId,
- name: row.categoryName,
- type: row.categoryType,
- });
- }
-
- const entry: CategoryTransaction = {
- id: row.id,
- name: row.name,
- amount: Math.abs(toNumber(row.amount)),
- purchaseDate: row.purchaseDate,
- logo: row.cardLogo ?? row.accountLogo ?? null,
- };
-
- if (!transactionsByCategory[categoryId]) {
- transactionsByCategory[categoryId] = [];
- }
-
- const categoryTransactions = transactionsByCategory[categoryId];
- if (categoryTransactions && categoryTransactions.length < 10) {
- categoryTransactions.push(entry);
- }
- }
-
- // Ordena as categories: receitas primeiro, depois despesas (alfabeticamente dentro de cada tipo)
- const categoryList = Array.from(categoriesMap.values()).sort((a, b) => {
- // Receita vem antes de despesa
- if (a.type !== b.type) {
- return a.type === "receita" ? -1 : 1;
- }
- // Dentro do mesmo tipo, ordena alfabeticamente
- return a.name.localeCompare(b.name);
- });
-
- return {
- categories: categoryList,
- transactionsByCategory,
- };
-}
diff --git a/src/features/dashboard/top-establishments-queries.ts b/src/features/dashboard/top-establishments-queries.ts
index fb86482..b7c1183 100644
--- a/src/features/dashboard/top-establishments-queries.ts
+++ b/src/features/dashboard/top-establishments-queries.ts
@@ -1,13 +1,3 @@
-import { and, eq, sql } from "drizzle-orm";
-import { cards, financialAccounts, transactions } from "@/db/schema";
-import {
- buildDashboardAdminPeriodFilters,
- excludeAutoGeneratedEntryNotes,
-} from "@/features/dashboard/transaction-filters";
-import { db } from "@/shared/lib/db";
-import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
-import { safeToNumber as toNumber } from "@/shared/utils/number";
-
export type TopEstablishment = {
id: string;
name: string;
@@ -19,78 +9,3 @@ export type TopEstablishment = {
export type TopEstablishmentsData = {
establishments: TopEstablishment[];
};
-
-const shouldIncludeEstablishment = (name: string) => {
- const normalized = name.trim().toLowerCase();
-
- if (normalized === "saldo inicial") {
- return false;
- }
-
- if (normalized.includes("fatura")) {
- return false;
- }
-
- return true;
-};
-
-export async function fetchTopEstablishments(
- userId: string,
- period: string,
-): Promise {
- const adminPayerId = await getAdminPayerId(userId);
- if (!adminPayerId) {
- return { establishments: [] };
- }
-
- const rows = await db
- .select({
- name: transactions.name,
- totalAmount: sql`coalesce(sum(${transactions.amount}), 0)`,
- occurrences: sql`count(${transactions.id})`,
- logo: sql<
- string | null
- >`max(coalesce(${cards.logo}, ${financialAccounts.logo}))`,
- })
- .from(transactions)
- .leftJoin(cards, eq(transactions.cardId, cards.id))
- .leftJoin(
- financialAccounts,
- eq(transactions.accountId, financialAccounts.id),
- )
- .where(
- and(
- ...buildDashboardAdminPeriodFilters({
- userId,
- period,
- adminPayerId,
- }),
- eq(transactions.transactionType, "Despesa"),
- excludeAutoGeneratedEntryNotes(),
- ),
- )
- .groupBy(transactions.name)
- .orderBy(
- sql`count(${transactions.id}) DESC`,
- sql`ABS(sum(${transactions.amount})) DESC`,
- )
- .limit(10);
-
- const establishments = rows
- .filter((row: (typeof rows)[number]) =>
- shouldIncludeEstablishment(row.name),
- )
- .map(
- (row: (typeof rows)[number]): TopEstablishment => ({
- id: row.name,
- name: row.name,
- amount: Math.abs(toNumber(row.totalAmount)),
- occurrences: Number(row.occurrences ?? 0),
- logo: row.logo ?? null,
- }),
- );
-
- return {
- establishments,
- };
-}
diff --git a/src/features/dashboard/transaction-filters.ts b/src/features/dashboard/transaction-filters.ts
index a4e85bb..7b2c2f2 100644
--- a/src/features/dashboard/transaction-filters.ts
+++ b/src/features/dashboard/transaction-filters.ts
@@ -1,4 +1,4 @@
-import { and, eq, ilike, isNull, ne, not, or } from "drizzle-orm";
+import { eq, ilike, isNull, ne, not, or } from "drizzle-orm";
import { financialAccounts, transactions } from "@/db/schema";
import {
ACCOUNT_AUTO_INVOICE_NOTE_PREFIX,
@@ -12,10 +12,6 @@ type DashboardAdminFiltersParams = {
adminPayerId: string;
};
-type DashboardAdminPeriodFiltersParams = DashboardAdminFiltersParams & {
- period: string;
-};
-
export const buildDashboardAdminFilters = ({
userId,
adminPayerId,
@@ -25,31 +21,12 @@ export const buildDashboardAdminFilters = ({
eq(transactions.payerId, adminPayerId),
] as const;
-export const buildDashboardAdminPeriodFilters = ({
- userId,
- period,
- adminPayerId,
-}: DashboardAdminPeriodFiltersParams) =>
- [
- ...buildDashboardAdminFilters({ userId, adminPayerId }),
- eq(transactions.period, period),
- ] as const;
-
export const excludeAutoInvoiceEntries = () =>
or(
isNull(transactions.note),
not(ilike(transactions.note, `${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`)),
);
-export const excludeAutoGeneratedEntryNotes = () =>
- or(
- isNull(transactions.note),
- and(
- ne(transactions.note, INITIAL_BALANCE_NOTE),
- not(ilike(transactions.note, `${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`)),
- ),
- );
-
export const excludeInitialBalanceWhenConfigured = () =>
or(
isNull(transactions.note),
diff --git a/src/features/dashboard/components/welcome-widget.ts b/src/features/dashboard/widget-registry/welcome-widget.ts
similarity index 100%
rename from src/features/dashboard/components/welcome-widget.ts
rename to src/features/dashboard/widget-registry/welcome-widget.ts
diff --git a/src/features/dashboard/widgets/actions.ts b/src/features/dashboard/widget-registry/widget-actions.ts
similarity index 100%
rename from src/features/dashboard/widgets/actions.ts
rename to src/features/dashboard/widget-registry/widget-actions.ts
diff --git a/src/features/dashboard/widgets/widgets-config.tsx b/src/features/dashboard/widget-registry/widget-config.tsx
similarity index 89%
rename from src/features/dashboard/widgets/widgets-config.tsx
rename to src/features/dashboard/widget-registry/widget-config.tsx
index 04d6c9f..287949a 100644
--- a/src/features/dashboard/widgets/widgets-config.tsx
+++ b/src/features/dashboard/widget-registry/widget-config.tsx
@@ -18,25 +18,25 @@ import {
} from "@remixicon/react";
import Link from "next/link";
import type { ReactNode } from "react";
-import { AttachmentsWidget } from "@/features/dashboard/components/attachments-widget";
-import { BillWidget } from "@/features/dashboard/components/bill-widget";
-import { CategoryTrendsWidget } from "@/features/dashboard/components/category-trends-widget";
-import { ExpensesByCategoryWidgetWithChart } from "@/features/dashboard/components/expenses-by-category-widget-with-chart";
-import { GoalsProgressWidget } from "@/features/dashboard/components/goals-progress-widget";
-import { InboxWidget } from "@/features/dashboard/components/inbox-widget";
-import { IncomeByCategoryWidgetWithChart } from "@/features/dashboard/components/income-by-category-widget-with-chart";
-import { IncomeExpenseBalanceWidget } from "@/features/dashboard/components/income-expense-balance-widget";
-import { InstallmentExpensesWidget } from "@/features/dashboard/components/installment-expenses-widget";
-import { InvoicesWidget } from "@/features/dashboard/components/invoices-widget";
-import { MyAccountsWidget } from "@/features/dashboard/components/my-accounts-widget";
-import { NotesWidget } from "@/features/dashboard/components/notes-widget";
-import { PayersWidget } from "@/features/dashboard/components/payers-widget";
-import { PaymentOverviewWidget } from "@/features/dashboard/components/payment-overview-widget";
-import { PaymentStatusWidget } from "@/features/dashboard/components/payment-status-widget";
-import { PurchasesByCategoryWidget } from "@/features/dashboard/components/purchases-by-category-widget";
-import { RecurringExpensesWidget } from "@/features/dashboard/components/recurring-expenses-widget";
-import { SpendingOverviewWidget } from "@/features/dashboard/components/spending-overview-widget";
-import type { WidgetPreferences } from "@/features/dashboard/widgets/actions";
+import { AttachmentsWidget } from "@/features/dashboard/components/widgets/attachments-widget";
+import { BillWidget } from "@/features/dashboard/components/widgets/bill-widget";
+import { CategoryTrendsWidget } from "@/features/dashboard/components/widgets/category-trends-widget";
+import { ExpensesByCategoryWidgetWithChart } from "@/features/dashboard/components/widgets/expenses-by-category-widget-with-chart";
+import { GoalsProgressWidget } from "@/features/dashboard/components/widgets/goals-progress-widget";
+import { InboxWidget } from "@/features/dashboard/components/widgets/inbox-widget";
+import { IncomeByCategoryWidgetWithChart } from "@/features/dashboard/components/widgets/income-by-category-widget-with-chart";
+import { IncomeExpenseBalanceWidget } from "@/features/dashboard/components/widgets/income-expense-balance-widget";
+import { InstallmentExpensesWidget } from "@/features/dashboard/components/widgets/installment-expenses-widget";
+import { InvoicesWidget } from "@/features/dashboard/components/widgets/invoices-widget";
+import { MyAccountsWidget } from "@/features/dashboard/components/widgets/my-accounts-widget";
+import { NotesWidget } from "@/features/dashboard/components/widgets/notes-widget";
+import { PayersWidget } from "@/features/dashboard/components/widgets/payers-widget";
+import { PaymentOverviewWidget } from "@/features/dashboard/components/widgets/payment-overview-widget";
+import { PaymentStatusWidget } from "@/features/dashboard/components/widgets/payment-status-widget";
+import { PurchasesByCategoryWidget } from "@/features/dashboard/components/widgets/purchases-by-category-widget";
+import { RecurringExpensesWidget } from "@/features/dashboard/components/widgets/recurring-expenses-widget";
+import { SpendingOverviewWidget } from "@/features/dashboard/components/widgets/spending-overview-widget";
+import type { WidgetPreferences } from "@/features/dashboard/widget-registry/widget-actions";
import type { SelectOption } from "@/features/transactions/components/types";
import type { DashboardData } from "../fetch-dashboard-data";