From ba059857255d047ac1dec876bf8ede96c38cfbed Mon Sep 17 00:00:00 2001 From: Felipe Coutinho Date: Mon, 20 Apr 2026 17:51:56 +0000 Subject: [PATCH] =?UTF-8?q?refactor(dashboard):=20reorganizar=20m=C3=B3dul?= =?UTF-8?q?os=20em=20subdiret=C3=B3rios=20e=20nova=20arquitetura=20de=20wi?= =?UTF-8?q?dgets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Arquivos de queries, helpers e controllers dispersos na raiz de dashboard/ foram movidos para subdiretórios temáticos (bills/, invoices/, notes/, notifications/, overview/, payments/, goals-progress/, categories/). ~25 widgets monolíticos obsoletos removidos em favor de nova arquitetura baseada em widget-registry com components/widgets/. Novos componentes: category-breakdown-chart/list, goals-progress-item, percentage-change-indicator. Imports atualizados em fetch-dashboard-data e transaction-filters limpos. Co-Authored-By: Claude Sonnet 4.6 --- src/features/dashboard/accounts-queries.ts | 2 +- src/features/dashboard/bills-queries.ts | 166 --------- .../dashboard/{ => bills}/bills-helpers.ts | 4 +- src/features/dashboard/bills/bills-queries.ts | 14 + .../{ => bills}/use-bill-widget-controller.ts | 8 +- ...kdown.ts => category-breakdown-helpers.ts} | 0 .../categories/category-history-queries.ts | 2 +- .../category-overview-queries.ts | 6 +- .../expenses-by-category-queries.ts | 81 +---- .../categories/income-by-category-queries.ts | 83 +---- .../purchases-by-category-queries.ts | 18 + .../components/bills/bill-list-item.tsx | 8 +- .../components/bills/bill-payment-dialog.tsx | 4 +- .../dashboard/components/bills/bills-list.tsx | 2 +- .../components/bills/bills-widget-view.tsx | 4 +- .../category-breakdown-chart.tsx | 161 +++++++++ .../category-breakdown-list-item.tsx | 129 +++++++ .../category-breakdown-list.tsx | 34 ++ .../category-breakdown-widget-view.tsx | 321 ++---------------- .../components/dashboard-grid-editable.tsx | 8 +- .../components/dashboard-metrics-cards.tsx | 83 ++--- .../components/dashboard-welcome.tsx | 14 +- ...gress-item.tsx => goals-progress-item.tsx} | 20 +- .../goals-progress/goals-progress-list.tsx | 4 +- .../goals-progress-widget-view.tsx | 2 +- .../installment-analysis-page.tsx | 2 +- .../installment-expense-list-item.tsx | 2 +- .../components/invoices/invoice-list-item.tsx | 16 +- .../components/invoices/invoice-logo.tsx | 2 +- .../invoices/invoice-payment-dialog.tsx | 4 +- .../components/invoices/invoices-list.tsx | 2 +- .../invoices/invoices-widget-view.tsx | 4 +- .../components/notes/note-list-item.tsx | 10 +- .../payment-breakdown-list-item.tsx | 2 +- .../payment-overview-widget-view.tsx | 12 +- .../percentage-change-indicator.tsx | 71 ++++ .../{ => widgets}/attachments-widget.tsx | 0 .../components/{ => widgets}/bill-widget.tsx | 6 +- .../{ => widgets}/category-history-widget.tsx | 0 .../{ => widgets}/category-trends-widget.tsx | 36 +- ...expenses-by-category-widget-with-chart.tsx | 2 +- .../{ => widgets}/goals-progress-widget.tsx | 6 +- .../components/{ => widgets}/inbox-widget.tsx | 12 +- .../income-by-category-widget-with-chart.tsx | 2 +- .../income-expense-balance-widget.tsx | 8 +- .../installment-expenses-widget.tsx | 2 +- .../{ => widgets}/invoices-widget.tsx | 6 +- .../{ => widgets}/my-accounts-widget.tsx | 2 +- .../components/{ => widgets}/notes-widget.tsx | 6 +- .../{ => widgets}/payers-widget.tsx | 24 +- .../{ => widgets}/payment-overview-widget.tsx | 4 +- .../{ => widgets}/payment-status-widget.tsx | 2 +- .../purchases-by-category-widget.tsx | 2 +- .../recurring-expenses-widget.tsx | 0 .../{ => widgets}/sortable-widget.tsx | 0 .../spending-overview-widget.tsx | 10 +- .../top-establishments-widget.tsx | 0 .../{ => widgets}/top-expenses-widget.tsx | 0 .../{ => widgets}/widget-settings-dialog.tsx | 2 +- .../dashboard/dashboard-metrics-queries.ts | 189 ----------- .../installment-expenses-helpers.ts | 0 .../expenses/installment-expenses-queries.ts | 85 ----- .../expenses/recurring-expenses-queries.ts | 61 ---- .../expenses/top-expenses-queries.ts | 73 ---- .../dashboard/fetch-dashboard-data.ts | 10 +- .../dashboard/goals-progress-queries.ts | 147 -------- .../goals-progress-helpers.ts | 2 +- .../goals-progress/goals-progress-queries.ts | 28 ++ .../use-goals-progress-widget-controller.ts | 6 +- .../income-expense-balance-queries.ts | 126 ------- .../{ => invoices}/invoices-helpers.ts | 4 +- .../{ => invoices}/invoices-queries.ts | 73 +++- .../use-invoices-widget-controller.ts | 8 +- src/features/dashboard/navbar-queries.ts | 4 +- .../dashboard/{ => notes}/notes-mappers.ts | 2 +- .../dashboard/{ => notes}/notes-queries.ts | 0 .../use-notes-widget-controller.ts | 6 +- .../notifications-actions.ts | 0 .../notifications-queries.ts | 2 +- .../current-period-overview-queries.ts | 6 +- .../overview/dashboard-metrics-queries.ts | 13 + .../income-expense-balance-queries.ts | 11 + .../{ => overview}/period-overview-queries.ts | 6 +- src/features/dashboard/page-data-queries.ts | 2 +- src/features/dashboard/payers-queries.ts | 2 +- .../payment-breakdown-formatters.ts | 0 .../payments/payment-conditions-queries.ts | 75 ---- .../payments/payment-methods-queries.ts | 75 ---- .../{ => payments}/payment-overview-tabs.ts | 0 .../payments/payment-status-queries.ts | 85 ----- .../use-payment-dialog-controller.ts | 0 .../use-payment-overview-widget-controller.ts | 4 +- src/features/dashboard/preferences-queries.ts | 4 +- .../purchases-by-category-queries.ts | 145 -------- .../dashboard/top-establishments-queries.ts | 85 ----- src/features/dashboard/transaction-filters.ts | 25 +- .../welcome-widget.ts | 0 .../widget-actions.ts} | 0 .../widget-config.tsx} | 38 +-- 99 files changed, 784 insertions(+), 2055 deletions(-) delete mode 100644 src/features/dashboard/bills-queries.ts rename src/features/dashboard/{ => bills}/bills-helpers.ts (89%) create mode 100644 src/features/dashboard/bills/bills-queries.ts rename src/features/dashboard/{ => bills}/use-bill-widget-controller.ts (80%) rename src/features/dashboard/categories/{category-breakdown.ts => category-breakdown-helpers.ts} (100%) rename src/features/dashboard/{ => categories}/category-overview-queries.ts (97%) create mode 100644 src/features/dashboard/categories/purchases-by-category-queries.ts create mode 100644 src/features/dashboard/components/category-breakdown/category-breakdown-chart.tsx create mode 100644 src/features/dashboard/components/category-breakdown/category-breakdown-list-item.tsx create mode 100644 src/features/dashboard/components/category-breakdown/category-breakdown-list.tsx rename src/features/dashboard/components/goals-progress/{goal-progress-item.tsx => goals-progress-item.tsx} (82%) create mode 100644 src/features/dashboard/components/percentage-change-indicator.tsx rename src/features/dashboard/components/{ => widgets}/attachments-widget.tsx (100%) rename src/features/dashboard/components/{ => widgets}/bill-widget.tsx (71%) rename src/features/dashboard/components/{ => widgets}/category-history-widget.tsx (100%) rename src/features/dashboard/components/{ => widgets}/category-trends-widget.tsx (75%) rename src/features/dashboard/components/{ => widgets}/expenses-by-category-widget-with-chart.tsx (81%) rename src/features/dashboard/components/{ => widgets}/goals-progress-widget.tsx (78%) rename src/features/dashboard/components/{ => widgets}/inbox-widget.tsx (96%) rename src/features/dashboard/components/{ => widgets}/income-by-category-widget-with-chart.tsx (80%) rename src/features/dashboard/components/{ => widgets}/income-expense-balance-widget.tsx (97%) rename src/features/dashboard/components/{ => widgets}/installment-expenses-widget.tsx (75%) rename src/features/dashboard/components/{ => widgets}/invoices-widget.tsx (84%) rename src/features/dashboard/components/{ => widgets}/my-accounts-widget.tsx (99%) rename src/features/dashboard/components/{ => widgets}/notes-widget.tsx (73%) rename src/features/dashboard/components/{ => widgets}/payers-widget.tsx (80%) rename src/features/dashboard/components/{ => widgets}/payment-overview-widget.tsx (85%) rename src/features/dashboard/components/{ => widgets}/payment-status-widget.tsx (77%) rename src/features/dashboard/components/{ => widgets}/purchases-by-category-widget.tsx (99%) rename src/features/dashboard/components/{ => widgets}/recurring-expenses-widget.tsx (100%) rename src/features/dashboard/components/{ => widgets}/sortable-widget.tsx (100%) rename src/features/dashboard/components/{ => widgets}/spending-overview-widget.tsx (88%) rename src/features/dashboard/components/{ => widgets}/top-establishments-widget.tsx (100%) rename src/features/dashboard/components/{ => widgets}/top-expenses-widget.tsx (100%) rename src/features/dashboard/components/{ => widgets}/widget-settings-dialog.tsx (96%) delete mode 100644 src/features/dashboard/dashboard-metrics-queries.ts rename src/features/dashboard/{ => expenses}/installment-expenses-helpers.ts (100%) delete mode 100644 src/features/dashboard/goals-progress-queries.ts rename src/features/dashboard/{ => goals-progress}/goals-progress-helpers.ts (94%) create mode 100644 src/features/dashboard/goals-progress/goals-progress-queries.ts rename src/features/dashboard/{ => goals-progress}/use-goals-progress-widget-controller.ts (87%) delete mode 100644 src/features/dashboard/income-expense-balance-queries.ts rename src/features/dashboard/{ => invoices}/invoices-helpers.ts (96%) rename src/features/dashboard/{ => invoices}/invoices-queries.ts (83%) rename src/features/dashboard/{ => invoices}/use-invoices-widget-controller.ts (86%) rename src/features/dashboard/{ => notes}/notes-mappers.ts (83%) rename src/features/dashboard/{ => notes}/notes-queries.ts (100%) rename src/features/dashboard/{ => notes}/use-notes-widget-controller.ts (91%) rename src/features/dashboard/{ => notifications}/notifications-actions.ts (100%) rename src/features/dashboard/{ => notifications}/notifications-queries.ts (99%) rename src/features/dashboard/{ => overview}/current-period-overview-queries.ts (99%) create mode 100644 src/features/dashboard/overview/dashboard-metrics-queries.ts create mode 100644 src/features/dashboard/overview/income-expense-balance-queries.ts rename src/features/dashboard/{ => overview}/period-overview-queries.ts (97%) rename src/features/dashboard/{ => payments}/payment-breakdown-formatters.ts (100%) rename src/features/dashboard/{ => payments}/payment-overview-tabs.ts (100%) rename src/features/dashboard/{ => payments}/use-payment-dialog-controller.ts (100%) rename src/features/dashboard/{ => payments}/use-payment-overview-widget-controller.ts (84%) delete mode 100644 src/features/dashboard/purchases-by-category-queries.ts rename src/features/dashboard/{components => widget-registry}/welcome-widget.ts (100%) rename src/features/dashboard/{widgets/actions.ts => widget-registry/widget-actions.ts} (100%) rename src/features/dashboard/{widgets/widgets-config.tsx => widget-registry/widget-config.tsx} (89%) 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) => ( +
+
+ + {entry.name} + +
+ ))} +
+
+ ); +} 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) => ( -
-
- - {entry.name} - -
- ))} -
-
+ ); 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" > -
+
{item.sourceAppName -
-

- {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";