Actualizar agenda.html
This commit is contained in:
199
agenda.html
199
agenda.html
@@ -10,6 +10,10 @@
|
||||
.fade-in { animation: fadeIn 0.3s ease-in-out; }
|
||||
.scroller::-webkit-scrollbar { width: 6px; }
|
||||
.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>
|
||||
</head>
|
||||
<body class="bg-slate-50 text-slate-800 font-sans h-screen overflow-hidden flex">
|
||||
@@ -27,20 +31,86 @@
|
||||
Control de Agenda
|
||||
</h2>
|
||||
|
||||
<div class="flex overflow-x-auto no-scrollbar gap-8">
|
||||
<button class="px-2 py-3 text-sm font-black text-blue-600 border-b-[3px] border-blue-600 whitespace-nowrap">
|
||||
<div class="flex overflow-x-auto no-scrollbar gap-8 border-b border-slate-200">
|
||||
<button onclick="switchTab('requests')" id="tab-requests" class="tab-btn tab-active px-2 py-3 text-sm whitespace-nowrap transition-colors">
|
||||
Solicitudes Pendientes
|
||||
</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 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>
|
||||
<p class="font-bold text-slate-400">Cargando solicitudes...</p>
|
||||
</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>
|
||||
</main>
|
||||
</div>
|
||||
@@ -85,9 +155,28 @@
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
if (!localStorage.getItem("token")) window.location.href = "index.html";
|
||||
// Pre-seleccionar pestaña 1
|
||||
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() {
|
||||
const container = document.getElementById('requestsContainer');
|
||||
try {
|
||||
@@ -150,13 +239,11 @@
|
||||
}).join('');
|
||||
|
||||
lucide.createIcons();
|
||||
|
||||
} catch (e) {
|
||||
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) {
|
||||
document.getElementById('aprvId').value = id;
|
||||
document.getElementById('aprvDateText').innerText = dateText;
|
||||
@@ -194,7 +281,7 @@
|
||||
|
||||
if (res.ok) {
|
||||
closeApproveModal();
|
||||
loadRequests(); // Recarga la lista
|
||||
loadRequests();
|
||||
} else {
|
||||
alert("Error al confirmar la cita");
|
||||
}
|
||||
@@ -207,20 +294,104 @@
|
||||
|
||||
async function rejectRequest(id) {
|
||||
if(!confirm("¿Rechazar esta solicitud? Se enviará un WhatsApp al cliente pidiéndole que elija otra hora.")) return;
|
||||
|
||||
try {
|
||||
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")}` }
|
||||
});
|
||||
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) {
|
||||
loadRequests();
|
||||
} else {
|
||||
alert("Error al rechazar");
|
||||
alert("Bloqueo guardado correctamente");
|
||||
loadActiveBlocks();
|
||||
}
|
||||
} 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>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user