diff --git a/apps/backend/src/modules/expenses/expenses.routes.ts b/apps/backend/src/modules/expenses/expenses.routes.ts index 80472a3..53ff3f3 100644 --- a/apps/backend/src/modules/expenses/expenses.routes.ts +++ b/apps/backend/src/modules/expenses/expenses.routes.ts @@ -7,6 +7,8 @@ import { generateMonthlyExpenseHandler } from "./handlers/generateMonthlyExpense import { listExpensesHandler } from "./handlers/listExpenses"; import { createNonPeriodicExpenseHandler } from "./handlers/createNonPeriodicExpense"; import { payExpenseHandler } from "./handlers/payExpense"; +import { getMonthlyPayedTotalHandler } from "./handlers/getMonthlyPayedTotal"; +import { getMonthlyTotalsHandler } from "./handlers/getMonthlyTotals"; const app = new Hono(); @@ -16,6 +18,8 @@ app.put("/periodic-expenses/:id", updatePeriodicExpenseHandler); app.delete("/periodic-expenses/:id", softDeletePeriodicExpenseHandler); app.post("/periodic-expenses/:id/generate-month", generateMonthlyExpenseHandler); +app.get("/expenses/monthly-total", getMonthlyPayedTotalHandler); +app.get("/expenses/totals", getMonthlyTotalsHandler); app.get("/expenses", listExpensesHandler); app.post("/expenses", createNonPeriodicExpenseHandler); app.put("/expenses/:id/pay", payExpenseHandler); diff --git a/apps/backend/src/modules/expenses/expenses.service.ts b/apps/backend/src/modules/expenses/expenses.service.ts index cb55f43..9842930 100644 --- a/apps/backend/src/modules/expenses/expenses.service.ts +++ b/apps/backend/src/modules/expenses/expenses.service.ts @@ -84,16 +84,27 @@ export async function generateMonthlyExpense(id: number) { }); } -export async function listExpenses(status?: string) { +export async function listExpenses(params: { + status?: string; + page?: number; + pageSize?: number; +}) { + const { status, page = 1, pageSize = 10 } = params; const where: Record = {}; if (status === "PENDING" || status === "PAYED") { where.status = status; } - return prisma.expense.findMany({ - where, - include: { periodicExpense: true }, - orderBy: { dueDate: "asc" }, - }); + const [data, total] = await Promise.all([ + prisma.expense.findMany({ + where, + include: { periodicExpense: true }, + orderBy: { dueDate: "desc" }, + skip: (page - 1) * pageSize, + take: pageSize, + }), + prisma.expense.count({ where }), + ]); + return { data, total, page, pageSize }; } export async function createNonPeriodicExpense(data: z.infer) { @@ -126,6 +137,32 @@ export async function payExpense(id: number, data: z.infer sum + Number(e.amountPayed ?? 0), 0); + return { total }; +} + +export async function getMonthlyTotals(year: number, month: number) { + const [payed, pending] = await Promise.all([ + prisma.expense.findMany({ + where: { year, month, status: PaymentStatus.PAYED }, + select: { amountPayed: true }, + }), + prisma.expense.findMany({ + where: { year, month, status: PaymentStatus.PENDING }, + select: { amount: true }, + }), + ]); + return { + payed: payed.reduce((sum, e) => sum + Number(e.amountPayed ?? 0), 0), + pending: pending.reduce((sum, e) => sum + Number(e.amount), 0), + }; +} + export async function generateExpensesForCurrentMonth() { const { year, month } = getCurrentYearMonthUTC(); diff --git a/apps/backend/src/modules/expenses/handlers/getMonthlyPayedTotal.ts b/apps/backend/src/modules/expenses/handlers/getMonthlyPayedTotal.ts new file mode 100644 index 0000000..4bb7e0f --- /dev/null +++ b/apps/backend/src/modules/expenses/handlers/getMonthlyPayedTotal.ts @@ -0,0 +1,9 @@ +import type { Context } from "hono"; +import { getMonthlyPayedTotal } from "../expenses.service"; + +export async function getMonthlyPayedTotalHandler(c: Context) { + const year = parseInt(c.req.query("year") ?? String(new Date().getUTCFullYear()), 10); + const month = parseInt(c.req.query("month") ?? String(new Date().getUTCMonth() + 1), 10); + const result = await getMonthlyPayedTotal(year, month); + return c.json(result); +} diff --git a/apps/backend/src/modules/expenses/handlers/getMonthlyTotals.ts b/apps/backend/src/modules/expenses/handlers/getMonthlyTotals.ts new file mode 100644 index 0000000..a7cedfd --- /dev/null +++ b/apps/backend/src/modules/expenses/handlers/getMonthlyTotals.ts @@ -0,0 +1,9 @@ +import type { Context } from "hono"; +import { getMonthlyTotals } from "../expenses.service"; + +export async function getMonthlyTotalsHandler(c: Context) { + const year = parseInt(c.req.query("year") ?? String(new Date().getUTCFullYear()), 10); + const month = parseInt(c.req.query("month") ?? String(new Date().getUTCMonth() + 1), 10); + const result = await getMonthlyTotals(year, month); + return c.json(result); +} diff --git a/apps/backend/src/modules/expenses/handlers/listExpenses.ts b/apps/backend/src/modules/expenses/handlers/listExpenses.ts index 2926dc4..e077d5a 100644 --- a/apps/backend/src/modules/expenses/handlers/listExpenses.ts +++ b/apps/backend/src/modules/expenses/handlers/listExpenses.ts @@ -3,6 +3,8 @@ import { listExpenses } from "../expenses.service"; export async function listExpensesHandler(c: Context) { const status = c.req.query("status"); - const expenses = await listExpenses(status); - return c.json(expenses); + const page = parseInt(c.req.query("page") ?? "1", 10); + const pageSize = parseInt(c.req.query("pageSize") ?? "10", 10); + const result = await listExpenses({ status, page, pageSize }); + return c.json(result); } diff --git a/apps/frontend/src/components/ui/pagination.tsx b/apps/frontend/src/components/ui/pagination.tsx new file mode 100644 index 0000000..96fdae7 --- /dev/null +++ b/apps/frontend/src/components/ui/pagination.tsx @@ -0,0 +1,129 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from "lucide-react" + +function Pagination({ className, ...props }: React.ComponentProps<"nav">) { + return ( +