feat(relatorios): refina indicadores e filtros

This commit is contained in:
Felipe Coutinho
2026-05-28 10:59:36 -03:00
parent 0171b0ce2f
commit 60b2612e8a
11 changed files with 168 additions and 101 deletions

View File

@@ -1,4 +1,4 @@
import { RiExternalLinkLine, RiWallet3Line } from "@remixicon/react";
import { RiExternalLinkLine } 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";
@@ -63,7 +63,7 @@ export function CategoryBreakdownListItem({
/>
</Link>
</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>
{formatPercentage(
category.percentageOfTotal,
@@ -77,10 +77,9 @@ export function CategoryBreakdownListItem({
<span
className={`flex items-center gap-1 ${budgetExceeded ? "text-destructive" : "text-info"}`}
>
<RiWallet3Line className="size-3 shrink-0" />
{budgetExceeded ? (
<>
excedeu{" "}
Excedeu{" "}
<span className="font-medium">
{formatCurrency(exceededAmount)}
</span>

View File

@@ -20,6 +20,7 @@ export function InstallmentExpenseListItem({
const {
compactLabel,
isLast,
remainingLabel,
remainingInstallments,
remainingAmount,
endDate,
@@ -65,15 +66,22 @@ export function InstallmentExpenseListItem({
/>
</div>
<p className="text-xs text-muted-foreground">
{endDate ? `Termina em ${endDate}` : null}
{" · Restante "}
<MoneyValues
amount={remainingAmount}
className="inline-block font-semibold"
/>{" "}
({remainingInstallments})
</p>
{remainingInstallments === 0 ? (
<p className="text-xs text-muted-foreground">
{endDate ? `Termina em ${endDate}` : null}
{" · Quitado"}
</p>
) : (
<p className="text-xs text-muted-foreground">
{endDate ? `Termina em ${endDate}` : null}
{` · ${remainingLabel}: `}
<MoneyValues
amount={remainingAmount}
className="inline-block font-semibold"
/>{" "}
({remainingInstallments}x)
</p>
)}
<Progress value={progress} className="mt-1 h-2" />
</div>

View File

@@ -2,10 +2,10 @@
import { RiInformationLine } from "@remixicon/react";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/shared/components/ui/hover-card";
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/shared/components/ui/tooltip";
type MetricsCardInfoButtonProps = {
label: string;
@@ -19,8 +19,8 @@ export function MetricsCardInfoButton({
helpLines,
}: MetricsCardInfoButtonProps) {
return (
<HoverCard openDelay={150}>
<HoverCardTrigger asChild>
<Tooltip>
<TooltipTrigger asChild>
<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"
@@ -28,17 +28,22 @@ export function MetricsCardInfoButton({
>
<RiInformationLine className="size-4" aria-hidden />
</button>
</HoverCardTrigger>
<HoverCardContent align="start" className="w-80 space-y-3">
</TooltipTrigger>
<TooltipContent
align="start"
side="bottom"
sideOffset={8}
className="max-w-80 space-y-3 p-3 text-left"
>
<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>
<ul className="space-y-2 text-xs text-muted-foreground">
<ul className="space-y-2 text-xs text-background/80">
{helpLines.map((line) => (
<li key={`${label}-${line}`}>{line}</li>
))}
</ul>
</HoverCardContent>
</HoverCard>
</TooltipContent>
</Tooltip>
);
}

View File

@@ -73,6 +73,7 @@ export function WidgetSettingsDialog({
</div>
</div>
<Switch
aria-label={`${isVisible ? "Ocultar" : "Exibir"} widget ${widget.title}`}
checked={isVisible}
onCheckedChange={() => onToggleWidget(widget.id)}
/>

View File

@@ -1,12 +1,11 @@
import type { InstallmentExpense } from "@/features/dashboard/expenses/installment-expenses-queries";
import {
calculateLastInstallmentDate,
formatLastInstallmentDate,
} from "@/shared/lib/installments/utils";
import { calculateLastInstallmentDate } from "@/shared/lib/installments/utils";
import { capitalize } from "@/shared/utils/string";
type InstallmentExpenseDisplay = {
compactLabel: string | null;
isLast: boolean;
remainingLabel: "Próx." | "Aberto";
remainingInstallments: number;
remainingAmount: number;
endDate: string | null;
@@ -38,21 +37,30 @@ const isInstallmentLast = (
const calculateInstallmentRemainingCount = (
currentInstallment: number | null,
installmentCount: number | null,
isSettled: boolean | null,
) => {
if (!currentInstallment || !installmentCount) {
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 = (
amount: number,
currentInstallment: number | null,
installmentCount: number | null,
isSettled: boolean | null,
) =>
amount *
calculateInstallmentRemainingCount(currentInstallment, installmentCount);
calculateInstallmentRemainingCount(
currentInstallment,
installmentCount,
isSettled,
);
const formatInstallmentEndDate = (
period: string,
@@ -69,7 +77,12 @@ const formatInstallmentEndDate = (
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 = (
@@ -89,7 +102,8 @@ const buildInstallmentProgress = (
export const buildInstallmentExpenseDisplay = (
expense: InstallmentExpense,
): InstallmentExpenseDisplay => {
const { amount, currentInstallment, installmentCount, period } = expense;
const { amount, currentInstallment, installmentCount, isSettled, period } =
expense;
return {
compactLabel: buildInstallmentCompactLabel(
@@ -97,14 +111,17 @@ export const buildInstallmentExpenseDisplay = (
installmentCount,
),
isLast: isInstallmentLast(currentInstallment, installmentCount),
remainingLabel: isSettled === true ? "Próx." : "Aberto",
remainingInstallments: calculateInstallmentRemainingCount(
currentInstallment,
installmentCount,
isSettled,
),
remainingAmount: calculateInstallmentRemainingAmount(
amount,
currentInstallment,
installmentCount,
isSettled,
),
endDate: formatInstallmentEndDate(
period,

View File

@@ -8,6 +8,7 @@ export type InstallmentExpense = {
dueDate: Date | null;
purchaseDate: Date;
period: string;
isSettled: boolean | null;
};
export type InstallmentExpensesData = {

View File

@@ -405,6 +405,7 @@ const buildInstallmentExpensesData = (
dueDate: row.dueDate,
purchaseDate: row.purchaseDate,
period: row.period,
isSettled: row.isSettled,
}))
.sort((a, b) => {
const remainingA =