fix(financeiro): alinhar saldo, métricas e relatórios

This commit is contained in:
Felipe Coutinho
2026-04-03 18:10:43 +00:00
parent acaf9d5c27
commit 549a5bdba1
32 changed files with 960 additions and 118 deletions

View File

@@ -1,36 +1,48 @@
import { RiInformationLine } from "@remixicon/react";
import {
Card,
CardFooter,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/shared/components/ui/card";
import { Separator } from "@/shared/components/ui/separator";
import { Skeleton } from "@/shared/components/ui/skeleton";
export function DashboardMetricsCardsSkeleton() {
return (
<div className="grid grid-cols-1 gap-3 @xl/main:grid-cols-2 @5xl/main:grid-cols-4">
{Array.from({ length: 4 }).map((_, index) => (
<Card
key={index}
className="@container/card min-h-36 justify-between gap-0"
>
<CardHeader className="gap-4 pb-3">
<CardTitle className="flex items-center gap-2">
<Skeleton className="size-8 rounded-md bg-foreground/10" />
<Skeleton className="h-4 w-24 rounded-md bg-foreground/10" />
</CardTitle>
<div className="flex flex-wrap items-end justify-between gap-3">
<Card key={index} className="gap-2 overflow-hidden">
<CardHeader>
<div className="flex items-start justify-between">
<div className="w-full">
<CardTitle className="flex items-center gap-1.5 tracking-tight">
<Skeleton className="size-4 rounded-sm bg-foreground/10" />
<Skeleton className="h-4 w-24 rounded-md bg-foreground/10" />
<RiInformationLine
className="size-4 text-muted-foreground/40"
aria-hidden
/>
</CardTitle>
<CardDescription className="mt-1.5 tracking-tight">
<Skeleton className="h-3 w-32 rounded-md bg-foreground/10" />
</CardDescription>
</div>
</div>
<Separator className="mt-1" />
</CardHeader>
<CardContent className="flex flex-col gap-3">
<div className="mt-1 flex flex-wrap items-center justify-between gap-2">
<Skeleton className="h-10 w-36 rounded-md bg-foreground/10" />
<Skeleton className="h-7 w-20 rounded-full bg-foreground/10" />
</div>
</CardHeader>
<CardFooter className="items-start pt-0">
<div className="flex flex-col items-start gap-1.5">
<div className="flex flex-col gap-1.5">
<Skeleton className="h-4 w-28 rounded-md bg-foreground/10" />
<Skeleton className="h-3 w-24 rounded-md bg-foreground/10" />
<Skeleton className="h-4 w-20 rounded-md bg-foreground/10" />
</div>
</CardFooter>
</CardContent>
</Card>
))}
</div>

View File

@@ -0,0 +1,9 @@
import { eq, isNull, or, sql } from "drizzle-orm";
import { financialAccounts, transactions } from "@/db/schema";
export const excludeTransactionsFromExcludedAccounts = () =>
or(
isNull(transactions.accountId),
isNull(financialAccounts.excludeFromBalance),
eq(financialAccounts.excludeFromBalance, false),
) ?? sql`true`;

View File

@@ -1,6 +1,9 @@
import {
buildDateOnlyStringFromPeriodDay,
formatDateOnlyLabel,
getBusinessDateString,
parseUtcDateString,
toDateOnlyString,
} from "@/shared/utils/date";
type FinancialStatusLabelInput = {
@@ -16,6 +19,8 @@ type FinancialDueDateInfo = {
date: string | null;
};
type RelativeFinancialDateContext = "due" | "paid";
export function formatFinancialDateLabel(
value: string | null,
prefix?: string,
@@ -24,6 +29,63 @@ export function formatFinancialDateLabel(
return formatDateOnlyLabel(value, prefix, options);
}
function getOffsetDateString(
referenceDate: string,
offset: number,
): string | null {
const parsedReference = parseUtcDateString(referenceDate);
if (!parsedReference) {
return null;
}
parsedReference.setUTCDate(parsedReference.getUTCDate() + offset);
return toDateOnlyString(parsedReference);
}
export function formatRelativeFinancialDateLabel(
value: string | null,
context: RelativeFinancialDateContext,
options?: {
referenceDate?: string | Date | null;
},
): string | null {
const normalizedValue = toDateOnlyString(value);
if (!normalizedValue) {
return null;
}
const referenceDate =
toDateOnlyString(options?.referenceDate) ?? getBusinessDateString();
const yesterday = getOffsetDateString(referenceDate, -1);
const tomorrow = getOffsetDateString(referenceDate, 1);
if (context === "due") {
if (normalizedValue === referenceDate) {
return "Vence hoje";
}
if (normalizedValue === tomorrow) {
return "Vence amanhã";
}
if (normalizedValue === yesterday) {
return "Venceu ontem";
}
return formatFinancialDateLabel(normalizedValue, "Vence em");
}
if (normalizedValue === referenceDate) {
return "Pago hoje";
}
if (normalizedValue === yesterday) {
return "Pago ontem";
}
return formatFinancialDateLabel(normalizedValue, "Pago em");
}
export function buildFinancialStatusLabel({
isSettled,
dueDate,
@@ -38,6 +100,18 @@ export function buildFinancialStatusLabel({
return formatFinancialDateLabel(dueDate, duePrefix);
}
export function buildRelativeFinancialStatusLabel({
isSettled,
dueDate,
paidAt,
}: FinancialStatusLabelInput): string | null {
if (isSettled) {
return formatRelativeFinancialDateLabel(paidAt, "paid");
}
return formatRelativeFinancialDateLabel(dueDate, "due");
}
export function buildDueDateInfoFromPeriodDay(
period: string,
dueDay: string,
@@ -64,3 +138,28 @@ export function buildDueDateInfoFromPeriodDay(
date: dueDate,
};
}
export function buildRelativeDueDateInfoFromPeriodDay(
period: string,
dueDay: string,
options?: {
fallbackPrefix?: string;
},
): FinancialDueDateInfo {
const fallbackPrefix = options?.fallbackPrefix ?? "Vence dia";
const dueDate = buildDateOnlyStringFromPeriodDay(period, dueDay);
if (!dueDate) {
return {
label: `${fallbackPrefix} ${dueDay}`,
date: null,
};
}
return {
label:
formatRelativeFinancialDateLabel(dueDate, "due") ??
`${fallbackPrefix} ${dueDay}`,
date: dueDate,
};
}