Añadir mapa en vivo (De Camino) y línea de tiempo visual del estado
This commit is contained in:
316
index.html
316
index.html
@@ -14,6 +14,8 @@
|
|||||||
<script src="https://unpkg.com/lucide@latest"></script>
|
<script src="https://unpkg.com/lucide@latest"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/signature_pad@4.1.7/dist/signature_pad.umd.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/signature_pad@4.1.7/dist/signature_pad.umd.min.js"></script>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
:root { --app-bg: #f8fafc; }
|
:root { --app-bg: #f8fafc; }
|
||||||
@@ -1111,6 +1113,115 @@ lucide.createIcons();
|
|||||||
} catch(e) { return dateStr; }
|
} catch(e) { return dateStr; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- LÍNEA DE TIEMPO VISUAL DEL ESTADO ---
|
||||||
|
function renderStatusTimeline(ctx) {
|
||||||
|
const { stNameLower, hasDate, hasWorker, isFinalized, isLate } = ctx;
|
||||||
|
|
||||||
|
// Un servicio anulado no encaja en una línea de progreso: mostramos un aviso aparte
|
||||||
|
if (stNameLower.includes('anulado')) {
|
||||||
|
return `
|
||||||
|
<div class="mt-4 flex items-center gap-2 bg-slate-50 border border-slate-200 rounded-xl px-3 py-2.5">
|
||||||
|
<i data-lucide="x-circle" class="w-4 h-4 text-slate-400 shrink-0"></i>
|
||||||
|
<p class="text-[10px] font-black uppercase tracking-widest text-slate-400">Servicio anulado</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{ label: 'Solicitado', icon: 'file-text' },
|
||||||
|
{ label: 'Asignado', icon: 'user-check' },
|
||||||
|
{ label: 'Citado', icon: 'calendar-check' },
|
||||||
|
{ label: 'En camino', icon: 'truck' },
|
||||||
|
{ label: 'Trabajando', icon: 'hammer' },
|
||||||
|
{ label: 'Finalizado', icon: 'flag' },
|
||||||
|
];
|
||||||
|
|
||||||
|
let currentIdx = 0;
|
||||||
|
let branchLabel = null;
|
||||||
|
let branchColor = 'indigo';
|
||||||
|
|
||||||
|
if (isFinalized) {
|
||||||
|
currentIdx = 5;
|
||||||
|
} else if (stNameLower.includes('camino')) {
|
||||||
|
currentIdx = 3;
|
||||||
|
} else if (stNameLower.includes('trabajando') || stNameLower.includes('reparación')) {
|
||||||
|
currentIdx = 4;
|
||||||
|
} else if (stNameLower.includes('incidencia')) {
|
||||||
|
currentIdx = hasDate ? 3 : 2;
|
||||||
|
branchLabel = 'Incidencia en gestión';
|
||||||
|
branchColor = 'rose';
|
||||||
|
} else if (stNameLower.includes('compañ') || stNameLower.includes('perito')) {
|
||||||
|
currentIdx = 1;
|
||||||
|
branchLabel = 'Esperando autorización de tu aseguradora';
|
||||||
|
branchColor = 'slate';
|
||||||
|
} else if (stNameLower.includes('desasignado')) {
|
||||||
|
currentIdx = 0;
|
||||||
|
branchLabel = 'Buscando un nuevo técnico';
|
||||||
|
branchColor = 'slate';
|
||||||
|
} else if (hasDate) {
|
||||||
|
currentIdx = 2;
|
||||||
|
if (isLate) { branchLabel = 'Retraso sobre la hora prevista'; branchColor = 'amber'; }
|
||||||
|
} else if (stNameLower.includes('esperando') || stNameLower.includes('asignado') || (hasWorker && !hasDate)) {
|
||||||
|
currentIdx = 1;
|
||||||
|
} else {
|
||||||
|
currentIdx = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorMap = {
|
||||||
|
indigo: { text: 'text-indigo-600', ring: 'ring-indigo-300', icon: 'info' },
|
||||||
|
rose: { text: 'text-rose-600', ring: 'ring-rose-300', icon: 'alert-triangle' },
|
||||||
|
amber: { text: 'text-amber-600', ring: 'ring-amber-300', icon: 'clock-4' },
|
||||||
|
slate: { text: 'text-slate-500', ring: 'ring-slate-300', icon: 'info' },
|
||||||
|
};
|
||||||
|
const activeColor = colorMap[branchLabel ? branchColor : 'indigo'];
|
||||||
|
|
||||||
|
const stepsHtml = steps.map((step, i) => {
|
||||||
|
const isDone = i < currentIdx;
|
||||||
|
const isActive = i === currentIdx;
|
||||||
|
const beforeColored = i <= currentIdx;
|
||||||
|
const afterColored = i < currentIdx;
|
||||||
|
|
||||||
|
let circleClasses, labelClasses;
|
||||||
|
if (isDone) {
|
||||||
|
circleClasses = 'bg-indigo-600 text-white';
|
||||||
|
labelClasses = 'text-slate-400';
|
||||||
|
} else if (isActive) {
|
||||||
|
circleClasses = `${branchLabel ? (branchColor === 'rose' ? 'bg-rose-500' : branchColor === 'amber' ? 'bg-amber-500' : 'bg-slate-400') : 'bg-indigo-600'} text-white ring-4 ring-offset-1 ${activeColor.ring}`;
|
||||||
|
labelClasses = `${activeColor.text} font-black`;
|
||||||
|
} else {
|
||||||
|
circleClasses = 'bg-slate-100 text-slate-300';
|
||||||
|
labelClasses = 'text-slate-300';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="flex-1 flex flex-col items-center relative">
|
||||||
|
<div class="flex items-center w-full">
|
||||||
|
<div class="flex-1 h-0.5 ${i === 0 ? 'bg-transparent' : (beforeColored ? 'bg-indigo-600' : 'bg-slate-200')}"></div>
|
||||||
|
<div class="w-6 h-6 rounded-full flex items-center justify-center shrink-0 z-10 ${circleClasses}">
|
||||||
|
<i data-lucide="${isDone ? 'check' : step.icon}" class="w-3 h-3"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 h-0.5 ${i === steps.length - 1 ? 'bg-transparent' : (afterColored ? 'bg-indigo-600' : 'bg-slate-200')}"></div>
|
||||||
|
</div>
|
||||||
|
<p class="text-[7px] font-black uppercase tracking-tight mt-1.5 text-center leading-tight ${labelClasses}">${step.label}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
const branchHtml = branchLabel ? `
|
||||||
|
<div class="mt-3 flex items-center gap-1.5 justify-center">
|
||||||
|
<i data-lucide="${activeColor.icon}" class="w-3 h-3 ${activeColor.text}"></i>
|
||||||
|
<p class="text-[9px] font-bold ${activeColor.text}">${branchLabel}</p>
|
||||||
|
</div>
|
||||||
|
` : '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="mt-4 pt-4 border-t border-slate-100">
|
||||||
|
<div class="flex items-start">${stepsHtml}</div>
|
||||||
|
${branchHtml}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
// --- RENDER PRINCIPAL DE AVISOS ---
|
// --- RENDER PRINCIPAL DE AVISOS ---
|
||||||
function renderPortal(client, company, allServices) {
|
function renderPortal(client, company, allServices) {
|
||||||
|
|
||||||
@@ -1174,6 +1285,19 @@ let cName = client && client.name ? client.name : "Cliente";
|
|||||||
let hasDate = (srv.scheduled_date && srv.scheduled_time);
|
let hasDate = (srv.scheduled_date && srv.scheduled_time);
|
||||||
let hasWorker = (srv.assigned_worker && srv.assigned_worker !== 'Pendiente' && srv.assigned_worker !== 'Sin asignar');
|
let hasWorker = (srv.assigned_worker && srv.assigned_worker !== 'Pendiente' && srv.assigned_worker !== 'Sin asignar');
|
||||||
let isUrgent = (srv.title && srv.title.includes('URGENTE')) || srv.is_urgent === true || (raw['Urgente'] && (raw['Urgente'].toLowerCase() === 'sí' || raw['Urgente'].toLowerCase() === 'si' || raw['Urgente'].toLowerCase() === 'true'));
|
let isUrgent = (srv.title && srv.title.includes('URGENTE')) || srv.is_urgent === true || (raw['Urgente'] && (raw['Urgente'].toLowerCase() === 'sí' || raw['Urgente'].toLowerCase() === 'si' || raw['Urgente'].toLowerCase() === 'true'));
|
||||||
|
|
||||||
|
// 🛑 NUEVO: Calculado aquí arriba para poder reutilizarlo tanto en la tarjeta de estado como en la línea de tiempo visual
|
||||||
|
let isLate = false;
|
||||||
|
if (hasDate) {
|
||||||
|
let endTChk = addOneHour(srv.scheduled_time);
|
||||||
|
let nowChk = new Date();
|
||||||
|
let schedPartsChk = srv.scheduled_date.split('-');
|
||||||
|
let endTimePartsChk = endTChk.split(':');
|
||||||
|
if (schedPartsChk.length === 3 && endTimePartsChk.length === 2) {
|
||||||
|
let limitDateChk = new Date(schedPartsChk[0], schedPartsChk[1] - 1, schedPartsChk[2], endTimePartsChk[0], endTimePartsChk[1], 0);
|
||||||
|
if (nowChk > limitDateChk) isLate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Recopilar dirección limpia para mostrar
|
// Recopilar dirección limpia para mostrar
|
||||||
let cAddr = raw["Dirección"] || raw["DOMICILIO"] || "Dirección no especificada";
|
let cAddr = raw["Dirección"] || raw["DOMICILIO"] || "Dirección no especificada";
|
||||||
@@ -1209,7 +1333,7 @@ let cName = client && client.name ? client.name : "Cliente";
|
|||||||
else if (stNameLower.includes('camino')) {
|
else if (stNameLower.includes('camino')) {
|
||||||
headerColor = "bg-indigo-100 text-indigo-600"; icon = "truck"; tagTitle = "Desplazamiento"; mainTitle = "¡En camino!"; subDesc = "El técnico se dirige a tu domicilio.";
|
headerColor = "bg-indigo-100 text-indigo-600"; icon = "truck"; tagTitle = "Desplazamiento"; mainTitle = "¡En camino!"; subDesc = "El técnico se dirige a tu domicilio.";
|
||||||
etasToInit.push({ id: srv.id, address: fullAddress });
|
etasToInit.push({ id: srv.id, address: fullAddress });
|
||||||
extras = `<div id="eta-container-${srv.id}" class="mt-3 bg-indigo-50 rounded-xl p-3 border border-indigo-100"><p class="text-[10px] font-bold text-indigo-500 flex items-center gap-1.5"><i data-lucide="loader-2" class="w-3 h-3 animate-spin"></i> Calculando ruta...</p></div>`;
|
extras = `<div id="map-container-${srv.id}" class="mt-3 rounded-xl overflow-hidden border border-indigo-100" style="height:190px; width:100%;"></div><div id="eta-container-${srv.id}" class="mt-3 bg-indigo-50 rounded-xl p-3 border border-indigo-100"><p class="text-[10px] font-bold text-indigo-500 flex items-center gap-1.5"><i data-lucide="loader-2" class="w-3 h-3 animate-spin"></i> Calculando ruta...</p></div>`;
|
||||||
}
|
}
|
||||||
else if (stNameLower.includes('trabajando') || stNameLower.includes('reparación')) {
|
else if (stNameLower.includes('trabajando') || stNameLower.includes('reparación')) {
|
||||||
headerColor = "bg-orange-100 text-orange-600"; icon = "hammer"; tagTitle = "En Domicilio"; mainTitle = "Trabajando"; subDesc = "El técnico está realizando la reparación.";
|
headerColor = "bg-orange-100 text-orange-600"; icon = "hammer"; tagTitle = "En Domicilio"; mainTitle = "Trabajando"; subDesc = "El técnico está realizando la reparación.";
|
||||||
@@ -1225,15 +1349,6 @@ let cName = client && client.name ? client.name : "Cliente";
|
|||||||
}
|
}
|
||||||
else if (hasDate && !stNameLower.includes('anulado') && !stNameLower.includes('desasignado')) {
|
else if (hasDate && !stNameLower.includes('anulado') && !stNameLower.includes('desasignado')) {
|
||||||
let endT = addOneHour(srv.scheduled_time);
|
let endT = addOneHour(srv.scheduled_time);
|
||||||
let now = new Date();
|
|
||||||
let schedParts = srv.scheduled_date.split('-');
|
|
||||||
let endTimeParts = endT.split(':');
|
|
||||||
|
|
||||||
let isLate = false;
|
|
||||||
if (schedParts.length === 3 && endTimeParts.length === 2) {
|
|
||||||
let limitDate = new Date(schedParts[0], schedParts[1] - 1, schedParts[2], endTimeParts[0], endTimeParts[1], 0);
|
|
||||||
if (now > limitDate) isLate = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLate) {
|
if (isLate) {
|
||||||
headerColor = "bg-amber-100 text-amber-600"; icon = "clock-4"; tagTitle = "Demora"; mainTitle = "Técnico Retrasado"; subDesc = `La cita estaba prevista hasta las ${endT}. Llegará lo antes posible.`;
|
headerColor = "bg-amber-100 text-amber-600"; icon = "clock-4"; tagTitle = "Demora"; mainTitle = "Técnico Retrasado"; subDesc = `La cita estaba prevista hasta las ${endT}. Llegará lo antes posible.`;
|
||||||
@@ -1293,10 +1408,13 @@ let cName = client && client.name ? client.name : "Cliente";
|
|||||||
if (!numRef) numRef = srv.id; // Si falla todo, ponemos el ID interno
|
if (!numRef) numRef = srv.id; // Si falla todo, ponemos el ID interno
|
||||||
|
|
||||||
// 🟢 MEDALLA DE PAGO ONLINE
|
// 🟢 MEDALLA DE PAGO ONLINE
|
||||||
let paidBadgeHtml = raw.is_paid
|
let paidBadgeHtml = raw.is_paid
|
||||||
? `<div class="absolute -top-3 right-6 bg-emerald-500 text-white text-[9px] font-black px-3 py-1.5 rounded-full shadow-md uppercase tracking-widest flex items-center gap-1 border-2 border-white z-10"><i data-lucide="badge-check" class="w-3 h-3"></i> Pagado Online</div>`
|
? `<div class="absolute -top-3 right-6 bg-emerald-500 text-white text-[9px] font-black px-3 py-1.5 rounded-full shadow-md uppercase tracking-widest flex items-center gap-1 border-2 border-white z-10"><i data-lucide="badge-check" class="w-3 h-3"></i> Pagado Online</div>`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
|
// 🛑 NUEVO: Línea de tiempo visual del proceso
|
||||||
|
let timelineHtml = renderStatusTimeline({ stNameLower, hasDate, hasWorker, isFinalized, isLate });
|
||||||
|
|
||||||
// Generación de la tarjeta Blanca
|
// Generación de la tarjeta Blanca
|
||||||
let cardHtml = `
|
let cardHtml = `
|
||||||
<div id="service-card-${srv.id}" class="bg-white rounded-[2rem] p-6 shadow-sm border border-slate-100 relative text-left transition-all duration-500 mt-3">
|
<div id="service-card-${srv.id}" class="bg-white rounded-[2rem] p-6 shadow-sm border border-slate-100 relative text-left transition-all duration-500 mt-3">
|
||||||
@@ -1325,6 +1443,8 @@ let cName = client && client.name ? client.name : "Cliente";
|
|||||||
</div>
|
</div>
|
||||||
${extras}
|
${extras}
|
||||||
|
|
||||||
|
${timelineHtml}
|
||||||
|
|
||||||
<hr class="border-slate-100 my-5">
|
<hr class="border-slate-100 my-5">
|
||||||
|
|
||||||
<p class="text-[10px] font-black text-slate-300 uppercase tracking-widest mb-3">Detalle de Avería</p>
|
<p class="text-[10px] font-black text-slate-300 uppercase tracking-widest mb-3">Detalle de Avería</p>
|
||||||
@@ -1375,89 +1495,125 @@ let cName = client && client.name ? client.name : "Cliente";
|
|||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- SISTEMA GPS ---
|
// --- MAPA EN VIVO (LEAFLET / OPENSTREETMAP) ---
|
||||||
|
window.portalMaps = window.portalMaps || {};
|
||||||
|
|
||||||
|
function updateServiceMap(serviceId, workerLat, workerLng, clientLat, clientLng) {
|
||||||
|
const mapDiv = document.getElementById(`map-container-${serviceId}`);
|
||||||
|
if (!mapDiv || typeof L === 'undefined') return;
|
||||||
|
|
||||||
|
if (!window.portalMaps[serviceId]) {
|
||||||
|
const map = L.map(mapDiv, { zoomControl: false, attributionControl: false, dragging: true, scrollWheelZoom: false });
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(map);
|
||||||
|
|
||||||
|
const truckIcon = L.divIcon({
|
||||||
|
className: '',
|
||||||
|
html: '<div style="background:#4f46e5;width:34px;height:34px;border-radius:50%;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 10px rgba(79,70,229,0.45);border:2px solid white;"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10 17h4V5H2v12h3"/><path d="M20 17h2v-3.34a4 4 0 0 0-1.17-2.83L19 9h-5v8h1"/><circle cx="7.5" cy="17.5" r="2.5"/><circle cx="17.5" cy="17.5" r="2.5"/></svg></div>',
|
||||||
|
iconSize: [34, 34], iconAnchor: [17, 17]
|
||||||
|
});
|
||||||
|
const homeIcon = L.divIcon({
|
||||||
|
className: '',
|
||||||
|
html: '<div style="background:#0f172a;width:26px;height:26px;border-radius:50%;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 8px rgba(0,0,0,0.35);border:2px solid white;"><svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg></div>',
|
||||||
|
iconSize: [26, 26], iconAnchor: [13, 13]
|
||||||
|
});
|
||||||
|
|
||||||
|
const workerMarker = L.marker([workerLat, workerLng], { icon: truckIcon }).addTo(map);
|
||||||
|
const homeMarker = L.marker([clientLat, clientLng], { icon: homeIcon }).addTo(map);
|
||||||
|
const line = L.polyline([[workerLat, workerLng], [clientLat, clientLng]], { color: '#818cf8', weight: 3, dashArray: '6,7', opacity: 0.85 }).addTo(map);
|
||||||
|
|
||||||
|
map.fitBounds(line.getBounds(), { padding: [36, 36] });
|
||||||
|
|
||||||
|
window.portalMaps[serviceId] = { map, workerMarker, homeMarker, line };
|
||||||
|
} else {
|
||||||
|
const inst = window.portalMaps[serviceId];
|
||||||
|
inst.workerMarker.setLatLng([workerLat, workerLng]);
|
||||||
|
inst.line.setLatLngs([[workerLat, workerLng], [clientLat, clientLng]]);
|
||||||
|
inst.map.fitBounds(inst.line.getBounds(), { padding: [36, 36] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- SISTEMA GPS (ETA + MAPA EN VIVO) ---
|
||||||
async function calculateClientETA(serviceId, destAddress) {
|
async function calculateClientETA(serviceId, destAddress) {
|
||||||
const container = document.getElementById(`eta-container-${serviceId}`);
|
const container = document.getElementById(`eta-container-${serviceId}`);
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
|
// Geocodificamos el domicilio del cliente UNA sola vez (no cambia entre refrescos)
|
||||||
|
let cLat = null, cLon = null;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}/public/portal/${urlToken}/location/${serviceId}`);
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
if (!data.ok || !data.location) {
|
|
||||||
container.innerHTML = `<p class="text-xs font-bold text-indigo-600 leading-tight">El técnico está en camino hacia tu domicilio.</p>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const wLat = parseFloat(data.location.lat);
|
|
||||||
const wLon = parseFloat(data.location.lng);
|
|
||||||
|
|
||||||
let geoRes = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(destAddress + ', España')}`);
|
let geoRes = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(destAddress + ', España')}`);
|
||||||
let geoData = await geoRes.json();
|
let geoData = await geoRes.json();
|
||||||
|
|
||||||
if (!geoData || geoData.length === 0) {
|
if (!geoData || geoData.length === 0) {
|
||||||
const parts = destAddress.split(',');
|
const parts = destAddress.split(',');
|
||||||
const fallbackDest = parts.length > 1 ? parts[parts.length - 1].trim() : destAddress;
|
const fallbackDest = parts.length > 1 ? parts[parts.length - 1].trim() : destAddress;
|
||||||
geoRes = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(fallbackDest + ', España')}`);
|
geoRes = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(fallbackDest + ', España')}`);
|
||||||
geoData = await geoRes.json();
|
geoData = await geoRes.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (geoData && geoData.length > 0) {
|
if (geoData && geoData.length > 0) {
|
||||||
const cLat = parseFloat(geoData[0].lat);
|
cLat = parseFloat(geoData[0].lat);
|
||||||
const cLon = parseFloat(geoData[0].lon);
|
cLon = parseFloat(geoData[0].lon);
|
||||||
|
}
|
||||||
const R = 6371;
|
} catch (e) { /* seguimos sin coordenadas de destino */ }
|
||||||
const dLat = (cLat - wLat) * Math.PI / 180;
|
|
||||||
const dLon = (cLon - wLon) * Math.PI / 180;
|
let totalMins = null;
|
||||||
const a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(wLat * Math.PI / 180) * Math.cos(cLat * Math.PI / 180) * Math.sin(dLon/2) * Math.sin(dLon/2);
|
|
||||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
async function tick() {
|
||||||
const km = R * c;
|
try {
|
||||||
|
const res = await fetch(`${API_URL}/public/portal/${urlToken}/location/${serviceId}`);
|
||||||
const totalMins = Math.round((km/35)*60) + 5;
|
const data = await res.json();
|
||||||
|
|
||||||
let startedAt = new Date().getTime();
|
if (!data.ok || !data.location) {
|
||||||
if (data.location.updated_at) {
|
container.innerHTML = `<p class="text-xs font-bold text-indigo-600 leading-tight">El técnico está en camino hacia tu domicilio.</p>`;
|
||||||
const parsed = new Date(data.location.updated_at).getTime();
|
return;
|
||||||
if (!isNaN(parsed)) startedAt = parsed;
|
}
|
||||||
}
|
|
||||||
|
const wLat = parseFloat(data.location.lat);
|
||||||
function renderETA() {
|
const wLon = parseFloat(data.location.lng);
|
||||||
const now = new Date().getTime();
|
|
||||||
const diffMins = Math.floor((now - startedAt) / 60000);
|
if (cLat === null || cLon === null) {
|
||||||
|
container.innerHTML = `<p class="text-xs font-bold text-indigo-600 leading-tight">El técnico está de camino.</p>`;
|
||||||
let remainingMins = totalMins - diffMins;
|
return;
|
||||||
if (remainingMins < 1) remainingMins = 1;
|
}
|
||||||
|
|
||||||
let progressPercent = (diffMins / totalMins) * 100;
|
const R = 6371;
|
||||||
if (progressPercent > 95) progressPercent = 95;
|
const dLat = (cLat - wLat) * Math.PI / 180;
|
||||||
if (progressPercent < 5) progressPercent = 5;
|
const dLon = (cLon - wLon) * Math.PI / 180;
|
||||||
|
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(wLat * Math.PI / 180) * Math.cos(cLat * Math.PI / 180) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||||
container.innerHTML = `
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||||
<p class="text-[10px] font-black text-indigo-500 uppercase tracking-widest mb-2 flex items-center gap-1.5">
|
const km = R * c;
|
||||||
<i data-lucide="clock" class="w-3.5 h-3.5"></i> Llegada en aprox. <span class="text-indigo-700 text-sm ml-0.5">${remainingMins} min</span>
|
|
||||||
</p>
|
const remainingMins = Math.max(1, Math.round((km / 35) * 60) + 5);
|
||||||
<div class="w-full bg-white rounded-full h-2 overflow-hidden shadow-inner border border-indigo-100">
|
if (totalMins === null) totalMins = remainingMins;
|
||||||
<div class="bg-indigo-500 h-full rounded-full transition-all duration-1000 relative" style="width: ${progressPercent}%">
|
|
||||||
<div class="absolute right-0 top-0 bottom-0 w-4 bg-white/40 animate-pulse"></div>
|
let progressPercent = totalMins ? ((totalMins - remainingMins) / totalMins) * 100 : 5;
|
||||||
</div>
|
if (progressPercent > 95) progressPercent = 95;
|
||||||
</div>
|
if (progressPercent < 5) progressPercent = 5;
|
||||||
<div class="flex justify-between items-center mt-1.5 px-1">
|
|
||||||
<p class="text-[8px] font-black uppercase text-indigo-300">Saliendo</p>
|
container.innerHTML = `
|
||||||
<p class="text-[8px] font-black uppercase text-indigo-400">A ${km.toFixed(1)} km</p>
|
<p class="text-[10px] font-black text-indigo-500 uppercase tracking-widest mb-2 flex items-center gap-1.5">
|
||||||
</div>
|
<i data-lucide="clock" class="w-3.5 h-3.5"></i> Llegada en aprox. <span class="text-indigo-700 text-sm ml-0.5">${remainingMins} min</span>
|
||||||
`;
|
</p>
|
||||||
lucide.createIcons();
|
<div class="w-full bg-white rounded-full h-2 overflow-hidden shadow-inner border border-indigo-100">
|
||||||
}
|
<div class="bg-indigo-500 h-full rounded-full transition-all duration-1000 relative" style="width: ${progressPercent}%">
|
||||||
|
<div class="absolute right-0 top-0 bottom-0 w-4 bg-white/40 animate-pulse"></div>
|
||||||
renderETA();
|
</div>
|
||||||
setInterval(renderETA, 60000);
|
</div>
|
||||||
|
<div class="flex justify-between items-center mt-1.5 px-1">
|
||||||
} else {
|
<p class="text-[8px] font-black uppercase text-indigo-300">Saliendo</p>
|
||||||
container.innerHTML = `<p class="text-xs font-bold text-indigo-600 leading-tight">El técnico está de camino.</p>`;
|
<p class="text-[8px] font-black uppercase text-indigo-400">A ${km.toFixed(1)} km</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
lucide.createIcons();
|
||||||
|
|
||||||
|
updateServiceMap(serviceId, wLat, wLon, cLat, cLon);
|
||||||
|
} catch (e) {
|
||||||
|
if (container) container.innerHTML = `<p class="text-xs font-bold text-indigo-600 leading-tight">El técnico está de camino.</p>`;
|
||||||
}
|
}
|
||||||
} catch(e) {
|
|
||||||
if (container) container.innerHTML = `<p class="text-xs font-bold text-indigo-600 leading-tight">El técnico está de camino.</p>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tick();
|
||||||
|
setInterval(tick, 30000);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user