diff --git a/calendario.html b/calendario.html index 24cf651..3d787ac 100644 --- a/calendario.html +++ b/calendario.html @@ -119,6 +119,8 @@ +
+
@@ -180,32 +182,144 @@ let calendarEvents = []; let calendarInstance = null; - // DICCIONARIO DE COLORES E ICONOS - const statusConfig = { - 'citado': { bg: '#3b82f6', border: '#2563eb', label: 'Citado / Agendado', icon: 'calendar' }, - 'de_camino': { bg: '#8b5cf6', border: '#7c3aed', label: 'Técnico de Camino', icon: 'truck' }, // COCHE/CAMIÓN - 'trabajando': { bg: '#f59e0b', border: '#d97706', label: 'En Reparación', icon: 'hammer' }, // MARTILLO - 'incidencia': { bg: '#ef4444', border: '#dc2626', label: 'Pausado / Incidencia', icon: 'alert-triangle' }, - 'terminado': { bg: '#10b981', border: '#059669', label: 'Trabajo Terminado', icon: 'check-circle' }, - 'SYSTEM_BLOCK': { bg: '#f43f5e', border: '#e11d48', label: 'BLOQUEO DE AGENDA', icon: 'shield-alert' } + // 🎨 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' } }; - // Función para detectar la clave correcta aunque venga el ID o un texto variable - function getStatusKey(status) { - if (!status) return 'citado'; - const s = String(status).toLowerCase(); - if (s.includes('camino')) return 'de_camino'; - if (s.includes('trabajando') || s.includes('reparacion')) return 'trabajando'; - if (s.includes('incidencia') || s.includes('pausado')) return 'incidencia'; - if (s.includes('terminado') || s.includes('finalizado')) return 'terminado'; - if (s === 'system_block') return 'SYSTEM_BLOCK'; - return 'citado'; + 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 ` + ${s.name} + `; + }).join(''); + el.innerHTML = chips + ` + Bloqueo + `; + } + + 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 loadOperators(); + await Promise.all([loadOperators(), loadStatuses()]); await loadServices(); }); @@ -257,8 +371,7 @@ const endObj = new Date(startObj.getTime() + duration * 60000); const endStr = new Date(endObj.getTime() - (endObj.getTimezoneOffset() * 60000)).toISOString().slice(0, 19); - const statusKey = isBlock ? 'SYSTEM_BLOCK' : getStatusKey(r.status_operativo); - const sConf = statusConfig[statusKey] || statusConfig['citado']; + const sConf = getStatusConf(r.status_operativo, isBlock); return { id: svc.id, @@ -272,7 +385,8 @@ companyName: isBlock ? 'SISTEMA' : (r['Compañía'] || r['COMPAÑIA'] || "Particular").toUpperCase(), operatorName: svc.assigned_name || "Sin Asignar", statusConf: sConf, - isBlock: isBlock + isBlock: isBlock, + isUrgent: !isBlock && svc.is_urgent === true } }; }); @@ -307,21 +421,28 @@ }, 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 { + + return { html: ` -
+
+ ${isUrgent ? `` : ''}
${timeText} @@ -331,10 +452,10 @@ ${op}
-
` +
` }; }, - + eventDidMount: function() { lucide.createIcons(); },