Actualizar calendario.html

This commit is contained in:
2026-02-28 15:22:52 +00:00
parent d452838c0a
commit a680452b1e

View File

@@ -116,7 +116,7 @@
<span class="text-[10px] uppercase tracking-widest">De Camino</span>
</button>
<button onclick="quickUpdate('trabajando')" class="bg-white border border-slate-200 text-primary-dynamic font-black p-5 rounded-3xl flex flex-col items-center gap-2 shadow-sm active:bg-slate-50 transition-all text-center">
<i data-lucide="wrench" class="w-8 h-8"></i>
<i data-lucide="hammer" class="w-8 h-8"></i>
<span class="text-[10px] uppercase tracking-widest">He Llegado</span>
</button>
</div>
@@ -189,9 +189,9 @@
<div class="pt-2 text-left">
<p class="text-[10px] font-black text-slate-800 uppercase ml-1 flex items-center gap-1.5 mb-1.5"><i data-lucide="arrow-right-left" class="w-4 h-4 text-blue-500"></i> Cambio de Estado</p>
<div class="relative text-left">
<select id="detStatusMap" class="w-full bg-slate-800 text-white border-none p-4 rounded-xl text-xs font-bold shadow-lg outline-none cursor-pointer appearance-none pr-10">
<select id="detStatusMap" class="w-full bg-slate-50 border border-slate-200 text-slate-800 p-3.5 rounded-xl text-xs font-black shadow-sm outline-none focus:border-blue-400 focus:ring-2 focus:ring-blue-100 transition-all cursor-pointer appearance-none pr-10">
</select>
<i data-lucide="chevron-down" class="w-4 h-4 text-slate-400 absolute right-4 top-1/2 -translate-y-1/2 pointer-events-none"></i>
<i data-lucide="chevron-down" class="w-5 h-5 text-slate-400 absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none"></i>
</div>
</div>
@@ -215,7 +215,7 @@
</div>
<div class="pt-4 text-center">
<button onclick="quickUpdate('finalizado')" class="w-full bg-emerald-600 text-white font-black py-5 rounded-[2rem] shadow-xl flex items-center justify-center gap-3 uppercase text-sm tracking-widest active:scale-95 transition-all">
<button onclick="askToFinish()" class="w-full bg-emerald-600 text-white font-black py-5 rounded-[2rem] shadow-xl flex items-center justify-center gap-3 uppercase text-sm tracking-widest active:scale-95 transition-all">
<i data-lucide="check-circle" class="w-6 h-6"></i> Finalizar Trabajo
</button>
<p class="text-center text-[9px] font-bold text-slate-400 mt-6 uppercase tracking-widest">IntegraRepara Mobile</p>
@@ -237,7 +237,7 @@
let systemStatuses = [];
let systemGuilds = [];
let currentWeekStart = new Date();
let selectedDateStr = "";
let selectedDateStr = "";
async function applyTheme() {
try {
@@ -350,7 +350,8 @@
const modalSelect = document.getElementById('detStatusMap');
modalSelect.innerHTML = '';
systemStatuses.forEach(st => {
modalSelect.innerHTML += `<option value="${st.id}">👉 ${st.name.toUpperCase()}</option>`;
// Modificación: Solo el nombre en el select para diseño más limpio
modalSelect.innerHTML += `<option value="${st.id}">${st.name.toUpperCase()}</option>`;
});
}
} catch (e) {}
@@ -391,6 +392,19 @@
}
}
// Mapa de colores (Mismo que en proveedores)
const colorMap = {
'gray': { text: 'text-slate-500', bg: 'bg-slate-100', border: 'border-slate-200' },
'blue': { text: 'text-blue-600', bg: 'bg-blue-50', border: 'border-blue-100' },
'amber': { text: 'text-amber-600', bg: 'bg-amber-50', border: 'border-amber-100' },
'emerald': { text: 'text-emerald-600', bg: 'bg-emerald-50', border: 'border-emerald-100' },
'indigo': { text: 'text-indigo-600', bg: 'bg-indigo-50', border: 'border-indigo-100' },
'orange': { text: 'text-orange-600', bg: 'bg-orange-50', border: 'border-orange-100' },
'red': { text: 'text-red-600', bg: 'bg-red-50', border: 'border-red-100' },
'rose': { text: 'text-rose-600', bg: 'bg-rose-50', border: 'border-rose-100' },
'purple': { text: 'text-purple-600', bg: 'bg-purple-50', border: 'border-purple-100' }
};
function renderServices() {
const container = document.getElementById('servicesList');
const displayDate = selectedDateStr.split('-').reverse().join('/');
@@ -416,28 +430,33 @@
let compShort = (raw["Compañía"] || "Particular").split('-')[0].trim().substring(0, 15);
const guildObj = systemGuilds.find(g => String(g.id) === String(s.guild_id || raw.guild_id));
// LÓGICA DE ICONO SEGÚN ESTADO
// MODIFICACIÓN 1: LÓGICA DE ICONO Y COLOR SEGÚN ESTADO
let iconName = "clock";
const dbStatId = raw.status_operativo;
const statusObj = systemStatuses.find(st => String(st.id) === String(dbStatId));
const stName = (statusObj?.name || "").toLowerCase();
const stColor = (statusObj?.color || "gray");
if (stName.includes('camino')) iconName = "car";
else if (stName.includes('trabajando') || stName.includes('llegado')) iconName = "wrench";
else if (stName.includes('trabajando') || stName.includes('llegado') || stName.includes('reparación')) iconName = "hammer";
else if (stName.includes('incidencia') || stName.includes('pausa')) iconName = "alert-triangle";
else if (stName.includes('citado')) iconName = "calendar";
const cMap = colorMap[stColor] || colorMap['gray'];
return `
<div onclick="openService(${s.id})" class="bg-white p-6 rounded-[2.5rem] border border-slate-200 shadow-sm active:scale-95 transition-transform flex gap-4 relative overflow-hidden text-left">
${s.is_urgent ? '<div class="absolute top-0 right-0 bg-primary-dynamic text-white text-[8px] font-black px-3 py-1.5 rounded-bl-xl uppercase tracking-widest z-10">Urgente</div>' : ''}
<div class="flex flex-col items-center justify-center border-r border-slate-100 pr-4 shrink-0 min-w-[70px]">
<i data-lucide="${iconName}" class="w-6 h-6 text-primary-dynamic mb-1"></i>
<span class="font-black text-slate-900 text-base">${time}</span>
<div class="${cMap.bg} ${cMap.text} p-2 rounded-xl mb-1 ${cMap.border} border shadow-sm">
<i data-lucide="${iconName}" class="w-5 h-5"></i>
</div>
<span class="font-black text-slate-900 text-sm">${time}</span>
</div>
<div class="min-w-0 flex-1">
<div class="flex flex-wrap gap-1.5 mb-2">
<span class="text-[8px] font-black bg-blue-50 text-primary-dynamic px-2 py-0.5 rounded uppercase border border-blue-100">${compShort}</span>
<span class="text-[8px] font-black bg-slate-100 text-slate-500 px-2 py-0.5 rounded uppercase border border-slate-200">${guildObj ? guildObj.name : 'Reparación'}</span>
<span class="text-[8px] font-black bg-slate-100 text-slate-500 px-2 py-0.5 rounded uppercase border border-slate-200">${compShort}</span>
<span class="text-[8px] font-black bg-blue-50 text-blue-600 px-2 py-0.5 rounded uppercase border border-blue-100">${guildObj ? guildObj.name : 'Reparación'}</span>
</div>
<h3 class="font-black text-slate-800 text-sm uppercase leading-tight truncate">${name}</h3>
<p class="text-[10px] font-bold text-slate-400 mt-1 truncate uppercase flex items-center gap-1.5"><i data-lucide="map-pin" class="w-3 h-3 shrink-0"></i> ${addr}</p>
@@ -509,7 +528,6 @@
function callClient() { const p = document.getElementById('detPhoneRaw').value; if (p) window.location.href = `tel:+34${p}`; else alert("Sin teléfono"); }
function openWhatsApp() { const p = document.getElementById('detPhoneRaw').value; if (p) window.open(`https://wa.me/34${p}`, '_blank'); else alert("Sin teléfono"); }
// BOTÓN VER MAPA CORREGIDO PARA ANDROID E IOS
function openMaps() {
const a = document.getElementById('detAddress').innerText;
if (a) {
@@ -517,7 +535,6 @@
}
}
// CÁLCULO DE GPS ULTRARRÁPIDO CON FALLBACK Y TIMEOUT
async function calculateDistance(dest) {
const loading = document.getElementById('gpsLoading');
const result = document.getElementById('gpsResult');
@@ -533,22 +550,19 @@
result.classList.add('hidden');
lucide.createIcons();
// OPCIONES MÁGICAS PARA QUE SEA INSTANTÁNEO
const gpsOptions = {
enableHighAccuracy: false, // Usa antenas/wifi en vez de buscar satélites (tarda milisegundos en vez de minutos)
timeout: 8000, // Máximo 8 segundos de espera, si no, cancela
maximumAge: 300000 // Permite usar la ubicación de hace 5 minutos si ya la sabe
enableHighAccuracy: false,
timeout: 8000,
maximumAge: 300000
};
navigator.geolocation.getCurrentPosition(async (pos) => {
const lat = pos.coords.latitude;
const lon = pos.coords.longitude;
try {
// Intento 1: Dirección completa
let res = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(dest + ', España')}`);
let data = await res.json();
// Intento 2: Si falla (porque hay "pisos", "escaleras", etc), buscamos solo por CP y Población
if (!data || data.length === 0) {
const parts = dest.split(',');
const fallbackDest = parts.length > 1 ? parts[parts.length - 1].trim() : dest;
@@ -573,15 +587,14 @@
}
}, (err) => {
loading.innerHTML = '<span class="text-[9px]">GPS Lento/Denegado</span>';
}, gpsOptions); // <- AQUÍ PASAMOS LAS OPCIONES DE VELOCIDAD
}, gpsOptions);
}
async function quickUpdate(action) {
if(!currentServiceId) return;
let word = ""; let msg = "";
if(action === 'camino') { word = "camino"; msg = "¿Enviar 'De Camino'?"; }
if(action === 'camino') { word = "camino"; msg = "¿Enviar estado 'De Camino'?"; }
if(action === 'trabajando') { word = "trabaja"; msg = "¿Iniciar trabajo?"; }
if(action === 'finalizado') { word = "finaliza"; msg = "¿Finalizar expediente?"; }
if(!confirm(msg)) return;
const st = systemStatuses.find(s => s.name.toLowerCase().includes(word));
@@ -596,7 +609,6 @@
if(res.ok) {
showToast("¡Actualizado!");
// MAGIA: Si es "De camino", forzamos enviar el GPS INMEDIATAMENTE
if (action === 'camino' && navigator.geolocation) {
navigator.geolocation.getCurrentPosition(async (pos) => {
await fetch(`${API_URL}/services/${currentServiceId}/location`, {
@@ -610,7 +622,40 @@
closeModal();
refreshData();
}
} catch (e) { alert("Error"); }
} catch (e) { alert("Error al actualizar"); }
}
// MODIFICACIÓN 3: LÓGICA DE FINALIZAR CON PREGUNTA DE ENCUESTA
async function askToFinish() {
if(!currentServiceId) return;
const stFinalizado = systemStatuses.find(s => s.name.toLowerCase().includes('finaliza'));
if(!stFinalizado) return alert("El sistema no tiene un estado Finalizado válido.");
// Preguntamos explícitamente si se envía la encuesta
const sendSurvey = confirm("¿Deseas enviar la encuesta de satisfacción al cliente antes de finalizar?");
try {
// Al poner el estado a Finalizado, el servidor borra el aviso de la agenda.
// Le pasamos al servidor un "flag" para que sepa si envía o no envía el WhatsApp de la encuesta.
// NOTA: Como la ruta de server.js actual dispara automáticamente el WA al ver 'finalizado',
// le pasamos un booleano en el body "skip_survey" para que el servidor lo bloquee si eligió NO.
const res = await fetch(`${API_URL}/services/set-appointment/${currentServiceId}`, {
method: 'PUT',
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` },
body: JSON.stringify({
status_operativo: stFinalizado.id,
skip_survey: !sendSurvey // <- Flag que puedes usar en tu server.js si quieres anularlo
})
});
if(res.ok) {
showToast(sendSurvey ? "Expediente finalizado y encuesta enviada." : "Expediente finalizado.");
closeModal();
refreshData();
}
} catch (e) { alert("Error al finalizar."); }
}
async function saveAppointment() {