mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-06-09 23:06:01 +00:00
feat(relatorios): refina indicadores e filtros
This commit is contained in:
@@ -5,6 +5,7 @@ import { Card } from "@/shared/components/ui/card";
|
|||||||
import type { CategoryType } from "@/shared/lib/categories/constants";
|
import type { CategoryType } from "@/shared/lib/categories/constants";
|
||||||
import { currencyFormatter } from "@/shared/utils/currency";
|
import { currencyFormatter } from "@/shared/utils/currency";
|
||||||
import { formatPercentage } from "@/shared/utils/percentage";
|
import { formatPercentage } from "@/shared/utils/percentage";
|
||||||
|
import { cn } from "@/shared/utils/ui";
|
||||||
|
|
||||||
type CategorySummary = {
|
type CategorySummary = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -32,19 +33,40 @@ export function CategoryDetailHeader({
|
|||||||
percentageChange,
|
percentageChange,
|
||||||
transactionCount,
|
transactionCount,
|
||||||
}: CategoryDetailHeaderProps) {
|
}: CategoryDetailHeaderProps) {
|
||||||
|
const absoluteChange = currentTotal - previousTotal;
|
||||||
const variationLabel =
|
const variationLabel =
|
||||||
typeof percentageChange === "number"
|
typeof percentageChange === "number"
|
||||||
? formatPercentage(percentageChange, {
|
? formatPercentage(percentageChange, {
|
||||||
minimumFractionDigits: 1,
|
minimumFractionDigits: 1,
|
||||||
maximumFractionDigits: 1,
|
maximumFractionDigits: 1,
|
||||||
absolute: true,
|
absolute: true,
|
||||||
signDisplay: percentageChange === 0 ? "auto" : "always",
|
|
||||||
})
|
})
|
||||||
: "—";
|
: "—";
|
||||||
|
const hasComparison = typeof percentageChange === "number";
|
||||||
|
const isFlat = absoluteChange === 0;
|
||||||
|
const changeDirection =
|
||||||
|
absoluteChange > 0 ? "increase" : absoluteChange < 0 ? "decrease" : "flat";
|
||||||
|
const comparisonTone =
|
||||||
|
isFlat || !hasComparison
|
||||||
|
? "neutral"
|
||||||
|
: category.type === "receita"
|
||||||
|
? changeDirection === "increase"
|
||||||
|
? "positive"
|
||||||
|
: "negative"
|
||||||
|
: changeDirection === "decrease"
|
||||||
|
? "positive"
|
||||||
|
: "negative";
|
||||||
|
const statusLabel = !hasComparison
|
||||||
|
? "Sem comparação"
|
||||||
|
: isFlat
|
||||||
|
? "Estável"
|
||||||
|
: changeDirection === "increase"
|
||||||
|
? "Aumento"
|
||||||
|
: "Queda";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="px-4">
|
<Card className="px-5 py-5">
|
||||||
<div className="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
|
<div className="flex flex-col gap-5">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<CategoryIconBadge
|
<CategoryIconBadge
|
||||||
icon={category.icon}
|
icon={category.icon}
|
||||||
@@ -59,44 +81,62 @@ export function CategoryDetailHeader({
|
|||||||
<TransactionTypeBadge kind={category.type} />
|
<TransactionTypeBadge kind={category.type} />
|
||||||
<span>
|
<span>
|
||||||
{transactionCount}{" "}
|
{transactionCount}{" "}
|
||||||
{transactionCount === 1 ? "lançamento" : "lançamentos"} no{" "}
|
{transactionCount === 1 ? "lançamento" : "lançamentos"} em{" "}
|
||||||
período
|
{currentPeriodLabel}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid w-full gap-4 sm:grid-cols-2 lg:w-auto lg:grid-cols-3">
|
<div className="grid gap-3 md:grid-cols-3">
|
||||||
<div>
|
<div className="rounded-md border border-dashed bg-muted/20 px-3 py-3">
|
||||||
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||||
Total em {currentPeriodLabel}
|
Total em {currentPeriodLabel}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1 text-2xl font-semibold">
|
<p className="mt-1 text-3xl font-semibold tracking-tight">
|
||||||
{currencyFormatter.format(currentTotal)}
|
{currencyFormatter.format(currentTotal)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
|
<div className="rounded-md border border-dashed bg-muted/20 px-3 py-3">
|
||||||
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||||
Total em {previousPeriodLabel}
|
Total em {previousPeriodLabel}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1 text-lg font-semibold text-muted-foreground">
|
<p className="mt-1 text-2xl font-semibold tracking-tight text-muted-foreground">
|
||||||
{currencyFormatter.format(previousTotal)}
|
{currencyFormatter.format(previousTotal)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
|
<div className="rounded-md border border-dashed bg-muted/20 px-3 py-3">
|
||||||
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||||
Variação vs mês anterior
|
Variação
|
||||||
</p>
|
</p>
|
||||||
|
<div className="mt-2 flex flex-wrap items-center gap-2">
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-6 items-center rounded-sm border px-2 text-xs font-medium",
|
||||||
|
comparisonTone === "positive" &&
|
||||||
|
"border-success/30 bg-success/5 text-success",
|
||||||
|
comparisonTone === "negative" &&
|
||||||
|
"border-destructive/30 bg-destructive/5 text-destructive",
|
||||||
|
comparisonTone === "neutral" &&
|
||||||
|
"border-muted-foreground/30 bg-muted/30 text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{statusLabel}
|
||||||
|
</span>
|
||||||
<PercentageChangeIndicator
|
<PercentageChangeIndicator
|
||||||
value={percentageChange}
|
value={percentageChange}
|
||||||
label={variationLabel}
|
label={variationLabel}
|
||||||
positiveTrend={category.type === "receita" ? "up" : "down"}
|
positiveTrend={category.type === "receita" ? "up" : "down"}
|
||||||
className="mt-1 gap-1 text-lg font-semibold"
|
className="gap-1 text-lg font-semibold"
|
||||||
iconClassName="size-4"
|
iconClassName="size-4"
|
||||||
|
showFlatIcon
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { RiExternalLinkLine, RiWallet3Line } from "@remixicon/react";
|
import { RiExternalLinkLine } from "@remixicon/react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import type { DashboardCategoryBreakdownItem } from "@/features/dashboard/categories/category-breakdown-helpers";
|
import type { DashboardCategoryBreakdownItem } from "@/features/dashboard/categories/category-breakdown-helpers";
|
||||||
import { PercentageChangeIndicator } from "@/features/dashboard/components/percentage-change-indicator";
|
import { PercentageChangeIndicator } from "@/features/dashboard/components/percentage-change-indicator";
|
||||||
@@ -63,7 +63,7 @@ export function CategoryBreakdownListItem({
|
|||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-muted-foreground">
|
<div className="flex flex-wrap items-center gap-x-1 text-xs text-muted-foreground">
|
||||||
<span>
|
<span>
|
||||||
{formatPercentage(
|
{formatPercentage(
|
||||||
category.percentageOfTotal,
|
category.percentageOfTotal,
|
||||||
@@ -77,10 +77,9 @@ export function CategoryBreakdownListItem({
|
|||||||
<span
|
<span
|
||||||
className={`flex items-center gap-1 ${budgetExceeded ? "text-destructive" : "text-info"}`}
|
className={`flex items-center gap-1 ${budgetExceeded ? "text-destructive" : "text-info"}`}
|
||||||
>
|
>
|
||||||
<RiWallet3Line className="size-3 shrink-0" />
|
|
||||||
{budgetExceeded ? (
|
{budgetExceeded ? (
|
||||||
<>
|
<>
|
||||||
excedeu{" "}
|
Excedeu{" "}
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{formatCurrency(exceededAmount)}
|
{formatCurrency(exceededAmount)}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export function InstallmentExpenseListItem({
|
|||||||
const {
|
const {
|
||||||
compactLabel,
|
compactLabel,
|
||||||
isLast,
|
isLast,
|
||||||
|
remainingLabel,
|
||||||
remainingInstallments,
|
remainingInstallments,
|
||||||
remainingAmount,
|
remainingAmount,
|
||||||
endDate,
|
endDate,
|
||||||
@@ -65,15 +66,22 @@ export function InstallmentExpenseListItem({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{remainingInstallments === 0 ? (
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{endDate ? `Termina em ${endDate}` : null}
|
{endDate ? `Termina em ${endDate}` : null}
|
||||||
{" · Restante "}
|
{" · Quitado"}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{endDate ? `Termina em ${endDate}` : null}
|
||||||
|
{` · ${remainingLabel}: `}
|
||||||
<MoneyValues
|
<MoneyValues
|
||||||
amount={remainingAmount}
|
amount={remainingAmount}
|
||||||
className="inline-block font-semibold"
|
className="inline-block font-semibold"
|
||||||
/>{" "}
|
/>{" "}
|
||||||
({remainingInstallments})
|
({remainingInstallments}x)
|
||||||
</p>
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
<Progress value={progress} className="mt-1 h-2" />
|
<Progress value={progress} className="mt-1 h-2" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
import { RiInformationLine } from "@remixicon/react";
|
import { RiInformationLine } from "@remixicon/react";
|
||||||
import {
|
import {
|
||||||
HoverCard,
|
Tooltip,
|
||||||
HoverCardContent,
|
TooltipContent,
|
||||||
HoverCardTrigger,
|
TooltipTrigger,
|
||||||
} from "@/shared/components/ui/hover-card";
|
} from "@/shared/components/ui/tooltip";
|
||||||
|
|
||||||
type MetricsCardInfoButtonProps = {
|
type MetricsCardInfoButtonProps = {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -19,8 +19,8 @@ export function MetricsCardInfoButton({
|
|||||||
helpLines,
|
helpLines,
|
||||||
}: MetricsCardInfoButtonProps) {
|
}: MetricsCardInfoButtonProps) {
|
||||||
return (
|
return (
|
||||||
<HoverCard openDelay={150}>
|
<Tooltip>
|
||||||
<HoverCardTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="inline-flex items-center rounded-full text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/60"
|
className="inline-flex items-center rounded-full text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/60"
|
||||||
@@ -28,17 +28,22 @@ export function MetricsCardInfoButton({
|
|||||||
>
|
>
|
||||||
<RiInformationLine className="size-4" aria-hidden />
|
<RiInformationLine className="size-4" aria-hidden />
|
||||||
</button>
|
</button>
|
||||||
</HoverCardTrigger>
|
</TooltipTrigger>
|
||||||
<HoverCardContent align="start" className="w-80 space-y-3">
|
<TooltipContent
|
||||||
|
align="start"
|
||||||
|
side="bottom"
|
||||||
|
sideOffset={8}
|
||||||
|
className="max-w-80 space-y-3 p-3 text-left"
|
||||||
|
>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-sm font-medium text-foreground">{helpTitle}</p>
|
<p className="text-sm font-medium text-background">{helpTitle}</p>
|
||||||
</div>
|
</div>
|
||||||
<ul className="space-y-2 text-xs text-muted-foreground">
|
<ul className="space-y-2 text-xs text-background/80">
|
||||||
{helpLines.map((line) => (
|
{helpLines.map((line) => (
|
||||||
<li key={`${label}-${line}`}>{line}</li>
|
<li key={`${label}-${line}`}>{line}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</HoverCardContent>
|
</TooltipContent>
|
||||||
</HoverCard>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ export function WidgetSettingsDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
|
aria-label={`${isVisible ? "Ocultar" : "Exibir"} widget ${widget.title}`}
|
||||||
checked={isVisible}
|
checked={isVisible}
|
||||||
onCheckedChange={() => onToggleWidget(widget.id)}
|
onCheckedChange={() => onToggleWidget(widget.id)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import type { InstallmentExpense } from "@/features/dashboard/expenses/installment-expenses-queries";
|
import type { InstallmentExpense } from "@/features/dashboard/expenses/installment-expenses-queries";
|
||||||
import {
|
import { calculateLastInstallmentDate } from "@/shared/lib/installments/utils";
|
||||||
calculateLastInstallmentDate,
|
import { capitalize } from "@/shared/utils/string";
|
||||||
formatLastInstallmentDate,
|
|
||||||
} from "@/shared/lib/installments/utils";
|
|
||||||
|
|
||||||
type InstallmentExpenseDisplay = {
|
type InstallmentExpenseDisplay = {
|
||||||
compactLabel: string | null;
|
compactLabel: string | null;
|
||||||
isLast: boolean;
|
isLast: boolean;
|
||||||
|
remainingLabel: "Próx." | "Aberto";
|
||||||
remainingInstallments: number;
|
remainingInstallments: number;
|
||||||
remainingAmount: number;
|
remainingAmount: number;
|
||||||
endDate: string | null;
|
endDate: string | null;
|
||||||
@@ -38,21 +37,30 @@ const isInstallmentLast = (
|
|||||||
const calculateInstallmentRemainingCount = (
|
const calculateInstallmentRemainingCount = (
|
||||||
currentInstallment: number | null,
|
currentInstallment: number | null,
|
||||||
installmentCount: number | null,
|
installmentCount: number | null,
|
||||||
|
isSettled: boolean | null,
|
||||||
) => {
|
) => {
|
||||||
if (!currentInstallment || !installmentCount) {
|
if (!currentInstallment || !installmentCount) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Math.max(0, installmentCount - currentInstallment);
|
const includeCurrentInstallment = isSettled !== true;
|
||||||
|
const currentOffset = includeCurrentInstallment ? 1 : 0;
|
||||||
|
|
||||||
|
return Math.max(0, installmentCount - currentInstallment + currentOffset);
|
||||||
};
|
};
|
||||||
|
|
||||||
const calculateInstallmentRemainingAmount = (
|
const calculateInstallmentRemainingAmount = (
|
||||||
amount: number,
|
amount: number,
|
||||||
currentInstallment: number | null,
|
currentInstallment: number | null,
|
||||||
installmentCount: number | null,
|
installmentCount: number | null,
|
||||||
|
isSettled: boolean | null,
|
||||||
) =>
|
) =>
|
||||||
amount *
|
amount *
|
||||||
calculateInstallmentRemainingCount(currentInstallment, installmentCount);
|
calculateInstallmentRemainingCount(
|
||||||
|
currentInstallment,
|
||||||
|
installmentCount,
|
||||||
|
isSettled,
|
||||||
|
);
|
||||||
|
|
||||||
const formatInstallmentEndDate = (
|
const formatInstallmentEndDate = (
|
||||||
period: string,
|
period: string,
|
||||||
@@ -69,7 +77,12 @@ const formatInstallmentEndDate = (
|
|||||||
installmentCount,
|
installmentCount,
|
||||||
);
|
);
|
||||||
|
|
||||||
return formatLastInstallmentDate(lastDate);
|
const month = new Intl.DateTimeFormat("pt-BR", {
|
||||||
|
month: "short",
|
||||||
|
timeZone: "UTC",
|
||||||
|
}).format(lastDate);
|
||||||
|
|
||||||
|
return `${capitalize(month)} de ${lastDate.getFullYear()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildInstallmentProgress = (
|
const buildInstallmentProgress = (
|
||||||
@@ -89,7 +102,8 @@ const buildInstallmentProgress = (
|
|||||||
export const buildInstallmentExpenseDisplay = (
|
export const buildInstallmentExpenseDisplay = (
|
||||||
expense: InstallmentExpense,
|
expense: InstallmentExpense,
|
||||||
): InstallmentExpenseDisplay => {
|
): InstallmentExpenseDisplay => {
|
||||||
const { amount, currentInstallment, installmentCount, period } = expense;
|
const { amount, currentInstallment, installmentCount, isSettled, period } =
|
||||||
|
expense;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
compactLabel: buildInstallmentCompactLabel(
|
compactLabel: buildInstallmentCompactLabel(
|
||||||
@@ -97,14 +111,17 @@ export const buildInstallmentExpenseDisplay = (
|
|||||||
installmentCount,
|
installmentCount,
|
||||||
),
|
),
|
||||||
isLast: isInstallmentLast(currentInstallment, installmentCount),
|
isLast: isInstallmentLast(currentInstallment, installmentCount),
|
||||||
|
remainingLabel: isSettled === true ? "Próx." : "Aberto",
|
||||||
remainingInstallments: calculateInstallmentRemainingCount(
|
remainingInstallments: calculateInstallmentRemainingCount(
|
||||||
currentInstallment,
|
currentInstallment,
|
||||||
installmentCount,
|
installmentCount,
|
||||||
|
isSettled,
|
||||||
),
|
),
|
||||||
remainingAmount: calculateInstallmentRemainingAmount(
|
remainingAmount: calculateInstallmentRemainingAmount(
|
||||||
amount,
|
amount,
|
||||||
currentInstallment,
|
currentInstallment,
|
||||||
installmentCount,
|
installmentCount,
|
||||||
|
isSettled,
|
||||||
),
|
),
|
||||||
endDate: formatInstallmentEndDate(
|
endDate: formatInstallmentEndDate(
|
||||||
period,
|
period,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export type InstallmentExpense = {
|
|||||||
dueDate: Date | null;
|
dueDate: Date | null;
|
||||||
purchaseDate: Date;
|
purchaseDate: Date;
|
||||||
period: string;
|
period: string;
|
||||||
|
isSettled: boolean | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type InstallmentExpensesData = {
|
export type InstallmentExpensesData = {
|
||||||
|
|||||||
@@ -405,6 +405,7 @@ const buildInstallmentExpensesData = (
|
|||||||
dueDate: row.dueDate,
|
dueDate: row.dueDate,
|
||||||
purchaseDate: row.purchaseDate,
|
purchaseDate: row.purchaseDate,
|
||||||
period: row.period,
|
period: row.period,
|
||||||
|
isSettled: row.isSettled,
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const remainingA =
|
const remainingA =
|
||||||
|
|||||||
@@ -31,8 +31,12 @@ import {
|
|||||||
getCurrentPeriod,
|
getCurrentPeriod,
|
||||||
periodToDate,
|
periodToDate,
|
||||||
} from "@/shared/utils/period";
|
} from "@/shared/utils/period";
|
||||||
|
import { slugify } from "@/shared/utils/string";
|
||||||
import type { CategoryReportFiltersProps } from "./types";
|
import type { CategoryReportFiltersProps } from "./types";
|
||||||
|
|
||||||
|
const getCategorySearchValue = (name: string, id: string) =>
|
||||||
|
`${name} ${slugify(name)} ${id}`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Category Report Filters Component
|
* Category Report Filters Component
|
||||||
* Provides filters for categories selection and date range
|
* Provides filters for categories selection and date range
|
||||||
@@ -53,7 +57,14 @@ export function CategoryReportFilters({
|
|||||||
const filteredCategories = useMemo(() => {
|
const filteredCategories = useMemo(() => {
|
||||||
if (!searchValue) return categories;
|
if (!searchValue) return categories;
|
||||||
const search = searchValue.toLowerCase();
|
const search = searchValue.toLowerCase();
|
||||||
return categories.filter((cat) => cat.name.toLowerCase().includes(search));
|
const normalizedSearch = slugify(searchValue);
|
||||||
|
return categories.filter((cat) => {
|
||||||
|
const categorySearchValue = getCategorySearchValue(cat.name, cat.id);
|
||||||
|
return (
|
||||||
|
categorySearchValue.toLowerCase().includes(search) ||
|
||||||
|
categorySearchValue.includes(normalizedSearch)
|
||||||
|
);
|
||||||
|
});
|
||||||
}, [categories, searchValue]);
|
}, [categories, searchValue]);
|
||||||
|
|
||||||
// Get selected categories for display
|
// Get selected categories for display
|
||||||
@@ -76,15 +87,6 @@ export function CategoryReportFilters({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle select all
|
|
||||||
const handleSelectAll = () => {
|
|
||||||
onFiltersChange({
|
|
||||||
...filters,
|
|
||||||
selectedCategories: categories.map((cat) => cat.id),
|
|
||||||
});
|
|
||||||
setOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle clear all
|
// Handle clear all
|
||||||
const handleClearAll = () => {
|
const handleClearAll = () => {
|
||||||
onFiltersChange({
|
onFiltersChange({
|
||||||
@@ -130,8 +132,6 @@ export function CategoryReportFilters({
|
|||||||
const selectedText =
|
const selectedText =
|
||||||
selectedCategories.length === 0
|
selectedCategories.length === 0
|
||||||
? "Categoria"
|
? "Categoria"
|
||||||
: selectedCategories.length === categories.length
|
|
||||||
? "Todas"
|
|
||||||
: selectedCategories.length === 1
|
: selectedCategories.length === 1
|
||||||
? selectedCategories[0].name
|
? selectedCategories[0].name
|
||||||
: `${selectedCategories.length} selecionadas`;
|
: `${selectedCategories.length} selecionadas`;
|
||||||
@@ -168,25 +168,18 @@ export function CategoryReportFilters({
|
|||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>Nenhuma categoria encontrada.</CommandEmpty>
|
<CommandEmpty>Nenhuma categoria encontrada.</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{/* Select All / Clear All */}
|
{filters.selectedCategories.length > 0 ? (
|
||||||
<div className="flex gap-1 p-2 border-b">
|
<div className="p-2 border-b">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-7 text-xs flex-1"
|
className="h-7 w-full text-xs"
|
||||||
onClick={handleSelectAll}
|
|
||||||
>
|
|
||||||
Todas
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-7 text-xs flex-1"
|
|
||||||
onClick={handleClearAll}
|
onClick={handleClearAll}
|
||||||
>
|
>
|
||||||
Limpar
|
Limpar seleção
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{/* Category List */}
|
{/* Category List */}
|
||||||
{filteredCategories.map((category) => {
|
{filteredCategories.map((category) => {
|
||||||
@@ -200,7 +193,10 @@ export function CategoryReportFilters({
|
|||||||
return (
|
return (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={category.id}
|
key={category.id}
|
||||||
value={category.id}
|
value={getCategorySearchValue(
|
||||||
|
category.name,
|
||||||
|
category.id,
|
||||||
|
)}
|
||||||
onSelect={() => handleCategoryToggle(category.id)}
|
onSelect={() => handleCategoryToggle(category.id)}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import Link from "next/link";
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { formatPeriodLabel } from "@/features/reports/lib/utils";
|
import { formatPeriodLabel } from "@/features/reports/lib/utils";
|
||||||
import { CategoryIconBadge } from "@/shared/components/entity-avatar";
|
import { CategoryIconBadge } from "@/shared/components/entity-avatar";
|
||||||
import StatusDot from "@/shared/components/feedback/status-dot";
|
|
||||||
import { Card } from "@/shared/components/ui/card";
|
import { Card } from "@/shared/components/ui/card";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
@@ -37,6 +36,13 @@ export function CategoryTable({
|
|||||||
categories,
|
categories,
|
||||||
periods,
|
periods,
|
||||||
}: CategoryTableProps) {
|
}: CategoryTableProps) {
|
||||||
|
const categoryColumnLabel =
|
||||||
|
title === "Despesas"
|
||||||
|
? "Categoria Despesa"
|
||||||
|
: title === "Receitas"
|
||||||
|
? "Categoria Receita"
|
||||||
|
: "Categoria";
|
||||||
|
|
||||||
// Calculate section totals
|
// Calculate section totals
|
||||||
const sectionTotals = useMemo(() => {
|
const sectionTotals = useMemo(() => {
|
||||||
const totalsMap = new Map<string, number>();
|
const totalsMap = new Map<string, number>();
|
||||||
@@ -73,7 +79,7 @@ export function CategoryTable({
|
|||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[240px] min-w-[240px] font-medium">
|
<TableHead className="w-[240px] min-w-[240px] font-medium">
|
||||||
Categoria
|
{categoryColumnLabel}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
{periods.map((period) => (
|
{periods.map((period) => (
|
||||||
<TableHead
|
<TableHead
|
||||||
@@ -114,14 +120,6 @@ export function CategoryTable({
|
|||||||
<TableRow key={category.categoryId}>
|
<TableRow key={category.categoryId}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<StatusDot
|
|
||||||
color={
|
|
||||||
category.type === "receita"
|
|
||||||
? "bg-success"
|
|
||||||
: "bg-destructive"
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CategoryIconBadge
|
<CategoryIconBadge
|
||||||
icon={category.icon}
|
icon={category.icon}
|
||||||
name={category.name}
|
name={category.name}
|
||||||
|
|||||||
@@ -66,14 +66,15 @@ export function ExpandableWidgetCard({
|
|||||||
contentClassName={EXPANDABLE_CONTENT_CLASSNAME}
|
contentClassName={EXPANDABLE_CONTENT_CLASSNAME}
|
||||||
overlay={
|
overlay={
|
||||||
hasOverflow ? (
|
hasOverflow ? (
|
||||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 flex justify-center bg-linear-to-t from-card to-transparent pt-12 pb-6">
|
<div className="pointer-events-none absolute inset-x-0 bottom-0 flex justify-center bg-linear-to-t from-card to-transparent pt-8 pb-4">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
className="pointer-events-auto text-xs"
|
className="pointer-events-auto text-xs"
|
||||||
onClick={() => setIsOpen(true)}
|
onClick={() => setIsOpen(true)}
|
||||||
aria-label="Expandir para ver todo o conteúdo"
|
aria-label="Expandir para ver todo o conteúdo"
|
||||||
>
|
>
|
||||||
Ver tudo{" "}
|
Expandir
|
||||||
<RiExpandDiagonalLine className="size-3" aria-hidden="true" />
|
<RiExpandDiagonalLine className="size-3" aria-hidden="true" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,7 +95,7 @@ export function ExpandableWidgetCard({
|
|||||||
<p className="text-muted-foreground text-sm">{subtitle}</p>
|
<p className="text-muted-foreground text-sm">{subtitle}</p>
|
||||||
) : null}
|
) : null}
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="scrollbar-hide max-h-[calc(85vh-6rem)] overflow-y-auto pb-6">
|
<div className="-mr-3 max-h-[calc(85vh-6rem)] overflow-y-auto pb-6 pr-3 [scrollbar-gutter:stable]">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user