feat(expenses): implement responsive dialog for expense management and create expense provider
- Added ResponsiveDialog component for handling responsive dialogs in the UI. - Created ExpensesProvider to manage state and API interactions for expenses. - Developed ExpensesTabContent to display expenses with filtering options and dialogs for new and pay expense actions. - Implemented ExpensesTable for rendering expense data in a tabular format with actions. - Added dialogs for creating new expenses and paying existing ones with form validation. - Introduced PeriodicExpenseForm for managing periodic expenses with month selection. - Created PeriodicExpensesTabContent to manage and display periodic expenses with create/edit functionality. - Added GenerateCurrentMonthDialog for confirming monthly expense generation. - Implemented PeriodicExpensesTable for displaying periodic expenses with edit and delete actions.
This commit is contained in:
160
apps/backend/src/modules/expenses/expenses.service.ts
Normal file
160
apps/backend/src/modules/expenses/expenses.service.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { z } from "zod";
|
||||
import { prisma } from "../../lib/prisma";
|
||||
import { PaymentStatus } from "../../generated/prisma/client";
|
||||
|
||||
export const createPeriodicExpenseSchema = z.object({
|
||||
description: z.string().min(1).max(50),
|
||||
defaultDueDay: z.number().int().min(1).max(31),
|
||||
defaultAmount: z.number().positive(),
|
||||
periods: z.array(z.number().int().min(1).max(12)).min(1),
|
||||
});
|
||||
|
||||
export const updatePeriodicExpenseSchema = createPeriodicExpenseSchema;
|
||||
|
||||
export const createNonPeriodicExpenseSchema = z.object({
|
||||
description: z.string().min(1).max(50),
|
||||
amount: z.number().positive(),
|
||||
dueDate: z.string().datetime(),
|
||||
});
|
||||
|
||||
export const payExpenseSchema = z.object({
|
||||
amountPayed: z.number().positive(),
|
||||
paymentDate: z.string().datetime().optional(),
|
||||
});
|
||||
|
||||
function lastDayOfMonth(year: number, month: number): number {
|
||||
return new Date(year, month, 0).getDate();
|
||||
}
|
||||
|
||||
export function buildDueDate(year: number, month: number, dueDay: number): Date {
|
||||
const clampedDay = Math.min(dueDay, lastDayOfMonth(year, month));
|
||||
return new Date(Date.UTC(year, month - 1, clampedDay, 0, 0, 0, 0));
|
||||
}
|
||||
|
||||
export function getCurrentYearMonthUTC(): { year: number; month: number } {
|
||||
const now = new Date();
|
||||
return { year: now.getUTCFullYear(), month: now.getUTCMonth() + 1 };
|
||||
}
|
||||
|
||||
export async function listPeriodicExpenses() {
|
||||
return prisma.periodicExpense.findMany({
|
||||
where: { isDeleted: false },
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
}
|
||||
|
||||
export async function createPeriodicExpense(data: z.infer<typeof createPeriodicExpenseSchema>) {
|
||||
const result = await prisma.periodicExpense.create({ data });
|
||||
const { year, month } = getCurrentYearMonthUTC();
|
||||
const currentMonthApplicable = data.periods.includes(month);
|
||||
return { ...result, currentMonthApplicable };
|
||||
}
|
||||
|
||||
export async function updatePeriodicExpense(id: number, data: z.infer<typeof updatePeriodicExpenseSchema>) {
|
||||
return prisma.periodicExpense.update({ where: { id }, data });
|
||||
}
|
||||
|
||||
export async function softDeletePeriodicExpense(id: number) {
|
||||
return prisma.periodicExpense.update({
|
||||
where: { id },
|
||||
data: { isDeleted: true },
|
||||
});
|
||||
}
|
||||
|
||||
export async function generateMonthlyExpense(id: number) {
|
||||
const periodic = await prisma.periodicExpense.findUniqueOrThrow({ where: { id } });
|
||||
const { year, month } = getCurrentYearMonthUTC();
|
||||
|
||||
const existing = await prisma.expense.findFirst({
|
||||
where: { periodicExpenseId: id, year, month },
|
||||
});
|
||||
if (existing) return existing;
|
||||
|
||||
const dueDate = buildDueDate(year, month, periodic.defaultDueDay);
|
||||
return prisma.expense.create({
|
||||
data: {
|
||||
description: periodic.description,
|
||||
periodicExpenseId: id,
|
||||
year,
|
||||
month,
|
||||
amount: periodic.defaultAmount,
|
||||
dueDate,
|
||||
status: PaymentStatus.PENDING,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function listExpenses(status?: string) {
|
||||
const where: Record<string, unknown> = {};
|
||||
if (status === "PENDING" || status === "PAYED") {
|
||||
where.status = status;
|
||||
}
|
||||
return prisma.expense.findMany({
|
||||
where,
|
||||
include: { periodicExpense: true },
|
||||
orderBy: { dueDate: "asc" },
|
||||
});
|
||||
}
|
||||
|
||||
export async function createNonPeriodicExpense(data: z.infer<typeof createNonPeriodicExpenseSchema>) {
|
||||
const dueDate = new Date(data.dueDate);
|
||||
const year = dueDate.getUTCFullYear();
|
||||
const month = dueDate.getUTCMonth() + 1;
|
||||
return prisma.expense.create({
|
||||
data: {
|
||||
description: data.description,
|
||||
amount: data.amount,
|
||||
amountPayed: data.amount,
|
||||
dueDate,
|
||||
paymentDate: dueDate,
|
||||
year,
|
||||
month,
|
||||
status: PaymentStatus.PAYED,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function payExpense(id: number, data: z.infer<typeof payExpenseSchema>) {
|
||||
const paymentDate = data.paymentDate ? new Date(data.paymentDate) : new Date();
|
||||
return prisma.expense.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: PaymentStatus.PAYED,
|
||||
amountPayed: data.amountPayed,
|
||||
paymentDate,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function generateExpensesForCurrentMonth() {
|
||||
const { year, month } = getCurrentYearMonthUTC();
|
||||
|
||||
const periodics = await prisma.periodicExpense.findMany({
|
||||
where: { isDeleted: false, periods: { has: month } },
|
||||
});
|
||||
|
||||
const results: Array<{ id: number; status: string }> = [];
|
||||
for (const periodic of periodics) {
|
||||
const existing = await prisma.expense.findFirst({
|
||||
where: { periodicExpenseId: periodic.id, year, month },
|
||||
});
|
||||
if (existing) {
|
||||
results.push({ id: periodic.id, status: "already_exists" });
|
||||
continue;
|
||||
}
|
||||
const dueDate = buildDueDate(year, month, periodic.defaultDueDay);
|
||||
await prisma.expense.create({
|
||||
data: {
|
||||
description: periodic.description,
|
||||
periodicExpenseId: periodic.id,
|
||||
year,
|
||||
month,
|
||||
amount: periodic.defaultAmount,
|
||||
dueDate,
|
||||
status: PaymentStatus.PENDING,
|
||||
},
|
||||
});
|
||||
results.push({ id: periodic.id, status: "created" });
|
||||
}
|
||||
return results;
|
||||
}
|
||||
Reference in New Issue
Block a user