Actualizar clientes.html
This commit is contained in:
207
clientes.html
207
clientes.html
@@ -34,6 +34,11 @@
|
|||||||
<i data-lucide="search" class="w-4 h-4 absolute left-3 top-3 text-gray-400"></i>
|
<i data-lucide="search" class="w-4 h-4 absolute left-3 top-3 text-gray-400"></i>
|
||||||
<input type="text" id="searchInput" oninput="debounceSearch()" placeholder="Buscar por nombre, teléfono..." class="w-full pl-10 pr-4 py-2 bg-gray-50 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all">
|
<input type="text" id="searchInput" oninput="debounceSearch()" placeholder="Buscar por nombre, teléfono..." class="w-full pl-10 pr-4 py-2 bg-gray-50 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 flex items-center gap-2 px-1">
|
||||||
|
<input type="checkbox" id="filterMultiAddress" onchange="searchClients()" class="w-4 h-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500 cursor-pointer">
|
||||||
|
<label for="filterMultiAddress" class="text-[10px] font-black text-slate-500 uppercase tracking-widest cursor-pointer select-none">Mostrar solo clientes con + de 1 dirección</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="clientsList" class="flex-1 overflow-y-auto no-scrollbar p-2 space-y-1">
|
<div id="clientsList" class="flex-1 overflow-y-auto no-scrollbar p-2 space-y-1">
|
||||||
@@ -91,6 +96,7 @@
|
|||||||
<h3 class="text-xs font-black text-gray-400 uppercase tracking-widest flex items-center gap-2">
|
<h3 class="text-xs font-black text-gray-400 uppercase tracking-widest flex items-center gap-2">
|
||||||
<i data-lucide="folder-clock" class="w-4 h-4 text-blue-500"></i> Historial de Expedientes
|
<i data-lucide="folder-clock" class="w-4 h-4 text-blue-500"></i> Historial de Expedientes
|
||||||
</h3>
|
</h3>
|
||||||
|
<span class="text-[9px] font-bold text-gray-400">Pulsa en un expediente para editarlo</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="w-full text-sm text-left">
|
<table class="w-full text-sm text-left">
|
||||||
@@ -101,6 +107,7 @@
|
|||||||
<th class="px-6 py-4">Compañía</th>
|
<th class="px-6 py-4">Compañía</th>
|
||||||
<th class="px-6 py-4">Técnico</th>
|
<th class="px-6 py-4">Técnico</th>
|
||||||
<th class="px-6 py-4">Estado</th>
|
<th class="px-6 py-4">Estado</th>
|
||||||
|
<th class="px-6 py-4 text-right"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="servicesTableBody" class="text-gray-600 font-medium">
|
<tbody id="servicesTableBody" class="text-gray-600 font-medium">
|
||||||
@@ -116,13 +123,84 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="serviceDetailModal" class="fixed inset-0 bg-slate-900/70 hidden z-[150] flex items-center justify-center backdrop-blur-sm p-4 text-left fade-in">
|
||||||
|
<div class="bg-white rounded-[2rem] shadow-2xl w-full max-w-4xl max-h-[95vh] flex flex-col overflow-hidden border border-slate-200">
|
||||||
|
|
||||||
|
<div class="p-6 border-b border-slate-100 flex justify-between items-center bg-slate-50 shrink-0">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="bg-blue-100 text-blue-600 p-2.5 rounded-xl shadow-inner"><i data-lucide="edit-3" class="w-5 h-5"></i></div>
|
||||||
|
<div>
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Editor de Expediente</p>
|
||||||
|
<h3 class="font-black text-slate-800 text-xl tracking-tight leading-none mt-1">#<span id="modalSvcRef" class="text-blue-600"></span></h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onclick="closeServiceDetailModal()" class="text-slate-400 hover:text-red-500 bg-white shadow-sm border border-slate-200 p-2.5 rounded-full transition-colors active:scale-90"><i data-lucide="x" class="w-5 h-5"></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onsubmit="saveServiceDetails(event)" class="flex-1 overflow-y-auto p-6 space-y-6 no-scrollbar bg-slate-50/50">
|
||||||
|
<input type="hidden" id="modalSvcId">
|
||||||
|
|
||||||
|
<div class="bg-white p-6 rounded-2xl shadow-sm border border-slate-200">
|
||||||
|
<h4 class="font-black text-slate-800 uppercase text-xs flex items-center gap-2 border-b border-slate-100 pb-3 mb-4"><i data-lucide="user" class="w-4 h-4 text-blue-500"></i> Datos Principales</h4>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||||
|
<div>
|
||||||
|
<label class="block text-[10px] font-black text-slate-500 uppercase tracking-widest mb-1.5 ml-1">Nombre / Asegurado</label>
|
||||||
|
<input type="text" id="modalSvcName" class="w-full bg-slate-50 border border-slate-200 px-4 py-3 rounded-xl text-sm font-bold text-slate-700 outline-none focus:bg-white focus:border-blue-500 focus:ring-2 focus:ring-blue-100 transition-all shadow-sm">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-[10px] font-black text-slate-500 uppercase tracking-widest mb-1.5 ml-1">Teléfono</label>
|
||||||
|
<input type="text" id="modalSvcPhone" class="w-full bg-slate-50 border border-slate-200 px-4 py-3 rounded-xl text-sm font-bold text-slate-700 outline-none focus:bg-white focus:border-blue-500 focus:ring-2 focus:ring-blue-100 transition-all shadow-sm">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-5">
|
||||||
|
<label class="block text-[10px] font-black text-slate-500 uppercase tracking-widest mb-1.5 ml-1">Dirección Completa</label>
|
||||||
|
<input type="text" id="modalSvcAddress" class="w-full bg-slate-50 border border-slate-200 px-4 py-3 rounded-xl text-sm font-bold text-slate-700 outline-none focus:bg-white focus:border-blue-500 focus:ring-2 focus:ring-blue-100 transition-all shadow-sm">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-5 mt-5">
|
||||||
|
<div>
|
||||||
|
<label class="block text-[10px] font-black text-slate-500 uppercase tracking-widest mb-1.5 ml-1">Población</label>
|
||||||
|
<input type="text" id="modalSvcPop" class="w-full bg-slate-50 border border-slate-200 px-4 py-3 rounded-xl text-sm font-bold text-slate-700 outline-none focus:bg-white focus:border-blue-500 focus:ring-2 focus:ring-blue-100 transition-all shadow-sm">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-[10px] font-black text-slate-500 uppercase tracking-widest mb-1.5 ml-1">Código Postal</label>
|
||||||
|
<input type="text" id="modalSvcCp" class="w-full bg-slate-50 border border-slate-200 px-4 py-3 rounded-xl text-sm font-bold text-slate-700 outline-none focus:bg-white focus:border-blue-500 focus:ring-2 focus:ring-blue-100 transition-all shadow-sm text-center tracking-widest">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white p-6 rounded-2xl shadow-sm border border-slate-200">
|
||||||
|
<h4 class="font-black text-slate-800 uppercase text-xs flex items-center gap-2 border-b border-slate-100 pb-3 mb-4"><i data-lucide="align-left" class="w-4 h-4 text-amber-500"></i> Descripción y Daños</h4>
|
||||||
|
<textarea id="modalSvcDesc" rows="5" class="w-full bg-slate-50 border border-slate-200 px-4 py-3 rounded-xl text-sm font-bold text-slate-700 outline-none focus:bg-white focus:border-blue-500 focus:ring-2 focus:ring-blue-100 transition-all shadow-sm resize-none"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white border border-slate-200 rounded-2xl shadow-sm overflow-hidden">
|
||||||
|
<button type="button" onclick="document.getElementById('modalSvcExtraContainer').classList.toggle('hidden')" class="w-full p-4 flex justify-between items-center bg-slate-50 hover:bg-slate-100 transition-colors">
|
||||||
|
<span class="text-[10px] font-black text-slate-500 uppercase tracking-widest flex items-center gap-2"><i data-lucide="server" class="w-4 h-4"></i> Resto de Datos Técnicos (Original)</span>
|
||||||
|
<i data-lucide="chevron-down" class="w-4 h-4 text-slate-400"></i>
|
||||||
|
</button>
|
||||||
|
<div id="modalSvcExtraContainer" class="hidden p-5 bg-slate-800 text-emerald-400 font-mono text-xs overflow-auto max-h-60 leading-relaxed border-t border-slate-200">
|
||||||
|
<div id="modalSvcExtra"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-2">
|
||||||
|
<button type="submit" id="btnSaveSvc" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-black px-6 py-4 rounded-xl uppercase tracking-widest text-sm transition-all shadow-lg active:scale-95 flex items-center justify-center gap-2 border border-blue-500">
|
||||||
|
<i data-lucide="save" class="w-5 h-5"></i> Guardar Modificaciones
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="toast" class="fixed bottom-5 right-5 bg-slate-800 text-white px-6 py-3 rounded-xl shadow-2xl translate-y-20 opacity-0 transition-all duration-300 z-[200] flex items-center gap-3 font-bold text-sm tracking-wide">
|
<div id="toast" class="fixed bottom-5 right-5 bg-slate-800 text-white px-6 py-3 rounded-xl shadow-2xl translate-y-20 opacity-0 transition-all duration-300 z-[200] flex items-center gap-3 font-bold text-sm tracking-wide">
|
||||||
<i data-lucide="info" class="w-4 h-4"></i> <span id="toastMsg"></span>
|
<i data-lucide="info" class="w-4 h-4"></i> <span id="toastMsg"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="js/layout.js"></script>
|
<script src="js/layout.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// La API_URL ya viene cargada automáticamente desde js/layout.js
|
// API_URL ya viene cargada desde js/layout.js
|
||||||
let systemStatuses = [];
|
let systemStatuses = [];
|
||||||
let searchTimeout = null;
|
let searchTimeout = null;
|
||||||
|
|
||||||
@@ -130,7 +208,6 @@
|
|||||||
if (!localStorage.getItem("token")) window.location.href = "index.html";
|
if (!localStorage.getItem("token")) window.location.href = "index.html";
|
||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
await loadStatuses();
|
await loadStatuses();
|
||||||
// Iniciar búsqueda vacía para traer los últimos clientes
|
|
||||||
loadClients("");
|
loadClients("");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -142,7 +219,6 @@
|
|||||||
} catch (e) { console.error("Error al cargar estados", e); }
|
} catch (e) { console.error("Error al cargar estados", e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEBOUNCE PARA NO SATURAR EL SERVIDOR AL ESCRIBIR
|
|
||||||
function debounceSearch() {
|
function debounceSearch() {
|
||||||
clearTimeout(searchTimeout);
|
clearTimeout(searchTimeout);
|
||||||
searchTimeout = setTimeout(() => {
|
searchTimeout = setTimeout(() => {
|
||||||
@@ -161,7 +237,6 @@
|
|||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Buscamos directamente en scraped_services (trae todos)
|
|
||||||
const res = await fetch(`${API_URL}/services/active`, {
|
const res = await fetch(`${API_URL}/services/active`, {
|
||||||
headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` }
|
headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` }
|
||||||
});
|
});
|
||||||
@@ -169,21 +244,19 @@
|
|||||||
|
|
||||||
if (!data.ok) throw new Error("Error en la carga");
|
if (!data.ok) throw new Error("Error en la carga");
|
||||||
|
|
||||||
// Agrupamos todos los servicios por teléfono para simular "Clientes"
|
|
||||||
let clientsMap = {};
|
let clientsMap = {};
|
||||||
|
|
||||||
data.services.forEach(s => {
|
data.services.forEach(s => {
|
||||||
const raw = s.raw_data || {};
|
const raw = s.raw_data || {};
|
||||||
let phone = raw['Teléfono'] || raw['TELEFONO'] || raw['TELEFONOS'] || "";
|
let phone = raw['Teléfono'] || raw['TELEFONO'] || raw['TELEFONOS'] || "";
|
||||||
|
|
||||||
// Normalizar teléfono
|
|
||||||
const matchPhone = String(phone).match(/[6789]\d{8}/);
|
const matchPhone = String(phone).match(/[6789]\d{8}/);
|
||||||
const cleanPhone = matchPhone ? matchPhone[0] : null;
|
const cleanPhone = matchPhone ? matchPhone[0] : null;
|
||||||
|
|
||||||
if (cleanPhone) {
|
if (cleanPhone) {
|
||||||
if (!clientsMap[cleanPhone]) {
|
if (!clientsMap[cleanPhone]) {
|
||||||
clientsMap[cleanPhone] = {
|
clientsMap[cleanPhone] = {
|
||||||
id: cleanPhone, // Usamos el tfno como ID
|
id: cleanPhone,
|
||||||
full_name: raw['Nombre Cliente'] || raw['CLIENTE'] || "Cliente Sin Nombre",
|
full_name: raw['Nombre Cliente'] || raw['CLIENTE'] || "Cliente Sin Nombre",
|
||||||
phone: cleanPhone,
|
phone: cleanPhone,
|
||||||
addresses: new Set(),
|
addresses: new Set(),
|
||||||
@@ -197,9 +270,15 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Convertir mapa a Array y Filtrar si hay texto
|
|
||||||
let clientsArray = Object.values(clientsMap);
|
let clientsArray = Object.values(clientsMap);
|
||||||
|
|
||||||
|
// NUEVO: FILTRO MÚLTIPLES DIRECCIONES
|
||||||
|
const filterMulti = document.getElementById('filterMultiAddress').checked;
|
||||||
|
if (filterMulti) {
|
||||||
|
clientsArray = clientsArray.filter(c => c.addresses.size > 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtro de texto
|
||||||
if (search !== "") {
|
if (search !== "") {
|
||||||
const sLower = search.toLowerCase();
|
const sLower = search.toLowerCase();
|
||||||
clientsArray = clientsArray.filter(c =>
|
clientsArray = clientsArray.filter(c =>
|
||||||
@@ -208,10 +287,8 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ordenar por número de expedientes
|
|
||||||
clientsArray.sort((a, b) => b.services.length - a.services.length);
|
clientsArray.sort((a, b) => b.services.length - a.services.length);
|
||||||
|
|
||||||
// Dibujar la lista
|
|
||||||
list.innerHTML = "";
|
list.innerHTML = "";
|
||||||
if(clientsArray.length === 0) {
|
if(clientsArray.length === 0) {
|
||||||
list.innerHTML = `<div class="text-center py-8 text-gray-400 text-xs font-bold uppercase tracking-widest border-2 border-dashed border-gray-200 rounded-xl m-2">Sin resultados</div>`;
|
list.innerHTML = `<div class="text-center py-8 text-gray-400 text-xs font-bold uppercase tracking-widest border-2 border-dashed border-gray-200 rounded-xl m-2">Sin resultados</div>`;
|
||||||
@@ -223,8 +300,11 @@
|
|||||||
el.className = `p-3 mb-1 rounded-xl cursor-pointer hover:bg-blue-50 transition border border-transparent hover:border-blue-100 flex justify-between items-center group`;
|
el.className = `p-3 mb-1 rounded-xl cursor-pointer hover:bg-blue-50 transition border border-transparent hover:border-blue-100 flex justify-between items-center group`;
|
||||||
el.id = `client-card-${c.id}`;
|
el.id = `client-card-${c.id}`;
|
||||||
|
|
||||||
// Al hacer click, pintamos el detalle
|
// Alojamos los datos del cliente globalmente al hacer clic
|
||||||
el.onclick = () => renderClientDetails(c, el.id);
|
el.onclick = () => {
|
||||||
|
window.currentClientData = c;
|
||||||
|
renderClientDetails(c, el.id);
|
||||||
|
};
|
||||||
|
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<div class="flex items-center gap-3 overflow-hidden">
|
<div class="flex items-center gap-3 overflow-hidden">
|
||||||
@@ -251,28 +331,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. RENDERIZAR DETALLES
|
|
||||||
function renderClientDetails(clientData, cardId) {
|
function renderClientDetails(clientData, cardId) {
|
||||||
// UI Toggle
|
|
||||||
document.getElementById('emptyState').classList.add('hidden');
|
document.getElementById('emptyState').classList.add('hidden');
|
||||||
document.getElementById('clientContent').classList.remove('hidden');
|
document.getElementById('clientContent').classList.remove('hidden');
|
||||||
document.getElementById('clientContent').classList.add('flex');
|
document.getElementById('clientContent').classList.add('flex');
|
||||||
|
|
||||||
// Resaltar seleccionado en la lista
|
|
||||||
document.querySelectorAll('#clientsList > div').forEach(d => d.classList.remove('selected-card'));
|
document.querySelectorAll('#clientsList > div').forEach(d => d.classList.remove('selected-card'));
|
||||||
const card = document.getElementById(cardId);
|
const card = document.getElementById(cardId);
|
||||||
if(card) card.classList.add('selected-card');
|
if(card) card.classList.add('selected-card');
|
||||||
|
|
||||||
// Rellenar Cabecera
|
|
||||||
document.getElementById('detailAvatar').innerText = clientData.full_name.charAt(0).toUpperCase();
|
document.getElementById('detailAvatar').innerText = clientData.full_name.charAt(0).toUpperCase();
|
||||||
document.getElementById('detailName').innerText = clientData.full_name;
|
document.getElementById('detailName').innerText = clientData.full_name;
|
||||||
document.getElementById('detailPhone').innerText = clientData.phone;
|
document.getElementById('detailPhone').innerText = clientData.phone;
|
||||||
|
|
||||||
// Botones Acción
|
|
||||||
document.getElementById('btnCall').href = `tel:+34${clientData.phone}`;
|
document.getElementById('btnCall').href = `tel:+34${clientData.phone}`;
|
||||||
document.getElementById('btnWhatsapp').href = `https://wa.me/34${clientData.phone}`;
|
document.getElementById('btnWhatsapp').href = `https://wa.me/34${clientData.phone}`;
|
||||||
|
|
||||||
// Direcciones
|
|
||||||
const addrContainer = document.getElementById('addressList');
|
const addrContainer = document.getElementById('addressList');
|
||||||
addrContainer.innerHTML = "";
|
addrContainer.innerHTML = "";
|
||||||
const addresses = Array.from(clientData.addresses);
|
const addresses = Array.from(clientData.addresses);
|
||||||
@@ -293,26 +367,24 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tabla de Servicios
|
|
||||||
const tbody = document.getElementById('servicesTableBody');
|
const tbody = document.getElementById('servicesTableBody');
|
||||||
tbody.innerHTML = "";
|
tbody.innerHTML = "";
|
||||||
|
|
||||||
// Ordenar servicios del más nuevo al más antiguo
|
|
||||||
clientData.services.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
clientData.services.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
||||||
|
|
||||||
clientData.services.forEach(s => {
|
clientData.services.forEach(s => {
|
||||||
const tr = document.createElement('tr');
|
const tr = document.createElement('tr');
|
||||||
tr.className = "border-b border-gray-100 hover:bg-blue-50/30 transition-colors";
|
// NUEVO: FILA CLICABLE QUE ABRE EL MODAL DE EDICIÓN
|
||||||
|
tr.className = "border-b border-gray-100 hover:bg-blue-50 transition-colors cursor-pointer group";
|
||||||
|
tr.onclick = () => openServiceDetailModal(s.id);
|
||||||
|
|
||||||
const raw = s.raw_data || {};
|
const raw = s.raw_data || {};
|
||||||
const date = new Date(s.created_at).toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit', year: '2-digit' });
|
const date = new Date(s.created_at).toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit', year: '2-digit' });
|
||||||
|
|
||||||
// Buscar estado real
|
|
||||||
const dbStatusId = raw.status_operativo;
|
const dbStatusId = raw.status_operativo;
|
||||||
const statusObj = systemStatuses.find(st => String(st.id) === String(dbStatusId));
|
const statusObj = systemStatuses.find(st => String(st.id) === String(dbStatusId));
|
||||||
const stName = statusObj ? statusObj.name : (s.status_name || 'Pendiente');
|
const stName = statusObj ? statusObj.name : (s.status_name || 'Pendiente');
|
||||||
|
|
||||||
// Color del estado (si es finalizado lo ponemos gris)
|
|
||||||
const isFinal = s.is_final || (statusObj && statusObj.is_final) || s.status === 'archived';
|
const isFinal = s.is_final || (statusObj && statusObj.is_final) || s.status === 'archived';
|
||||||
const badgeClass = isFinal
|
const badgeClass = isFinal
|
||||||
? "bg-slate-100 text-slate-500 border-slate-200"
|
? "bg-slate-100 text-slate-500 border-slate-200"
|
||||||
@@ -320,7 +392,7 @@
|
|||||||
|
|
||||||
tr.innerHTML = `
|
tr.innerHTML = `
|
||||||
<td class="px-6 py-4 font-mono text-xs font-bold text-slate-500">${date}</td>
|
<td class="px-6 py-4 font-mono text-xs font-bold text-slate-500">${date}</td>
|
||||||
<td class="px-6 py-4 font-black text-slate-800 text-xs">#${s.service_ref}</td>
|
<td class="px-6 py-4 font-black text-slate-800 text-xs group-hover:text-blue-600 transition-colors">#${s.service_ref}</td>
|
||||||
<td class="px-6 py-4 text-xs font-bold text-slate-500 uppercase">${raw['Compañía'] || 'Particular'}</td>
|
<td class="px-6 py-4 text-xs font-bold text-slate-500 uppercase">${raw['Compañía'] || 'Particular'}</td>
|
||||||
<td class="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest flex items-center gap-1.5 mt-1.5">
|
<td class="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest flex items-center gap-1.5 mt-1.5">
|
||||||
<i data-lucide="hard-hat" class="w-3 h-3"></i> ${s.assigned_name || 'Sin Asignar'}
|
<i data-lucide="hard-hat" class="w-3 h-3"></i> ${s.assigned_name || 'Sin Asignar'}
|
||||||
@@ -330,6 +402,9 @@
|
|||||||
${stName}
|
${stName}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="px-6 py-4 text-right">
|
||||||
|
<span class="text-blue-500 opacity-0 group-hover:opacity-100 transition-opacity"><i data-lucide="edit-3" class="w-4 h-4"></i></span>
|
||||||
|
</td>
|
||||||
`;
|
`;
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
});
|
});
|
||||||
@@ -337,10 +412,92 @@
|
|||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. NUEVAS FUNCIONES DEL MODAL DE EDICIÓN
|
||||||
|
function openServiceDetailModal(id) {
|
||||||
|
if(!window.currentClientData) return;
|
||||||
|
const s = window.currentClientData.services.find(srv => srv.id === id);
|
||||||
|
if(!s) return;
|
||||||
|
const raw = s.raw_data || {};
|
||||||
|
|
||||||
|
document.getElementById('modalSvcId').value = s.id;
|
||||||
|
document.getElementById('modalSvcRef').innerText = s.service_ref || "SIN-REF";
|
||||||
|
|
||||||
|
// Llenar campos editables
|
||||||
|
document.getElementById('modalSvcName').value = raw['Nombre Cliente'] || raw['CLIENTE'] || "";
|
||||||
|
document.getElementById('modalSvcPhone').value = raw['Teléfono'] || raw['TELEFONOS'] || raw['TELEFONO'] || "";
|
||||||
|
document.getElementById('modalSvcAddress').value = raw['Dirección'] || raw['DOMICILIO'] || "";
|
||||||
|
document.getElementById('modalSvcPop').value = raw['Población'] || raw['POBLACION-PROVINCIA'] || "";
|
||||||
|
document.getElementById('modalSvcCp').value = raw['Código Postal'] || raw['C.P.'] || "";
|
||||||
|
document.getElementById('modalSvcDesc').value = raw['Descripción'] || raw['DESCRIPCION'] || "";
|
||||||
|
|
||||||
|
// Volcar el resto de campos no editables al cajón negro
|
||||||
|
let extraHtml = '';
|
||||||
|
const knownKeys = ['Nombre Cliente', 'CLIENTE', 'Teléfono', 'TELEFONOS', 'TELEFONO', 'Dirección', 'DOMICILIO', 'Población', 'POBLACION-PROVINCIA', 'Código Postal', 'C.P.', 'Descripción', 'DESCRIPCION'];
|
||||||
|
|
||||||
|
for (let key in raw) {
|
||||||
|
if (!knownKeys.includes(key)) {
|
||||||
|
let val = raw[key];
|
||||||
|
if (val === null || val === undefined || val === "") continue;
|
||||||
|
if (typeof val === 'object') val = JSON.stringify(val);
|
||||||
|
extraHtml += `<div class="mb-2 pb-2 border-b border-slate-700 last:border-0"><strong class="text-white">${key}:</strong> <br><span class="break-words">${val}</span></div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.getElementById('modalSvcExtra').innerHTML = extraHtml || 'No hay datos extra adicionales.';
|
||||||
|
|
||||||
|
document.getElementById('serviceDetailModal').classList.remove('hidden');
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeServiceDetailModal() {
|
||||||
|
document.getElementById('serviceDetailModal').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveServiceDetails(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const id = document.getElementById('modalSvcId').value;
|
||||||
|
const btn = document.getElementById('btnSaveSvc');
|
||||||
|
const origText = btn.innerHTML;
|
||||||
|
btn.innerHTML = '<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i> Guardando...';
|
||||||
|
btn.disabled = true;
|
||||||
|
lucide.createIcons();
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
name: document.getElementById('modalSvcName').value,
|
||||||
|
phone: document.getElementById('modalSvcPhone').value,
|
||||||
|
address: document.getElementById('modalSvcAddress').value,
|
||||||
|
cp: document.getElementById('modalSvcCp').value,
|
||||||
|
description: document.getElementById('modalSvcDesc').value,
|
||||||
|
"Población": document.getElementById('modalSvcPop').value // Extra field, handled by the backend '...extra' mapping
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_URL}/providers/scraped/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
if(res.ok) {
|
||||||
|
showToast("¡Expediente actualizado correctamente!");
|
||||||
|
closeServiceDetailModal();
|
||||||
|
// Refrescamos la lista para que se vean los cambios
|
||||||
|
await searchClients();
|
||||||
|
} else {
|
||||||
|
showToast("Error al guardar en el servidor", true);
|
||||||
|
}
|
||||||
|
} catch(err) {
|
||||||
|
showToast("Error de conexión", true);
|
||||||
|
} finally {
|
||||||
|
btn.innerHTML = origText;
|
||||||
|
btn.disabled = false;
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function showToast(msg, isError = false) {
|
function showToast(msg, isError = false) {
|
||||||
const t = document.getElementById('toast');
|
const t = document.getElementById('toast');
|
||||||
const m = document.getElementById('toastMsg');
|
const m = document.getElementById('toastMsg');
|
||||||
t.className = `fixed bottom-5 right-5 px-6 py-4 rounded-xl shadow-2xl transition-all duration-300 z-[200] flex items-center gap-3 font-bold text-sm tracking-wide ${isError ? 'bg-red-600' : 'bg-slate-800'} text-white`;
|
t.className = `fixed bottom-5 right-5 px-6 py-4 rounded-xl shadow-2xl transition-all duration-300 z-[250] flex items-center gap-3 font-bold text-sm tracking-wide ${isError ? 'bg-red-600' : 'bg-slate-800'} text-white`;
|
||||||
m.innerText = msg;
|
m.innerText = msg;
|
||||||
t.classList.remove('translate-y-20', 'opacity-0');
|
t.classList.remove('translate-y-20', 'opacity-0');
|
||||||
setTimeout(() => t.classList.add('translate-y-20', 'opacity-0'), 3000);
|
setTimeout(() => t.classList.add('translate-y-20', 'opacity-0'), 3000);
|
||||||
|
|||||||
Reference in New Issue
Block a user