Actualizar agenda.html

This commit is contained in:
2026-02-21 16:50:52 +00:00
parent 0414d8936e
commit b4c0270ded

View File

@@ -10,6 +10,10 @@
.fade-in { animation: fadeIn 0.3s ease-in-out; } .fade-in { animation: fadeIn 0.3s ease-in-out; }
.scroller::-webkit-scrollbar { width: 6px; } .scroller::-webkit-scrollbar { width: 6px; }
.scroller::-webkit-scrollbar-thumb { background-color: #cbd5e1; border-radius: 4px; } .scroller::-webkit-scrollbar-thumb { background-color: #cbd5e1; border-radius: 4px; }
/* Clases para tabs activas/inactivas */
.tab-active { color: #2563eb; border-bottom-width: 3px; border-color: #2563eb; font-weight: 900; }
.tab-inactive { color: #64748b; font-weight: 500; border-bottom-width: 3px; border-color: transparent; }
.tab-inactive:hover { color: #334155; }
</style> </style>
</head> </head>
<body class="bg-slate-50 text-slate-800 font-sans h-screen overflow-hidden flex"> <body class="bg-slate-50 text-slate-800 font-sans h-screen overflow-hidden flex">
@@ -27,20 +31,86 @@
Control de Agenda Control de Agenda
</h2> </h2>
<div class="flex overflow-x-auto no-scrollbar gap-8"> <div class="flex overflow-x-auto no-scrollbar gap-8 border-b border-slate-200">
<button class="px-2 py-3 text-sm font-black text-blue-600 border-b-[3px] border-blue-600 whitespace-nowrap"> <button onclick="switchTab('requests')" id="tab-requests" class="tab-btn tab-active px-2 py-3 text-sm whitespace-nowrap transition-colors">
Solicitudes Pendientes Solicitudes Pendientes
</button> </button>
</div> <button onclick="switchTab('blocks')" id="tab-blocks" class="tab-btn tab-inactive px-2 py-3 text-sm whitespace-nowrap transition-colors">
Bloqueos de Agenda
</button>
</div>
</div> </div>
<div class="flex-1 overflow-y-auto scroller p-6 bg-slate-50"> <div class="flex-1 overflow-y-auto scroller p-6 bg-slate-50">
<div class="max-w-5xl mx-auto space-y-4" id="requestsContainer">
<div class="text-center py-20"> <div id="view-requests" class="max-w-5xl mx-auto space-y-4 tab-view">
<div class="text-center py-20" id="requestsContainer">
<i data-lucide="loader-2" class="w-10 h-10 animate-spin text-slate-300 mx-auto mb-4"></i> <i data-lucide="loader-2" class="w-10 h-10 animate-spin text-slate-300 mx-auto mb-4"></i>
<p class="font-bold text-slate-400">Cargando solicitudes...</p> <p class="font-bold text-slate-400">Cargando solicitudes...</p>
</div> </div>
</div> </div>
<div id="view-blocks" class="max-w-5xl mx-auto space-y-6 tab-view hidden fade-in">
<div class="bg-white p-6 rounded-[2rem] border border-slate-200 shadow-sm flex flex-col md:flex-row gap-6 items-end">
<div class="flex-1 w-full space-y-4">
<div>
<label class="block text-xs font-black text-slate-500 uppercase tracking-widest mb-1.5">1. Selecciona Operario</label>
<select id="blockWorker" class="w-full bg-slate-50 border border-slate-200 text-sm font-bold text-slate-700 p-3 rounded-xl outline-none focus:ring-2 focus:ring-blue-500">
<option value="">Cargando operarios...</option>
</select>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-xs font-black text-slate-500 uppercase tracking-widest mb-1.5">2. Fecha</label>
<input type="date" id="blockDate" class="w-full bg-slate-50 border border-slate-200 text-sm font-bold text-slate-700 p-3 rounded-xl outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div class="grid grid-cols-2 gap-2">
<div>
<label class="block text-xs font-black text-slate-500 uppercase tracking-widest mb-1.5">Desde</label>
<select id="blockTimeStart" class="w-full bg-slate-50 border border-slate-200 text-sm font-bold text-slate-700 p-3 rounded-xl outline-none focus:ring-2 focus:ring-blue-500">
<option value="09:00">09:00</option><option value="10:00">10:00</option>
<option value="11:00">11:00</option><option value="12:00">12:00</option>
<option value="13:00">13:00</option><option value="16:00">16:00</option>
<option value="17:00">17:00</option><option value="18:00">18:00</option>
<option value="19:00">19:00</option>
</select>
</div>
<div>
<label class="block text-xs font-black text-slate-500 uppercase tracking-widest mb-1.5">Hasta</label>
<select id="blockTimeEnd" class="w-full bg-slate-50 border border-slate-200 text-sm font-bold text-slate-700 p-3 rounded-xl outline-none focus:ring-2 focus:ring-blue-500">
<option value="10:00">10:00</option><option value="11:00">11:00</option>
<option value="12:00">12:00</option><option value="13:00">13:00</option>
<option value="14:00">14:00</option><option value="17:00">17:00</option>
<option value="18:00">18:00</option><option value="19:00">19:00</option>
<option value="20:00">20:00</option>
</select>
</div>
</div>
</div>
</div>
<div class="flex-1 w-full space-y-4">
<div>
<label class="block text-xs font-black text-slate-500 uppercase tracking-widest mb-1.5">Motivo (Opcional)</label>
<input type="text" id="blockReason" placeholder="Ej: Médico, Asuntos propios, Vacaciones..." class="w-full bg-slate-50 border border-slate-200 text-sm font-bold text-slate-700 p-3 rounded-xl outline-none focus:ring-2 focus:ring-blue-500">
</div>
<button onclick="saveBlock()" class="w-full bg-slate-900 text-white font-black py-3 rounded-xl shadow-lg hover:bg-blue-600 transition-all uppercase tracking-widest text-xs flex items-center justify-center gap-2">
<i data-lucide="lock" class="w-4 h-4"></i> Generar Bloqueo
</button>
</div>
</div>
<h3 class="font-black text-slate-800 text-lg flex items-center gap-2 mt-8">
<i data-lucide="shield" class="w-5 h-5 text-rose-500"></i> Bloqueos Activos
</h3>
<div id="blocksList" class="space-y-3">
<p class="text-sm text-slate-400 font-medium">Cargando...</p>
</div>
</div>
</div> </div>
</main> </main>
</div> </div>
@@ -85,9 +155,28 @@
<script> <script>
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
if (!localStorage.getItem("token")) window.location.href = "index.html"; if (!localStorage.getItem("token")) window.location.href = "index.html";
// Pre-seleccionar pestaña 1
loadRequests(); loadRequests();
// Cargar datos para pestaña 2 en segundo plano
loadOperators();
loadActiveBlocks();
}); });
// --- SISTEMA DE PESTAÑAS ---
function switchTab(tabId) {
document.querySelectorAll('.tab-view').forEach(el => el.classList.add('hidden'));
document.getElementById(`view-${tabId}`).classList.remove('hidden');
document.querySelectorAll('.tab-btn').forEach(el => {
el.classList.remove('tab-active');
el.classList.add('tab-inactive');
});
document.getElementById(`tab-${tabId}`).classList.remove('tab-inactive');
document.getElementById(`tab-${tabId}`).classList.add('tab-active');
}
// --- LÓGICA DE SOLICITUDES ---
async function loadRequests() { async function loadRequests() {
const container = document.getElementById('requestsContainer'); const container = document.getElementById('requestsContainer');
try { try {
@@ -150,13 +239,11 @@
}).join(''); }).join('');
lucide.createIcons(); lucide.createIcons();
} catch (e) { } catch (e) {
container.innerHTML = '<p class="text-center text-red-500 font-bold">Error de conexión</p>'; container.innerHTML = '<p class="text-center text-red-500 font-bold">Error de conexión</p>';
} }
} }
// --- FUNCIONES DEL MODAL DE APROBACIÓN ---
function openApproveModal(id, dateText) { function openApproveModal(id, dateText) {
document.getElementById('aprvId').value = id; document.getElementById('aprvId').value = id;
document.getElementById('aprvDateText').innerText = dateText; document.getElementById('aprvDateText').innerText = dateText;
@@ -194,7 +281,7 @@
if (res.ok) { if (res.ok) {
closeApproveModal(); closeApproveModal();
loadRequests(); // Recarga la lista loadRequests();
} else { } else {
alert("Error al confirmar la cita"); alert("Error al confirmar la cita");
} }
@@ -207,20 +294,104 @@
async function rejectRequest(id) { async function rejectRequest(id) {
if(!confirm("¿Rechazar esta solicitud? Se enviará un WhatsApp al cliente pidiéndole que elija otra hora.")) return; if(!confirm("¿Rechazar esta solicitud? Se enviará un WhatsApp al cliente pidiéndole que elija otra hora.")) return;
try { try {
const res = await fetch(`${API_URL}/agenda/requests/${id}/reject`, { const res = await fetch(`${API_URL}/agenda/requests/${id}/reject`, {
method: 'POST', method: 'POST', headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` }
});
if (res.ok) loadRequests();
else alert("Error al rechazar");
} catch (e) { alert("Error de conexión"); }
}
// --- LÓGICA DE BLOQUEOS DE AGENDA ---
async function loadOperators() {
try {
const res = await fetch(`${API_URL}/operators`, {
headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` }
}); });
const data = await res.json();
if(data.ok) {
const sel = document.getElementById('blockWorker');
sel.innerHTML = '<option value="">Seleccione Operario...</option>' +
data.operators.map(o => `<option value="${o.id}">${o.full_name}</option>`).join('');
}
} catch (e) { console.error(e); }
}
async function saveBlock() {
const workerId = document.getElementById('blockWorker').value;
const date = document.getElementById('blockDate').value;
const timeStart = document.getElementById('blockTimeStart').value;
const timeEnd = document.getElementById('blockTimeEnd').value;
const reason = document.getElementById('blockReason').value || "Bloqueo Administrativo";
if(!workerId || !date) return alert("Falta operario o fecha");
// Calculamos duración en minutos
const startMins = parseInt(timeStart.split(':')[0]) * 60;
const endMins = parseInt(timeEnd.split(':')[0]) * 60;
const duration = endMins - startMins;
if(duration <= 0) return alert("La hora de fin debe ser mayor a la de inicio");
try {
const res = await fetch(`${API_URL}/agenda/blocks`, {
method: 'POST',
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` },
body: JSON.stringify({ worker_id: workerId, date: date, time: timeStart, duration: duration, reason: reason })
});
if (res.ok) { if (res.ok) {
loadRequests(); alert("Bloqueo guardado correctamente");
} else { loadActiveBlocks();
alert("Error al rechazar");
} }
} catch (e) { alert("Error de conexión"); } } catch(e) { alert("Error de conexión"); }
} }
async function loadActiveBlocks() {
const container = document.getElementById('blocksList');
try {
const res = await fetch(`${API_URL}/agenda/blocks`, {
headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` }
});
const data = await res.json();
if (!data.ok || data.blocks.length === 0) {
container.innerHTML = `<p class="text-sm text-slate-400 italic">No hay bloqueos activos a futuro.</p>`;
return;
}
container.innerHTML = data.blocks.map(b => {
return `
<div class="bg-white p-4 rounded-xl border border-rose-100 flex justify-between items-center shadow-sm">
<div>
<p class="font-black text-slate-800 flex items-center gap-2">
<i data-lucide="hard-hat" class="w-4 h-4 text-slate-400"></i> ${b.worker_name}
</p>
<p class="text-xs text-rose-600 font-bold mt-1">${b.date} | ${b.time} (${b.duration} min)</p>
<p class="text-[10px] text-slate-500 uppercase mt-0.5">${b.reason}</p>
</div>
<button onclick="deleteBlock(${b.id})" class="text-slate-300 hover:text-red-500 p-2 transition-colors">
<i data-lucide="trash-2" class="w-5 h-5"></i>
</button>
</div>
`;
}).join('');
lucide.createIcons();
} catch(e) { container.innerHTML = "Error"; }
}
async function deleteBlock(id) {
if(!confirm("¿Eliminar este bloqueo?")) return;
try {
const res = await fetch(`${API_URL}/agenda/blocks/${id}`, {
method: 'DELETE',
headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` }
});
if(res.ok) loadActiveBlocks();
} catch(e) { alert("Error"); }
}
</script> </script>
</body> </body>
</html> </html>