forked from git.gladyson/openmonetis
refactor: ajustar feature de análise de parcelas
Melhorias na funcionalidade de análise de parcelas: - Cálculo correto de vencimento baseado no dia de vencimento do cartão - Identificação de parcelas pagas com indicador visual - Parcelas pagas não podem ser selecionadas - Remoção completa da funcionalidade de faturas (apenas parcelas) - Layout mais compacto com espaçamentos reduzidos - Botão "Análise" discreto ao lado do título do widget - Card de resumo simplificado - Tamanhos de fonte e ícones reduzidos - Progress bar mais fina (h-1.5)
This commit is contained in:
@@ -1,12 +1,28 @@
|
||||
import { cartoes, faturas, lancamentos, pagadores } from "@/db/schema";
|
||||
import { cartoes, lancamentos } from "@/db/schema";
|
||||
import {
|
||||
ACCOUNT_AUTO_INVOICE_NOTE_PREFIX,
|
||||
INITIAL_BALANCE_NOTE,
|
||||
} from "@/lib/accounts/constants";
|
||||
import { db } from "@/lib/db";
|
||||
import { toNumber } from "@/lib/dashboard/common";
|
||||
import { INVOICE_PAYMENT_STATUS } from "@/lib/faturas";
|
||||
import { and, eq, isNotNull, isNull, ne, or, sql } from "drizzle-orm";
|
||||
import { and, eq, isNotNull, isNull, or, sql } from "drizzle-orm";
|
||||
|
||||
// Calcula a data de vencimento baseada no período e dia de vencimento do cartão
|
||||
function calculateDueDate(period: string, dueDay: string | null): Date | null {
|
||||
if (!dueDay) return null;
|
||||
|
||||
try {
|
||||
const [year, month] = period.split("-");
|
||||
if (!year || !month) return null;
|
||||
|
||||
const day = parseInt(dueDay, 10);
|
||||
if (isNaN(day)) return null;
|
||||
|
||||
return new Date(parseInt(year), parseInt(month) - 1, day);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export type InstallmentDetail = {
|
||||
id: string;
|
||||
@@ -16,6 +32,7 @@ export type InstallmentDetail = {
|
||||
period: string;
|
||||
isAnticipated: boolean;
|
||||
purchaseDate: Date;
|
||||
isSettled: boolean;
|
||||
};
|
||||
|
||||
export type InstallmentGroup = {
|
||||
@@ -24,6 +41,7 @@ export type InstallmentGroup = {
|
||||
paymentMethod: string;
|
||||
cartaoId: string | null;
|
||||
cartaoName: string | null;
|
||||
cartaoDueDay: string | null;
|
||||
totalInstallments: number;
|
||||
paidInstallments: number;
|
||||
pendingInstallments: InstallmentDetail[];
|
||||
@@ -31,38 +49,15 @@ export type InstallmentGroup = {
|
||||
firstPurchaseDate: Date;
|
||||
};
|
||||
|
||||
export type PendingInvoiceLancamento = {
|
||||
id: string;
|
||||
name: string;
|
||||
amount: number;
|
||||
purchaseDate: Date;
|
||||
condition: string;
|
||||
currentInstallment: number | null;
|
||||
installmentCount: number | null;
|
||||
};
|
||||
|
||||
export type PendingInvoice = {
|
||||
invoiceId: string | null;
|
||||
cartaoId: string;
|
||||
cartaoName: string;
|
||||
cartaoLogo: string | null;
|
||||
period: string;
|
||||
totalAmount: number;
|
||||
dueDay: string;
|
||||
lancamentos: PendingInvoiceLancamento[];
|
||||
};
|
||||
|
||||
export type InstallmentAnalysisData = {
|
||||
installmentGroups: InstallmentGroup[];
|
||||
pendingInvoices: PendingInvoice[];
|
||||
totalPendingInstallments: number;
|
||||
totalPendingInvoices: number;
|
||||
};
|
||||
|
||||
export async function fetchInstallmentAnalysis(
|
||||
userId: string
|
||||
): Promise<InstallmentAnalysisData> {
|
||||
// 1. Buscar todos os lançamentos parcelados não antecipados e não pagos
|
||||
// 1. Buscar todos os lançamentos parcelados não antecipados
|
||||
const installmentRows = await db
|
||||
.select({
|
||||
id: lancamentos.id,
|
||||
@@ -75,9 +70,11 @@ export async function fetchInstallmentAnalysis(
|
||||
dueDate: lancamentos.dueDate,
|
||||
period: lancamentos.period,
|
||||
isAnticipated: lancamentos.isAnticipated,
|
||||
isSettled: lancamentos.isSettled,
|
||||
purchaseDate: lancamentos.purchaseDate,
|
||||
cartaoId: lancamentos.cartaoId,
|
||||
cartaoName: cartoes.name,
|
||||
cartaoDueDay: cartoes.dueDay,
|
||||
})
|
||||
.from(lancamentos)
|
||||
.leftJoin(cartoes, eq(lancamentos.cartaoId, cartoes.id))
|
||||
@@ -106,14 +103,21 @@ export async function fetchInstallmentAnalysis(
|
||||
if (!row.seriesId) continue;
|
||||
|
||||
const amount = Math.abs(toNumber(row.amount));
|
||||
|
||||
// Calcular vencimento correto baseado no período e dia de vencimento do cartão
|
||||
const calculatedDueDate = row.cartaoDueDay
|
||||
? calculateDueDate(row.period, row.cartaoDueDay)
|
||||
: row.dueDate;
|
||||
|
||||
const installmentDetail: InstallmentDetail = {
|
||||
id: row.id,
|
||||
currentInstallment: row.currentInstallment ?? 1,
|
||||
amount,
|
||||
dueDate: row.dueDate,
|
||||
dueDate: calculatedDueDate,
|
||||
period: row.period,
|
||||
isAnticipated: row.isAnticipated ?? false,
|
||||
purchaseDate: row.purchaseDate,
|
||||
isSettled: row.isSettled ?? false,
|
||||
};
|
||||
|
||||
if (seriesMap.has(row.seriesId)) {
|
||||
@@ -127,6 +131,7 @@ export async function fetchInstallmentAnalysis(
|
||||
paymentMethod: row.paymentMethod,
|
||||
cartaoId: row.cartaoId,
|
||||
cartaoName: row.cartaoName,
|
||||
cartaoDueDay: row.cartaoDueDay,
|
||||
totalInstallments: row.installmentCount ?? 0,
|
||||
paidInstallments: 0,
|
||||
pendingInstallments: [installmentDetail],
|
||||
@@ -145,92 +150,14 @@ export async function fetchInstallmentAnalysis(
|
||||
return group;
|
||||
});
|
||||
|
||||
// 2. Buscar faturas pendentes
|
||||
const invoiceRows = await db
|
||||
.select({
|
||||
invoiceId: faturas.id,
|
||||
cardId: cartoes.id,
|
||||
cardName: cartoes.name,
|
||||
cardLogo: cartoes.logo,
|
||||
dueDay: cartoes.dueDay,
|
||||
period: faturas.period,
|
||||
paymentStatus: faturas.paymentStatus,
|
||||
})
|
||||
.from(faturas)
|
||||
.innerJoin(cartoes, eq(faturas.cartaoId, cartoes.id))
|
||||
.where(
|
||||
and(
|
||||
eq(faturas.userId, userId),
|
||||
eq(faturas.paymentStatus, INVOICE_PAYMENT_STATUS.PENDING)
|
||||
)
|
||||
);
|
||||
|
||||
// Buscar lançamentos de cada fatura pendente
|
||||
const pendingInvoices: PendingInvoice[] = [];
|
||||
|
||||
for (const invoice of invoiceRows) {
|
||||
const invoiceLancamentos = await db
|
||||
.select({
|
||||
id: lancamentos.id,
|
||||
name: lancamentos.name,
|
||||
amount: lancamentos.amount,
|
||||
purchaseDate: lancamentos.purchaseDate,
|
||||
condition: lancamentos.condition,
|
||||
currentInstallment: lancamentos.currentInstallment,
|
||||
installmentCount: lancamentos.installmentCount,
|
||||
})
|
||||
.from(lancamentos)
|
||||
.where(
|
||||
and(
|
||||
eq(lancamentos.userId, userId),
|
||||
eq(lancamentos.cartaoId, invoice.cardId),
|
||||
eq(lancamentos.period, invoice.period ?? "")
|
||||
)
|
||||
)
|
||||
.orderBy(lancamentos.purchaseDate);
|
||||
|
||||
const totalAmount = invoiceLancamentos.reduce(
|
||||
(sum, l) => sum + Math.abs(toNumber(l.amount)),
|
||||
0
|
||||
);
|
||||
|
||||
if (totalAmount > 0) {
|
||||
pendingInvoices.push({
|
||||
invoiceId: invoice.invoiceId,
|
||||
cartaoId: invoice.cardId,
|
||||
cartaoName: invoice.cardName,
|
||||
cartaoLogo: invoice.cardLogo,
|
||||
period: invoice.period ?? "",
|
||||
totalAmount,
|
||||
dueDay: invoice.dueDay,
|
||||
lancamentos: invoiceLancamentos.map((l) => ({
|
||||
id: l.id,
|
||||
name: l.name,
|
||||
amount: Math.abs(toNumber(l.amount)),
|
||||
purchaseDate: l.purchaseDate,
|
||||
condition: l.condition,
|
||||
currentInstallment: l.currentInstallment,
|
||||
installmentCount: l.installmentCount,
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Calcular totais
|
||||
const totalPendingInstallments = installmentGroups.reduce(
|
||||
(sum, group) => sum + group.totalPendingAmount,
|
||||
0
|
||||
);
|
||||
|
||||
const totalPendingInvoices = pendingInvoices.reduce(
|
||||
(sum, invoice) => sum + invoice.totalAmount,
|
||||
0
|
||||
);
|
||||
|
||||
return {
|
||||
installmentGroups,
|
||||
pendingInvoices,
|
||||
totalPendingInstallments,
|
||||
totalPendingInvoices,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { RecentTransactionsWidget } from "@/components/dashboard/recent-transact
|
||||
import { RecurringExpensesWidget } from "@/components/dashboard/recurring-expenses-widget";
|
||||
import { TopEstablishmentsWidget } from "@/components/dashboard/top-establishments-widget";
|
||||
import { TopExpensesWidget } from "@/components/dashboard/top-expenses-widget";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
RiArrowUpDoubleLine,
|
||||
RiBarChartBoxLine,
|
||||
@@ -38,6 +39,7 @@ export type WidgetConfig = {
|
||||
subtitle: string;
|
||||
icon: ReactNode;
|
||||
component: (props: { data: DashboardData; period: string }) => ReactNode;
|
||||
action?: ReactNode;
|
||||
};
|
||||
|
||||
export const widgetsConfig: WidgetConfig[] = [
|
||||
@@ -134,6 +136,14 @@ export const widgetsConfig: WidgetConfig[] = [
|
||||
component: ({ data }) => (
|
||||
<InstallmentExpensesWidget data={data.installmentExpensesData} />
|
||||
),
|
||||
action: (
|
||||
<Link
|
||||
href="/dashboard/analise-parcelas"
|
||||
className="text-xs font-medium text-muted-foreground hover:text-primary transition-colors"
|
||||
>
|
||||
Análise
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "top-expenses",
|
||||
|
||||
Reference in New Issue
Block a user