Actualizar calendario.html
This commit is contained in:
161
calendario.html
161
calendario.html
@@ -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">
|
||||||
@@ -126,11 +130,12 @@
|
|||||||
<p class="text-[9px] font-black text-primary-dynamic uppercase tracking-widest" id="detCompany">Compañía</p>
|
<p class="text-[9px] font-black text-primary-dynamic uppercase tracking-widest" id="detCompany">Compañía</p>
|
||||||
<span id="detRef" class="text-[9px] font-black text-slate-400 uppercase"></span>
|
<span id="detRef" class="text-[9px] font-black text-slate-400 uppercase"></span>
|
||||||
</div>
|
</div>
|
||||||
<h2 id="detName" class="text-2xl font-black text-slate-900 uppercase leading-none mb-3">Nombre Cliente</h2>
|
<h2 id="detName" class="text-2xl font-black text-slate-900 uppercase leading-none mb-3">Nombre Cliente</h2>
|
||||||
|
|
||||||
<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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user