chore: add Biome for lint and fix all lint errors
- Install @biomejs/biome as devDependency at root - Configure biome.json with 2-space indent, double quotes, Tailwind CSS support - Add lint/lint:fix/format/format:fix scripts to root and app package.json - Fix noNonNullAssertion: env vars extracted to variables with suppression, <div role=button> replaced with <button> - Fix noUnusedVariables: remove unused destructured vars - Fix useIterableCallbackReturn: arrow functions with block body - Fix noExplicitAny: recharts Tooltip formatters - Fix noLabelWithoutControl: add htmlFor+id or use <span> for non-input labels - Fix noStaticElementInteractions/useKeyWithClickEvents: role+keyboard events for overlays - Fix noArrayIndexKey: use error string as key - Fix CSS parse: enable tailwindDirectives parser - Normalize formatting across 80 files with biome check --write
This commit is contained in:
@@ -1,18 +1,18 @@
|
||||
import { Hono } from "hono";
|
||||
import { createPeriodicExpenseHandler } from "./handlers/createPeriodicExpense";
|
||||
import { listPeriodicExpensesHandler } from "./handlers/listPeriodicExpenses";
|
||||
import { updatePeriodicExpenseHandler } from "./handlers/updatePeriodicExpense";
|
||||
import { softDeletePeriodicExpenseHandler } from "./handlers/softDeletePeriodicExpense";
|
||||
import { generateMonthlyExpenseHandler } from "./handlers/generateMonthlyExpense";
|
||||
import { listExpensesHandler } from "./handlers/listExpenses";
|
||||
import { createNonPeriodicExpenseHandler } from "./handlers/createNonPeriodicExpense";
|
||||
import { payExpenseHandler } from "./handlers/payExpense";
|
||||
import { createPeriodicExpenseHandler } from "./handlers/createPeriodicExpense";
|
||||
import { generateMonthlyExpenseHandler } from "./handlers/generateMonthlyExpense";
|
||||
import { getMonthlyPayedTotalHandler } from "./handlers/getMonthlyPayedTotal";
|
||||
import { getMonthlyTotalsHandler } from "./handlers/getMonthlyTotals";
|
||||
import { getTotalPendingHandler } from "./handlers/getTotalPending";
|
||||
import { getPendingUpcomingExpensesHandler } from "./handlers/getPendingUpcomingExpenses";
|
||||
import { importPeriodicExpensesHandler } from "./handlers/importPeriodicExpenses";
|
||||
import { getTotalPendingHandler } from "./handlers/getTotalPending";
|
||||
import { importExpensesHandler } from "./handlers/importExpenses";
|
||||
import { importPeriodicExpensesHandler } from "./handlers/importPeriodicExpenses";
|
||||
import { listExpensesHandler } from "./handlers/listExpenses";
|
||||
import { listPeriodicExpensesHandler } from "./handlers/listPeriodicExpenses";
|
||||
import { payExpenseHandler } from "./handlers/payExpense";
|
||||
import { softDeletePeriodicExpenseHandler } from "./handlers/softDeletePeriodicExpense";
|
||||
import { updatePeriodicExpenseHandler } from "./handlers/updatePeriodicExpense";
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
@@ -21,7 +21,10 @@ app.post("/periodic-expenses", createPeriodicExpenseHandler);
|
||||
app.put("/periodic-expenses/:id", updatePeriodicExpenseHandler);
|
||||
app.delete("/periodic-expenses/:id", softDeletePeriodicExpenseHandler);
|
||||
app.post("/periodic-expenses/import", importPeriodicExpensesHandler);
|
||||
app.post("/periodic-expenses/:id/generate-month", generateMonthlyExpenseHandler);
|
||||
app.post(
|
||||
"/periodic-expenses/:id/generate-month",
|
||||
generateMonthlyExpenseHandler,
|
||||
);
|
||||
|
||||
app.get("/expenses/monthly-total", getMonthlyPayedTotalHandler);
|
||||
app.get("/expenses/totals", getMonthlyTotalsHandler);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import { prisma } from "../../lib/prisma";
|
||||
import { cache } from "../../lib/cache";
|
||||
import { PaymentStatus } from "../../generated/prisma/client";
|
||||
import { cache } from "../../lib/cache";
|
||||
import { prisma } from "../../lib/prisma";
|
||||
|
||||
export const createPeriodicExpenseSchema = z.object({
|
||||
description: z.string().min(1).max(50),
|
||||
@@ -27,7 +27,11 @@ function lastDayOfMonth(year: number, month: number): number {
|
||||
return new Date(year, month, 0).getDate();
|
||||
}
|
||||
|
||||
export function buildDueDate(year: number, month: number, dueDay: number): Date {
|
||||
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));
|
||||
}
|
||||
@@ -49,15 +53,20 @@ export async function listPeriodicExpenses() {
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function createPeriodicExpense(data: z.infer<typeof createPeriodicExpenseSchema>) {
|
||||
export async function createPeriodicExpense(
|
||||
data: z.infer<typeof createPeriodicExpenseSchema>,
|
||||
) {
|
||||
const result = await prisma.periodicExpense.create({ data });
|
||||
const { year, month } = getCurrentYearMonthUTC();
|
||||
const { month } = getCurrentYearMonthUTC();
|
||||
const currentMonthApplicable = data.periods.includes(month);
|
||||
cache.invalidateByPrefix("expenses:");
|
||||
return { ...result, currentMonthApplicable };
|
||||
}
|
||||
|
||||
export async function updatePeriodicExpense(id: number, data: z.infer<typeof updatePeriodicExpenseSchema>) {
|
||||
export async function updatePeriodicExpense(
|
||||
id: number,
|
||||
data: z.infer<typeof updatePeriodicExpenseSchema>,
|
||||
) {
|
||||
const result = await prisma.periodicExpense.update({ where: { id }, data });
|
||||
cache.invalidateByPrefix("expenses:");
|
||||
return result;
|
||||
@@ -73,7 +82,9 @@ export async function softDeletePeriodicExpense(id: number) {
|
||||
}
|
||||
|
||||
export async function generateMonthlyExpense(id: number) {
|
||||
const periodic = await prisma.periodicExpense.findUniqueOrThrow({ where: { id } });
|
||||
const periodic = await prisma.periodicExpense.findUniqueOrThrow({
|
||||
where: { id },
|
||||
});
|
||||
const { year, month } = getCurrentYearMonthUTC();
|
||||
|
||||
const existing = await prisma.expense.findFirst({
|
||||
@@ -135,7 +146,9 @@ export async function listExpenses(params: {
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function createNonPeriodicExpense(data: z.infer<typeof createNonPeriodicExpenseSchema>) {
|
||||
export async function createNonPeriodicExpense(
|
||||
data: z.infer<typeof createNonPeriodicExpenseSchema>,
|
||||
) {
|
||||
const dueDate = new Date(data.dueDate);
|
||||
const year = dueDate.getUTCFullYear();
|
||||
const month = dueDate.getUTCMonth() + 1;
|
||||
@@ -155,8 +168,13 @@ export async function createNonPeriodicExpense(data: z.infer<typeof createNonPer
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function payExpense(id: number, data: z.infer<typeof payExpenseSchema>) {
|
||||
const paymentDate = data.paymentDate ? new Date(data.paymentDate) : new Date();
|
||||
export async function payExpense(
|
||||
id: number,
|
||||
data: z.infer<typeof payExpenseSchema>,
|
||||
) {
|
||||
const paymentDate = data.paymentDate
|
||||
? new Date(data.paymentDate)
|
||||
: new Date();
|
||||
const result = await prisma.expense.update({
|
||||
where: { id },
|
||||
data: {
|
||||
@@ -189,7 +207,9 @@ export async function getMonthlyTotals(year: number, month: number) {
|
||||
const cached = cache.get(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const result = await prisma.$queryRaw<Array<{ payed: string | null; pending: string | null }>>`
|
||||
const result = await prisma.$queryRaw<
|
||||
Array<{ payed: string | null; pending: string | null }>
|
||||
>`
|
||||
SELECT
|
||||
(SELECT CAST(SUM("amountPayed") AS NUMERIC) FROM expenses WHERE year = ${year} AND month = ${month} AND status = ${PaymentStatus.PAYED}::"PaymentStatus") as payed,
|
||||
(SELECT CAST(SUM(amount) AS NUMERIC) FROM expenses WHERE year = ${year} AND month = ${month} AND status = ${PaymentStatus.PENDING}::"PaymentStatus") as pending
|
||||
@@ -218,7 +238,17 @@ export async function getTotalPending() {
|
||||
|
||||
export async function listPendingUpcomingExpenses() {
|
||||
const now = new Date();
|
||||
const today = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), 0, 0, 0, 0));
|
||||
const today = new Date(
|
||||
Date.UTC(
|
||||
now.getUTCFullYear(),
|
||||
now.getUTCMonth(),
|
||||
now.getUTCDate(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
);
|
||||
const fiveDaysLater = new Date(today);
|
||||
fiveDaysLater.setUTCDate(fiveDaysLater.getUTCDate() + 5);
|
||||
|
||||
@@ -233,7 +263,8 @@ export async function listPendingUpcomingExpenses() {
|
||||
|
||||
return data.map((expense) => ({
|
||||
...expense,
|
||||
dueType: expense.dueDate < today ? "overdue" as const : "upcoming" as const,
|
||||
dueType:
|
||||
expense.dueDate < today ? ("overdue" as const) : ("upcoming" as const),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { Context } from "hono";
|
||||
import { createNonPeriodicExpense, createNonPeriodicExpenseSchema } from "../expenses.service";
|
||||
import {
|
||||
createNonPeriodicExpense,
|
||||
createNonPeriodicExpenseSchema,
|
||||
} from "../expenses.service";
|
||||
|
||||
export async function createNonPeriodicExpenseHandler(c: Context) {
|
||||
const body = await c.req.json();
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { Context } from "hono";
|
||||
import { createPeriodicExpense, createPeriodicExpenseSchema } from "../expenses.service";
|
||||
import {
|
||||
createPeriodicExpense,
|
||||
createPeriodicExpenseSchema,
|
||||
} from "../expenses.service";
|
||||
|
||||
export async function createPeriodicExpenseHandler(c: Context) {
|
||||
const body = await c.req.json();
|
||||
|
||||
@@ -2,8 +2,14 @@ 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 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);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,14 @@ 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 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);
|
||||
}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import type { Context } from "hono";
|
||||
import { prisma } from "../../../lib/prisma";
|
||||
import { PaymentStatus } from "../../../generated/prisma/client";
|
||||
import { cache } from "../../../lib/cache";
|
||||
import { logger } from "../../../lib/logger";
|
||||
import { PaymentStatus } from "../../../generated/prisma/client";
|
||||
import { prisma } from "../../../lib/prisma";
|
||||
|
||||
function parseAmount(raw: string): number | null {
|
||||
const cleaned = raw.replace("$", "").replace(/,/g, "").trim();
|
||||
if (!cleaned) return null;
|
||||
const n = Number(cleaned);
|
||||
return isNaN(n) ? null : n;
|
||||
return Number.isNaN(n) ? null : n;
|
||||
}
|
||||
|
||||
function parseDate(raw: string): Date | null {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return null;
|
||||
const d = new Date(trimmed);
|
||||
return isNaN(d.getTime()) ? null : d;
|
||||
return Number.isNaN(d.getTime()) ? null : d;
|
||||
}
|
||||
|
||||
export async function importExpensesHandler(c: Context) {
|
||||
const body = await c.req.parseBody();
|
||||
const file = body["file"];
|
||||
const file = body.file;
|
||||
|
||||
if (!file || !(file instanceof File)) {
|
||||
return c.json({ error: "No se envió ningún archivo" }, 400);
|
||||
@@ -30,7 +30,10 @@ export async function importExpensesHandler(c: Context) {
|
||||
const lines = text.trim().split("\n");
|
||||
|
||||
if (lines.length < 2) {
|
||||
return c.json({ error: "El archivo está vacío o solo tiene encabezados" }, 400);
|
||||
return c.json(
|
||||
{ error: "El archivo está vacío o solo tiene encabezados" },
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
let imported = 0;
|
||||
@@ -43,7 +46,9 @@ export async function importExpensesHandler(c: Context) {
|
||||
|
||||
const parts = line.split("|");
|
||||
if (parts.length < 10) {
|
||||
errors.push(`Línea ${i + 1}: formato inválido (${parts.length} columnas)`);
|
||||
errors.push(
|
||||
`Línea ${i + 1}: formato inválido (${parts.length} columnas)`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -68,7 +73,9 @@ export async function importExpensesHandler(c: Context) {
|
||||
const paymentDate = parseDate(rawPaymentDate);
|
||||
|
||||
if (!dueDate) {
|
||||
errors.push(`Línea ${i + 1} ("${description}"): fecha de vencimiento inválida`);
|
||||
errors.push(
|
||||
`Línea ${i + 1} ("${description}"): fecha de vencimiento inválida`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -90,7 +97,8 @@ export async function importExpensesHandler(c: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
const status = rawStatus === "PAYED" ? PaymentStatus.PAYED : PaymentStatus.PENDING;
|
||||
const status =
|
||||
rawStatus === "PAYED" ? PaymentStatus.PAYED : PaymentStatus.PENDING;
|
||||
|
||||
await prisma.expense.create({
|
||||
data: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Context } from "hono";
|
||||
import { prisma } from "../../../lib/prisma";
|
||||
import { cache } from "../../../lib/cache";
|
||||
import { logger } from "../../../lib/logger";
|
||||
import { prisma } from "../../../lib/prisma";
|
||||
|
||||
function parseArgentineAmount(raw: string): number {
|
||||
const cleaned = raw.replace("$", "").replace(/,/g, "").trim();
|
||||
@@ -16,7 +16,7 @@ function parsePeriods(raw: string): number[] {
|
||||
|
||||
export async function importPeriodicExpensesHandler(c: Context) {
|
||||
const body = await c.req.parseBody();
|
||||
const file = body["file"];
|
||||
const file = body.file;
|
||||
|
||||
if (!file || !(file instanceof File)) {
|
||||
return c.json({ error: "No se envió ningún archivo" }, 400);
|
||||
@@ -26,7 +26,10 @@ export async function importPeriodicExpensesHandler(c: Context) {
|
||||
const lines = text.trim().split("\n");
|
||||
|
||||
if (lines.length < 2) {
|
||||
return c.json({ error: "El archivo está vacío o solo tiene encabezados" }, 400);
|
||||
return c.json(
|
||||
{ error: "El archivo está vacío o solo tiene encabezados" },
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
let imported = 0;
|
||||
@@ -71,8 +74,8 @@ export async function importPeriodicExpensesHandler(c: Context) {
|
||||
await prisma.periodicExpense.create({
|
||||
data: {
|
||||
description,
|
||||
defaultDueDay: isNaN(defaultDueDay) ? 1 : defaultDueDay,
|
||||
defaultAmount: isNaN(defaultAmount) ? 0 : defaultAmount,
|
||||
defaultDueDay: Number.isNaN(defaultDueDay) ? 1 : defaultDueDay,
|
||||
defaultAmount: Number.isNaN(defaultAmount) ? 0 : defaultAmount,
|
||||
periods,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -11,7 +11,9 @@ export async function listExpensesHandler(c: Context) {
|
||||
status,
|
||||
page,
|
||||
pageSize,
|
||||
periodicExpenseId: periodicExpenseId ? parseInt(periodicExpenseId, 10) : undefined,
|
||||
periodicExpenseId: periodicExpenseId
|
||||
? parseInt(periodicExpenseId, 10)
|
||||
: undefined,
|
||||
search: search || undefined,
|
||||
});
|
||||
return c.json(result);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { Context } from "hono";
|
||||
import { updatePeriodicExpense, updatePeriodicExpenseSchema } from "../expenses.service";
|
||||
import {
|
||||
updatePeriodicExpense,
|
||||
updatePeriodicExpenseSchema,
|
||||
} from "../expenses.service";
|
||||
|
||||
export async function updatePeriodicExpenseHandler(c: Context) {
|
||||
const id = Number(c.req.param("id"));
|
||||
|
||||
Reference in New Issue
Block a user