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:
Jose Selesan
2026-06-04 10:48:07 -03:00
parent 53a797703e
commit 5b6ccf0fa6
81 changed files with 1565 additions and 794 deletions

View File

@@ -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);

View File

@@ -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),
}));
}

View File

@@ -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();

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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: {

View File

@@ -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,
},
});

View File

@@ -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);

View File

@@ -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"));