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:
@@ -71,8 +71,14 @@
|
|||||||
<div class="cal-heading">
|
<div class="cal-heading">
|
||||||
<h2 id="cal-title">Calendario</h2>
|
<h2 id="cal-title">Calendario</h2>
|
||||||
<span class="badge" id="cal-status"></span>
|
<span class="badge" id="cal-status"></span>
|
||||||
|
<button class="btn btn--ghost btn--small" id="cal-name-edit" title="Editar nombre de la ronda">✎ Nombre</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="cal-name-editor" class="cal-name-editor hidden">
|
||||||
|
<input id="cal-name-input" type="text" maxlength="80" placeholder="Nombre del local" />
|
||||||
|
<button class="btn btn--small" id="cal-name-save">Guardar</button>
|
||||||
|
<button class="btn btn--ghost btn--small" id="cal-name-cancel">Cancelar</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<section class="card cal-limits">
|
<section class="card cal-limits">
|
||||||
<h3>Límite de solicitudes por día y cargo</h3>
|
<h3>Límite de solicitudes por día y cargo</h3>
|
||||||
|
|||||||
@@ -309,6 +309,7 @@ input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(194, 86, 4
|
|||||||
.legend .sw--pending { background: var(--pending); border-color: #c98e1d; }
|
.legend .sw--pending { background: var(--pending); border-color: #c98e1d; }
|
||||||
.legend .sw--approved { background: var(--approved); border-color: var(--approved); }
|
.legend .sw--approved { background: var(--approved); border-color: var(--approved); }
|
||||||
.legend .sw--rejected { background: #f6e3df; border-color: #e2b7af; }
|
.legend .sw--rejected { background: #f6e3df; border-color: #e2b7af; }
|
||||||
|
.legend .sw--blocked { background: #d3c6a8; border-color: #bdae8c; }
|
||||||
|
|
||||||
/* ---------- calendario ---------- */
|
/* ---------- calendario ---------- */
|
||||||
|
|
||||||
@@ -412,6 +413,22 @@ input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(194, 86, 4
|
|||||||
color: #a99b7e;
|
color: #a99b7e;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
/* día bloqueado por el admin: no elegible por nadie */
|
||||||
|
.day.blocked {
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
-45deg, #e6dccb, #e6dccb 4px, #d3c6a8 4px, #d3c6a8 8px
|
||||||
|
);
|
||||||
|
color: #8f836a;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.day.blocked::after {
|
||||||
|
content: '🔒';
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 3px;
|
||||||
|
font-size: 9px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
.day .count.is-full { background: var(--rejected); }
|
.day .count.is-full { background: var(--rejected); }
|
||||||
|
|
||||||
.day .count {
|
.day .count {
|
||||||
@@ -777,6 +794,36 @@ body.cal-open .page--wide { max-width: 1080px; }
|
|||||||
}
|
}
|
||||||
.cday .rchip.is-full { background: var(--rejected); color: #fff; }
|
.cday .rchip.is-full { background: var(--rejected); color: #fff; }
|
||||||
|
|
||||||
|
/* días bloqueados por el admin (no elegibles por nadie) */
|
||||||
|
.cday--blocked {
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
-45deg, #efe6d2, #efe6d2 4px, #e6dabf 4px, #e6dabf 8px
|
||||||
|
);
|
||||||
|
color: #a99b7e;
|
||||||
|
border-color: #cdbf9d;
|
||||||
|
}
|
||||||
|
.cday--blocked::after,
|
||||||
|
.cday.is-blocked::after {
|
||||||
|
content: '🔒';
|
||||||
|
position: absolute;
|
||||||
|
top: 1px;
|
||||||
|
left: 3px;
|
||||||
|
font-size: 9px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* editor del nombre de la ronda */
|
||||||
|
.cal-name-editor {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin: 6px 0 4px;
|
||||||
|
}
|
||||||
|
.cal-name-editor input { width: min(320px, 100%); margin: 0; }
|
||||||
|
.cal-name-editor .btn { margin-top: 0; }
|
||||||
|
#cal-name-edit { margin-top: 0; }
|
||||||
|
|
||||||
/* barra de acción del admin (aprobar / rechazar / pendiente / quitar) */
|
/* barra de acción del admin (aprobar / rechazar / pendiente / quitar) */
|
||||||
.actionbar {
|
.actionbar {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
+65
-5
@@ -218,6 +218,7 @@
|
|||||||
$('cal-status').textContent = open ? 'Abierta' : 'Cerrada';
|
$('cal-status').textContent = open ? 'Abierta' : 'Cerrada';
|
||||||
$('cal-status').className = `badge ${open ? 'open' : 'closed'}`;
|
$('cal-status').className = `badge ${open ? 'open' : 'closed'}`;
|
||||||
showPanelView('calendar');
|
showPanelView('calendar');
|
||||||
|
closeNameEditor();
|
||||||
renderLimitsEditor();
|
renderLimitsEditor();
|
||||||
rerender();
|
rerender();
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
@@ -225,6 +226,42 @@
|
|||||||
|
|
||||||
$('cal-back').addEventListener('click', () => loadRounds());
|
$('cal-back').addEventListener('click', () => loadRounds());
|
||||||
|
|
||||||
|
// ----- editar el nombre de la ronda -----
|
||||||
|
|
||||||
|
function closeNameEditor() {
|
||||||
|
$('cal-name-editor').classList.add('hidden');
|
||||||
|
$('cal-name-edit').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
$('cal-name-edit').addEventListener('click', () => {
|
||||||
|
$('cal-name-input').value = cal.round.name;
|
||||||
|
$('cal-name-editor').classList.remove('hidden');
|
||||||
|
$('cal-name-edit').classList.add('hidden');
|
||||||
|
$('cal-name-input').focus();
|
||||||
|
$('cal-name-input').select();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('cal-name-cancel').addEventListener('click', closeNameEditor);
|
||||||
|
|
||||||
|
$('cal-name-save').addEventListener('click', async () => {
|
||||||
|
const name = $('cal-name-input').value.trim();
|
||||||
|
if (!name) { toast('Escribe un nombre', true); return; }
|
||||||
|
try {
|
||||||
|
const data = await fetchJSON(`/api/admin/rounds/${cal.round.id}/name`, { method: 'PUT', body: { name } });
|
||||||
|
cal.round.name = data.name;
|
||||||
|
$('cal-title').textContent = `${data.name} ${cal.round.year}`;
|
||||||
|
toast('✓ Nombre actualizado');
|
||||||
|
closeNameEditor();
|
||||||
|
} catch (ex) {
|
||||||
|
toast(ex.message, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('cal-name-input').addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter') { e.preventDefault(); $('cal-name-save').click(); }
|
||||||
|
if (e.key === 'Escape') { e.preventDefault(); closeNameEditor(); }
|
||||||
|
});
|
||||||
|
|
||||||
// ----- editor de límites -----
|
// ----- editor de límites -----
|
||||||
|
|
||||||
function renderLimitsEditor() {
|
function renderLimitsEditor() {
|
||||||
@@ -256,6 +293,26 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ----- bloquear / desbloquear días -----
|
||||||
|
|
||||||
|
// Alterna el bloqueo de un día para toda la ronda (envía la lista completa).
|
||||||
|
async function toggleBlocked(date) {
|
||||||
|
const current = new Set(cal.blocked || []);
|
||||||
|
const willBlock = !current.has(date);
|
||||||
|
if (willBlock) current.add(date); else current.delete(date);
|
||||||
|
try {
|
||||||
|
const data = await fetchJSON(`/api/admin/rounds/${cal.round.id}/blocked`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: { dates: [...current] },
|
||||||
|
});
|
||||||
|
cal.blocked = data.blocked;
|
||||||
|
toast(willBlock ? '🔒 Día bloqueado para todo el equipo' : 'Día desbloqueado');
|
||||||
|
rerender();
|
||||||
|
} catch (ex) {
|
||||||
|
toast(ex.message, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ----- datos derivados -----
|
// ----- datos derivados -----
|
||||||
|
|
||||||
// Ocupación por fecha y cargo: {date: {role: {approved, pending, rejected}}}.
|
// Ocupación por fecha y cargo: {date: {role: {approved, pending, rejected}}}.
|
||||||
@@ -345,14 +402,15 @@
|
|||||||
|
|
||||||
function renderGrid() {
|
function renderGrid() {
|
||||||
const occ = occupancy();
|
const occ = occupancy();
|
||||||
|
const blockedSet = new Set(cal.blocked || []);
|
||||||
const worker = workerById(selectedWorkerId);
|
const worker = workerById(selectedWorkerId);
|
||||||
const myReq = worker ? new Map(worker.requests.map((r) => [r.date, r.status])) : null;
|
const myReq = worker ? new Map(worker.requests.map((r) => [r.date, r.status])) : null;
|
||||||
|
|
||||||
const hint = document.createElement('p');
|
const hint = document.createElement('p');
|
||||||
hint.className = 'hint cal-mode-hint';
|
hint.className = 'hint cal-mode-hint';
|
||||||
hint.innerHTML = worker
|
hint.innerHTML = worker
|
||||||
? `Editando a <b>${esc(worker.name)}</b> · ${esc(cal.roles[worker.role] || worker.role)}. Toca un día para aprobarlo, rechazarlo, dejarlo pendiente o quitarlo.`
|
? `Editando a <b>${esc(worker.name)}</b> · ${esc(cal.roles[worker.role] || worker.role)}. Toca un día para aprobarlo, rechazarlo, dejarlo pendiente o quitarlo. Puedes asignar días aunque estén <b>completos o bloqueados</b>.`
|
||||||
: 'Vista de ocupación de todo el equipo. Elige un empleado en la izquierda para aprobar o editar sus días.';
|
: 'Vista de ocupación de todo el equipo. Toca un día para <b>bloquearlo o desbloquearlo</b> para todo el equipo (🔒 no elegible por nadie). Elige un empleado en la izquierda para aprobar o editar sus días.';
|
||||||
|
|
||||||
const grid = $('cal-grid');
|
const grid = $('cal-grid');
|
||||||
grid.innerHTML = '';
|
grid.innerHTML = '';
|
||||||
@@ -388,6 +446,7 @@
|
|||||||
if (date === focusedDate) cls += ' focused';
|
if (date === focusedDate) cls += ' focused';
|
||||||
btn.className = cls;
|
btn.className = cls;
|
||||||
btn.innerHTML = `<span class="n">${d}</span>`;
|
btn.innerHTML = `<span class="n">${d}</span>`;
|
||||||
|
if (blockedSet.has(date)) btn.classList.add('is-blocked');
|
||||||
const others = held(occ[date]?.[worker.role]) - (status && status !== 'rejected' ? 1 : 0);
|
const others = held(occ[date]?.[worker.role]) - (status && status !== 'rejected' ? 1 : 0);
|
||||||
const lim = cal.limits[worker.role];
|
const lim = cal.limits[worker.role];
|
||||||
if (others > 0) {
|
if (others > 0) {
|
||||||
@@ -398,9 +457,9 @@
|
|||||||
}
|
}
|
||||||
btn.addEventListener('click', () => focusDay(date));
|
btn.addEventListener('click', () => focusDay(date));
|
||||||
} else {
|
} else {
|
||||||
// Modo ocupación: chips por cargo con su carga.
|
// Modo ocupación: chips por cargo con su carga. Tocar bloquea/desbloquea
|
||||||
btn.className = 'cday cday--info';
|
// el día para todo el equipo.
|
||||||
btn.disabled = true;
|
btn.className = 'cday' + (blockedSet.has(date) ? ' cday--blocked' : '');
|
||||||
btn.innerHTML = `<span class="n">${d}</span>`;
|
btn.innerHTML = `<span class="n">${d}</span>`;
|
||||||
const cell = occ[date];
|
const cell = occ[date];
|
||||||
if (cell) {
|
if (cell) {
|
||||||
@@ -418,6 +477,7 @@
|
|||||||
}
|
}
|
||||||
if (chips.children.length) btn.appendChild(chips);
|
if (chips.children.length) btn.appendChild(chips);
|
||||||
}
|
}
|
||||||
|
btn.addEventListener('click', () => toggleBlocked(date));
|
||||||
}
|
}
|
||||||
days.appendChild(btn);
|
days.appendChild(btn);
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-2
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
let state = null; // respuesta del servidor
|
let state = null; // respuesta del servidor
|
||||||
let todayIso = ''; // hoy según el servidor (zona de España)
|
let todayIso = ''; // hoy según el servidor (zona de España)
|
||||||
|
let blockedSet = new Set(); // días bloqueados por el admin (no elegibles)
|
||||||
let myStatus = new Map(); // fecha → 'approved' | 'rejected' | 'pending' (guardado)
|
let myStatus = new Map(); // fecha → 'approved' | 'rejected' | 'pending' (guardado)
|
||||||
let savedPending = new Set();// pendientes ya guardadas en el servidor
|
let savedPending = new Set();// pendientes ya guardadas en el servidor
|
||||||
let pending = new Set(); // pendientes en edición (con cambios sin guardar)
|
let pending = new Set(); // pendientes en edición (con cambios sin guardar)
|
||||||
@@ -82,6 +83,7 @@
|
|||||||
const others = state.counts[date]?.[role] || 0;
|
const others = state.counts[date]?.[role] || 0;
|
||||||
const limit = state.limits[role];
|
const limit = state.limits[role];
|
||||||
const full = limit ? others >= limit : false;
|
const full = limit ? others >= limit : false;
|
||||||
|
const blocked = blockedSet.has(date);
|
||||||
const past = date <= todayIso;
|
const past = date <= todayIso;
|
||||||
const open = state.round.status === 'open';
|
const open = state.round.status === 'open';
|
||||||
|
|
||||||
@@ -104,6 +106,7 @@
|
|||||||
} else {
|
} else {
|
||||||
// Día libre para este trabajador.
|
// Día libre para este trabajador.
|
||||||
if (past) { cls += ' past'; disabled = true; }
|
if (past) { cls += ' past'; disabled = true; }
|
||||||
|
else if (blocked) { cls += ' blocked'; disabled = true; }
|
||||||
else if (full) { cls += ' full'; disabled = true; }
|
else if (full) { cls += ' full'; disabled = true; }
|
||||||
else clickable = !disabled;
|
else clickable = !disabled;
|
||||||
}
|
}
|
||||||
@@ -202,8 +205,8 @@
|
|||||||
toast('✓ Petición guardada');
|
toast('✓ Petición guardada');
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
toast(ex.message, true);
|
toast(ex.message, true);
|
||||||
// Si el servidor rechazó por días completos, recargamos para ver la realidad.
|
// Si el servidor rechazó por días completos o bloqueados, recargamos para ver la realidad.
|
||||||
if (/complet/i.test(ex.message)) load();
|
if (/complet|bloquead/i.test(ex.message)) load();
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
}
|
}
|
||||||
@@ -219,6 +222,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
todayIso = state.today || new Date().toISOString().slice(0, 10);
|
todayIso = state.today || new Date().toISOString().slice(0, 10);
|
||||||
|
blockedSet = new Set(state.blocked || []);
|
||||||
$('round-title').innerHTML =
|
$('round-title').innerHTML =
|
||||||
`${state.round.name} <em>${state.round.year}</em>`;
|
`${state.round.name} <em>${state.round.year}</em>`;
|
||||||
$('round-sub').textContent = state.me
|
$('round-sub').textContent = state.me
|
||||||
|
|||||||
+2
-1
@@ -49,13 +49,14 @@
|
|||||||
<p class="hint" id="calendar-hint">
|
<p class="hint" id="calendar-hint">
|
||||||
Toca los días que quieres pedir. El número <span class="chip">2</span> indica
|
Toca los días que quieres pedir. El número <span class="chip">2</span> indica
|
||||||
cuántas personas de tu mismo cargo ya han pedido ese día. Los días completos
|
cuántas personas de tu mismo cargo ya han pedido ese día. Los días completos
|
||||||
para tu cargo, pasados o de hoy no se pueden elegir.
|
para tu cargo, bloqueados (🔒), pasados o de hoy no se pueden elegir.
|
||||||
</p>
|
</p>
|
||||||
<div class="legend hidden" id="calendar-legend">
|
<div class="legend hidden" id="calendar-legend">
|
||||||
<span class="legend-item"><i class="sw sw--choosing"></i>Eligiendo</span>
|
<span class="legend-item"><i class="sw sw--choosing"></i>Eligiendo</span>
|
||||||
<span class="legend-item"><i class="sw sw--pending"></i>Pendiente</span>
|
<span class="legend-item"><i class="sw sw--pending"></i>Pendiente</span>
|
||||||
<span class="legend-item"><i class="sw sw--approved"></i>Aprobado</span>
|
<span class="legend-item"><i class="sw sw--approved"></i>Aprobado</span>
|
||||||
<span class="legend-item"><i class="sw sw--rejected"></i>Rechazado</span>
|
<span class="legend-item"><i class="sw sw--rejected"></i>Rechazado</span>
|
||||||
|
<span class="legend-item"><i class="sw sw--blocked"></i>Bloqueado</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="calendar"></div>
|
<div id="calendar"></div>
|
||||||
<footer class="colophon">Buen servicio · Buen descanso</footer>
|
<footer class="colophon">Buen servicio · Buen descanso</footer>
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ const hasColumn = (table, col) =>
|
|||||||
if (!hasColumn('rounds', 'roles')) db.exec('ALTER TABLE rounds ADD COLUMN roles TEXT');
|
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}).
|
// 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');
|
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.
|
// Estado de aprobación de cada solicitud: pending | approved | rejected.
|
||||||
if (!hasColumn('requests', 'status')) {
|
if (!hasColumn('requests', 'status')) {
|
||||||
db.exec("ALTER TABLE requests ADD COLUMN status TEXT NOT NULL DEFAULT 'pending'");
|
db.exec("ALTER TABLE requests ADD COLUMN status TEXT NOT NULL DEFAULT 'pending'");
|
||||||
@@ -107,6 +109,19 @@ function roundLimits(round) {
|
|||||||
return out;
|
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").
|
// Convierte una etiqueta ("Camarero/a") en una clave estable ("camarero_a").
|
||||||
const slugify = (s) =>
|
const slugify = (s) =>
|
||||||
String(s)
|
String(s)
|
||||||
@@ -248,6 +263,7 @@ app.get('/api/round/:token', (req, res) => {
|
|||||||
round: { name: round.name, year: round.year, status: round.status },
|
round: { name: round.name, year: round.year, status: round.status },
|
||||||
roles: roundRoles(round),
|
roles: roundRoles(round),
|
||||||
limits: roundLimits(round),
|
limits: roundLimits(round),
|
||||||
|
blocked: roundBlocked(round),
|
||||||
today: todayISO(),
|
today: todayISO(),
|
||||||
me: worker
|
me: worker
|
||||||
? { name: worker.name, role: worker.role, requests: workerRequests(worker.id) }
|
? { 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.' });
|
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
|
// 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).
|
// solicitudes de los demás que no estén rechazadas).
|
||||||
const limit = roundLimits(round)[worker.role];
|
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 });
|
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),
|
// Vista de calendario del administrador: empleados con sus solicitudes (y estado),
|
||||||
// más los cargos y límites de la ronda.
|
// más los cargos y límites de la ronda.
|
||||||
app.get('/api/admin/rounds/:id/calendar', requireAdmin, (req, res) => {
|
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 },
|
round: { id: round.id, name: round.name, year: round.year, status: round.status },
|
||||||
roles: roundRoles(round),
|
roles: roundRoles(round),
|
||||||
limits: roundLimits(round),
|
limits: roundLimits(round),
|
||||||
|
blocked: roundBlocked(round),
|
||||||
today: todayISO(),
|
today: todayISO(),
|
||||||
workers,
|
workers,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Aprobar / rechazar / volver a pendiente / añadir / quitar días de un empleado.
|
// 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) => {
|
app.post('/api/admin/rounds/:id/requests/set', requireAdmin, (req, res) => {
|
||||||
const round = db.prepare('SELECT * FROM rounds WHERE id = ?').get(req.params.id);
|
const round = db.prepare('SELECT * FROM rounds WHERE id = ?').get(req.params.id);
|
||||||
if (!round) return res.status(404).json({ error: 'Ronda no encontrada' });
|
if (!round) return res.status(404).json({ error: 'Ronda no encontrada' });
|
||||||
|
|||||||
Reference in New Issue
Block a user