Bloqueo de días, override del admin y edición del nombre de ronda

- Admin puede bloquear/desbloquear días (rounds.blocked) desde la vista de
  ocupación; los trabajadores no pueden elegirlos (🔒).
- El admin puede asignar cualquier día aunque supere el límite por día/cargo
  o esté bloqueado (override explícito en requests/set).
- Editar el nombre de la ronda con edición inline en el calendario
  (PUT /api/admin/rounds/:id/name).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-26 23:21:20 +02:00
parent 9c9d2d25f9
commit bc4e232a80
6 changed files with 177 additions and 8 deletions
+51
View File
@@ -56,6 +56,8 @@ const hasColumn = (table, col) =>
if (!hasColumn('rounds', 'roles')) db.exec('ALTER TABLE rounds ADD COLUMN roles TEXT');
// Límite de solicitudes por día y cargo (JSON {clave: máximo}).
if (!hasColumn('rounds', 'limits')) db.exec('ALTER TABLE rounds ADD COLUMN limits TEXT');
// Días bloqueados por el admin para toda la ronda (JSON array de fechas).
if (!hasColumn('rounds', 'blocked')) db.exec('ALTER TABLE rounds ADD COLUMN blocked TEXT');
// Estado de aprobación de cada solicitud: pending | approved | rejected.
if (!hasColumn('requests', 'status')) {
db.exec("ALTER TABLE requests ADD COLUMN status TEXT NOT NULL DEFAULT 'pending'");
@@ -107,6 +109,19 @@ function roundLimits(round) {
return out;
}
// Días bloqueados por el admin (no elegibles por ningún trabajador). Devuelve
// una lista de fechas válidas del año de la ronda, sin duplicados y ordenadas.
function roundBlocked(round) {
if (!round.blocked) return [];
try {
const parsed = JSON.parse(round.blocked);
if (Array.isArray(parsed)) {
return [...new Set(parsed.map(String).filter((d) => isValidDate(d, round.year)))].sort();
}
} catch { /* sin bloqueos */ }
return [];
}
// Convierte una etiqueta ("Camarero/a") en una clave estable ("camarero_a").
const slugify = (s) =>
String(s)
@@ -248,6 +263,7 @@ app.get('/api/round/:token', (req, res) => {
round: { name: round.name, year: round.year, status: round.status },
roles: roundRoles(round),
limits: roundLimits(round),
blocked: roundBlocked(round),
today: todayISO(),
me: worker
? { name: worker.name, role: worker.role, requests: workerRequests(worker.id) }
@@ -309,6 +325,14 @@ app.put('/api/round/:token/requests', (req, res) => {
return res.status(400).json({ error: 'No puedes elegir días pasados ni el día de hoy.' });
}
// Los días nuevos no pueden estar bloqueados por la administración.
const blocked = new Set(roundBlocked(round));
if (added.some((d) => blocked.has(d))) {
return res.status(409).json({
error: 'Algún día que elegiste está bloqueado por la administración. Recarga la página.',
});
}
// Los días nuevos no pueden superar el límite de tu cargo (cuentan las
// solicitudes de los demás que no estén rechazadas).
const limit = roundLimits(round)[worker.role];
@@ -422,6 +446,30 @@ app.put('/api/admin/rounds/:id/limits', requireAdmin, (req, res) => {
res.json({ ok: true, limits });
});
// Editar el nombre de una ronda (igual que se editan los límites).
app.put('/api/admin/rounds/:id/name', requireAdmin, (req, res) => {
const name = String(req.body.name ?? '').trim();
if (!name || name.length > 80) return res.status(400).json({ error: 'Indica el nombre del local' });
const info = db.prepare('UPDATE rounds SET name = ? WHERE id = ?').run(name, req.params.id);
if (!info.changes) return res.status(404).json({ error: 'Ronda no encontrada' });
res.json({ ok: true, name });
});
// Bloquear / desbloquear días de una ronda. Los días bloqueados no son elegibles
// por ningún trabajador, pero el admin sí puede asignarlos manualmente. Recibe la
// lista completa de días bloqueados y la guarda (reemplazo total).
app.put('/api/admin/rounds/:id/blocked', requireAdmin, (req, res) => {
const round = db.prepare('SELECT * FROM rounds WHERE id = ?').get(req.params.id);
if (!round) return res.status(404).json({ error: 'Ronda no encontrada' });
const dates = req.body.dates;
if (!Array.isArray(dates) || dates.length > 366) {
return res.status(400).json({ error: 'Días no válidos' });
}
const blocked = [...new Set(dates.map(String))].filter((d) => isValidDate(d, round.year)).sort();
db.prepare('UPDATE rounds SET blocked = ? WHERE id = ?').run(JSON.stringify(blocked), round.id);
res.json({ ok: true, blocked });
});
// Vista de calendario del administrador: empleados con sus solicitudes (y estado),
// más los cargos y límites de la ronda.
app.get('/api/admin/rounds/:id/calendar', requireAdmin, (req, res) => {
@@ -435,12 +483,15 @@ app.get('/api/admin/rounds/:id/calendar', requireAdmin, (req, res) => {
round: { id: round.id, name: round.name, year: round.year, status: round.status },
roles: roundRoles(round),
limits: roundLimits(round),
blocked: roundBlocked(round),
today: todayISO(),
workers,
});
});
// Aprobar / rechazar / volver a pendiente / añadir / quitar días de un empleado.
// El admin manda: no se aplica el límite por día/cargo ni el bloqueo de días,
// así que puede asignar cualquier día aunque esté completo o bloqueado.
app.post('/api/admin/rounds/:id/requests/set', requireAdmin, (req, res) => {
const round = db.prepare('SELECT * FROM rounds WHERE id = ?').get(req.params.id);
if (!round) return res.status(404).json({ error: 'Ronda no encontrada' });