mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-06-09 23:06:01 +00:00
refactor(dashboard): reorganizar módulos em subdiretórios e nova arquitetura de widgets
Arquivos de queries, helpers e controllers dispersos na raiz de dashboard/ foram movidos para subdiretórios temáticos (bills/, invoices/, notes/, notifications/, overview/, payments/, goals-progress/, categories/). ~25 widgets monolíticos obsoletos removidos em favor de nova arquitetura baseada em widget-registry com components/widgets/. Novos componentes: category-breakdown-chart/list, goals-progress-item, percentage-change-indicator. Imports atualizados em fetch-dashboard-data e transaction-filters limpos. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,8 +3,8 @@ import {
|
||||
buildBillStatusLabel,
|
||||
buildBillWidgetStatusLabel,
|
||||
isBillOverdue,
|
||||
} from "@/features/dashboard/bills-helpers";
|
||||
import type { DashboardBill } from "@/features/dashboard/bills-queries";
|
||||
} from "@/features/dashboard/bills/bills-helpers";
|
||||
import type { DashboardBill } from "@/features/dashboard/bills/bills-queries";
|
||||
import { EstablishmentLogo } from "@/shared/components/entity-avatar";
|
||||
import MoneyValues from "@/shared/components/money-values";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
@@ -82,8 +82,8 @@ export function BillListItem({ bill, onPay }: BillListItemProps) {
|
||||
onClick={() => onPay(bill.id)}
|
||||
>
|
||||
{bill.isSettled ? (
|
||||
<span className="flex items-center gap-1 text-success">
|
||||
<RiCheckboxCircleFill className="size-4" /> Pago
|
||||
<span className="flex items-center gap-0.5 text-success">
|
||||
<RiCheckboxCircleFill className="size-3.5" /> Pago
|
||||
</span>
|
||||
) : overdue ? (
|
||||
<span className="overdue-blink">
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { Pie, PieChart, Tooltip } from "recharts";
|
||||
import type { DashboardCategoryBreakdownItem } from "@/features/dashboard/categories/category-breakdown-helpers";
|
||||
import { type ChartConfig, ChartContainer } from "@/shared/components/ui/chart";
|
||||
import { formatCurrency } from "@/shared/utils/currency";
|
||||
import { formatPercentage as formatPercentageValue } from "@/shared/utils/percentage";
|
||||
|
||||
const CATEGORY_BREAKDOWN_COLORS = [
|
||||
"var(--chart-1)",
|
||||
"var(--chart-2)",
|
||||
"var(--chart-3)",
|
||||
"var(--chart-4)",
|
||||
"var(--chart-5)",
|
||||
"var(--chart-1)",
|
||||
"var(--chart-2)",
|
||||
];
|
||||
|
||||
const formatPercentage = (value: number, digits: number) =>
|
||||
formatPercentageValue(value, {
|
||||
minimumFractionDigits: digits,
|
||||
maximumFractionDigits: digits,
|
||||
absolute: true,
|
||||
});
|
||||
|
||||
type CategoryBreakdownChartProps = {
|
||||
categories: DashboardCategoryBreakdownItem[];
|
||||
percentageDigits: number;
|
||||
};
|
||||
|
||||
export function CategoryBreakdownChart({
|
||||
categories,
|
||||
percentageDigits,
|
||||
}: CategoryBreakdownChartProps) {
|
||||
const chartConfig = useMemo(() => {
|
||||
const nextConfig: ChartConfig = {};
|
||||
|
||||
const topCategories = categories.slice(0, 7);
|
||||
topCategories.forEach((category, index) => {
|
||||
nextConfig[category.categoryId] = {
|
||||
label: category.categoryName,
|
||||
color:
|
||||
CATEGORY_BREAKDOWN_COLORS[index % CATEGORY_BREAKDOWN_COLORS.length],
|
||||
};
|
||||
});
|
||||
|
||||
if (categories.length > 7) {
|
||||
nextConfig.outros = { label: "Outros", color: "var(--chart-6)" };
|
||||
}
|
||||
|
||||
return nextConfig;
|
||||
}, [categories]);
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
if (categories.length <= 7) {
|
||||
return categories.map((category) => ({
|
||||
category: category.categoryId,
|
||||
name: category.categoryName,
|
||||
value: category.currentAmount,
|
||||
percentage: category.percentageOfTotal,
|
||||
fill: chartConfig[category.categoryId]?.color,
|
||||
}));
|
||||
}
|
||||
|
||||
const topCategories = categories.slice(0, 7);
|
||||
const otherCategories = categories.slice(7);
|
||||
const otherTotal = otherCategories.reduce(
|
||||
(sum, c) => sum + c.currentAmount,
|
||||
0,
|
||||
);
|
||||
const otherPercentage = otherCategories.reduce(
|
||||
(sum, c) => sum + c.percentageOfTotal,
|
||||
0,
|
||||
);
|
||||
|
||||
const groupedData = topCategories.map((category) => ({
|
||||
category: category.categoryId,
|
||||
name: category.categoryName,
|
||||
value: category.currentAmount,
|
||||
percentage: category.percentageOfTotal,
|
||||
fill: chartConfig[category.categoryId]?.color,
|
||||
}));
|
||||
|
||||
if (otherCategories.length > 0) {
|
||||
groupedData.push({
|
||||
category: "outros",
|
||||
name: "Outros",
|
||||
value: otherTotal,
|
||||
percentage: otherPercentage,
|
||||
fill: chartConfig.outros?.color,
|
||||
});
|
||||
}
|
||||
|
||||
return groupedData;
|
||||
}, [categories, chartConfig]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-4">
|
||||
<ChartContainer config={chartConfig} className="h-[280px] flex-1">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={chartData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
labelLine={false}
|
||||
label={({ payload }) =>
|
||||
formatPercentage(
|
||||
(payload as { percentage?: number } | undefined)?.percentage ??
|
||||
0,
|
||||
percentageDigits,
|
||||
)
|
||||
}
|
||||
outerRadius={75}
|
||||
dataKey="value"
|
||||
nameKey="category"
|
||||
/>
|
||||
<Tooltip
|
||||
content={({ active, payload }) => {
|
||||
if (!active || !payload?.length) return null;
|
||||
const entry = payload[0]?.payload;
|
||||
if (!entry) return null;
|
||||
return (
|
||||
<div className="rounded-lg border bg-background p-2 shadow-sm">
|
||||
<div className="grid gap-2">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs uppercase text-muted-foreground">
|
||||
{entry.name}
|
||||
</span>
|
||||
<span className="font-medium text-foreground">
|
||||
{formatCurrency(entry.value)}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatPercentage(entry.percentage, percentageDigits)}{" "}
|
||||
do total
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
|
||||
<div className="min-w-[140px] flex flex-col gap-2">
|
||||
{chartData.map((entry, index) => (
|
||||
<div key={`legend-${index}`} className="flex items-center gap-2">
|
||||
<div
|
||||
className="size-3 shrink-0 rounded-sm"
|
||||
style={{ backgroundColor: entry.fill }}
|
||||
/>
|
||||
<span className="truncate text-xs text-muted-foreground">
|
||||
{entry.name}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { RiExternalLinkLine, RiWallet3Line } from "@remixicon/react";
|
||||
import Link from "next/link";
|
||||
import type { DashboardCategoryBreakdownItem } from "@/features/dashboard/categories/category-breakdown-helpers";
|
||||
import { PercentageChangeIndicator } from "@/features/dashboard/components/percentage-change-indicator";
|
||||
import { CategoryIconBadge } from "@/shared/components/entity-avatar";
|
||||
import MoneyValues from "@/shared/components/money-values";
|
||||
import { formatCurrency } from "@/shared/utils/currency";
|
||||
import { formatPercentage as formatPercentageValue } from "@/shared/utils/percentage";
|
||||
|
||||
type CategoryBreakdownListItemConfig = {
|
||||
shareLabel: string;
|
||||
percentageDigits: number;
|
||||
positiveTrend: "up" | "down";
|
||||
includeBudgetAmount: boolean;
|
||||
};
|
||||
|
||||
type CategoryBreakdownListItemProps = {
|
||||
category: DashboardCategoryBreakdownItem;
|
||||
periodParam: string;
|
||||
config: CategoryBreakdownListItemConfig;
|
||||
};
|
||||
|
||||
const formatPercentage = (value: number, digits: number) =>
|
||||
formatPercentageValue(value, {
|
||||
minimumFractionDigits: digits,
|
||||
maximumFractionDigits: digits,
|
||||
absolute: true,
|
||||
});
|
||||
|
||||
export function CategoryBreakdownListItem({
|
||||
category,
|
||||
periodParam,
|
||||
config,
|
||||
}: CategoryBreakdownListItemProps) {
|
||||
const hasBudget = category.budgetAmount !== null;
|
||||
const budgetExceeded =
|
||||
hasBudget &&
|
||||
category.budgetUsedPercentage !== null &&
|
||||
category.budgetUsedPercentage > 100;
|
||||
const exceededAmount =
|
||||
budgetExceeded && category.budgetAmount
|
||||
? category.currentAmount - category.budgetAmount
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between gap-3 transition-all duration-300 py-2">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<CategoryIconBadge
|
||||
icon={category.categoryIcon}
|
||||
name={category.categoryName}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
href={`/categories/${category.categoryId}?periodo=${periodParam}`}
|
||||
className="flex max-w-full items-center gap-1 text-sm font-medium text-foreground underline-offset-2 hover:underline"
|
||||
>
|
||||
<span className="truncate">{category.categoryName}</span>
|
||||
<RiExternalLinkLine
|
||||
className="size-3 shrink-0 text-muted-foreground"
|
||||
aria-hidden
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-muted-foreground">
|
||||
<span>
|
||||
{formatPercentage(
|
||||
category.percentageOfTotal,
|
||||
config.percentageDigits,
|
||||
)}{" "}
|
||||
da {config.shareLabel}
|
||||
</span>
|
||||
{hasBudget && category.budgetUsedPercentage !== null ? (
|
||||
<>
|
||||
<span aria-hidden>·</span>
|
||||
<span
|
||||
className={`flex items-center gap-1 ${budgetExceeded ? "text-destructive" : "text-info"}`}
|
||||
>
|
||||
<RiWallet3Line className="size-3 shrink-0" />
|
||||
{budgetExceeded ? (
|
||||
<>
|
||||
excedeu{" "}
|
||||
<span className="font-medium">
|
||||
{formatCurrency(exceededAmount)}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{formatPercentage(
|
||||
category.budgetUsedPercentage,
|
||||
config.percentageDigits,
|
||||
)}{" "}
|
||||
do limite
|
||||
{config.includeBudgetAmount &&
|
||||
category.budgetAmount !== null
|
||||
? ` ${formatCurrency(category.budgetAmount)}`
|
||||
: ""}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 flex-col items-end gap-0.5">
|
||||
<MoneyValues
|
||||
className="text-foreground font-medium"
|
||||
amount={category.currentAmount}
|
||||
/>
|
||||
<PercentageChangeIndicator
|
||||
value={category.percentageChange}
|
||||
label={
|
||||
category.percentageChange !== null
|
||||
? formatPercentage(
|
||||
category.percentageChange,
|
||||
config.percentageDigits,
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
positiveTrend={config.positiveTrend}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { DashboardCategoryBreakdownItem } from "@/features/dashboard/categories/category-breakdown-helpers";
|
||||
import { CategoryBreakdownListItem } from "./category-breakdown-list-item";
|
||||
|
||||
type CategoryBreakdownListConfig = {
|
||||
shareLabel: string;
|
||||
percentageDigits: number;
|
||||
positiveTrend: "up" | "down";
|
||||
includeBudgetAmount: boolean;
|
||||
};
|
||||
|
||||
type CategoryBreakdownListProps = {
|
||||
categories: DashboardCategoryBreakdownItem[];
|
||||
periodParam: string;
|
||||
config: CategoryBreakdownListConfig;
|
||||
};
|
||||
|
||||
export function CategoryBreakdownList({
|
||||
categories,
|
||||
periodParam,
|
||||
config,
|
||||
}: CategoryBreakdownListProps) {
|
||||
return (
|
||||
<div>
|
||||
{categories.map((category) => (
|
||||
<CategoryBreakdownListItem
|
||||
key={category.categoryId}
|
||||
category={category}
|
||||
periodParam={periodParam}
|
||||
config={config}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
RiArrowDownSFill,
|
||||
RiArrowUpSFill,
|
||||
RiExternalLinkLine,
|
||||
RiListUnordered,
|
||||
RiPieChart2Line,
|
||||
RiPieChartLine,
|
||||
RiWallet3Line,
|
||||
} from "@remixicon/react";
|
||||
import Link from "next/link";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Pie, PieChart, Tooltip } from "recharts";
|
||||
import type { DashboardCategoryBreakdownData } from "@/features/dashboard/categories/category-breakdown";
|
||||
import { CategoryIconBadge } from "@/shared/components/entity-avatar";
|
||||
import MoneyValues from "@/shared/components/money-values";
|
||||
import { type ChartConfig, ChartContainer } from "@/shared/components/ui/chart";
|
||||
import { useState } from "react";
|
||||
import type { DashboardCategoryBreakdownData } from "@/features/dashboard/categories/category-breakdown-helpers";
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
@@ -23,9 +14,9 @@ import {
|
||||
TabsTrigger,
|
||||
} from "@/shared/components/ui/tabs";
|
||||
import { WidgetEmptyState } from "@/shared/components/widget-empty-state";
|
||||
import { formatCurrency } from "@/shared/utils/currency";
|
||||
import { formatPercentage as formatPercentageValue } from "@/shared/utils/percentage";
|
||||
import { formatPeriodForUrl } from "@/shared/utils/period";
|
||||
import { CategoryBreakdownChart } from "./category-breakdown-chart";
|
||||
import { CategoryBreakdownList } from "./category-breakdown-list";
|
||||
|
||||
type CategoryBreakdownVariant = "income" | "expense";
|
||||
|
||||
@@ -35,16 +26,6 @@ type CategoryBreakdownWidgetViewProps = {
|
||||
variant: CategoryBreakdownVariant;
|
||||
};
|
||||
|
||||
const CATEGORY_BREAKDOWN_COLORS = [
|
||||
"var(--chart-1)",
|
||||
"var(--chart-2)",
|
||||
"var(--chart-3)",
|
||||
"var(--chart-4)",
|
||||
"var(--chart-5)",
|
||||
"var(--chart-1)",
|
||||
"var(--chart-2)",
|
||||
];
|
||||
|
||||
const VARIANT_CONFIG = {
|
||||
income: {
|
||||
emptyTitle: "Nenhuma receita encontrada",
|
||||
@@ -52,10 +33,7 @@ const VARIANT_CONFIG = {
|
||||
"Quando houver receitas registradas, elas aparecerão aqui.",
|
||||
shareLabel: "receita total",
|
||||
percentageDigits: 1,
|
||||
changeClassName: {
|
||||
increase: "text-success",
|
||||
decrease: "text-destructive",
|
||||
},
|
||||
positiveTrend: "up",
|
||||
includeBudgetAmount: true,
|
||||
},
|
||||
expense: {
|
||||
@@ -64,21 +42,11 @@ const VARIANT_CONFIG = {
|
||||
"Quando houver despesas registradas, elas aparecerão aqui.",
|
||||
shareLabel: "despesa total",
|
||||
percentageDigits: 0,
|
||||
changeClassName: {
|
||||
increase: "text-destructive",
|
||||
decrease: "text-success",
|
||||
},
|
||||
positiveTrend: "down",
|
||||
includeBudgetAmount: false,
|
||||
},
|
||||
} as const;
|
||||
|
||||
const formatPercentage = (value: number, digits: number) =>
|
||||
formatPercentageValue(value, {
|
||||
minimumFractionDigits: digits,
|
||||
maximumFractionDigits: digits,
|
||||
absolute: true,
|
||||
});
|
||||
|
||||
export function CategoryBreakdownWidgetView({
|
||||
data,
|
||||
period,
|
||||
@@ -88,78 +56,6 @@ export function CategoryBreakdownWidgetView({
|
||||
const periodParam = formatPeriodForUrl(period);
|
||||
const config = VARIANT_CONFIG[variant];
|
||||
|
||||
const chartConfig = useMemo(() => {
|
||||
const nextConfig: ChartConfig = {};
|
||||
|
||||
if (data.categories.length <= 7) {
|
||||
data.categories.forEach((category, index) => {
|
||||
nextConfig[category.categoryId] = {
|
||||
label: category.categoryName,
|
||||
color:
|
||||
CATEGORY_BREAKDOWN_COLORS[index % CATEGORY_BREAKDOWN_COLORS.length],
|
||||
};
|
||||
});
|
||||
} else {
|
||||
const topCategories = data.categories.slice(0, 7);
|
||||
topCategories.forEach((category, index) => {
|
||||
nextConfig[category.categoryId] = {
|
||||
label: category.categoryName,
|
||||
color:
|
||||
CATEGORY_BREAKDOWN_COLORS[index % CATEGORY_BREAKDOWN_COLORS.length],
|
||||
};
|
||||
});
|
||||
nextConfig.outros = {
|
||||
label: "Outros",
|
||||
color: "var(--chart-6)",
|
||||
};
|
||||
}
|
||||
|
||||
return nextConfig;
|
||||
}, [data.categories]);
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
if (data.categories.length <= 7) {
|
||||
return data.categories.map((category) => ({
|
||||
category: category.categoryId,
|
||||
name: category.categoryName,
|
||||
value: category.currentAmount,
|
||||
percentage: category.percentageOfTotal,
|
||||
fill: chartConfig[category.categoryId]?.color,
|
||||
}));
|
||||
}
|
||||
|
||||
const topCategories = data.categories.slice(0, 7);
|
||||
const otherCategories = data.categories.slice(7);
|
||||
const otherTotal = otherCategories.reduce(
|
||||
(sum, category) => sum + category.currentAmount,
|
||||
0,
|
||||
);
|
||||
const otherPercentage = otherCategories.reduce(
|
||||
(sum, category) => sum + category.percentageOfTotal,
|
||||
0,
|
||||
);
|
||||
|
||||
const groupedData = topCategories.map((category) => ({
|
||||
category: category.categoryId,
|
||||
name: category.categoryName,
|
||||
value: category.currentAmount,
|
||||
percentage: category.percentageOfTotal,
|
||||
fill: chartConfig[category.categoryId]?.color,
|
||||
}));
|
||||
|
||||
if (otherCategories.length > 0) {
|
||||
groupedData.push({
|
||||
category: "outros",
|
||||
name: "Outros",
|
||||
value: otherTotal,
|
||||
percentage: otherPercentage,
|
||||
fill: chartConfig.outros?.color,
|
||||
});
|
||||
}
|
||||
|
||||
return groupedData;
|
||||
}, [data.categories, chartConfig]);
|
||||
|
||||
if (data.categories.length === 0) {
|
||||
return (
|
||||
<WidgetEmptyState
|
||||
@@ -178,11 +74,17 @@ export function CategoryBreakdownWidgetView({
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<TabsList className="grid grid-cols-2">
|
||||
<TabsTrigger value="list" className="text-xs">
|
||||
<TabsTrigger
|
||||
value="list"
|
||||
className="text-xs data-[state=active]:bg-transparent"
|
||||
>
|
||||
<RiListUnordered className="mr-1 size-3.5" />
|
||||
Lista
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="chart" className="text-xs">
|
||||
<TabsTrigger
|
||||
value="chart"
|
||||
className="text-xs data-[state=active]:bg-transparent"
|
||||
>
|
||||
<RiPieChart2Line className="mr-1 size-3.5" />
|
||||
Gráfico
|
||||
</TabsTrigger>
|
||||
@@ -190,195 +92,18 @@ export function CategoryBreakdownWidgetView({
|
||||
</div>
|
||||
|
||||
<TabsContent value="list" className="mt-0">
|
||||
<div>
|
||||
{data.categories.map((category, index) => {
|
||||
const hasIncrease =
|
||||
category.percentageChange !== null &&
|
||||
category.percentageChange > 0;
|
||||
const hasDecrease =
|
||||
category.percentageChange !== null &&
|
||||
category.percentageChange < 0;
|
||||
const hasBudget = category.budgetAmount !== null;
|
||||
const budgetExceeded =
|
||||
hasBudget &&
|
||||
category.budgetUsedPercentage !== null &&
|
||||
category.budgetUsedPercentage > 100;
|
||||
const exceededAmount =
|
||||
budgetExceeded && category.budgetAmount
|
||||
? category.currentAmount - category.budgetAmount
|
||||
: 0;
|
||||
const changeClassName = hasIncrease
|
||||
? config.changeClassName.increase
|
||||
: hasDecrease
|
||||
? config.changeClassName.decrease
|
||||
: "text-muted-foreground";
|
||||
|
||||
return (
|
||||
<div key={category.categoryId}>
|
||||
<div className="flex items-center justify-between gap-3 transition-all duration-300 py-2">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<CategoryIconBadge
|
||||
icon={category.categoryIcon}
|
||||
name={category.categoryName}
|
||||
/>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
href={`/categories/${category.categoryId}?periodo=${periodParam}`}
|
||||
className="flex max-w-full items-center gap-1 text-sm font-medium text-foreground underline-offset-2 hover:underline"
|
||||
>
|
||||
<span className="truncate">
|
||||
{category.categoryName}
|
||||
</span>
|
||||
<RiExternalLinkLine
|
||||
className="size-3 shrink-0 text-muted-foreground"
|
||||
aria-hidden
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-muted-foreground">
|
||||
<span>
|
||||
{formatPercentage(
|
||||
category.percentageOfTotal,
|
||||
config.percentageDigits,
|
||||
)}{" "}
|
||||
da {config.shareLabel}
|
||||
</span>
|
||||
{hasBudget && category.budgetUsedPercentage !== null ? (
|
||||
<>
|
||||
<span aria-hidden>·</span>
|
||||
<span
|
||||
className={`flex items-center gap-1 ${budgetExceeded ? "text-destructive" : "text-info"}`}
|
||||
>
|
||||
<RiWallet3Line className="size-3 shrink-0" />
|
||||
{budgetExceeded ? (
|
||||
<>
|
||||
excedeu{" "}
|
||||
<span className="font-medium">
|
||||
{formatCurrency(exceededAmount)}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{formatPercentage(
|
||||
category.budgetUsedPercentage,
|
||||
config.percentageDigits,
|
||||
)}{" "}
|
||||
do limite
|
||||
{config.includeBudgetAmount &&
|
||||
category.budgetAmount !== null
|
||||
? ` ${formatCurrency(category.budgetAmount)}`
|
||||
: ""}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 flex-col items-end gap-0.5">
|
||||
<MoneyValues
|
||||
className="text-foreground font-medium"
|
||||
amount={category.currentAmount}
|
||||
/>
|
||||
{category.percentageChange !== null ? (
|
||||
<span
|
||||
className={`flex items-center gap-0.5 text-xs font-medium ${changeClassName}`}
|
||||
>
|
||||
{hasIncrease ? (
|
||||
<RiArrowUpSFill className="size-3" />
|
||||
) : null}
|
||||
{hasDecrease ? (
|
||||
<RiArrowDownSFill className="size-3" />
|
||||
) : null}
|
||||
{formatPercentage(
|
||||
category.percentageChange,
|
||||
config.percentageDigits,
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<CategoryBreakdownList
|
||||
categories={data.categories}
|
||||
periodParam={periodParam}
|
||||
config={config}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="chart" className="mt-0">
|
||||
<div className="flex items-center gap-4">
|
||||
<ChartContainer config={chartConfig} className="h-[280px] flex-1">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={chartData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
labelLine={false}
|
||||
label={({ payload }) =>
|
||||
formatPercentage(
|
||||
(payload as { percentage?: number } | undefined)
|
||||
?.percentage ?? 0,
|
||||
config.percentageDigits,
|
||||
)
|
||||
}
|
||||
outerRadius={75}
|
||||
dataKey="value"
|
||||
nameKey="category"
|
||||
/>
|
||||
<Tooltip
|
||||
content={({ active, payload }) => {
|
||||
if (!active || !payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const entry = payload[0]?.payload;
|
||||
if (!entry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border bg-background p-2 shadow-sm">
|
||||
<div className="grid gap-2">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs uppercase text-muted-foreground">
|
||||
{entry.name}
|
||||
</span>
|
||||
<span className="font-medium text-foreground">
|
||||
{formatCurrency(entry.value)}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatPercentage(
|
||||
entry.percentage,
|
||||
config.percentageDigits,
|
||||
)}{" "}
|
||||
do total
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
|
||||
<div className="min-w-[140px] flex flex-col gap-2">
|
||||
{chartData.map((entry, index) => (
|
||||
<div key={`legend-${index}`} className="flex items-center gap-2">
|
||||
<div
|
||||
className="size-3 shrink-0 rounded-sm"
|
||||
style={{ backgroundColor: entry.fill }}
|
||||
/>
|
||||
<span className="truncate text-xs text-muted-foreground">
|
||||
{entry.name}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<CategoryBreakdownChart
|
||||
categories={data.categories}
|
||||
percentageDigits={config.percentageDigits}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import {
|
||||
RiArrowDownLine,
|
||||
RiArrowDownSFill,
|
||||
RiArrowUpLine,
|
||||
RiArrowUpSFill,
|
||||
RiCalendarCheckLine,
|
||||
RiScalesLine,
|
||||
RiSubtractLine,
|
||||
RiArrowLeftRightLine,
|
||||
RiArrowRightDownLine,
|
||||
RiArrowRightUpLine,
|
||||
RiCalendar2Line,
|
||||
} from "@remixicon/react";
|
||||
import { MetricsCardInfoButton } from "@/features/dashboard/components/metrics-card-info-button";
|
||||
import type { DashboardCardMetrics } from "@/features/dashboard/dashboard-metrics-queries";
|
||||
import { PercentageChangeIndicator } from "@/features/dashboard/components/percentage-change-indicator";
|
||||
import type { DashboardCardMetrics } from "@/features/dashboard/overview/dashboard-metrics-queries";
|
||||
import MoneyValues from "@/shared/components/money-values";
|
||||
import {
|
||||
Card,
|
||||
@@ -34,7 +32,7 @@ const CARDS = [
|
||||
label: "Receitas",
|
||||
subtitle: "Entradas do período",
|
||||
key: "receitas",
|
||||
icon: RiArrowDownLine,
|
||||
icon: RiArrowRightDownLine,
|
||||
invertTrend: false,
|
||||
iconClass: "text-success",
|
||||
helpTitle: "Como calculamos receitas",
|
||||
@@ -50,7 +48,7 @@ const CARDS = [
|
||||
label: "Despesas",
|
||||
subtitle: "Saídas do período",
|
||||
key: "despesas",
|
||||
icon: RiArrowUpLine,
|
||||
icon: RiArrowRightUpLine,
|
||||
invertTrend: true,
|
||||
iconClass: "text-destructive",
|
||||
helpTitle: "Como calculamos despesas",
|
||||
@@ -66,7 +64,7 @@ const CARDS = [
|
||||
label: "Balanço",
|
||||
subtitle: "Receitas, despesas e ajustes entre contas",
|
||||
key: "balanco",
|
||||
icon: RiScalesLine,
|
||||
icon: RiArrowLeftRightLine,
|
||||
invertTrend: false,
|
||||
iconClass: "text-warning",
|
||||
helpTitle: "Como calculamos o balanço",
|
||||
@@ -81,7 +79,7 @@ const CARDS = [
|
||||
label: "Previsto",
|
||||
subtitle: "Saldo acumulado projetado",
|
||||
key: "previsto",
|
||||
icon: RiCalendarCheckLine,
|
||||
icon: RiCalendar2Line,
|
||||
invertTrend: false,
|
||||
iconClass: "text-cyan-600",
|
||||
helpTitle: "Como calculamos o previsto",
|
||||
@@ -94,12 +92,6 @@ const CARDS = [
|
||||
},
|
||||
] as const;
|
||||
|
||||
const TREND_ICONS = {
|
||||
up: RiArrowUpSFill,
|
||||
down: RiArrowDownSFill,
|
||||
flat: RiSubtractLine,
|
||||
} as const;
|
||||
|
||||
const getTrend = (current: number, previous: number): Trend => {
|
||||
const diff = current - previous;
|
||||
if (diff > TREND_THRESHOLD) return "up";
|
||||
@@ -126,12 +118,6 @@ const getPercentChange = (current: number, previous: number): string => {
|
||||
});
|
||||
};
|
||||
|
||||
const getTrendBadgeClass = (trend: Trend, invertTrend: boolean): string => {
|
||||
if (trend === "flat") return "text-muted-foreground";
|
||||
const isPositive = invertTrend ? trend === "down" : trend === "up";
|
||||
return isPositive ? "text-success" : "text-destructive";
|
||||
};
|
||||
|
||||
export function DashboardMetricsCards({ metrics }: DashboardMetricsCardsProps) {
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-3 @xl/main:grid-cols-2 @5xl/main:grid-cols-4">
|
||||
@@ -148,8 +134,6 @@ export function DashboardMetricsCards({ metrics }: DashboardMetricsCardsProps) {
|
||||
}) => {
|
||||
const metric = metrics[key];
|
||||
const trend = getTrend(metric.current, metric.previous);
|
||||
const TrendIcon = TREND_ICONS[trend];
|
||||
const trendBadgeClass = getTrendBadgeClass(trend, invertTrend);
|
||||
const percentChange = getPercentChange(
|
||||
metric.current,
|
||||
metric.previous,
|
||||
@@ -157,23 +141,19 @@ export function DashboardMetricsCards({ metrics }: DashboardMetricsCardsProps) {
|
||||
|
||||
return (
|
||||
<Card key={label} className="gap-2 overflow-hidden">
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-1.5 ">
|
||||
<Icon className={cn("size-4", iconClass)} aria-hidden />
|
||||
{label}
|
||||
<MetricsCardInfoButton
|
||||
label={label}
|
||||
helpTitle={helpTitle}
|
||||
helpLines={helpLines}
|
||||
/>
|
||||
</CardTitle>
|
||||
<CardDescription className="mt-1.5 tracking-tight">
|
||||
{subtitle}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<CardHeader className="gap-1">
|
||||
<CardTitle className="flex items-center gap-1">
|
||||
<Icon className={cn("size-4", iconClass)} aria-hidden />
|
||||
{label}
|
||||
<MetricsCardInfoButton
|
||||
label={label}
|
||||
helpTitle={helpTitle}
|
||||
helpLines={helpLines}
|
||||
/>
|
||||
</CardTitle>
|
||||
<CardDescription className="mt-1 tracking-tight">
|
||||
{subtitle}
|
||||
</CardDescription>
|
||||
<Separator className="mt-1" />
|
||||
</CardHeader>
|
||||
|
||||
@@ -183,15 +163,14 @@ export function DashboardMetricsCards({ metrics }: DashboardMetricsCardsProps) {
|
||||
className="text-2xl leading-none font-medium"
|
||||
amount={metric.current}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 text-xs font-medium",
|
||||
trendBadgeClass,
|
||||
)}
|
||||
>
|
||||
<TrendIcon className="size-3.5" aria-hidden />
|
||||
<span>{percentChange}</span>
|
||||
</div>
|
||||
<PercentageChangeIndicator
|
||||
trend={trend}
|
||||
label={percentChange}
|
||||
positiveTrend={invertTrend ? "down" : "up"}
|
||||
showFlatIcon
|
||||
className="gap-1"
|
||||
iconClassName="size-3.5"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-muted-foreground">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { formatCurrentDate, getGreeting } from "./welcome-widget";
|
||||
import { formatCurrentDate, getGreeting } from "@/features/dashboard/widget-registry/welcome-widget";
|
||||
|
||||
type DashboardWelcomeProps = {
|
||||
name?: string | null;
|
||||
@@ -10,13 +10,11 @@ export function DashboardWelcome({ name }: DashboardWelcomeProps) {
|
||||
const greeting = getGreeting();
|
||||
|
||||
return (
|
||||
<section className="py-4">
|
||||
<div>
|
||||
<h1 className="text-xl tracking-tight">
|
||||
{greeting}, {displayName}
|
||||
</h1>
|
||||
<h2 className="mt-1 text-sm text-muted-foreground">{formattedDate}</h2>
|
||||
</div>
|
||||
<section className="py-4 space-y-1">
|
||||
<h1 className="text-xl tracking-tight">
|
||||
<span className="text-muted-foreground">{greeting},</span> {displayName}
|
||||
</h1>
|
||||
<h2 className="text-sm text-muted-foreground">{formattedDate}</h2>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { RiPencilLine } from "@remixicon/react";
|
||||
import { PercentageChangeIndicator } from "@/features/dashboard/components/percentage-change-indicator";
|
||||
import {
|
||||
clampGoalProgress,
|
||||
formatGoalProgressPercentage,
|
||||
} from "@/features/dashboard/goals-progress-helpers";
|
||||
import type { GoalProgressItem as GoalProgressItemData } from "@/features/dashboard/goals-progress-queries";
|
||||
} from "@/features/dashboard/goals-progress/goals-progress-helpers";
|
||||
import type { GoalProgressItem as GoalProgressItemData } from "@/features/dashboard/goals-progress/goals-progress-queries";
|
||||
import { CategoryIconBadge } from "@/shared/components/entity-avatar";
|
||||
import MoneyValues from "@/shared/components/money-values";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
@@ -22,12 +23,6 @@ export function GoalProgressItem({
|
||||
}: GoalProgressItemProps) {
|
||||
const progressValue = clampGoalProgress(item.usedPercentage, 0, 100);
|
||||
const percentageDelta = item.usedPercentage - 100;
|
||||
const deltaColor =
|
||||
percentageDelta > 0
|
||||
? "text-destructive"
|
||||
: percentageDelta < 0
|
||||
? "text-success"
|
||||
: "text-muted-foreground";
|
||||
const isExceeded = item.status === "exceeded";
|
||||
|
||||
return (
|
||||
@@ -47,9 +42,12 @@ export function GoalProgressItem({
|
||||
<MoneyValues className="font-medium" amount={item.spentAmount} />{" "}
|
||||
de{" "}
|
||||
<MoneyValues className="font-medium" amount={item.budgetAmount} />
|
||||
<span className={`ml-1.5 font-medium ${deltaColor}`}>
|
||||
{formatGoalProgressPercentage(percentageDelta, true)}
|
||||
</span>
|
||||
<PercentageChangeIndicator
|
||||
value={percentageDelta}
|
||||
label={formatGoalProgressPercentage(percentageDelta, true)}
|
||||
positiveTrend="down"
|
||||
className="ml-1.5 align-middle"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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[];
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ export function InstallmentAnalysisPage({
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Card de resumo principal */}
|
||||
<Card className="border-none bg-primary/15">
|
||||
<Card className="border-none bg-primary/10 dark:bg-primary/10">
|
||||
<CardContent className="flex flex-col items-start justify-center gap-2 py-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Se você pagar tudo que está selecionado:
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { RiCheckboxCircleFill, RiExternalLinkLine } from "@remixicon/react";
|
||||
import Link from "next/link";
|
||||
import { PercentageChangeIndicator } from "@/features/dashboard/components/percentage-change-indicator";
|
||||
import {
|
||||
buildInvoiceDetailsHref,
|
||||
buildInvoiceInitials,
|
||||
@@ -8,8 +9,8 @@ import {
|
||||
getInvoiceShareLabel,
|
||||
parseInvoiceDueDate,
|
||||
parseInvoiceWidgetDueDate,
|
||||
} from "@/features/dashboard/invoices-helpers";
|
||||
import type { DashboardInvoice } from "@/features/dashboard/invoices-queries";
|
||||
} from "@/features/dashboard/invoices/invoices-helpers";
|
||||
import type { DashboardInvoice } from "@/features/dashboard/invoices/invoices-queries";
|
||||
import MoneyValues from "@/shared/components/money-values";
|
||||
import {
|
||||
Avatar,
|
||||
@@ -83,7 +84,7 @@ export function InvoiceListItem({ invoice, onPay }: InvoiceListItemProps) {
|
||||
{hasBreakdown ? (
|
||||
<HoverCard openDelay={150}>
|
||||
<HoverCardTrigger asChild>{linkNode}</HoverCardTrigger>
|
||||
<HoverCardContent align="start" className="w-72 space-y-3">
|
||||
<HoverCardContent align="start" className="w-80 space-y-3">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Distribuição por pagador
|
||||
</p>
|
||||
@@ -115,11 +116,14 @@ export function InvoiceListItem({ invoice, onPay }: InvoiceListItemProps) {
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
<div className="flex shrink-0 flex-col items-end gap-0.5 text-sm font-medium text-foreground">
|
||||
<MoneyValues
|
||||
className="font-medium"
|
||||
amount={share.amount}
|
||||
/>
|
||||
<PercentageChangeIndicator
|
||||
value={share.percentageChange}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
@@ -179,8 +183,8 @@ export function InvoiceListItem({ invoice, onPay }: InvoiceListItemProps) {
|
||||
onClick={() => onPay(invoice.id)}
|
||||
>
|
||||
{isPaid ? (
|
||||
<span className="flex items-center gap-1 text-success">
|
||||
<RiCheckboxCircleFill className="size-4" /> Pago
|
||||
<span className="flex items-center gap-0.5 text-success">
|
||||
<RiCheckboxCircleFill className="size-3.5" /> Pago
|
||||
</span>
|
||||
) : isOverdue ? (
|
||||
<span className="overdue-blink">
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -29,14 +29,12 @@ export function NoteListItem({
|
||||
{displayTitle}
|
||||
</p>
|
||||
<div className="mt-1 flex items-center gap-2">
|
||||
<Badge variant="outline" className="h-5 px-1.5 text-[10px]">
|
||||
<Badge variant="outline" className="h-5 px-1.5 text-xs">
|
||||
{getNoteTasksSummary(note)}
|
||||
</Badge>
|
||||
{createdAtLabel ? (
|
||||
<p className="truncate text-xs text-muted-foreground">
|
||||
{createdAtLabel}
|
||||
</p>
|
||||
) : null}
|
||||
<p className="truncate text-xs text-muted-foreground">
|
||||
{createdAtLabel}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RiMoneyDollarCircleLine, RiSlideshowLine } from "@remixicon/react";
|
||||
import type { PaymentOverviewTab } from "@/features/dashboard/payment-overview-tabs";
|
||||
import type { PaymentOverviewTab } from "@/features/dashboard/payments/payment-overview-tabs";
|
||||
import type { PaymentConditionsData } from "@/features/dashboard/payments/payment-conditions-queries";
|
||||
import type { PaymentMethodsData } from "@/features/dashboard/payments/payment-methods-queries";
|
||||
import {
|
||||
@@ -31,11 +31,17 @@ export function PaymentOverviewWidgetView({
|
||||
return (
|
||||
<Tabs value={activeTab} onValueChange={onTabChange} className="w-full">
|
||||
<TabsList className="grid grid-cols-2">
|
||||
<TabsTrigger value="conditions" className="text-xs">
|
||||
<TabsTrigger
|
||||
value="conditions"
|
||||
className="text-xs data-[state=active]:bg-transparent"
|
||||
>
|
||||
<RiSlideshowLine className="mr-1 size-3.5" />
|
||||
Condições
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="methods" className="text-xs">
|
||||
<TabsTrigger
|
||||
value="methods"
|
||||
className="text-xs data-[state=active]:bg-transparent"
|
||||
>
|
||||
<RiMoneyDollarCircleLine className="mr-1 size-3.5" />
|
||||
Formas
|
||||
</TabsTrigger>
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
RiArrowDownSFill,
|
||||
RiArrowUpSFill,
|
||||
RiSubtractLine,
|
||||
} from "@remixicon/react";
|
||||
import { formatPercentage } from "@/shared/utils/percentage";
|
||||
import { cn } from "@/shared/utils/ui";
|
||||
|
||||
export type PercentageChangeTrend = "up" | "down" | "flat";
|
||||
|
||||
type PercentageChangeIndicatorProps = {
|
||||
value?: number | null;
|
||||
label?: string;
|
||||
trend?: PercentageChangeTrend;
|
||||
positiveTrend?: Exclude<PercentageChangeTrend, "flat">;
|
||||
showFlatIcon?: boolean;
|
||||
className?: string;
|
||||
iconClassName?: string;
|
||||
};
|
||||
|
||||
export function PercentageChangeIndicator({
|
||||
value,
|
||||
label,
|
||||
trend,
|
||||
positiveTrend = "down",
|
||||
showFlatIcon = false,
|
||||
className,
|
||||
iconClassName,
|
||||
}: PercentageChangeIndicatorProps) {
|
||||
const hasNumericValue = typeof value === "number" && Number.isFinite(value);
|
||||
const resolvedTrend =
|
||||
trend ??
|
||||
(hasNumericValue
|
||||
? value > 0
|
||||
? "up"
|
||||
: value < 0
|
||||
? "down"
|
||||
: "flat"
|
||||
: "flat");
|
||||
const resolvedLabel =
|
||||
label ?? (hasNumericValue ? formatPercentage(value) : null);
|
||||
|
||||
if (!resolvedLabel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center gap-0.5 text-xs font-medium",
|
||||
resolvedTrend === "flat"
|
||||
? "text-muted-foreground"
|
||||
: resolvedTrend === positiveTrend
|
||||
? "text-success"
|
||||
: "text-destructive",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{resolvedTrend === "up" ? (
|
||||
<RiArrowUpSFill className={cn("size-3", iconClassName)} />
|
||||
) : null}
|
||||
{resolvedTrend === "down" ? (
|
||||
<RiArrowDownSFill className={cn("size-3", iconClassName)} />
|
||||
) : null}
|
||||
{resolvedTrend === "flat" && showFlatIcon ? (
|
||||
<RiSubtractLine className={cn("size-3", iconClassName)} />
|
||||
) : null}
|
||||
{resolvedLabel}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import {
|
||||
formatBusinessCurrentDate,
|
||||
getBusinessGreeting,
|
||||
} from "@/shared/utils/date";
|
||||
|
||||
export const formatCurrentDate = (date = new Date()) =>
|
||||
formatBusinessCurrentDate(date);
|
||||
|
||||
export const getGreeting = (date = new Date()) => getBusinessGreeting(date);
|
||||
@@ -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[];
|
||||
@@ -1,15 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
RiArrowDownSFill,
|
||||
RiArrowUpSFill,
|
||||
RiLineChartLine,
|
||||
} from "@remixicon/react";
|
||||
import type { DashboardCategoryBreakdownItem } from "@/features/dashboard/categories/category-breakdown";
|
||||
import { RiLineChartLine } from "@remixicon/react";
|
||||
import type { DashboardCategoryBreakdownItem } from "@/features/dashboard/categories/category-breakdown-helpers";
|
||||
import { PercentageChangeIndicator } from "@/features/dashboard/components/percentage-change-indicator";
|
||||
import { CategoryIconBadge } from "@/shared/components/entity-avatar";
|
||||
import MoneyValues from "@/shared/components/money-values";
|
||||
import { WidgetEmptyState } from "@/shared/components/widget-empty-state";
|
||||
import { cn } from "@/shared/utils/ui";
|
||||
import { formatPercentage } from "@/shared/utils/percentage";
|
||||
|
||||
type CategoryTrendsWidgetProps = {
|
||||
categories: DashboardCategoryBreakdownItem[];
|
||||
@@ -40,7 +37,6 @@ export function CategoryTrendsWidget({
|
||||
<ul className="flex flex-col space-y-1">
|
||||
{trending.map((category) => {
|
||||
const change = category.percentageChange ?? 0;
|
||||
const isUp = change > 0;
|
||||
|
||||
return (
|
||||
<li key={category.categoryId}>
|
||||
@@ -62,19 +58,17 @@ export function CategoryTrendsWidget({
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex shrink-0 items-center gap-0.5 font-semibold text-sm",
|
||||
isUp ? " text-destructive" : " text-success",
|
||||
)}
|
||||
>
|
||||
{isUp ? (
|
||||
<RiArrowUpSFill className="size-3.5" />
|
||||
) : (
|
||||
<RiArrowDownSFill className="size-3.5" />
|
||||
)}
|
||||
{Math.abs(change).toFixed(0)}%
|
||||
</span>
|
||||
<PercentageChangeIndicator
|
||||
value={change}
|
||||
label={formatPercentage(change, {
|
||||
absolute: true,
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
})}
|
||||
positiveTrend="down"
|
||||
className="shrink-0 text-sm font-semibold"
|
||||
iconClassName="size-3.5"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -10,7 +10,7 @@ import { useRouter } from "next/navigation";
|
||||
import { useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import type { DashboardInboxSnapshot } from "@/features/dashboard/inbox-snapshot-queries";
|
||||
import type { DashboardWidgetQuickActionOptions } from "@/features/dashboard/widgets/widgets-config";
|
||||
import type { DashboardWidgetQuickActionOptions } from "@/features/dashboard/widget-registry/widget-config";
|
||||
import {
|
||||
discardInboxItemAction,
|
||||
markInboxAsProcessedAction,
|
||||
@@ -178,7 +178,7 @@ export function InboxWidget({
|
||||
key={item.id}
|
||||
className="flex items-center justify-between py-1.5"
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2 py-1">
|
||||
<div className="flex flex-1 items-center gap-2">
|
||||
<Image
|
||||
src={displayLogo}
|
||||
alt={item.sourceAppName ?? ""}
|
||||
@@ -188,9 +188,11 @@ export function InboxWidget({
|
||||
unoptimized
|
||||
/>
|
||||
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-medium text-foreground">
|
||||
{displayName}
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
{displayName.length > 30
|
||||
? `${displayName.slice(0, 30)}...`
|
||||
: displayName}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
{item.sourceAppName && <span>{item.sourceAppName}</span>}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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[];
|
||||
@@ -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";
|
||||
@@ -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[];
|
||||
@@ -1,13 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
RiArrowDownSFill,
|
||||
RiArrowUpSFill,
|
||||
RiExternalLinkLine,
|
||||
RiGroupLine,
|
||||
RiVerifiedBadgeFill,
|
||||
} from "@remixicon/react";
|
||||
import Link from "next/link";
|
||||
import { PercentageChangeIndicator } from "@/features/dashboard/components/percentage-change-indicator";
|
||||
import type { DashboardPagador } from "@/features/dashboard/payers-queries";
|
||||
import MoneyValues from "@/shared/components/money-values";
|
||||
import {
|
||||
@@ -18,7 +17,6 @@ import {
|
||||
import { WidgetEmptyState } from "@/shared/components/widget-empty-state";
|
||||
import { getAvatarSrc } from "@/shared/lib/payers/utils";
|
||||
import { buildInitials } from "@/shared/utils/initials";
|
||||
import { formatPercentage } from "@/shared/utils/percentage";
|
||||
|
||||
type PayersWidgetProps = {
|
||||
payers: DashboardPagador[];
|
||||
@@ -87,25 +85,7 @@ export function PayersWidget({ payers }: PayersWidgetProps) {
|
||||
className="font-medium"
|
||||
amount={payer.totalExpenses}
|
||||
/>
|
||||
{percentageChange !== null && (
|
||||
<span
|
||||
className={`flex items-center gap-0.5 text-xs font-medium ${
|
||||
percentageChange > 0
|
||||
? "text-destructive"
|
||||
: percentageChange < 0
|
||||
? "text-success"
|
||||
: "text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{percentageChange > 0 && (
|
||||
<RiArrowUpSFill className="size-3" />
|
||||
)}
|
||||
{percentageChange < 0 && (
|
||||
<RiArrowDownSFill className="size-3" />
|
||||
)}
|
||||
{formatPercentage(percentageChange)}
|
||||
</span>
|
||||
)}
|
||||
<PercentageChangeIndicator value={percentageChange} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 {
|
||||
@@ -37,11 +37,17 @@ export function SpendingOverviewWidget({
|
||||
className="w-full"
|
||||
>
|
||||
<TabsList className="grid grid-cols-2">
|
||||
<TabsTrigger value="expenses" className="text-xs">
|
||||
<TabsTrigger
|
||||
value="expenses"
|
||||
className="text-xs data-[state=active]:bg-transparent"
|
||||
>
|
||||
<RiArrowUpDoubleLine className="mr-1 size-3.5" />
|
||||
Top gastos
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="establishments" className="text-xs">
|
||||
<TabsTrigger
|
||||
value="establishments"
|
||||
className="text-xs data-[state=active]:bg-transparent"
|
||||
>
|
||||
<RiStore2Line className="mr-1 size-3.5" />
|
||||
Estabelecimentos
|
||||
</TabsTrigger>
|
||||
@@ -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,
|
||||
Reference in New Issue
Block a user