Files
web/agenda.html
2026-02-22 22:02:14 +00:00

474 lines
28 KiB
HTML

<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Agenda - IntegraRepara</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<style>
.fade-in { animation: fadeIn 0.3s ease-in-out; }
.scroller::-webkit-scrollbar { width: 6px; }
.scroller::-webkit-scrollbar-thumb { background-color: #cbd5e1; border-radius: 4px; }
.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">
<div id="sidebar-container" class="h-full shrink-0"></div>
<div class="flex-1 flex flex-col h-full relative min-w-0">
<div id="header-container"></div>
<main class="flex-1 flex flex-col overflow-hidden relative">
<div class="bg-white p-6 pb-0 z-20 shrink-0 border-b border-slate-100">
<h2 class="text-2xl font-black text-slate-800 mb-6 flex items-center gap-3">
<span class="bg-blue-100 p-2.5 rounded-xl text-blue-600 shadow-sm"><i data-lucide="calendar"></i></span>
Control de Agenda
</h2>
<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>
<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 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-start md:items-end">
<div class="flex-1 w-full space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-xs font-black text-slate-500 uppercase tracking-widest mb-1.5">1. 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>
<label class="block text-xs font-black text-slate-500 uppercase tracking-widest mb-1.5">2. Gremio (Opcional)</label>
<select id="blockGuild" 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="">Bloqueo Total (Todos los gremios)</option>
</select>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-xs font-black text-slate-500 uppercase tracking-widest mb-1.5">3. 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, Curso, Saturación..." 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>
<div class="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm flex flex-col md:flex-row items-center justify-between mt-8 gap-4">
<div class="flex items-center gap-3 w-full md:w-auto">
<div class="bg-blue-50 p-2.5 rounded-xl text-blue-600 shrink-0">
<i data-lucide="calendar-search" class="w-5 h-5"></i>
</div>
<div>
<h3 class="text-sm font-black text-slate-800 uppercase tracking-widest leading-none">Filtrar por Mes</h3>
<p class="text-[10px] text-slate-500 font-medium mt-1">Busca bloqueos pasados o futuros</p>
</div>
</div>
<div class="flex items-center gap-3 w-full md:w-auto">
<input type="month" id="blockMonthFilter" onchange="loadActiveBlocks()" class="w-full md:w-auto 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 cursor-pointer transition-all">
<button onclick="document.getElementById('blockMonthFilter').value=''; loadActiveBlocks();" class="text-xs font-bold text-slate-400 hover:text-red-500 transition-colors shrink-0">Ver Todos</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 a Futuro
</h3>
<div id="blocksList" class="space-y-3 mt-4">
<p class="text-sm text-slate-400 font-medium">Cargando...</p>
</div>
<h3 class="font-black text-slate-800 text-lg flex items-center gap-2 mt-12 opacity-60 border-t border-slate-200 pt-6">
<i data-lucide="history" class="w-5 h-5 text-slate-400"></i> Historial Pasado
</h3>
<div id="pastBlocksList" class="space-y-3 mt-4 opacity-80 grayscale-[20%]">
<p class="text-sm text-slate-400 font-medium">Cargando...</p>
</div>
</div>
</div>
</main>
</div>
<div id="approveModal" class="fixed inset-0 bg-slate-900/60 hidden z-[100] flex items-center justify-center backdrop-blur-sm p-4">
<div class="bg-white rounded-[2rem] shadow-2xl w-full max-w-md overflow-hidden fade-in">
<div class="p-6 border-b border-slate-100 bg-slate-50 flex justify-between items-center">
<h3 class="font-black text-slate-800 text-lg">Confirmar Cita</h3>
<button onclick="closeApproveModal()" class="text-slate-400 hover:text-red-500"><i data-lucide="x"></i></button>
</div>
<div class="p-6 space-y-6">
<input type="hidden" id="aprvId">
<div class="bg-blue-50 p-4 rounded-2xl border border-blue-100 text-blue-800">
<p class="text-xs font-bold uppercase tracking-widest text-blue-500 mb-1">Cita Solicitada:</p>
<p class="font-black text-lg" id="aprvDateText"></p>
</div>
<div>
<label class="block text-xs font-black text-slate-600 uppercase tracking-widest mb-3">Duración estimada del trabajo</label>
<div class="relative">
<select id="aprvDuration" 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 appearance-none pr-8 cursor-pointer">
<option value="15">15 minutos</option>
<option value="30">30 minutos</option>
<option value="45">45 minutos</option>
<option value="60" selected>1 hora</option>
<option value="75">1 hora 15 min</option>
<option value="90">1 hora 30 min</option>
<option value="105">1 hora 45 min</option>
<option value="120">2 horas</option>
<option value="150">2 horas 30 min</option>
<option value="180">3 horas</option>
<option value="210">3 horas 30 min</option>
<option value="240">4 horas (Máx)</option>
</select>
<i data-lucide="clock" class="w-4 h-4 absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none"></i>
</div>
</div>
<button onclick="submitApproval()" id="btnSubmitAprv" class="w-full bg-slate-900 text-white font-black py-4 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="check-circle" class="w-4 h-4"></i> Bloquear Calendario
</button>
</div>
</div>
</div>
<script src="js/layout.js"></script>
<script>
document.addEventListener("DOMContentLoaded", () => {
if (!localStorage.getItem("token")) window.location.href = "index.html";
loadRequests();
loadOperatorsAndGuilds();
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 {
const res = await fetch(`${API_URL}/agenda/requests`, {
headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` }
});
const data = await res.json();
if (!data.ok || data.requests.length === 0) {
container.innerHTML = `
<div class="text-center py-20 bg-white rounded-[2rem] border border-slate-100 shadow-sm">
<div class="w-20 h-20 bg-slate-50 text-slate-300 rounded-full flex items-center justify-center mx-auto mb-4"><i data-lucide="calendar-check" class="w-10 h-10"></i></div>
<h3 class="font-black text-slate-800 text-xl">Agenda al Día</h3>
<p class="text-slate-500 font-medium">No tienes solicitudes pendientes de confirmar.</p>
</div>
`;
lucide.createIcons();
return;
}
container.innerHTML = data.requests.map(r => {
const raw = r.raw_data;
const dateParts = raw.requested_date.split('-');
const niceDate = `${dateParts[2]}/${dateParts[1]}/${dateParts[0]}`;
return `
<div class="bg-white p-6 rounded-[2rem] border border-slate-200 shadow-sm hover:shadow-md transition-shadow flex flex-col md:flex-row gap-6 justify-between items-start md:items-center">
<div class="flex items-start gap-4">
<div class="bg-amber-100 text-amber-600 p-4 rounded-2xl shrink-0">
<i data-lucide="calendar-clock" class="w-8 h-8"></i>
</div>
<div>
<div class="flex items-center gap-2 mb-1">
<span class="bg-slate-100 text-slate-600 text-[10px] font-black uppercase px-2 py-1 rounded-md">#${r.service_ref}</span>
<span class="bg-blue-50 text-blue-600 text-[10px] font-black uppercase px-2 py-1 rounded-md flex items-center gap-1"><i data-lucide="hard-hat" class="w-3 h-3"></i> ${r.assigned_name}</span>
</div>
<h3 class="font-black text-slate-800 text-lg uppercase">${raw["Nombre Cliente"] || "Cliente"}</h3>
<p class="text-sm font-medium text-slate-500 mt-1 flex items-center gap-1.5">
<i data-lucide="map-pin" class="w-4 h-4 text-slate-400"></i> ${raw["Población"] || raw["POBLACION-PROVINCIA"] || ""} - ${raw["Dirección"]}
</p>
</div>
</div>
<div class="flex flex-col items-end gap-4 w-full md:w-auto">
<div class="text-right">
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Fecha Propuesta</p>
<p class="font-black text-xl text-slate-800">${niceDate} <span class="text-blue-600 ml-2">${raw.requested_time}</span></p>
</div>
<div class="flex gap-2 w-full md:w-auto">
<button onclick="rejectRequest(${r.id})" class="flex-1 md:flex-none bg-white border border-rose-200 text-rose-600 hover:bg-rose-50 font-black px-5 py-2.5 rounded-xl transition-all shadow-sm">
Rechazar
</button>
<button onclick="openApproveModal(${r.id}, '${niceDate} a las ${raw.requested_time}')" class="flex-1 md:flex-none bg-emerald-500 hover:bg-emerald-600 text-white font-black px-6 py-2.5 rounded-xl transition-all shadow-md shadow-emerald-200">
Aprobar
</button>
</div>
</div>
</div>
`;
}).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;
document.getElementById('aprvDuration').value = "60"; // Reset al valor por defecto
document.getElementById('approveModal').classList.remove('hidden');
}
function closeApproveModal() {
document.getElementById('approveModal').classList.add('hidden');
}
async function submitApproval() {
const id = document.getElementById('aprvId').value;
const duration = document.getElementById('aprvDuration').value;
const btn = document.getElementById('btnSubmitAprv');
btn.innerHTML = '<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i> Confirmando...';
lucide.createIcons();
try {
const res = await fetch(`${API_URL}/agenda/requests/${id}/approve`, {
method: 'POST',
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` },
body: JSON.stringify({ duration: parseInt(duration) })
});
if (res.ok) {
closeApproveModal();
loadRequests();
} else {
alert("Error al confirmar la cita");
}
} catch (e) { alert("Error de conexión"); }
finally {
btn.innerHTML = '<i data-lucide="check-circle" class="w-4 h-4"></i> Bloquear Calendario';
lucide.createIcons();
}
}
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', 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 Y GREMIOS ---
async function loadOperatorsAndGuilds() {
try {
// Cargar Operarios
const resOp = await fetch(`${API_URL}/operators`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
const dataOp = await resOp.json();
if(dataOp.ok) {
document.getElementById('blockWorker').innerHTML = '<option value="">Seleccione Operario...</option>' +
dataOp.operators.map(o => `<option value="${o.id}">${o.full_name}</option>`).join('');
}
// Cargar Gremios
const resG = await fetch(`${API_URL}/guilds`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
const dataG = await resG.json();
if(dataG.ok) {
document.getElementById('blockGuild').innerHTML = '<option value="">Bloqueo Total (Todos los gremios)</option>' +
dataG.guilds.map(g => `<option value="${g.id}">${g.name}</option>`).join('');
}
} catch (e) { console.error(e); }
}
async function saveBlock() {
const workerId = document.getElementById('blockWorker').value;
const guildSelect = document.getElementById('blockGuild');
const guildId = guildSelect.value;
const guildName = guildId ? guildSelect.options[guildSelect.selectedIndex].text : null;
const date = document.getElementById('blockDate').value;
const timeStart = document.getElementById('blockTimeStart').value;
const timeEnd = document.getElementById('blockTimeEnd').value;
const reason = document.getElementById('blockReason').value || (guildId ? `Bloqueo de ${guildName}` : "Bloqueo Total");
if(!workerId || !date) return alert("Falta operario o fecha");
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, guild_id: guildId, guild_name: guildName })
});
if (res.ok) {
alert("Bloqueo guardado correctamente");
loadActiveBlocks();
}
} catch(e) { alert("Error de conexión"); }
}
async function loadActiveBlocks() {
const activeContainer = document.getElementById('blocksList');
const pastContainer = document.getElementById('pastBlocksList');
// Leemos el valor del filtro de mes (Ej: "2026-02")
const monthFilter = document.getElementById('blockMonthFilter').value;
try {
const res = await fetch(`${API_URL}/agenda/blocks`, {
headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` }
});
const data = await res.json();
if (!data.ok) throw new Error("Error fetching blocks");
let allBlocks = data.blocks;
// APLICAMOS EL FILTRO POR MES SI HAY ALGUNO SELECCIONADO
if (monthFilter) {
// Nos quedamos solo con los bloqueos cuya fecha empiece por "YYYY-MM"
allBlocks = allBlocks.filter(b => b.date.startsWith(monthFilter));
}
// Sacamos la fecha de hoy (YYYY-MM-DD) para comparar
const todayStr = new Date().toISOString().split('T')[0];
const activeBlocks = allBlocks.filter(b => b.date >= todayStr).reverse();
const pastBlocks = allBlocks.filter(b => b.date < todayStr);
// Pintar Activos
if (activeBlocks.length === 0) {
activeContainer.innerHTML = `<p class="text-sm text-slate-400 italic bg-white p-4 rounded-xl border border-slate-200 text-center shadow-sm">No hay bloqueos activos en este periodo.</p>`;
} else {
activeContainer.innerHTML = activeBlocks.map(b => buildBlockHtml(b, false)).join('');
}
// Pintar Caducados
if (pastBlocks.length === 0) {
pastContainer.innerHTML = `<p class="text-sm text-slate-400 italic bg-transparent p-4 text-center">No hay bloqueos antiguos en este periodo.</p>`;
} else {
pastContainer.innerHTML = pastBlocks.map(b => buildBlockHtml(b, true)).join('');
}
lucide.createIcons();
} catch(e) {
activeContainer.innerHTML = "Error cargando";
pastContainer.innerHTML = "Error cargando";
}
}
// Helper visual para pintar la tarjeta según sea activa o caducada
function buildBlockHtml(b, isPast) {
const badge = b.guild_name
? `<span class="bg-indigo-100 text-indigo-700 px-2 py-0.5 rounded text-[9px] uppercase tracking-widest font-black">Solo ${b.guild_name}</span>`
: `<span class="bg-rose-100 text-rose-700 px-2 py-0.5 rounded text-[9px] uppercase tracking-widest font-black">Bloqueo Total</span>`;
return `
<div class="bg-white p-4 rounded-xl border ${isPast ? 'border-slate-100 shadow-none' : 'border-slate-200 shadow-sm'} flex justify-between items-center transition-all hover:border-red-200">
<div>
<div class="flex items-center gap-2">
<p class="font-black ${isPast ? 'text-slate-500' : 'text-slate-800'} flex items-center gap-1.5">
<i data-lucide="hard-hat" class="w-4 h-4 ${isPast ? 'text-slate-300' : 'text-slate-400'}"></i> ${b.worker_name}
</p>
${badge}
</div>
<p class="text-xs ${isPast ? 'text-slate-400' : 'text-slate-600'} font-bold mt-1.5">${b.date} | ${b.time} (${b.duration} min)</p>
<p class="text-[10px] text-slate-400 uppercase mt-0.5">${b.reason || 'Sin motivo especificado'}</p>
</div>
<button onclick="deleteBlock(${b.id})" class="text-slate-300 hover:text-red-500 bg-slate-50 hover:bg-red-50 p-2.5 rounded-lg transition-colors">
<i data-lucide="trash-2" class="w-4 h-4"></i>
</button>
</div>
`;
}
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>