537 lines
30 KiB
HTML
537 lines
30 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Calendario Inteligente - IntegraReparaPro</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script src="https://unpkg.com/lucide@latest"></script>
|
|
|
|
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.10/index.global.min.js'></script>
|
|
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.10/locales/es.global.min.js'></script>
|
|
|
|
<style>
|
|
.fade-in { animation: fadeIn 0.3s ease-in-out; }
|
|
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
|
.no-scrollbar::-webkit-scrollbar { display: none; }
|
|
|
|
/* CUSTOMIZACIÓN PREMIUM DEL CALENDARIO */
|
|
.fc { font-family: inherit; }
|
|
.fc-toolbar-title { font-weight: 900 !important; font-size: 1.25rem !important; text-transform: uppercase; color: #1e293b; letter-spacing: -0.025em; }
|
|
|
|
.fc-button-primary {
|
|
background-color: #f1f5f9 !important;
|
|
color: #475569 !important;
|
|
border: none !important;
|
|
border-radius: 1rem !important;
|
|
font-weight: 800 !important;
|
|
text-transform: uppercase !important;
|
|
font-size: 0.7rem !important;
|
|
letter-spacing: 0.05em;
|
|
padding: 0.6rem 1.2rem !important;
|
|
transition: all 0.2s !important;
|
|
margin-left: 0.25rem !important;
|
|
}
|
|
.fc-button-primary:hover { background-color: #e2e8f0 !important; color: #0f172a !important; }
|
|
.fc-button-active { background-color: #2563eb !important; color: #ffffff !important; box-shadow: 0 10px 15px -3px rgba(37,99,235,0.3) !important; }
|
|
.fc-button-primary:focus { box-shadow: none !important; }
|
|
|
|
.fc-theme-standard td, .fc-theme-standard th { border-color: #f1f5f9 !important; }
|
|
.fc-col-header-cell-cushion { padding: 12px 0 !important; font-size: 0.75rem; text-transform: uppercase; font-weight: 900; color: #64748b; }
|
|
.fc-timegrid-slot-label-cushion { font-size: 0.7rem; font-weight: 800; color: #94a3b8; }
|
|
|
|
/* CORRECCIÓN: Forzamos fondo transparente en la caja del evento para que mande nuestro HTML interno */
|
|
.fc-event {
|
|
border: none !important;
|
|
border-radius: 0.5rem !important;
|
|
background-color: transparent !important;
|
|
box-shadow: none !important;
|
|
transition: transform 0.2s;
|
|
cursor: pointer;
|
|
}
|
|
.fc-event:hover { transform: scale(1.02); z-index: 50 !important; }
|
|
.fc-event-main { padding: 0 !important; border-radius: 0.5rem !important; }
|
|
.fc-v-event .fc-event-main-frame { padding: 0 !important; }
|
|
|
|
.fc-today-button { background-color: #0f172a !important; color: white !important; }
|
|
.fc-timegrid-col.fc-day-today { background-color: #f8fafc !important; }
|
|
|
|
/* ESTILO PARA EL POPOVER (La ventanita "+X más") */
|
|
.fc-popover {
|
|
border: none !important;
|
|
border-radius: 1rem !important;
|
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1) !important;
|
|
overflow: hidden;
|
|
z-index: 200 !important;
|
|
}
|
|
.fc-popover-header {
|
|
background-color: #2563eb !important;
|
|
color: white !important;
|
|
font-weight: 900 !important;
|
|
text-transform: uppercase;
|
|
padding: 0.75rem 1rem !important;
|
|
font-size: 0.8rem !important;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
.fc-popover-body {
|
|
padding: 0.75rem !important;
|
|
background-color: #f8fafc !important;
|
|
}
|
|
.fc-popover-close-icon { color: white !important; opacity: 0.7; }
|
|
.fc-popover-close-icon:hover { opacity: 1; }
|
|
|
|
/* Ocultamos los puntos por defecto del popover para usar nuestras tarjetas completas */
|
|
.fc-daygrid-event-dot { display: none !important; }
|
|
.fc-popover .fc-event { margin-bottom: 0.5rem !important; }
|
|
</style>
|
|
</head>
|
|
<body class="bg-gray-50 text-gray-800 font-sans antialiased overflow-hidden text-left">
|
|
|
|
<div class="flex h-screen overflow-hidden text-left">
|
|
<div id="sidebar-container" class="h-full shrink-0"></div>
|
|
|
|
<div class="flex-1 flex flex-col overflow-hidden relative">
|
|
<div id="header-container"></div>
|
|
|
|
<main class="flex-1 overflow-x-hidden overflow-y-auto bg-gray-50 p-6 relative">
|
|
<div class="fade-in max-w-7xl mx-auto flex flex-col h-full gap-6">
|
|
|
|
<div class="bg-white p-6 rounded-[2rem] shadow-sm border border-slate-100 flex flex-col xl:flex-row justify-between items-center gap-6">
|
|
<div>
|
|
<h2 class="text-2xl font-black text-slate-800 flex items-center gap-3 tracking-tight">
|
|
<span class="bg-blue-600 p-2.5 rounded-xl text-white shadow-lg shadow-blue-200"><i data-lucide="calendar-days"></i></span>
|
|
Calendario Inteligente
|
|
</h2>
|
|
<p class="text-sm text-slate-500 mt-1 font-medium">Control de agendas, rutas y operarios citados.</p>
|
|
</div>
|
|
|
|
<div class="flex flex-wrap gap-3 items-center w-full xl:w-auto">
|
|
<div class="relative flex-1 xl:w-64">
|
|
<i data-lucide="search" class="w-4 h-4 absolute left-4 top-1/2 -translate-y-1/2 text-slate-400"></i>
|
|
<input type="text" id="searchFilter" oninput="applyFilters()" placeholder="Buscar cliente, REF..." class="w-full pl-11 pr-4 py-3 bg-slate-50 border border-slate-200 rounded-2xl text-xs font-bold focus:ring-2 focus:ring-blue-500 outline-none transition-all">
|
|
</div>
|
|
<select id="opFilter" onchange="applyFilters()" class="bg-slate-50 border border-slate-200 text-xs font-black px-4 py-3 rounded-2xl outline-none focus:ring-2 focus:ring-blue-500 uppercase tracking-widest text-slate-600">
|
|
<option value="ALL">TODOS LOS OPERARIOS</option>
|
|
</select>
|
|
<select id="compFilter" onchange="applyFilters()" class="bg-slate-50 border border-slate-200 text-xs font-black px-4 py-3 rounded-2xl outline-none focus:ring-2 focus:ring-blue-500 uppercase tracking-widest text-slate-600">
|
|
<option value="ALL">TODAS LAS COMPAÑÍAS</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="statusLegend" class="flex flex-wrap gap-2 -mt-2"></div>
|
|
|
|
<div class="bg-white p-6 rounded-[2.5rem] shadow-xl shadow-slate-200/40 border border-slate-100 flex-1 min-h-[700px] overflow-hidden">
|
|
<div id="calendar" class="h-full"></div>
|
|
</div>
|
|
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="eventModal" class="fixed inset-0 bg-slate-900/80 hidden z-[150] flex items-center justify-center backdrop-blur-sm p-4 fade-in text-left">
|
|
<div class="bg-white rounded-[2.5rem] shadow-2xl w-full max-w-md overflow-hidden flex flex-col border border-slate-100">
|
|
<div id="emHeader" class="p-6 border-b flex justify-between items-center text-white">
|
|
<div class="flex items-center gap-3">
|
|
<div class="bg-white/20 p-2 rounded-xl backdrop-blur-md"><i data-lucide="wrench" class="w-5 h-5 text-white"></i></div>
|
|
<div>
|
|
<p class="text-[9px] font-black uppercase tracking-widest opacity-80" id="emStatusLabel">ESTADO</p>
|
|
<h3 class="font-black text-lg leading-tight uppercase tracking-tight" id="emRef">#REF</h3>
|
|
</div>
|
|
</div>
|
|
<button onclick="closeEventModal()" class="text-white/70 hover:text-white bg-white/10 p-2 rounded-full transition-all"><i data-lucide="x" class="w-5 h-5"></i></button>
|
|
</div>
|
|
|
|
<div class="p-8 space-y-6 bg-slate-50 overflow-y-auto max-h-[70vh] no-scrollbar">
|
|
<div>
|
|
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest mb-1" id="emClientLabel">Asegurado / Cliente</p>
|
|
<p class="font-black text-slate-800 text-xl uppercase leading-none" id="emName"></p>
|
|
<a href="#" id="emPhoneLink" class="inline-flex items-center gap-1.5 mt-2 bg-slate-200/50 hover:bg-emerald-100 text-emerald-700 px-3 py-1.5 rounded-lg text-xs font-black transition-all">
|
|
<i data-lucide="phone" class="w-3.5 h-3.5"></i> <span id="emPhone"></span>
|
|
</a>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="bg-white p-4 rounded-2xl border border-slate-100 shadow-sm">
|
|
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest mb-1"><i data-lucide="hard-hat" class="w-3 h-3 inline mr-1"></i>Operario</p>
|
|
<p class="font-bold text-slate-700 text-xs uppercase" id="emOperator"></p>
|
|
</div>
|
|
<div class="bg-white p-4 rounded-2xl border border-slate-100 shadow-sm" id="boxCompany">
|
|
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest mb-1"><i data-lucide="building-2" class="w-3 h-3 inline mr-1"></i>Compañía</p>
|
|
<p class="font-bold text-slate-700 text-xs uppercase" id="emCompany"></p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white p-4 rounded-2xl border border-slate-100 shadow-sm" id="boxAddress">
|
|
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest mb-1"><i data-lucide="map-pin" class="w-3 h-3 inline mr-1"></i>Dirección Exacta</p>
|
|
<p class="font-bold text-slate-600 text-sm uppercase leading-snug" id="emAddress"></p>
|
|
</div>
|
|
|
|
<div>
|
|
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest mb-2 ml-2" id="emDescLabel">Descripción de la Avería</p>
|
|
<div class="bg-amber-50 border border-amber-100 p-4 rounded-2xl text-xs font-medium text-slate-700 leading-relaxed max-h-32 overflow-y-auto no-scrollbar" id="emDesc"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="js/layout.js"></script>
|
|
<script>
|
|
let rawServices = [];
|
|
let calendarEvents = [];
|
|
let calendarInstance = null;
|
|
|
|
// 🎨 ESTADOS REALES: vienen de /statuses (BBDD), no son texto libre.
|
|
// id (string) -> { name, color, is_final, ... }
|
|
let statusMap = {};
|
|
|
|
// Paleta de apoyo: traduce el nombre de color (tal cual se guarda en service_statuses.color,
|
|
// p.ej. "indigo", "emerald") al hex real que pintamos en el calendario.
|
|
const TAILWIND_HEX = {
|
|
gray: { bg: '#6b7280', border: '#4b5563' },
|
|
slate: { bg: '#64748b', border: '#475569' },
|
|
blue: { bg: '#3b82f6', border: '#2563eb' },
|
|
amber: { bg: '#f59e0b', border: '#d97706' },
|
|
emerald: { bg: '#10b981', border: '#059669' },
|
|
indigo: { bg: '#6366f1', border: '#4f46e5' },
|
|
orange: { bg: '#f97316', border: '#ea580c' },
|
|
red: { bg: '#ef4444', border: '#dc2626' },
|
|
rose: { bg: '#f43f5e', border: '#e11d48' },
|
|
purple: { bg: '#a855f7', border: '#9333ea' },
|
|
green: { bg: '#22c55e', border: '#16a34a' },
|
|
teal: { bg: '#14b8a6', border: '#0d9488' },
|
|
cyan: { bg: '#06b6d4', border: '#0891b2' },
|
|
violet: { bg: '#8b5cf6', border: '#7c3aed' },
|
|
pink: { bg: '#ec4899', border: '#db2777' },
|
|
yellow: { bg: '#eab308', border: '#ca8a04' },
|
|
lime: { bg: '#84cc16', border: '#65a30d' },
|
|
sky: { bg: '#0ea5e9', border: '#0284c7' }
|
|
};
|
|
|
|
function iconForStatusName(name) {
|
|
const s = (name || '').toLowerCase();
|
|
if (s.includes('camino')) return 'truck';
|
|
if (s.includes('trabajando') || s.includes('reparacion')) return 'hammer';
|
|
if (s.includes('incidencia') || s.includes('pausado')) return 'alert-triangle';
|
|
if (s.includes('anulado')) return 'x-circle';
|
|
if (s.includes('desasignado')) return 'user-x';
|
|
if (s.includes('terminado') || s.includes('finalizado')) return 'check-circle';
|
|
if (s.includes('esperando') || s.includes('pendiente')) return 'clock';
|
|
if (s.includes('compañ') || s.includes('perito')) return 'building-2';
|
|
if (s.includes('asignado')) return 'user-check';
|
|
return 'calendar';
|
|
}
|
|
|
|
// 🩹 FIX: antes esto comparaba el estado como si fuera texto ("trabajando", "camino"...),
|
|
// pero en los datos reales status_operativo es un ID numérico (24, 29, 30...) que apunta
|
|
// a la tabla service_statuses. Por eso NUNCA coincidía con ninguna palabra y todas las
|
|
// citas salían siempre del mismo color. Ahora resolvemos el ID contra /statuses de verdad.
|
|
function getStatusConf(statusOperativo, isBlock) {
|
|
if (isBlock) return { bg: '#f43f5e', border: '#e11d48', label: 'BLOQUEO DE AGENDA', icon: 'shield-alert', is_final: false };
|
|
|
|
const key = (statusOperativo !== null && statusOperativo !== undefined && statusOperativo !== '') ? String(statusOperativo) : null;
|
|
const st = key ? statusMap[key] : null;
|
|
|
|
if (st) {
|
|
const hex = TAILWIND_HEX[st.color] || TAILWIND_HEX.blue;
|
|
return { bg: hex.bg, border: hex.border, label: st.name, icon: iconForStatusName(st.name), is_final: !!st.is_final };
|
|
}
|
|
// Estado huérfano/desconocido (dato antiguo, borrado, etc.): lo dejamos visible pero distinguible
|
|
return { bg: '#94a3b8', border: '#64748b', label: 'Sin Estado', icon: 'help-circle', is_final: false };
|
|
}
|
|
|
|
async function loadStatuses() {
|
|
try {
|
|
const res = await fetch(`${API_URL}/statuses`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
|
|
const data = await res.json();
|
|
if (data.ok && Array.isArray(data.statuses)) {
|
|
statusMap = {};
|
|
data.statuses.forEach(s => { statusMap[String(s.id)] = s; });
|
|
renderStatusLegend(data.statuses);
|
|
}
|
|
} catch (e) { console.error("Error al cargar los estados", e); }
|
|
}
|
|
|
|
function renderStatusLegend(statuses) {
|
|
const el = document.getElementById('statusLegend');
|
|
if (!el) return;
|
|
const chips = statuses.map(s => {
|
|
const hex = TAILWIND_HEX[s.color] || TAILWIND_HEX.blue;
|
|
return `<span class="inline-flex items-center gap-1.5 bg-slate-50 border border-slate-200 rounded-full px-3 py-1 text-[10px] font-bold text-slate-600 uppercase tracking-wide">
|
|
<span class="w-2.5 h-2.5 rounded-full shrink-0" style="background:${hex.bg}"></span>${s.name}
|
|
</span>`;
|
|
}).join('');
|
|
el.innerHTML = chips + `<span class="inline-flex items-center gap-1.5 bg-slate-50 border border-slate-200 rounded-full px-3 py-1 text-[10px] font-bold text-slate-600 uppercase tracking-wide">
|
|
<span class="w-2.5 h-2.5 rounded-full shrink-0" style="background:#f43f5e"></span>Bloqueo
|
|
</span>`;
|
|
}
|
|
|
|
function toLocalDateStr(d) {
|
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
}
|
|
function toLocalTimeStr(d) {
|
|
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
|
}
|
|
|
|
// 🖱️ Arrastrar una cita para cambiarla de hora/día
|
|
async function handleEventDrop(info) {
|
|
const svcId = info.event.id;
|
|
const newDate = toLocalDateStr(info.event.start);
|
|
const newTime = toLocalTimeStr(info.event.start);
|
|
try {
|
|
const res = await fetch(`${API_URL}/providers/scraped/${svcId}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json', "Authorization": `Bearer ${localStorage.getItem("token")}` },
|
|
body: JSON.stringify({ scheduled_date: newDate, scheduled_time: newTime })
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok || data.ok === false) throw new Error('No se pudo mover la cita');
|
|
info.event.extendedProps.originalData.raw_data.scheduled_date = newDate;
|
|
info.event.extendedProps.originalData.raw_data.scheduled_time = newTime;
|
|
} catch (e) {
|
|
console.error("Error moviendo la cita:", e);
|
|
info.revert();
|
|
alert("No se ha podido mover la cita. Inténtalo de nuevo.");
|
|
}
|
|
}
|
|
|
|
// ↔️ Alargar/acortar una cita cambia su duración
|
|
async function handleEventResize(info) {
|
|
const svcId = info.event.id;
|
|
const durationMin = Math.round((info.event.end - info.event.start) / 60000);
|
|
try {
|
|
const res = await fetch(`${API_URL}/providers/scraped/${svcId}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json', "Authorization": `Bearer ${localStorage.getItem("token")}` },
|
|
body: JSON.stringify({ duration_minutes: durationMin })
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok || data.ok === false) throw new Error('No se pudo cambiar la duración');
|
|
info.event.extendedProps.originalData.raw_data.duration_minutes = durationMin;
|
|
} catch (e) {
|
|
console.error("Error cambiando la duración:", e);
|
|
info.revert();
|
|
alert("No se ha podido cambiar la duración de la cita.");
|
|
}
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", async () => {
|
|
if (!localStorage.getItem("token")) window.location.href = "index.html";
|
|
lucide.createIcons();
|
|
await Promise.all([loadOperators(), loadStatuses()]);
|
|
await loadServices();
|
|
});
|
|
|
|
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('opFilter');
|
|
data.operators.forEach(op => sel.innerHTML += `<option value="${op.full_name}">${op.full_name}</option>`);
|
|
}
|
|
} catch(e) {}
|
|
}
|
|
|
|
async function loadServices() {
|
|
try {
|
|
const res = await fetch(`${API_URL}/services/active`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
|
|
const data = await res.json();
|
|
|
|
if (data.ok) {
|
|
rawServices = data.services.filter(s => s.raw_data && s.raw_data.scheduled_date && s.raw_data.scheduled_date !== "");
|
|
|
|
const compSelect = document.getElementById('compFilter');
|
|
const uniqueComps = [...new Set(rawServices.filter(s => s.provider !== 'SYSTEM_BLOCK').map(s => (s.raw_data['Compañía'] || s.raw_data['COMPAÑIA'] || "Particular").toString().toUpperCase()))].sort();
|
|
|
|
compSelect.innerHTML = '<option value="ALL">TODAS LAS COMPAÑÍAS</option>';
|
|
uniqueComps.forEach(c => compSelect.innerHTML += `<option value="${c}">${c}</option>`);
|
|
|
|
processEventsAndRender();
|
|
}
|
|
} catch (e) { console.error("Error al cargar calendario", e); }
|
|
}
|
|
|
|
function processEventsAndRender() {
|
|
calendarEvents = rawServices.map(svc => {
|
|
const r = svc.raw_data;
|
|
const isBlock = svc.provider === 'SYSTEM_BLOCK';
|
|
|
|
const name = r['Nombre Cliente'] || r['CLIENTE'] || "Sin Nombre";
|
|
const pop = r['Población'] || r['POBLACION-PROVINCIA'] || "";
|
|
const title = isBlock ? `BLOQUEO: ${r['Descripción'] || 'Motivo'}` : `${name} - ${pop}`;
|
|
|
|
const dateIso = r.scheduled_date;
|
|
const timeIso = r.scheduled_time ? r.scheduled_time + ':00' : '09:00:00';
|
|
const startStr = `${dateIso}T${timeIso}`;
|
|
|
|
const startObj = new Date(startStr);
|
|
const duration = parseInt(r.duration_minutes || 60);
|
|
const endObj = new Date(startObj.getTime() + duration * 60000);
|
|
const endStr = new Date(endObj.getTime() - (endObj.getTimezoneOffset() * 60000)).toISOString().slice(0, 19);
|
|
|
|
const sConf = getStatusConf(r.status_operativo, isBlock);
|
|
|
|
return {
|
|
id: svc.id,
|
|
title: title,
|
|
start: startStr,
|
|
end: endStr,
|
|
backgroundColor: sConf.bg,
|
|
borderColor: sConf.border,
|
|
extendedProps: {
|
|
originalData: svc,
|
|
companyName: isBlock ? 'SISTEMA' : (r['Compañía'] || r['COMPAÑIA'] || "Particular").toUpperCase(),
|
|
operatorName: svc.assigned_name || "Sin Asignar",
|
|
statusConf: sConf,
|
|
isBlock: isBlock,
|
|
isUrgent: !isBlock && svc.is_urgent === true
|
|
}
|
|
};
|
|
});
|
|
|
|
if (calendarInstance) {
|
|
calendarInstance.removeAllEventSources();
|
|
calendarInstance.addEventSource(calendarEvents);
|
|
} else {
|
|
initCalendar();
|
|
}
|
|
}
|
|
|
|
function initCalendar() {
|
|
const calendarEl = document.getElementById('calendar');
|
|
|
|
calendarInstance = new FullCalendar.Calendar(calendarEl, {
|
|
initialView: 'timeGridWeek',
|
|
locale: 'es',
|
|
firstDay: 1,
|
|
slotMinTime: '08:00:00',
|
|
slotMaxTime: '22:00:00',
|
|
expandRows: true,
|
|
allDaySlot: false,
|
|
nowIndicator: true,
|
|
dayMaxEvents: 3,
|
|
moreLinkText: "más",
|
|
buttonText: { today: 'Hoy', month: 'Mes', week: 'Semana', day: 'Día', list: 'Agenda' },
|
|
headerToolbar: {
|
|
left: 'prev,next today',
|
|
center: 'title',
|
|
right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek'
|
|
},
|
|
events: calendarEvents,
|
|
|
|
editable: true,
|
|
eventDurationEditable: true,
|
|
eventDrop: handleEventDrop,
|
|
eventResize: handleEventResize,
|
|
|
|
eventContent: function(arg) {
|
|
let timeText = arg.timeText;
|
|
if (!timeText && arg.event.start) {
|
|
timeText = arg.event.start.toLocaleTimeString('es-ES', {hour: '2-digit', minute:'2-digit'});
|
|
}
|
|
|
|
const title = arg.event.title;
|
|
const op = arg.event.extendedProps.operatorName;
|
|
const isBlock = arg.event.extendedProps.isBlock;
|
|
const isUrgent = arg.event.extendedProps.isUrgent;
|
|
const conf = arg.event.extendedProps.statusConf;
|
|
const bgColor = arg.event.backgroundColor;
|
|
|
|
return {
|
|
html: `
|
|
<div class="flex flex-col h-full w-full justify-start p-1.5 text-white overflow-hidden font-sans rounded-lg shadow-sm border-l-4 relative" style="background-color: ${bgColor}; border-color: ${arg.event.borderColor};">
|
|
${isUrgent ? `<span class="absolute -top-1 -right-1 bg-white rounded-full p-0.5 shadow-md"><i data-lucide="flame" class="w-3 h-3 text-red-600"></i></span>` : ''}
|
|
<div class="flex justify-between items-center mb-1">
|
|
<span class="text-[9px] font-black bg-black/20 px-1.5 py-0.5 rounded-md leading-none whitespace-nowrap">${timeText}</span>
|
|
<i data-lucide="${conf.icon}" class="w-3 h-3 text-white"></i>
|
|
</div>
|
|
<p class="text-[10px] font-black leading-tight tracking-tight whitespace-normal line-clamp-2">${title}</p>
|
|
<div class="flex items-center gap-1 opacity-90 mt-1">
|
|
<i data-lucide="${isBlock ? 'shield-alert' : 'user'}" class="w-2.5 h-2.5"></i>
|
|
<span class="text-[8px] font-bold uppercase truncate">${op}</span>
|
|
</div>
|
|
</div>`
|
|
};
|
|
},
|
|
|
|
eventDidMount: function() {
|
|
lucide.createIcons();
|
|
},
|
|
|
|
eventClick: function(info) {
|
|
openEventDetails(info.event.extendedProps);
|
|
}
|
|
});
|
|
|
|
calendarInstance.render();
|
|
}
|
|
|
|
function applyFilters() {
|
|
const textFilter = document.getElementById('searchFilter').value.toUpperCase();
|
|
const opFilter = document.getElementById('opFilter').value;
|
|
const compFilter = document.getElementById('compFilter').value;
|
|
|
|
const filteredEvents = calendarEvents.filter(ev => {
|
|
const props = ev.extendedProps;
|
|
if (props.isBlock) return (opFilter === 'ALL' || props.operatorName === opFilter);
|
|
|
|
const matchText = ev.title.toUpperCase().includes(textFilter) || String(props.originalData.service_ref).toUpperCase().includes(textFilter);
|
|
const matchOp = opFilter === 'ALL' || props.operatorName === opFilter;
|
|
const matchComp = compFilter === 'ALL' || props.companyName === compFilter;
|
|
|
|
return matchText && matchOp && matchComp;
|
|
});
|
|
|
|
calendarInstance.removeAllEventSources();
|
|
calendarInstance.addEventSource(filteredEvents);
|
|
setTimeout(() => lucide.createIcons(), 50);
|
|
}
|
|
|
|
function openEventDetails(props) {
|
|
const raw = props.originalData.raw_data;
|
|
const ref = props.originalData.service_ref;
|
|
const conf = props.statusConf;
|
|
const isBlock = props.isBlock;
|
|
|
|
const header = document.getElementById('emHeader');
|
|
header.style.backgroundColor = conf.bg;
|
|
document.getElementById('emStatusLabel').innerText = conf.label;
|
|
document.getElementById('emRef').innerText = isBlock ? 'BLOQUEO TÉCNICO' : `EXP. #${ref}`;
|
|
document.getElementById('emOperator').innerText = props.operatorName;
|
|
|
|
if (isBlock) {
|
|
document.getElementById('emClientLabel').innerText = "TIPO DE BLOQUEO";
|
|
document.getElementById('emName').innerText = raw.blocked_guild_name ? `SOLO GREMIO: ${raw.blocked_guild_name}` : "BLOQUEO TOTAL (NO CITAR)";
|
|
document.getElementById('emPhoneLink').classList.add('hidden');
|
|
document.getElementById('boxCompany').classList.add('hidden');
|
|
document.getElementById('boxAddress').classList.add('hidden');
|
|
document.getElementById('emDescLabel').innerText = "MOTIVO / NOTAS DEL BLOQUEO";
|
|
} else {
|
|
document.getElementById('emClientLabel').innerText = "Asegurado / Cliente";
|
|
document.getElementById('emName').innerText = raw['Nombre Cliente'] || raw['CLIENTE'] || "Desconocido";
|
|
const phone = (raw['Teléfono'] || raw['TELEFONOS'] || raw['TELEFONO'] || "").match(/[6789]\d{8}/)?.[0] || "Sin Teléfono";
|
|
document.getElementById('emPhone').innerText = phone;
|
|
document.getElementById('emPhoneLink').classList.remove('hidden');
|
|
document.getElementById('emPhoneLink').href = phone !== "Sin Teléfono" ? `tel:+34${phone}` : '#';
|
|
document.getElementById('boxCompany').classList.remove('hidden');
|
|
document.getElementById('emCompany').innerText = props.companyName;
|
|
document.getElementById('boxAddress').classList.remove('hidden');
|
|
const addr = raw['Dirección'] || raw['DOMICILIO'] || "";
|
|
const pop = raw['Población'] || raw['POBLACION-PROVINCIA'] || "";
|
|
document.getElementById('emAddress').innerText = `${addr}, ${pop} (CP: ${raw['Código Postal'] || '---'})`;
|
|
document.getElementById('emDescLabel').innerText = "Descripción de la Avería";
|
|
}
|
|
|
|
document.getElementById('emDesc').innerHTML = (raw['Descripción'] || raw['DESCRIPCION'] || "Sin información.").replace(/\n/g, '<br>');
|
|
document.getElementById('eventModal').classList.remove('hidden');
|
|
lucide.createIcons();
|
|
}
|
|
|
|
function closeEventModal() {
|
|
document.getElementById('eventModal').classList.add('hidden');
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |