first commit

This commit is contained in:
unknown
2026-06-03 16:59:27 -04:00
commit b35cc2f54b
36 changed files with 5634 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
import { Router } from 'express';
import bcrypt from 'bcryptjs';
import { db } from '../db.js';
import { signToken, authRequired } from '../middleware/auth.js';
const router = Router();
router.post('/login', (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Usuario y contraseña requeridos' });
}
const user = db.prepare('SELECT * FROM users WHERE username = ? AND active = 1').get(username);
if (!user || !bcrypt.compareSync(password, user.password_hash)) {
return res.status(401).json({ error: 'Credenciales inválidas' });
}
const permissions = db
.prepare('SELECT sheet_key, can_view, can_edit FROM permissions WHERE user_id = ?')
.all(user.id);
const token = signToken(user);
res.json({
token,
user: {
id: user.id,
username: user.username,
isAdmin: !!user.is_admin,
permissions: permissions.reduce((acc, p) => {
acc[p.sheet_key] = { view: !!p.can_view, edit: !!p.can_edit };
return acc;
}, {}),
},
});
});
router.get('/me', authRequired, (req, res) => {
const user = db.prepare('SELECT id, username, is_admin, active FROM users WHERE id = ?').get(req.user.id);
if (!user || !user.active) return res.status(401).json({ error: 'Usuario inactivo' });
const permissions = db
.prepare('SELECT sheet_key, can_view, can_edit FROM permissions WHERE user_id = ?')
.all(user.id);
res.json({
id: user.id,
username: user.username,
isAdmin: !!user.is_admin,
permissions: permissions.reduce((acc, p) => {
acc[p.sheet_key] = { view: !!p.can_view, edit: !!p.can_edit };
return acc;
}, {}),
});
});
export default router;

View File

@@ -0,0 +1,169 @@
import { Router } from 'express';
import { db } from '../db.js';
import { authRequired } from '../middleware/auth.js';
import {
inRange,
sumIngresos,
sumGastos,
sumPersonal,
weekKey,
monthKey,
parseFecha,
} from '../utils/calculations.js';
const router = Router();
router.use(authRequired);
function loadRows(sheetType) {
return db
.prepare('SELECT data FROM registros WHERE sheet_type = ?')
.all(sheetType)
.map((r) => JSON.parse(r.data));
}
function filterByDate(rows, fechaField, desde, hasta) {
return rows.filter((r) => inRange(r[fechaField], desde, hasta));
}
function buildReport(desde, hasta, include = {}) {
const inc = {
ventas: include.ventas !== false,
servicios: include.servicios !== false,
alquileres: include.alquileres !== false,
gastos_diarios: include.gastos_diarios !== false,
gastos_fijos: include.gastos_fijos !== false,
personal: include.personal !== false,
};
const ventas = inc.ventas ? filterByDate(loadRows('ventas'), 'fecha', desde, hasta) : [];
const servicios = inc.servicios ? filterByDate(loadRows('servicios'), 'fecha', desde, hasta) : [];
const alquileres = inc.alquileres ? filterByDate(loadRows('alquileres'), 'fecha', desde, hasta) : [];
const gastosDiarios = inc.gastos_diarios
? filterByDate(loadRows('gastos_diarios'), 'fecha', desde, hasta)
: [];
const gastosFijos = inc.gastos_fijos
? filterByDate(loadRows('gastos_fijos'), 'fecha', desde, hasta)
: [];
const personalRows = inc.personal ? loadRows('personal') : [];
const mensualizados = personalRows.filter((r) => r.tipo === 'mensualizado');
const eventuales = inc.personal
? filterByDate(
personalRows.filter((r) => r.tipo === 'eventual'),
'fecha',
desde,
hasta
)
: [];
const tVentas = sumIngresos(ventas);
const tServicios = sumIngresos(servicios);
const tAlquileres = sumIngresos(alquileres);
const totalIngresos =
tVentas.totalIngresos + tServicios.totalIngresos + tAlquileres.totalIngresos;
const totalUtilidad =
tVentas.totalUtilidad + tServicios.totalUtilidad + tAlquileres.totalUtilidad;
const gastosDiariosTotal = sumGastos(gastosDiarios);
const gastosFijosTotal = sumGastos(gastosFijos);
const gastosVariables = [...gastosDiarios, ...gastosFijos].filter(
(g) => g.clasificacion === 'variable'
);
const gastosVariablesTotal = sumGastos(gastosVariables);
const totalGastos = gastosDiariosTotal + gastosFijosTotal;
const totalPersonal = sumPersonal(mensualizados, eventuales);
const resultadoNeto = totalIngresos - totalGastos - totalPersonal;
return {
periodo: { desde: desde || null, hasta: hasta || null },
desglose: {
ventas: tVentas,
servicios: tServicios,
alquileres: tAlquileres,
gastosDiarios: { total: gastosDiariosTotal, filas: gastosDiarios.length },
gastosFijos: { total: gastosFijosTotal, filas: gastosFijos.length },
gastosVariables: { total: gastosVariablesTotal },
personal: { total: totalPersonal, mensualizados: mensualizados.length, eventuales: eventuales.length },
},
totales: {
totalIngresos,
totalUtilidad,
totalGastos,
totalPersonal,
resultadoNeto,
estado: resultadoNeto >= 0 ? 'utilidad' : 'perdida',
},
};
}
router.get('/', (req, res) => {
const { desde, hasta, tipo = 'mensual', include } = req.query;
let d = desde;
let h = hasta;
const today = new Date();
if (tipo === 'diario') {
const f = (h || d || today.toISOString().slice(0, 10));
d = f;
h = f;
} else if (tipo === 'semanal') {
const ref = parseFecha(h || d || today.toISOString().slice(0, 10));
const day = ref.getDay();
const diff = day === 0 ? -6 : 1 - day;
const monday = new Date(ref);
monday.setDate(ref.getDate() + diff);
const sunday = new Date(monday);
sunday.setDate(monday.getDate() + 6);
d = monday.toISOString().slice(0, 10);
h = sunday.toISOString().slice(0, 10);
} else if (tipo === 'mensual' && !d && !h) {
d = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-01`;
const last = new Date(today.getFullYear(), today.getMonth() + 1, 0);
h = last.toISOString().slice(0, 10);
}
let includeObj = {};
if (include) {
try {
includeObj = typeof include === 'string' ? JSON.parse(include) : include;
} catch {
includeObj = {};
}
}
const report = buildReport(d, h, includeObj);
if (tipo === 'semanal' || tipo === 'mensual') {
const gastosDiariosAll = loadRows('gastos_diarios');
const weekly = {};
for (const g of gastosDiariosAll) {
const wk = weekKey(g.fecha);
if (!inRange(g.fecha, d, h)) continue;
weekly[wk] = (weekly[wk] || 0) + (Number(g.monto) || 0);
}
report.cierreSemanal = weekly;
report.totalGastosSemanales = Object.values(weekly).reduce((a, b) => a + b, 0);
if (tipo === 'mensual') {
const monthly = {};
for (const g of gastosDiariosAll) {
const mk = monthKey(g.fecha);
if (!inRange(g.fecha, d, h)) continue;
monthly[mk] = (monthly[mk] || 0) + (Number(g.monto) || 0);
}
const fijosAll = filterByDate(loadRows('gastos_fijos'), 'fecha', d, h);
report.cierreMensual = {
gastosDiariosPorMes: monthly,
gastosFijosEnPeriodo: sumGastos(fijosAll),
totalGastosMensualesConsolidado:
Object.values(monthly).reduce((a, b) => a + b, 0) + sumGastos(fijosAll),
};
}
}
res.json(report);
});
export default router;

View File

@@ -0,0 +1,95 @@
import { Router } from 'express';
import { db, SHEETS } from '../db.js';
import { authRequired } from '../middleware/auth.js';
import { sumIngresos, sumGastos, sumPersonal } from '../utils/calculations.js';
const router = Router();
router.use(authRequired);
const INGRESO_SHEETS = ['ventas', 'servicios', 'alquileres'];
const GASTO_SHEETS = ['gastos_diarios', 'gastos_fijos'];
function checkPermission(req, sheet, needEdit = false) {
if (req.user.isAdmin) return true;
const row = db
.prepare('SELECT can_view, can_edit FROM permissions WHERE user_id = ? AND sheet_key = ?')
.get(req.user.id, sheet);
if (!row) return false;
if (needEdit) return !!row.can_edit;
return !!row.can_view;
}
function getSheetPayload(sheetType) {
const rows = db
.prepare('SELECT id, data, updated_at FROM registros WHERE sheet_type = ? ORDER BY id')
.all(sheetType);
const parsed = rows.map((r) => ({ id: r.id, ...JSON.parse(r.data), updated_at: r.updated_at }));
if (INGRESO_SHEETS.includes(sheetType)) {
const totals = sumIngresos(parsed);
return { rows: parsed, totals };
}
if (GASTO_SHEETS.includes(sheetType)) {
const total = sumGastos(parsed);
return { rows: parsed, totals: { totalGastos: total } };
}
if (sheetType === 'personal') {
const mensualizados = parsed.filter((r) => r.tipo === 'mensualizado');
const eventuales = parsed.filter((r) => r.tipo === 'eventual');
return {
mensualizados,
eventuales,
totals: { totalPersonal: sumPersonal(mensualizados, eventuales) },
};
}
return { rows: parsed };
}
router.get('/:sheetType', (req, res) => {
const { sheetType } = req.params;
if (!SHEETS.includes(sheetType) || sheetType === 'reportes') {
return res.status(400).json({ error: 'Planilla inválida' });
}
if (!checkPermission(req, sheetType)) {
return res.status(403).json({ error: 'Sin permiso para ver esta planilla' });
}
res.json(getSheetPayload(sheetType));
});
router.put('/:sheetType', (req, res) => {
const { sheetType } = req.params;
if (!SHEETS.includes(sheetType) || sheetType === 'reportes') {
return res.status(400).json({ error: 'Planilla inválida' });
}
if (!checkPermission(req, sheetType, true)) {
return res.status(403).json({ error: 'Sin permiso para editar' });
}
const { rows, mensualizados, eventuales } = req.body;
db.prepare('DELETE FROM registros WHERE sheet_type = ?').run(sheetType);
const insert = db.prepare(
'INSERT INTO registros (sheet_type, data, created_by) VALUES (?, ?, ?)'
);
if (sheetType === 'personal') {
const all = [
...(mensualizados || []).map((r) => ({ ...r, tipo: 'mensualizado' })),
...(eventuales || []).map((r) => ({ ...r, tipo: 'eventual' })),
];
for (const row of all) {
const { id: _id, updated_at: _u, ...data } = row;
insert.run(sheetType, JSON.stringify(data), req.user.id);
}
} else {
for (const row of rows || []) {
const { id: _id, updated_at: _u, ...data } = row;
insert.run(sheetType, JSON.stringify(data), req.user.id);
}
}
res.json(getSheetPayload(sheetType));
});
export default router;

View File

@@ -0,0 +1,96 @@
import { Router } from 'express';
import bcrypt from 'bcryptjs';
import { db, SHEETS } from '../db.js';
import { authRequired, adminRequired } from '../middleware/auth.js';
const router = Router();
router.use(authRequired, adminRequired);
router.get('/', (_req, res) => {
const users = db
.prepare('SELECT id, username, is_admin, active, created_at FROM users ORDER BY username')
.all();
const withPerms = users.map((u) => {
const permissions = db
.prepare('SELECT sheet_key, can_view, can_edit FROM permissions WHERE user_id = ?')
.all(u.id);
return {
...u,
isAdmin: !!u.is_admin,
active: !!u.active,
permissions: permissions.reduce((acc, p) => {
acc[p.sheet_key] = { view: !!p.can_view, edit: !!p.can_edit };
return acc;
}, {}),
};
});
res.json(withPerms);
});
router.post('/', (req, res) => {
const { username, password, permissions = {} } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Usuario y contraseña requeridos' });
}
const hash = bcrypt.hashSync(password, 10);
try {
const result = db
.prepare('INSERT INTO users (username, password_hash, is_admin, active) VALUES (?, ?, 0, 1)')
.run(username, hash);
const userId = result.lastInsertRowid;
const permStmt = db.prepare(
'INSERT INTO permissions (user_id, sheet_key, can_view, can_edit) VALUES (?, ?, ?, ?)'
);
for (const sheet of SHEETS) {
if (sheet === 'reportes') continue;
const p = permissions[sheet] || { view: false, edit: false };
permStmt.run(userId, sheet, p.view ? 1 : 0, p.edit ? 1 : 0);
}
permStmt.run(userId, 'reportes', 1, 0);
res.status(201).json({ id: userId, username });
} catch (e) {
if (e.code === 'SQLITE_CONSTRAINT_UNIQUE') {
return res.status(409).json({ error: 'El usuario ya existe' });
}
throw e;
}
});
router.patch('/:id', (req, res) => {
const id = Number(req.params.id);
const { password, active, permissions } = req.body;
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(id);
if (!user) return res.status(404).json({ error: 'Usuario no encontrado' });
if (user.is_admin && req.user.id !== id && active === 0) {
return res.status(400).json({ error: 'No se puede desactivar al administrador principal' });
}
if (password) {
db.prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(bcrypt.hashSync(password, 10), id);
}
if (active !== undefined) {
db.prepare('UPDATE users SET active = ? WHERE id = ?').run(active ? 1 : 0, id);
}
if (permissions) {
const upsert = db.prepare(`
INSERT INTO permissions (user_id, sheet_key, can_view, can_edit)
VALUES (?, ?, ?, ?)
ON CONFLICT(user_id, sheet_key) DO UPDATE SET can_view = excluded.can_view, can_edit = excluded.can_edit
`);
for (const [sheet, p] of Object.entries(permissions)) {
if (!SHEETS.includes(sheet)) continue;
upsert.run(id, sheet, p.view ? 1 : 0, p.edit ? 1 : 0);
}
}
res.json({ ok: true });
});
router.delete('/:id', (req, res) => {
const id = Number(req.params.id);
const user = db.prepare('SELECT is_admin FROM users WHERE id = ?').get(id);
if (!user) return res.status(404).json({ error: 'Usuario no encontrado' });
if (user.is_admin) return res.status(400).json({ error: 'No se puede eliminar al administrador' });
db.prepare('DELETE FROM users WHERE id = ?').run(id);
res.json({ ok: true });
});
export default router;