348 lines
18 KiB
HTML
348 lines
18 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
|
<title>Ficha del Cliente - IntegraRepara</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script src="https://unpkg.com/lucide@latest"></script>
|
|
<script src="js/layout.js"></script>
|
|
<style>
|
|
:root {
|
|
--primary: #1e3a8a;
|
|
--secondary: #2563eb;
|
|
--app-bg: #f8fafc;
|
|
}
|
|
body { background-color: var(--app-bg); -webkit-tap-highlight-color: transparent; }
|
|
.fade-in { animation: fadeIn 0.3s ease-out forwards; }
|
|
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
|
.no-scrollbar::-webkit-scrollbar { display: none; }
|
|
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
|
.main-content { padding-bottom: 60px; }
|
|
.bg-primary-dynamic { background-color: var(--primary) !important; }
|
|
.text-primary-dynamic { color: var(--primary) !important; }
|
|
.border-primary-dynamic { border-color: var(--primary) !important; }
|
|
</style>
|
|
</head>
|
|
<body class="text-slate-800 font-sans antialiased h-screen flex flex-col overflow-hidden relative text-left">
|
|
|
|
<header class="bg-white px-5 pt-safe mt-6 pb-4 shadow-sm z-20 shrink-0 border-b border-slate-200">
|
|
<div class="flex items-center gap-3">
|
|
<button onclick="volver()" class="w-10 h-10 shrink-0 bg-slate-50 rounded-full flex items-center justify-center text-slate-600 border border-slate-200 active:scale-95 transition-transform">
|
|
<i data-lucide="arrow-left" class="w-5 h-5"></i>
|
|
</button>
|
|
<div class="flex-1 min-w-0 text-left">
|
|
<p class="text-[10px] font-black text-primary-dynamic uppercase tracking-widest mb-0.5">Ficha del Cliente</p>
|
|
<h1 class="text-xl font-black tracking-tight text-slate-900 leading-none truncate" id="headerName">Cargando...</h1>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="flex-1 overflow-y-auto no-scrollbar p-5 main-content relative z-10" id="mainArea">
|
|
|
|
<div id="loader" class="text-center py-16 opacity-50">
|
|
<i data-lucide="loader-2" class="w-8 h-8 animate-spin mx-auto mb-2 text-primary-dynamic"></i>
|
|
<p class="text-xs font-black uppercase tracking-widest text-slate-400">Cargando ficha...</p>
|
|
</div>
|
|
|
|
<div id="errorBox" class="hidden bg-rose-50 border border-rose-200 text-rose-700 rounded-2xl p-5 text-sm font-bold text-center"></div>
|
|
|
|
<div id="content" class="hidden flex flex-col gap-5 fade-in">
|
|
|
|
<!-- Datos de contacto -->
|
|
<div class="bg-white p-6 rounded-[2rem] shadow-sm border border-slate-200 text-left">
|
|
<div class="w-14 h-14 bg-blue-100 text-blue-600 rounded-2xl flex items-center justify-center mb-4 shadow-inner">
|
|
<i data-lucide="user-round" class="w-7 h-7"></i>
|
|
</div>
|
|
<h2 id="clientName" class="text-2xl font-black text-slate-900 uppercase leading-none mb-1">--</h2>
|
|
<p id="clientPhone" class="text-sm font-bold text-slate-400 mb-4">--</p>
|
|
|
|
<div id="addressesBox" class="flex flex-col gap-2 mb-5"></div>
|
|
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<button onclick="llamarCliente()" class="bg-primary-dynamic text-white font-black py-3.5 rounded-2xl shadow-md flex items-center justify-center gap-2 uppercase text-xs tracking-widest active:scale-95 transition-all">
|
|
<i data-lucide="phone" class="w-4 h-4 fill-current"></i> Llamar
|
|
</button>
|
|
<button onclick="abrirWhatsAppCliente()" class="bg-slate-50 text-slate-700 font-black py-3.5 rounded-2xl border border-slate-200 flex items-center justify-center gap-2 text-xs uppercase tracking-widest active:scale-95 transition-all">
|
|
<i data-lucide="message-circle" class="w-4 h-4 text-emerald-500"></i> WhatsApp
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Servicios anteriores -->
|
|
<div class="bg-white rounded-[2rem] shadow-sm border border-slate-200 overflow-hidden text-left">
|
|
<div class="p-5 border-b border-slate-100 flex items-center justify-between bg-slate-50/50">
|
|
<div class="flex items-center gap-2 text-slate-800">
|
|
<i data-lucide="history" class="w-5 h-5 text-primary-dynamic"></i>
|
|
<h3 class="font-black text-sm uppercase tracking-wider">Servicios Anteriores</h3>
|
|
</div>
|
|
<span id="servicesCount" class="bg-slate-100 text-slate-500 text-[10px] font-black px-2.5 py-1 rounded-full uppercase tracking-widest">0</span>
|
|
</div>
|
|
<div id="servicesList" class="p-4 flex flex-col gap-3"></div>
|
|
<div id="servicesEmpty" class="hidden p-8 text-center text-xs font-bold text-slate-400 uppercase tracking-widest">Sin servicios anteriores</div>
|
|
</div>
|
|
|
|
<!-- Notas del equipo -->
|
|
<div class="bg-white rounded-[2rem] shadow-sm border border-slate-200 overflow-hidden text-left">
|
|
<div class="p-5 border-b border-slate-100 flex items-center gap-2 bg-amber-50/50">
|
|
<i data-lucide="notebook-pen" class="w-5 h-5 text-amber-600"></i>
|
|
<h3 class="font-black text-sm uppercase tracking-wider text-slate-800">Notas del Equipo</h3>
|
|
</div>
|
|
|
|
<div class="p-4 border-b border-slate-100 space-y-3">
|
|
<textarea id="noteInput" rows="3" placeholder="Apunta aquí cualquier cosa útil sobre este cliente (avisos, incidencias, preferencias de horario...)" class="w-full bg-slate-50 border border-slate-200 rounded-xl p-3 text-sm font-medium outline-none focus:ring-2 focus:ring-primary-dynamic resize-none"></textarea>
|
|
<button onclick="guardarNota()" id="btnGuardarNota" class="w-full bg-amber-500 hover:bg-amber-600 text-white font-black py-3 rounded-xl text-xs uppercase tracking-widest shadow-md active:scale-95 transition-all flex items-center justify-center gap-2">
|
|
<i data-lucide="plus" class="w-4 h-4"></i> Añadir Nota
|
|
</button>
|
|
</div>
|
|
|
|
<div id="notesList" class="p-4 flex flex-col gap-3"></div>
|
|
<div id="notesEmpty" class="hidden p-8 text-center text-xs font-bold text-slate-400 uppercase tracking-widest">Todavía no hay notas de este cliente</div>
|
|
</div>
|
|
|
|
</div>
|
|
</main>
|
|
|
|
<script>
|
|
const API_URL = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
|
|
? 'http://localhost:3000'
|
|
: 'https://integrarepara-api.integrarepara.es';
|
|
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const phone = (urlParams.get('phone') || '').trim();
|
|
const nombreInicial = urlParams.get('nombre') || '';
|
|
const direccionInicial = urlParams.get('direccion') || '';
|
|
|
|
let currentClientId = null;
|
|
let currentClientPhone = phone;
|
|
|
|
const authHeaders = () => ({ 'Authorization': `Bearer ${localStorage.getItem('token')}` });
|
|
const authHeadersJson = () => ({ 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('token')}` });
|
|
|
|
const colorDict = {
|
|
'gray': { bg: 'bg-slate-100', text: 'text-slate-600' },
|
|
'blue': { bg: 'bg-blue-100', text: 'text-blue-700' },
|
|
'emerald': { bg: 'bg-emerald-100', text: 'text-emerald-700' },
|
|
'amber': { bg: 'bg-amber-100', text: 'text-amber-700' },
|
|
'orange': { bg: 'bg-orange-100', text: 'text-orange-700' },
|
|
'red': { bg: 'bg-red-100', text: 'text-red-700' },
|
|
'purple': { bg: 'bg-purple-100', text: 'text-purple-700' },
|
|
'indigo': { bg: 'bg-indigo-100', text: 'text-indigo-700' },
|
|
'rose': { bg: 'bg-rose-100', text: 'text-rose-700' }
|
|
};
|
|
|
|
const providerLabels = { multiasistencia: 'Multiasistencia', homeserve: 'HomeServe' };
|
|
|
|
function safeLoadIcons() {
|
|
try { if (typeof lucide !== 'undefined') lucide.createIcons(); } catch (e) {}
|
|
}
|
|
|
|
function volver() {
|
|
if (window.history.length > 1) window.history.back();
|
|
else window.location.href = 'calendario.html';
|
|
}
|
|
|
|
async function applyTheme() {
|
|
try {
|
|
let theme = JSON.parse(localStorage.getItem('app_theme'));
|
|
if (theme) {
|
|
document.documentElement.style.setProperty('--primary', theme.primary);
|
|
document.documentElement.style.setProperty('--secondary', theme.secondary);
|
|
document.documentElement.style.setProperty('--app-bg', theme.bg);
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
|
|
function mostrarError(msg) {
|
|
document.getElementById('loader').classList.add('hidden');
|
|
const box = document.getElementById('errorBox');
|
|
box.textContent = msg;
|
|
box.classList.remove('hidden');
|
|
}
|
|
|
|
async function cargarFicha() {
|
|
if (!phone) { mostrarError('No se ha indicado un teléfono de cliente para esta ficha.'); return; }
|
|
try {
|
|
// 1. Localizamos (o creamos) el cliente a partir del teléfono
|
|
const ensureRes = await fetch(`${API_URL}/clients/ensure`, {
|
|
method: 'POST',
|
|
headers: authHeadersJson(),
|
|
body: JSON.stringify({ phone, name: nombreInicial, address: direccionInicial })
|
|
});
|
|
const ensureData = await ensureRes.json();
|
|
if (!ensureData.ok || !ensureData.client || !ensureData.client.id) {
|
|
throw new Error('No se ha podido identificar al cliente');
|
|
}
|
|
currentClientId = ensureData.client.id;
|
|
|
|
// 2. Cargamos ficha + historial de servicios y notas en paralelo
|
|
const [histRes, notesRes] = await Promise.all([
|
|
fetch(`${API_URL}/clients/${currentClientId}/history`, { headers: authHeaders() }),
|
|
fetch(`${API_URL}/clients/${currentClientId}/notes`, { headers: authHeaders() })
|
|
]);
|
|
const histData = await histRes.json();
|
|
const notesData = await notesRes.json();
|
|
|
|
if (!histData.ok) throw new Error('No se ha podido cargar el historial del cliente');
|
|
|
|
renderClientInfo(histData.client);
|
|
renderServices(histData.services || []);
|
|
renderNotes(notesData.ok ? (notesData.notes || []) : []);
|
|
|
|
document.getElementById('loader').classList.add('hidden');
|
|
document.getElementById('content').classList.remove('hidden');
|
|
safeLoadIcons();
|
|
} catch (e) {
|
|
console.error(e);
|
|
mostrarError('No se ha podido cargar la ficha del cliente. Comprueba tu conexión e inténtalo de nuevo.');
|
|
}
|
|
}
|
|
|
|
function renderClientInfo(client) {
|
|
const nombre = client.full_name || nombreInicial || 'Cliente';
|
|
document.getElementById('headerName').innerText = nombre;
|
|
document.getElementById('clientName').innerText = nombre;
|
|
document.getElementById('clientPhone').innerText = client.phone || phone;
|
|
currentClientPhone = client.phone || phone;
|
|
|
|
const addrBox = document.getElementById('addressesBox');
|
|
let addresses = [];
|
|
try { addresses = Array.isArray(client.addresses) ? client.addresses : JSON.parse(client.addresses || '[]'); } catch (e) { addresses = []; }
|
|
addresses = addresses.filter(a => a && String(a).trim() !== '');
|
|
if (addresses.length === 0 && direccionInicial) addresses = [direccionInicial];
|
|
|
|
if (addresses.length === 0) {
|
|
addrBox.innerHTML = '<p class="text-xs text-slate-400 font-medium">Sin dirección registrada.</p>';
|
|
} else {
|
|
addrBox.innerHTML = addresses.map(a => `
|
|
<div class="flex items-start gap-2 bg-slate-50 border border-slate-100 rounded-xl p-3">
|
|
<i data-lucide="map-pin" class="w-4 h-4 text-slate-400 shrink-0 mt-0.5"></i>
|
|
<span class="text-xs font-bold text-slate-600 uppercase leading-tight">${a}</span>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
}
|
|
|
|
function formatFecha(iso) {
|
|
if (!iso) return '--';
|
|
const d = new Date(iso);
|
|
return d.toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
|
}
|
|
|
|
function renderServices(services) {
|
|
document.getElementById('servicesCount').innerText = services.length;
|
|
const list = document.getElementById('servicesList');
|
|
const empty = document.getElementById('servicesEmpty');
|
|
|
|
if (services.length === 0) {
|
|
list.innerHTML = '';
|
|
empty.classList.remove('hidden');
|
|
return;
|
|
}
|
|
empty.classList.add('hidden');
|
|
|
|
list.innerHTML = services.map(s => {
|
|
const raw = s.raw_data || {};
|
|
const compania = raw['Compañía'] || raw['COMPAÑIA'] || providerLabels[s.provider] || s.provider || 'Sin compañía';
|
|
const direccion = raw['Dirección'] || raw['DOMICILIO'] || '';
|
|
const desc = (raw['Descripción'] || raw['DESCRIPCION'] || '').trim();
|
|
const colorInfo = colorDict[s.status_color] || colorDict['gray'];
|
|
const estado = s.status_name || (s.status === 'archived' ? 'Archivado' : 'Sin estado');
|
|
|
|
return `
|
|
<div class="bg-slate-50 border border-slate-100 rounded-2xl p-4 flex flex-col gap-2">
|
|
<div class="flex items-center justify-between gap-2">
|
|
<span class="text-[9px] font-black text-slate-400 uppercase tracking-widest">${formatFecha(s.created_at)} · ${providerLabels[s.provider] || s.provider || ''}</span>
|
|
<span class="text-[9px] font-black ${colorInfo.bg} ${colorInfo.text} px-2.5 py-1 rounded-full uppercase tracking-widest shrink-0">${estado}</span>
|
|
</div>
|
|
<p class="text-sm font-black text-slate-800 uppercase leading-tight">${compania}</p>
|
|
${direccion ? `<p class="text-xs font-bold text-slate-500 uppercase leading-tight">${direccion}</p>` : ''}
|
|
${desc ? `<p class="text-xs font-medium text-slate-600 leading-snug line-clamp-2">${desc}</p>` : ''}
|
|
${s.service_ref ? `<span class="text-[9px] font-black text-slate-400 uppercase tracking-widest">Ref. #${s.service_ref}</span>` : ''}
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
function renderNotes(notes) {
|
|
const list = document.getElementById('notesList');
|
|
const empty = document.getElementById('notesEmpty');
|
|
|
|
if (notes.length === 0) {
|
|
list.innerHTML = '';
|
|
empty.classList.remove('hidden');
|
|
return;
|
|
}
|
|
empty.classList.add('hidden');
|
|
|
|
list.innerHTML = notes.map(n => {
|
|
const d = new Date(n.created_at);
|
|
const fecha = d.toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit' });
|
|
const hora = d.toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' });
|
|
return `
|
|
<div class="bg-amber-50/60 border border-amber-100 rounded-2xl p-4">
|
|
<div class="flex items-center justify-between mb-1.5">
|
|
<span class="text-[9px] font-black text-amber-700 uppercase tracking-widest">${n.user_name || 'Usuario'}</span>
|
|
<span class="text-[9px] font-bold text-slate-400">${fecha} ${hora}</span>
|
|
</div>
|
|
<p class="text-sm font-medium text-slate-700 whitespace-pre-wrap">${n.note}</p>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
async function guardarNota() {
|
|
const input = document.getElementById('noteInput');
|
|
const note = input.value.trim();
|
|
if (!note) return;
|
|
if (!currentClientId) return;
|
|
|
|
const btn = document.getElementById('btnGuardarNota');
|
|
btn.disabled = true;
|
|
input.disabled = true;
|
|
try {
|
|
const res = await fetch(`${API_URL}/clients/${currentClientId}/notes`, {
|
|
method: 'POST',
|
|
headers: authHeadersJson(),
|
|
body: JSON.stringify({ note })
|
|
});
|
|
const data = await res.json();
|
|
if (data.ok) {
|
|
input.value = '';
|
|
const notesRes = await fetch(`${API_URL}/clients/${currentClientId}/notes`, { headers: authHeaders() });
|
|
const notesData = await notesRes.json();
|
|
renderNotes(notesData.ok ? (notesData.notes || []) : []);
|
|
safeLoadIcons();
|
|
} else {
|
|
alert('No se ha podido guardar la nota.');
|
|
}
|
|
} catch (e) {
|
|
alert('Error de conexión al guardar la nota.');
|
|
} finally {
|
|
btn.disabled = false;
|
|
input.disabled = false;
|
|
}
|
|
}
|
|
|
|
function llamarCliente() {
|
|
if (currentClientPhone) window.location.href = `tel:+34${currentClientPhone.replace(/\D/g, '').slice(-9)}`;
|
|
}
|
|
|
|
function abrirWhatsAppCliente() {
|
|
if (currentClientPhone) window.open(`https://wa.me/34${currentClientPhone.replace(/\D/g, '').slice(-9)}`, '_blank');
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
const role = localStorage.getItem('role');
|
|
if (!localStorage.getItem('token') || (role !== 'operario' && role !== 'operario_cerrado')) {
|
|
window.location.href = 'index.html';
|
|
return;
|
|
}
|
|
safeLoadIcons();
|
|
applyTheme();
|
|
await cargarFicha();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|