148 lines
3.7 KiB
TypeScript
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,
|
|
};
|
|
}
|