Files
openmonetis/lib/dashboard/goals-progress.ts
2026-03-02 17:20:28 +00:00

148 lines
3.7 KiB
TypeScript

import { and, eq, ne, sql } from "drizzle-orm";
import { categorias, lancamentos, orcamentos } from "@/db/schema";
import { toNumber } from "@/lib/dashboard/common";
import { db } from "@/lib/db";
import { getAdminPagadorId } from "@/lib/pagadores/get-admin-id";
const BUDGET_CRITICAL_THRESHOLD = 80;
export type GoalProgressStatus = "on-track" | "critical" | "exceeded";
export type GoalProgressItem = {
id: string;
categoryId: string | null;
categoryName: string;
categoryIcon: string | null;
period: string;
createdAt: string;
budgetAmount: number;
spentAmount: number;
usedPercentage: number;
status: GoalProgressStatus;
};
export type GoalProgressCategory = {
id: string;
name: string;
icon: string | null;
};
export type GoalsProgressData = {
items: GoalProgressItem[];
categories: GoalProgressCategory[];
totalBudgets: number;
exceededCount: number;
criticalCount: number;
};
const resolveStatus = (usedPercentage: number): GoalProgressStatus => {
if (usedPercentage >= 100) {
return "exceeded";
}
if (usedPercentage >= BUDGET_CRITICAL_THRESHOLD) {
return "critical";
}
return "on-track";
};
export async function fetchGoalsProgressData(
userId: string,
period: string,
): Promise<GoalsProgressData> {
const adminPagadorId = await getAdminPagadorId(userId);
if (!adminPagadorId) {
return {
items: [],
categories: [],
totalBudgets: 0,
exceededCount: 0,
criticalCount: 0,
};
}
const [rows, categoryRows] = await Promise.all([
db
.select({
orcamentoId: orcamentos.id,
categoryId: categorias.id,
categoryName: categorias.name,
categoryIcon: categorias.icon,
period: orcamentos.period,
createdAt: orcamentos.createdAt,
budgetAmount: orcamentos.amount,
spentAmount: sql<number>`COALESCE(SUM(ABS(${lancamentos.amount})), 0)`,
})
.from(orcamentos)
.innerJoin(categorias, eq(orcamentos.categoriaId, categorias.id))
.leftJoin(
lancamentos,
and(
eq(lancamentos.categoriaId, orcamentos.categoriaId),
eq(lancamentos.userId, orcamentos.userId),
eq(lancamentos.period, orcamentos.period),
eq(lancamentos.pagadorId, adminPagadorId),
eq(lancamentos.transactionType, "Despesa"),
ne(lancamentos.condition, "cancelado"),
),
)
.where(and(eq(orcamentos.userId, userId), eq(orcamentos.period, period)))
.groupBy(
orcamentos.id,
categorias.id,
categorias.name,
categorias.icon,
orcamentos.period,
orcamentos.createdAt,
orcamentos.amount,
),
db.query.categorias.findMany({
where: and(eq(categorias.userId, userId), eq(categorias.type, "despesa")),
orderBy: (category, { asc }) => [asc(category.name)],
}),
]);
const categories: GoalProgressCategory[] = categoryRows.map((category) => ({
id: category.id,
name: category.name,
icon: category.icon,
}));
const items: GoalProgressItem[] = rows
.map((row) => {
const budgetAmount = toNumber(row.budgetAmount);
const spentAmount = toNumber(row.spentAmount);
const usedPercentage =
budgetAmount > 0 ? (spentAmount / budgetAmount) * 100 : 0;
return {
id: row.orcamentoId,
categoryId: row.categoryId,
categoryName: row.categoryName,
categoryIcon: row.categoryIcon,
period: row.period,
createdAt: row.createdAt.toISOString(),
budgetAmount,
spentAmount,
usedPercentage,
status: resolveStatus(usedPercentage),
};
})
.sort((a, b) => b.usedPercentage - a.usedPercentage);
const exceededCount = items.filter(
(item) => item.status === "exceeded",
).length;
const criticalCount = items.filter(
(item) => item.status === "critical",
).length;
return {
items,
categories,
totalBudgets: items.length,
exceededCount,
criticalCount,
};
}