Actualizar calendario.html

This commit is contained in:
2026-03-07 17:07:46 +00:00
parent ebc622257b
commit b91ccba313

View File

@@ -44,6 +44,10 @@
/* Caja de descripción sobria */ /* Caja de descripción sobria */
.desc-box { background: white; border: 1px solid #e2e8f0; border-radius: 1.5rem; padding: 1.25rem; } .desc-box { background: white; border: 1px solid #e2e8f0; border-radius: 1.5rem; padding: 1.25rem; }
/* Estilos para el chat */
.msg-me { background-color: #dcfce7; border-bottom-right-radius: 4px; border: 1px solid #bbf7d0; align-self: flex-end; }
.msg-other { background-color: #f1f5f9; border-bottom-left-radius: 4px; border: 1px solid #e2e8f0; align-self: flex-start; }
</style> </style>
</head> </head>
<body class="text-slate-800 font-sans antialiased h-screen flex flex-col overflow-hidden relative text-left"> <body class="text-slate-800 font-sans antialiased h-screen flex flex-col overflow-hidden relative text-left">
@@ -131,6 +135,7 @@
<button onclick="copyAndOpenPortal()" id="btnPortal" class="w-full mb-6 bg-indigo-50 text-indigo-700 hover:bg-indigo-100 font-black py-2.5 rounded-xl border border-indigo-200 flex items-center justify-center gap-2 text-[10px] uppercase tracking-widest active:scale-95 transition-all text-center"> <button onclick="copyAndOpenPortal()" id="btnPortal" class="w-full mb-6 bg-indigo-50 text-indigo-700 hover:bg-indigo-100 font-black py-2.5 rounded-xl border border-indigo-200 flex items-center justify-center gap-2 text-[10px] uppercase tracking-widest active:scale-95 transition-all text-center">
<i data-lucide="external-link" class="w-4 h-4"></i> Copiar y Abrir Portal <i data-lucide="external-link" class="w-4 h-4"></i> Copiar y Abrir Portal
</button> </button>
<button onclick="callClient()" class="w-full bg-primary-dynamic text-white font-black py-4 rounded-2xl shadow-xl flex items-center justify-center gap-3 uppercase text-sm tracking-widest active:scale-95 transition-all"> <button onclick="callClient()" class="w-full bg-primary-dynamic text-white font-black py-4 rounded-2xl shadow-xl flex items-center justify-center gap-3 uppercase text-sm tracking-widest active:scale-95 transition-all">
<i data-lucide="phone" class="w-5 h-5 fill-current"></i> Llamar al Cliente <i data-lucide="phone" class="w-5 h-5 fill-current"></i> Llamar al Cliente
</button> </button>
@@ -139,6 +144,25 @@
</button> </button>
</div> </div>
<div class="bg-white border border-slate-200 rounded-3xl overflow-hidden shadow-sm text-left">
<button onclick="toggleChat()" class="w-full p-5 flex justify-between items-center text-xs font-black text-slate-800 uppercase tracking-widest bg-blue-50/50">
<div class="flex items-center gap-2 text-blue-600">
<i data-lucide="message-square" class="w-5 h-5"></i> Chat con Oficina
</div>
<i data-lucide="chevron-down" id="chatChevron" class="w-5 h-5 text-blue-400 transition-transform"></i>
</button>
<div id="chatContainer" class="hidden flex-col h-80 bg-slate-50">
<div id="chatBox" class="flex-1 overflow-y-auto p-4 space-y-3 flex flex-col no-scrollbar bg-slate-100">
</div>
<div class="p-3 bg-white border-t border-slate-200 shrink-0 flex gap-2">
<input type="text" id="chatInput" placeholder="Mensaje para oficina..." class="flex-1 bg-slate-50 border border-slate-200 rounded-xl px-4 py-3 text-sm font-medium outline-none focus:ring-2 focus:ring-blue-500">
<button onclick="sendChatMessage()" class="bg-blue-600 text-white p-3 rounded-xl shadow-md active:scale-95 shrink-0">
<i data-lucide="send" class="w-5 h-5"></i>
</button>
</div>
</div>
</div>
<div class="bg-white p-6 rounded-[2.5rem] shadow-sm border border-slate-200 relative overflow-hidden text-left"> <div class="bg-white p-6 rounded-[2.5rem] shadow-sm border border-slate-200 relative overflow-hidden text-left">
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2">Ubicación</p> <p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2">Ubicación</p>
<p id="detAddress" class="text-base font-bold text-slate-900 uppercase leading-tight mb-6"></p> <p id="detAddress" class="text-base font-bold text-slate-900 uppercase leading-tight mb-6"></p>
@@ -353,7 +377,6 @@
const modalSelect = document.getElementById('detStatusMap'); const modalSelect = document.getElementById('detStatusMap');
modalSelect.innerHTML = ''; modalSelect.innerHTML = '';
systemStatuses.forEach(st => { systemStatuses.forEach(st => {
// Modificación: Solo el nombre en el select para diseño más limpio
modalSelect.innerHTML += `<option value="${st.id}">${st.name.toUpperCase()}</option>`; modalSelect.innerHTML += `<option value="${st.id}">${st.name.toUpperCase()}</option>`;
}); });
} }
@@ -395,7 +418,6 @@
} }
} }
// Mapa de colores (Mismo que en proveedores)
const colorMap = { const colorMap = {
'gray': { text: 'text-slate-500', bg: 'bg-slate-100', border: 'border-slate-200' }, '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' }, 'blue': { text: 'text-blue-600', bg: 'bg-blue-50', border: 'border-blue-100' },
@@ -433,7 +455,6 @@
let compShort = (raw["Compañía"] || "Particular").split('-')[0].trim().substring(0, 15); 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)); const guildObj = systemGuilds.find(g => String(g.id) === String(s.guild_id || raw.guild_id));
// MODIFICACIÓN 1: LÓGICA DE ICONO Y COLOR SEGÚN ESTADO
let iconName = "clock"; let iconName = "clock";
const dbStatId = raw.status_operativo; const dbStatId = raw.status_operativo;
const statusObj = systemStatuses.find(st => String(st.id) === String(dbStatId)); const statusObj = systemStatuses.find(st => String(st.id) === String(dbStatId));
@@ -520,6 +541,11 @@
modal.style.display = 'flex'; modal.style.display = 'flex';
setTimeout(() => modal.classList.remove('translate-y-full'), 10); setTimeout(() => modal.classList.remove('translate-y-full'), 10);
calculateDistance(fullAddress); calculateDistance(fullAddress);
// Cerrar chat al abrir otro servicio
document.getElementById('chatContainer').classList.add('hidden');
document.getElementById('chatChevron').classList.remove('rotate-180');
safeLoadIcons(); safeLoadIcons();
} }
@@ -529,17 +555,15 @@
setTimeout(() => modal.style.display = 'none', 300); setTimeout(() => modal.style.display = 'none', 300);
} }
// --- FUNCIÓN SECRETA DE LOGS ---
function sendLog(action, details) { function sendLog(action, details) {
if(!currentServiceId) return; if(!currentServiceId) return;
fetch(`${API_URL}/services/${currentServiceId}/log`, { fetch(`${API_URL}/services/${currentServiceId}/log`, {
method: 'POST', method: 'POST',
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` }, headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` },
body: JSON.stringify({ action, details }) body: JSON.stringify({ action, details })
}).catch(()=>{}); // Si falla, que no rompa la app, es un log silencioso }).catch(()=>{});
} }
// --- BOTONES MODIFICADOS ---
function callClient() { function callClient() {
const p = document.getElementById('detPhoneRaw').value; const p = document.getElementById('detPhoneRaw').value;
if (p) { if (p) {
@@ -601,6 +625,7 @@
lucide.createIcons(); lucide.createIcons();
} }
} }
function openMaps() { function openMaps() {
const a = document.getElementById('detAddress').innerText; const a = document.getElementById('detAddress').innerText;
if (a) { if (a) {
@@ -698,28 +723,21 @@
} catch (e) { alert("Error al actualizar"); } } catch (e) { alert("Error al actualizar"); }
} }
// MODIFICACIÓN 3: LÓGICA DE FINALIZAR CON PREGUNTA DE ENCUESTA
async function askToFinish() { async function askToFinish() {
if(!currentServiceId) return; if(!currentServiceId) return;
const stFinalizado = systemStatuses.find(s => s.name.toLowerCase().includes('finaliza')); const stFinalizado = systemStatuses.find(s => s.name.toLowerCase().includes('finaliza'));
if(!stFinalizado) return alert("El sistema no tiene un estado Finalizado válido."); 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?"); const sendSurvey = confirm("¿Deseas enviar la encuesta de satisfacción al cliente antes de finalizar?");
try { 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}`, { const res = await fetch(`${API_URL}/services/set-appointment/${currentServiceId}`, {
method: 'PUT', method: 'PUT',
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` }, headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` },
body: JSON.stringify({ body: JSON.stringify({
status_operativo: stFinalizado.id, status_operativo: stFinalizado.id,
skip_survey: !sendSurvey // <- Flag que puedes usar en tu server.js si quieres anularlo skip_survey: !sendSurvey
}) })
}); });
@@ -768,9 +786,112 @@
finally { btn.innerHTML = originalContent; btn.disabled = false; safeLoadIcons(); } finally { btn.innerHTML = originalContent; btn.disabled = false; safeLoadIcons(); }
} }
function showToast(m) { // --- SISTEMA DE CHAT OPERARIO ---
const t = document.getElementById('toast'); document.getElementById('toastMsg').innerText = m; function toggleChat() {
t.classList.remove('opacity-0', 'pointer-events-none', '-translate-y-10'); t.classList.add('translate-y-0'); const container = document.getElementById('chatContainer');
const chevron = document.getElementById('chatChevron');
if (container.classList.contains('hidden')) {
container.classList.remove('hidden');
container.classList.add('flex');
chevron.classList.add('rotate-180');
loadChat(currentServiceId);
} else {
container.classList.add('hidden');
container.classList.remove('flex');
chevron.classList.remove('rotate-180');
}
}
async function loadChat(serviceId) {
const chatBox = document.getElementById('chatBox');
chatBox.innerHTML = '<div class="text-center text-xs font-bold text-slate-400 mt-10 uppercase tracking-widest"><i data-lucide="loader-2" class="w-4 h-4 animate-spin mx-auto mb-2"></i> Cargando...</div>';
safeLoadIcons();
try {
const res = await fetch(`${API_URL}/services/${serviceId}/chat`, {
headers: { 'Authorization': `Bearer ${localStorage.getItem("token")}` }
});
const data = await res.json();
if (data.ok) {
chatBox.innerHTML = '';
if (data.messages.length === 0) {
chatBox.innerHTML = '<div class="text-center text-xs font-bold text-slate-400 mt-10 uppercase tracking-widest">No hay mensajes aún</div>';
return;
}
data.messages.forEach(msg => {
// Ocultar notas internas si por alguna razón el server las mandara
if (msg.is_internal) return;
const isMe = msg.sender_role === 'operario' || msg.sender_role === 'operario_cerrado';
const time = new Date(msg.created_at).toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' });
const date = new Date(msg.created_at).toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit' });
let bubbleClass = isMe ? 'msg-me' : 'msg-other';
let headerColor = isMe ? 'text-emerald-700' : 'text-slate-600';
const bubbleHtml = `
<div class="flex flex-col p-3 shadow-sm ${bubbleClass} max-w-[85%]">
<div class="flex justify-between items-center border-b ${isMe ? 'border-emerald-200' : 'border-slate-200'} pb-1 mb-1.5 gap-4">
<span class="text-[9px] font-black uppercase ${headerColor} truncate">${isMe ? 'Tú' : msg.sender_name}</span>
<span class="text-[8px] font-bold text-slate-400 shrink-0">${date} ${time}</span>
</div>
<p class="text-xs font-medium text-slate-700 whitespace-pre-wrap">${msg.message}</p>
</div>
`;
chatBox.innerHTML += bubbleHtml;
});
setTimeout(() => { chatBox.scrollTop = chatBox.scrollHeight; }, 100);
}
} catch (e) {
chatBox.innerHTML = '<div class="text-center text-xs font-bold text-red-400 mt-10 uppercase tracking-widest">Error de carga</div>';
}
}
async function sendChatMessage() {
if(!currentServiceId) return;
const input = document.getElementById('chatInput');
const message = input.value.trim();
if (!message) return;
input.disabled = true;
try {
const res = await fetch(`${API_URL}/services/${currentServiceId}/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem("token")}` },
body: JSON.stringify({ message, is_internal: false })
});
if (res.ok) {
input.value = '';
loadChat(currentServiceId);
} else {
showToast("Error al enviar mensaje");
}
} catch (e) {
showToast("Error de conexión");
} finally {
input.disabled = false;
}
}
function showToast(m, isError = false) {
const t = document.getElementById('toast');
document.getElementById('toastMsg').innerText = m;
if(isError) {
t.classList.replace('bg-slate-900', 'bg-red-600');
t.innerHTML = `<i data-lucide="alert-circle" class="w-4 h-4 text-white"></i> <span id="toastMsg">${m}</span>`;
} else {
t.classList.replace('bg-red-600', 'bg-slate-900');
t.innerHTML = `<i data-lucide="check" class="w-4 h-4 text-emerald-400"></i> <span id="toastMsg">${m}</span>`;
}
safeLoadIcons();
t.classList.remove('opacity-0', 'pointer-events-none', '-translate-y-10');
t.classList.add('translate-y-0');
setTimeout(() => { t.classList.add('opacity-0', 'pointer-events-none', '-translate-y-10'); t.classList.remove('translate-y-0'); }, 2000); setTimeout(() => { t.classList.add('opacity-0', 'pointer-events-none', '-translate-y-10'); t.classList.remove('translate-y-0'); }, 2000);
} }