import { db } from '../db.js'; const LEGACY_KEY = 'fecha_cierre_gestion'; export function todayStr() { return new Date().toISOString().slice(0, 10); } function addDays(isoDate, days) { const d = new Date(isoDate + 'T12:00:00'); d.setDate(d.getDate() + days); return d.toISOString().slice(0, 10); } function etiquetaFromRange(inicio, fin) { const ref = fin || inicio; const [y, m] = ref.split('-'); const meses = [ 'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre', ]; return `${meses[Number(m) - 1]} ${y}`; } function mapGestion(row) { if (!row) return null; return { id: row.id, etiqueta: row.etiqueta, fechaInicio: row.fecha_inicio, fechaFin: row.fecha_fin, esActual: row.fecha_fin === null, cerradaAt: row.cerrada_at, }; } export function ensureGestionesMigrated() { const count = db.prepare('SELECT COUNT(*) AS n FROM gestiones').get().n; if (count > 0) return; const legacy = db.prepare('SELECT value FROM settings WHERE key = ?').get(LEGACY_KEY); if (legacy?.value) { const fin = legacy.value; const inicio = `${fin.slice(0, 7)}-01`; db.prepare( `INSERT INTO gestiones (etiqueta, fecha_inicio, fecha_fin, cerrada_at) VALUES (?, ?, ?, datetime('now'))` ).run(etiquetaFromRange(inicio, fin), inicio, fin); const nuevoInicio = addDays(fin, 1); db.prepare( `INSERT INTO gestiones (etiqueta, fecha_inicio, fecha_fin) VALUES (?, ?, NULL)` ).run('Gestión actual', nuevoInicio); db.prepare(`DELETE FROM settings WHERE key = ?`).run(LEGACY_KEY); return; } db.prepare( `INSERT INTO gestiones (etiqueta, fecha_inicio, fecha_fin) VALUES ('Gestión actual', '2020-01-01', NULL)` ).run(); } export function listGestiones() { ensureGestionesMigrated(); return db .prepare('SELECT * FROM gestiones ORDER BY fecha_inicio DESC') .all() .map(mapGestion); } export function getGestionActual() { ensureGestionesMigrated(); const row = db .prepare('SELECT * FROM gestiones WHERE fecha_fin IS NULL ORDER BY id DESC LIMIT 1') .get(); return mapGestion(row); } export function getGestionById(id) { ensureGestionesMigrated(); const row = db.prepare('SELECT * FROM gestiones WHERE id = ?').get(id); return mapGestion(row); } export function findGestionForFecha(fecha) { if (!fecha || String(fecha).trim() === '') return getGestionActual(); const f = String(fecha).slice(0, 10); const row = db .prepare( `SELECT * FROM gestiones WHERE fecha_inicio <= ? AND (fecha_fin IS NULL OR fecha_fin >= ?) ORDER BY id DESC LIMIT 1` ) .get(f, f); return mapGestion(row); } export function rowBelongsToGestion(row, gestion, fechaField = 'fecha') { if (!gestion) return true; const fecha = row[fechaField]; if (!fecha || String(fecha).trim() === '') { return gestion.esActual; } const f = String(fecha).slice(0, 10); if (f < gestion.fechaInicio) return false; if (gestion.fechaFin && f > gestion.fechaFin) return false; return true; } /** Personal mensualizado: por gestion_id en el registro */ export function mensualBelongsToGestion(row, gestion) { if (!gestion) return true; if (gestion.esActual) { return !row.gestionId || row.gestionId === gestion.id; } return row.gestionId === gestion.id; } export function filterRowsByGestion(rows, gestion, fechaField = 'fecha') { if (!gestion) return rows; return rows.filter((r) => rowBelongsToGestion(r, gestion, fechaField)); } export function filterPersonalByGestion(mensualizados, eventuales, gestion) { if (!gestion) return { mensualizados, eventuales }; return { mensualizados: mensualizados.filter((r) => mensualBelongsToGestion(r, gestion)), eventuales: eventuales.filter((r) => rowBelongsToGestion(r, gestion, 'fecha')), }; } export function isFechaEnPeriodoCerrado(fecha) { const g = findGestionForFecha(fecha); return g ? !g.esActual : false; } export function isCierreActivo() { return db.prepare('SELECT COUNT(*) AS n FROM gestiones WHERE fecha_fin IS NOT NULL').get().n > 0 || !!getGestionActual()?.fechaInicio; } export function getGestionStatus() { ensureGestionesMigrated(); const actual = getGestionActual(); const historial = listGestiones().filter((g) => !g.esActual); const ultimaCerrada = historial[0]; return { gestionActual: actual, historial, fechaCierreGestion: ultimaCerrada?.fechaFin || null, gestionCerrada: historial.length > 0, hoy: todayStr(), }; } export function cerrarGestionActual(fechaFin, userId) { ensureGestionesMigrated(); const actual = db .prepare('SELECT * FROM gestiones WHERE fecha_fin IS NULL ORDER BY id DESC LIMIT 1') .get(); if (!actual) throw new Error('No hay gestión actual abierta'); if (fechaFin < actual.fecha_inicio) { throw new Error('La fecha de cierre no puede ser anterior al inicio de la gestión actual'); } const etiqueta = etiquetaFromRange(actual.fecha_inicio, fechaFin); db.prepare( `UPDATE gestiones SET etiqueta = ?, fecha_fin = ?, cerrada_at = datetime('now'), cerrada_por = ? WHERE id = ?` ).run(etiqueta, fechaFin, userId, actual.id); const personalRows = db.prepare("SELECT id, data FROM registros WHERE sheet_type = 'personal'").all(); for (const r of personalRows) { const data = JSON.parse(r.data); if (data.tipo === 'mensualizado' && !data.gestionId) { data.gestionId = actual.id; db.prepare('UPDATE registros SET data = ? WHERE id = ?').run(JSON.stringify(data), r.id); } } const nuevoInicio = addDays(fechaFin, 1); const result = db .prepare( `INSERT INTO gestiones (etiqueta, fecha_inicio, fecha_fin) VALUES ('Gestión actual', ?, NULL)` ) .run(nuevoInicio); db.prepare(`DELETE FROM settings WHERE key = ?`).run(LEGACY_KEY); return { cerrada: mapGestion({ ...actual, etiqueta, fecha_fin: fechaFin }), nueva: mapGestion({ id: result.lastInsertRowid, etiqueta: 'Gestión actual', fecha_inicio: nuevoInicio, fecha_fin: null, cerrada_at: null, cerrada_por: null, }), }; } function stripMeta(row) { const { id, updated_at, bloqueada, gestionId: _g, ...rest } = row; return rest; } export function mergeRowsForSave(existing, incoming, isAdmin, gestionActual, fechaField = 'fecha') { const gid = gestionActual?.id; const historial = existing.filter((r) => { const f = r[fechaField]; if (!f || String(f).trim() === '') return false; return !rowBelongsToGestion(r, gestionActual, fechaField); }); let actuales; if (isAdmin) { actuales = (incoming || []).map((r) => ({ ...stripMeta(r), gestionId: gid })); } else { const locked = existing.filter( (r) => rowBelongsToGestion(r, gestionActual, fechaField) && isFechaEnPeriodoCerrado(r[fechaField]) ); const unlockedIncoming = (incoming || []).filter((r) => !isFechaEnPeriodoCerrado(r[fechaField])); actuales = [ ...locked.map(stripMeta), ...unlockedIncoming.map((r) => ({ ...stripMeta(r), gestionId: gid })), ]; } return [...historial.map(stripMeta), ...actuales]; } export function mergePersonalForSave(existing, incomingMensual, incomingEventual, isAdmin, gestionActual) { const gid = gestionActual?.id; const existingM = existing.filter((r) => r.tipo === 'mensualizado'); const existingE = existing.filter((r) => r.tipo === 'eventual'); const historialM = existingM.filter((r) => r.gestionId && r.gestionId !== gid); const historialE = existingE.filter((r) => !rowBelongsToGestion(r, gestionActual, 'fecha')); let actualesM; let actualesE; if (isAdmin) { actualesM = (incomingMensual || []).map((r) => ({ ...stripMeta(r), gestionId: r.gestionId || gid })); actualesE = (incomingEventual || []).map((r) => ({ ...stripMeta(r), gestionId: gid })); } else { const unlockedM = (incomingMensual || []).filter((r) => !r.gestionId || r.gestionId === gid); const lockedE = existingE.filter( (r) => rowBelongsToGestion(r, gestionActual, 'fecha') && isFechaEnPeriodoCerrado(r.fecha) ); const unlockedE = (incomingEventual || []).filter((r) => !isFechaEnPeriodoCerrado(r.fecha)); actualesM = unlockedM.map((r) => ({ ...stripMeta(r), gestionId: gid })); actualesE = [ ...lockedE.map(stripMeta), ...unlockedE.map((r) => ({ ...stripMeta(r), gestionId: gid })), ]; } return { mensualizados: [...historialM.map(stripMeta), ...actualesM], eventuales: [...historialE.map(stripMeta), ...actualesE], }; } /** Etiqueta sugerida: último día del mes actual */ export function sugerirCierreMensual() { const today = new Date(); const last = new Date(today.getFullYear(), today.getMonth() + 1, 0); return last.toISOString().slice(0, 10); }