+
Guardado correctamente
@@ -436,7 +378,6 @@
setInterval(refreshPanel, 20000);
});
- // Helpers de Fechas
function getWeekNumber(d) {
d = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay()||7));
@@ -460,7 +401,6 @@
if (data.ok) {
systemStatuses = data.statuses;
renderStatusPills();
-
const modalSelect = document.getElementById('detStatusMap');
modalSelect.innerHTML = '';
systemStatuses.forEach(st => {
@@ -470,43 +410,28 @@
} catch (e) { console.error("Error cargando estados:", e); }
}
- function renderStatusPills() {
+ function renderStatusPills() {
const container = document.getElementById('statusPills');
- let html = `
-
- `;
+ let html = `
`;
systemStatuses.forEach(st => {
const isActive = activeStatusFilter === String(st.id);
const c = colorDict[st.color] || colorDict['gray'];
const colorBase = c.bg.split('-')[1];
-
const activeClasses = `bg-white border-${colorBase}-400 text-${colorBase}-700 shadow-md ring-2 ring-${colorBase}-100`;
const inactiveClasses = `bg-white border-slate-200 text-slate-500 hover:bg-slate-50`;
- html += `
-
- `;
+ html += `
`;
});
container.innerHTML = html;
}
- function setStatusFilter(id) {
- activeStatusFilter = id;
- renderStatusPills();
- renderLists();
- }
+ function setStatusFilter(id) { activeStatusFilter = id; renderStatusPills(); renderLists(); }
async function refreshPanel() {
try {
- const res = await fetch(`${API_URL}/services/active`, {
- headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` }
- });
+ const res = await fetch(`${API_URL}/services/active`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
const data = await res.json();
if (data.ok) {
localData = data.services.filter(s => s.provider !== 'SYSTEM_BLOCK');
@@ -533,19 +458,16 @@
if (!s.assigned_name && (s.automation_status === 'in_progress' || s.automation_status === 'failed')) {
return { id: 'bolsa', name: s.automation_status === 'in_progress' ? 'Buscando Operario' : 'Fallo en Bolsa', color: s.automation_status === 'in_progress' ? 'amber' : 'red', isBlocked: false, is_final: false };
}
-
if (!s.assigned_name && (!dbStat || dbStat === 'sin_asignar')) {
const found = systemStatuses.find(st => st.name.toLowerCase().includes('pendiente de asignar')) || systemStatuses[0];
return { ...found, isBlocked: false };
}
-
const foundById = systemStatuses.find(st => String(st.id) === String(dbStat));
if (foundById) return { ...foundById, isBlocked: false };
if (s.assigned_name && (!raw.scheduled_date || raw.scheduled_date === "")) {
const asignado = systemStatuses.find(st => st.name.toLowerCase() === 'asignado');
const esperando = systemStatuses.find(st => st.name.toLowerCase().includes('esperando'));
-
if (dbStat === 'esperando_cliente' && esperando) return { ...esperando, isBlocked: false };
if ((dbStat === 'asignado_operario' || !dbStat) && asignado) return { ...asignado, isBlocked: false };
if (asignado) return { ...asignado, isBlocked: false };
@@ -567,12 +489,11 @@
function renderLists() {
if(systemStatuses.length === 0) return;
-
const searchTerm = document.getElementById('searchFilter').value.toLowerCase();
const selectedOp = document.getElementById('opFilter').value;
+ const weekValue = document.getElementById('weekFilter').value;
- // 1. Añadimos la variable para el nuevo KPI
- let kpiUnassigned = 0, kpiAssignedNoDate = 0, kpiScheduled = 0, kpiWaiting = 0, kpiIncident = 0;
+ let kpiUnassigned = 0, kpiScheduled = 0, kpiWaiting = 0, kpiIncident = 0;
const filteredData = localData.filter(s => {
const raw = s.raw_data || {};
@@ -580,55 +501,34 @@
s._stateInfo = stateInfo;
const stName = stateInfo.name.toLowerCase();
- // --- LÓGICA REVISADA Y EXACTA DE CONTADORES ---
-
- // 1. SIN ASIGNAR: No tiene operario o está en la bolsa
- if (!s.assigned_name || stateInfo.id === 'bolsa' || stName.includes('asignar') || stName.includes('desasignado')) {
- kpiUnassigned++;
- }
- // 2. INCIDENCIA: El estado incluye la palabra "incidencia" o "pausa"
- else if (stName.includes('incidencia') || stName.includes('pausa')) {
- kpiIncident++;
- }
- // 3. ESPERA CLIENTE: El estado es literalmente "Esperando al cliente"
- else if (stName.includes('espera')) {
- kpiWaiting++;
- }
- // 4. CITADO CONFIRMADO: El estado incluye la palabra "citado"
- else if (stName.includes('citado')) {
- kpiScheduled++;
- }
- // 5. ASIGNADO SIN CITA: Tiene operario (ya pasó el primer if), el estado es "asignado" y NO tiene fecha
- else if (stName.includes('asignado') && (!raw.scheduled_date || raw.scheduled_date === "")) {
- kpiAssignedNoDate++;
- }
+ if (!s.assigned_name || stateInfo.id === 'bolsa' || stName.includes('asignar')) kpiUnassigned++;
+ else if (stName.includes('incidencia') || stName.includes('pausa')) kpiIncident++;
+ else if (raw.scheduled_date && raw.scheduled_date !== "" && !stateInfo.is_final) kpiScheduled++;
+ else if (!stateInfo.is_final && !raw.scheduled_date) kpiWaiting++;
- // ... (El resto del código de los filtros visuales se mantiene igual)
-
- // Filtro de búsqueda visual (esto no cambia)
- const matchesSearch = searchTerm === "" ||
- (raw["Nombre Cliente"] || "").toLowerCase().includes(searchTerm) ||
- (s.service_ref || "").toLowerCase().includes(searchTerm) ||
- (raw["Teléfono"] || "").toLowerCase().includes(searchTerm);
-
+ const name = (raw["Nombre Cliente"] || raw["CLIENTE"] || "").toLowerCase();
+ const ref = (s.service_ref || "").toLowerCase();
+ const matchesSearch = searchTerm === "" || name.includes(searchTerm) || ref.includes(searchTerm);
const matchesOp = selectedOp === "ALL" || s.assigned_name === selectedOp;
- const matchesStatus = (activeStatusFilter === "ALL") ? true : String(stateInfo.id) === activeStatusFilter;
-
- return matchesSearch && matchesOp && matchesStatus;
+
+ let matchesWeek = true;
+ if (weekValue !== "" && raw.scheduled_date) {
+ matchesWeek = isDateInWeekString(raw.scheduled_date, weekValue);
+ } else if (weekValue !== "") {
+ matchesWeek = false;
+ }
+
+ let matchesStatus = (activeStatusFilter === "ALL") ? true : String(stateInfo.id) === activeStatusFilter;
+ return matchesSearch && matchesOp && matchesWeek && matchesStatus;
});
- // 2. Mandamos los números calculados a los círculos del HTML
document.getElementById('kpi-unassigned').innerText = kpiUnassigned;
- document.getElementById('kpi-assigned-no-date').innerText = kpiAssignedNoDate; // <- NUEVO
document.getElementById('kpi-scheduled').innerText = kpiScheduled;
document.getElementById('kpi-waiting').innerText = kpiWaiting;
document.getElementById('kpi-incident').innerText = kpiIncident;
const grid = document.getElementById('servicesGrid');
- grid.innerHTML = filteredData.length > 0
- ? filteredData.map(s => buildGridCard(s)).join('')
- : `
No hay servicios con estos filtros
`;
-
+ grid.innerHTML = filteredData.length > 0 ? filteredData.map(s => buildGridCard(s)).join('') : `
No hay servicios que coincidan
`;
lucide.createIcons();
}
@@ -640,7 +540,6 @@
const fullAddr = `${addr} ${pop}`.trim();
const cita = raw.scheduled_date ? `${raw.scheduled_date.split('-').reverse().slice(0,2).join('/')} | ${raw.scheduled_time}` : 'Pte. Cita';
const companyName = raw['Compañía'] || raw['COMPAÑIA'] || raw['Procedencia'] || (s.provider === 'MANUAL' ? 'PARTICULAR' : 'ASEGURADORA');
-
const isUrgent = s.is_urgent === true || (raw['Urgente'] && raw['Urgente'].toLowerCase() === 'sí') || (raw['URGENTE'] && raw['URGENTE'].toLowerCase() === 'si');
const stateInfo = s._stateInfo;
@@ -665,12 +564,10 @@
${isUrgent ? `
🔥 URGENTE` : `
#${s.service_ref}`}
${fullAddr}
@@ -729,17 +626,18 @@
const s = localData.find(x => x.id === id);
if (!s) return;
const raw = s.raw_data;
+
document.getElementById('detId').value = s.id;
document.getElementById('detRef').innerText = s.service_ref;
document.getElementById('detCp').value = raw["Código Postal"] || "00000";
const companyName = raw['Compañía'] || raw['COMPAÑIA'] || raw['Procedencia'] || "Particular";
document.getElementById('detCompany').innerText = companyName;
-
- // RELLENADO DE CAMPOS PARA EL EDITOR Y VISTA
+
+ // Rellenar campos del editor
const clientName = raw["Nombre Cliente"] || raw["CLIENTE"] || "Asegurado Sin Nombre";
if(document.getElementById('editName')) document.getElementById('editName').value = clientName;
-
+
const rawPhone = raw["Teléfono"] || raw["TELEFONOS"] || raw["TELEFONO"] || "";
const matchPhone = rawPhone.toString().match(/[6789]\d{8}/);
const singlePhone = matchPhone ? matchPhone[0] : "";
@@ -784,6 +682,7 @@
document.getElementById('detailModal').classList.remove('hidden');
+ // Activar modo edición si se ha pulsado el lápiz
if (startInEditMode) {
enableEditing();
} else {
@@ -796,7 +695,6 @@
async function stopAutomation() {
const id = document.getElementById('detId').value;
if(!confirm("¿Deseas cancelar la búsqueda del robot para asignar este servicio manualmente?")) return;
-
try {
await fetch(`${API_URL}/providers/scraped/${id}`, {
method: 'PUT',
@@ -806,9 +704,7 @@
showToast("Bolsa detenida. Ya puedes asignar el servicio.");
closeDetailModal();
refreshPanel();
- } catch (e) {
- alert("Error al detener el automatismo.");
- }
+ } catch (e) { alert("Error al detener el automatismo."); }
}
async function saveAppointment() {
@@ -818,7 +714,6 @@
const statusMap = document.getElementById('detStatusMap').value;
const selectedSt = systemStatuses.find(st => String(st.id) === String(statusMap));
-
if (selectedSt && !selectedSt.is_final && !date && !selectedSt.name.toLowerCase().includes('pausa') && !selectedSt.name.toLowerCase().includes('asignar')) {
if(!confirm("No has asignado Fecha para este estado. ¿Deseas continuar?")) return;
}
@@ -834,7 +729,6 @@
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` },
body: JSON.stringify({ date, time, status_operativo: statusMap })
});
-
closeDetailModal(); showToast("Estado actualizado"); refreshPanel();
} catch (e) { alert("Error"); }
finally { btn.innerHTML = originalContent; btn.disabled = false; }
@@ -846,7 +740,6 @@
const cp = document.getElementById('detCp').value;
if(!guild_id) return alert("Selecciona un gremio primero.");
-
const btn = document.getElementById('btnAuto');
btn.innerHTML = '';
@@ -894,12 +787,8 @@
})
});
- closeDetailModal();
- showToast("Asignado y notificado correctamente");
- refreshPanel();
- } catch (e) {
- alert("Error en la asignación manual");
- }
+ closeDetailModal(); showToast("Asignado y notificado correctamente"); refreshPanel();
+ } catch (e) { alert("Error en la asignación manual"); }
}
async function saveNewService(e) {
@@ -931,23 +820,20 @@
}
function closeDetailModal() { document.getElementById('detailModal').classList.add('hidden'); }
-
- async function openCreateModal() {
+ function openCreateModal() {
document.getElementById('createModal').classList.remove('hidden');
document.getElementById('isCompanyCheck').checked = false;
toggleCompanyFields(false);
- try {
- const res = await fetch(`${API_URL}/companies`, {
- headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` }
- });
- const data = await res.json();
- const sel = document.getElementById('nCompanySelect');
- sel.innerHTML = '';
- data.companies.forEach(c => {
- sel.innerHTML += ``;
- });
- } catch(e) {}
+ fetch(`${API_URL}/companies`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } })
+ .then(r => r.json())
+ .then(data => {
+ const sel = document.getElementById('nCompanySelect');
+ sel.innerHTML = '';
+ if(data.companies) {
+ data.companies.forEach(c => sel.innerHTML += ``);
+ }
+ }).catch(e=>{});
lucide.createIcons();
}
@@ -962,19 +848,15 @@
async function checkDuplicateRef(ref) {
if(!ref) return;
- const res = await fetch(`${API_URL}/services/check-ref?ref=${ref}`, {
- headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` }
- });
+ const res = await fetch(`${API_URL}/services/check-ref?ref=${ref}`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
const data = await res.json();
const alertEl = document.getElementById('refAlert');
const inputEl = document.getElementById('nExpRef');
if(data.exists) {
- alertEl.classList.remove('hidden');
- inputEl.classList.add('border-red-500', 'bg-red-50');
+ alertEl.classList.remove('hidden'); inputEl.classList.add('border-red-500', 'bg-red-50');
} else {
- alertEl.classList.add('hidden');
- inputEl.classList.remove('border-red-500', 'bg-red-50');
+ alertEl.classList.add('hidden'); inputEl.classList.remove('border-red-500', 'bg-red-50');
}
}
@@ -998,18 +880,18 @@
if(data.ok) sel.innerHTML = '' + data.operators.map(o => ``).join('');
}
+ // --- SOLUCIÓN PARA COPIAR PORTAL ANTI-BLOQUEO HTTP ---
async function copyClientPortalLink() {
- // Cogemos los datos (funciona tanto si es input como texto normal)
- const phoneEl = document.getElementById('editPhone') || document.getElementById('detPhone');
- const nameEl = document.getElementById('editName') || document.getElementById('detName');
- const addrEl = document.getElementById('editAddr') || document.getElementById('detAddrText');
+ const phoneEl = document.getElementById('editPhone');
+ const nameEl = document.getElementById('editName');
+ const addrEl = document.getElementById('editAddr');
- const phone = phoneEl.value || phoneEl.innerText;
- const name = nameEl.value || nameEl.innerText;
- const addr = addrEl.value || addrEl.innerText;
- const serviceId = document.getElementById('detId').value; // Añadimos el ID del servicio
+ const phone = phoneEl ? phoneEl.value : "";
+ const name = nameEl ? nameEl.value : "";
+ const addr = addrEl ? addrEl.value : "";
+ const serviceId = document.getElementById('detId').value;
- if (!phone || phone === "Sin Teléfono") {
+ if (!phone || phone.length < 9) {
showToast("No hay un teléfono válido para este cliente.", "warning");
return;
}
@@ -1017,6 +899,7 @@
const btn = document.getElementById('btnPortalLink');
const originalHtml = btn.innerHTML;
btn.innerHTML = ' Generando...';
+ lucide.createIcons();
try {
const res = await fetch(`${API_URL}/clients/ensure`, {
@@ -1029,40 +912,48 @@
if (data.ok && data.client && data.client.portal_token) {
const portalLink = `https://portal.integrarepara.es/?token=${data.client.portal_token}&service=${serviceId}`;
- // PLAN ANTI-BLOQUEO HTTP (Evita que el navegador bloquee la copia)
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(portalLink);
} else {
const textArea = document.createElement("textarea");
textArea.value = portalLink;
+ textArea.style.position = "fixed";
+ textArea.style.left = "-999999px";
document.body.appendChild(textArea);
+ textArea.focus();
textArea.select();
document.execCommand('copy');
textArea.remove();
}
btn.innerHTML = ' ¡Copiado!';
+ btn.classList.replace('text-blue-600', 'text-emerald-600');
+ btn.classList.replace('bg-blue-50', 'bg-emerald-50');
+ btn.classList.replace('border-blue-100', 'border-emerald-200');
showToast("Enlace copiado. ¡Listo para pegar en WhatsApp!");
- setTimeout(() => { btn.innerHTML = originalHtml; }, 3000);
+
+ setTimeout(() => {
+ btn.innerHTML = originalHtml;
+ btn.className = "text-[10px] font-black bg-blue-50 border border-blue-100 hover:bg-blue-600 hover:text-white text-blue-600 px-3 py-1.5 rounded-lg flex items-center gap-1.5 transition-all shadow-sm active:scale-95 shrink-0";
+ lucide.createIcons();
+ }, 3000);
} else {
- showToast("Error generando token", "warning");
+ showToast("No se pudo generar el token. Revisa la consola.", "warning");
btn.innerHTML = originalHtml;
+ lucide.createIcons();
}
} catch (error) {
- showToast("Error de conexión", "warning");
+ console.error("Error obteniendo token:", error);
+ showToast("Error al conectar con el servidor", "warning");
btn.innerHTML = originalHtml;
+ lucide.createIcons();
}
}
async function searchClientByPhone(phone) {
- if (phone.length < 9) {
- document.getElementById('addrSuggestions').classList.add('hidden');
- return;
- }
+ if (phone.length < 9) { document.getElementById('addrSuggestions').classList.add('hidden'); return; }
try {
- const res = await fetch(`${API_URL}/clients/search?phone=${phone}`, {
- headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` }
- });
+ const res = await fetch(`${API_URL}/clients/search?phone=${phone}`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
const data = await res.json();
if (data.ok && data.client) {
@@ -1071,20 +962,16 @@
nameInput.value = data.client.full_name;
nameInput.classList.add('bg-emerald-50');
}
-
const addrDiv = document.getElementById('addrSuggestions');
if (data.client.addresses && data.client.addresses.length > 0) {
addrDiv.classList.remove('hidden');
- if (!document.getElementById('nAddr').value) {
- document.getElementById('nAddr').value = data.client.addresses[0];
- }
+ if (!document.getElementById('nAddr').value) { document.getElementById('nAddr').value = data.client.addresses[0]; }
}
}
} catch (e) { console.error("Error en buscador rápido:", e); }
}
- // --- FUNCIONES DEL EDITOR ---
-
+ // --- MOTOR DE EDICIÓN DE FICHA ---
function enableEditing() {
document.getElementById('viewActions').classList.add('hidden');
document.getElementById('editActions').classList.remove('hidden');