Actualizar servicios.html

This commit is contained in:
2026-02-08 14:42:26 +00:00
parent 4bd5b21561
commit 7d40ab337f

View File

@@ -66,7 +66,9 @@
</h2> </h2>
<form onsubmit="handleFormSubmit(event)" class="space-y-6"> <form onsubmit="handleFormSubmit(event)" class="space-y-6">
<input type="hidden" id="editServiceId"> <div class="bg-white p-6 rounded-xl shadow-sm border border-gray-100"> <input type="hidden" id="editServiceId">
<div class="bg-white p-6 rounded-xl shadow-sm border border-gray-100">
<h3 class="text-lg font-bold text-gray-700 mb-4 border-b pb-2 flex items-center gap-2"> <h3 class="text-lg font-bold text-gray-700 mb-4 border-b pb-2 flex items-center gap-2">
<i data-lucide="user" class="w-5 h-5 text-gray-400"></i> Datos del Cliente <i data-lucide="user" class="w-5 h-5 text-gray-400"></i> Datos del Cliente
</h3> </h3>
@@ -245,7 +247,6 @@
let allStatuses = []; let allStatuses = [];
let currentServiceId = null; let currentServiceId = null;
let autocomplete; let autocomplete;
let currentServiceData = null; // Guardamos datos del servicio actual para edición
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");
@@ -272,12 +273,12 @@
document.getElementById('servicesListView').classList.remove('hidden'); document.getElementById('servicesListView').classList.remove('hidden');
fetchServices(); fetchServices();
} else { } else {
// RESETEAR FORMULARIO SI ES NUEVO
if(document.getElementById('editServiceId').value !== "") { if(document.getElementById('editServiceId').value !== "") {
document.getElementById('editServiceId').value = ""; // Limpiar ID document.getElementById('editServiceId').value = "";
document.querySelector('form').reset(); document.querySelector('form').reset();
document.getElementById('formTitle').innerHTML = '<i data-lucide="file-plus" class="text-green-600"></i> Alta de Nuevo Servicio'; document.getElementById('formTitle').innerHTML = '<i data-lucide="file-plus" class="text-green-600"></i> Alta de Nuevo Servicio';
document.getElementById('sDate').valueAsDate = new Date(); // Reset fecha document.getElementById('sDate').valueAsDate = new Date();
document.getElementById('sCreateStatus').disabled = false; // Reactivar al crear
lucide.createIcons(); lucide.createIcons();
} }
document.getElementById('createServiceView').classList.remove('hidden'); document.getElementById('createServiceView').classList.remove('hidden');
@@ -285,16 +286,71 @@
} }
} }
// --- FUNCIONES EDICIÓN Y BORRADO (NUEVAS) --- // --- CARGA DE DATOS ---
async function fetchStatuses() {
try {
const res = await fetch(`${API_URL}/statuses`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
const data = await res.json();
if(data.ok) {
allStatuses = data.statuses;
const createSel = document.getElementById('sCreateStatus');
createSel.innerHTML = '';
allStatuses.forEach(s => {
const isDef = s.is_default ? 'selected' : '';
createSel.innerHTML += `<option value="${s.id}" ${isDef}>${s.name}</option>`;
});
}
} catch(e) {}
}
// MODIFICADO: Devuelve promesa y no borra si ya existen
async function loadCompanies() {
try {
const sel = document.getElementById('sCompanyId');
// Si ya tiene datos (>1 porque la primera es "--Seleccionar--"), no recargamos
if (sel.options.length > 1) return;
const res = await fetch(`${API_URL}/companies`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
const data = await res.json();
if (data.ok) {
sel.innerHTML = '<option value="">-- Seleccionar --</option>';
data.companies.forEach(c => sel.innerHTML += `<option value="${c.id}">${c.name}</option>`);
}
} catch (e) {}
}
async function quickAddCompany() {
const name = prompt("Nombre de la nueva compañía:");
if(!name) return;
try {
const res = await fetch(`${API_URL}/companies`, {
method: 'POST',
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` },
body: JSON.stringify({ name })
});
if(res.ok) { showToast("Compañía añadida"); document.getElementById('sCompanyId').innerHTML = ""; await loadCompanies(); }
} catch(e) { alert("Error"); }
}
// --- EDICIÓN Y LOGICA DE FORMULARIO ---
function toggleCompanyFields() {
const isChecked = document.getElementById('sIsCompany').checked;
const div = document.getElementById('companyFields');
if(isChecked) {
div.classList.remove('hidden');
loadCompanies();
} else {
div.classList.add('hidden');
}
}
async function editService() { async function editService() {
if(!currentServiceId) return; if(!currentServiceId) return;
// Cargar datos completos si no los tenemos
try { try {
const res = await fetch(`${API_URL}/services/${currentServiceId}`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } }); const res = await fetch(`${API_URL}/services/${currentServiceId}`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
const json = await res.json(); const json = await res.json();
if(json.ok) { if(json.ok) {
fillEditForm(json.service); await fillEditForm(json.service); // AWAIT IMPORTANTE
closeDetailPanel(); closeDetailPanel();
document.getElementById('createServiceView').classList.remove('hidden'); document.getElementById('createServiceView').classList.remove('hidden');
document.getElementById('servicesListView').classList.add('hidden'); document.getElementById('servicesListView').classList.add('hidden');
@@ -302,7 +358,8 @@
} catch(e) { showToast("Error cargando datos", true); } } catch(e) { showToast("Error cargando datos", true); }
} }
function fillEditForm(s) { // MODIFICADO: AWAIT para esperar carga de compañías antes de asignar valor
async function fillEditForm(s) {
document.getElementById('editServiceId').value = s.id; document.getElementById('editServiceId').value = s.id;
document.getElementById('formTitle').innerHTML = '<i data-lucide="edit-2" class="text-blue-600"></i> Editando Servicio #' + s.id; document.getElementById('formTitle').innerHTML = '<i data-lucide="edit-2" class="text-blue-600"></i> Editando Servicio #' + s.id;
@@ -312,7 +369,6 @@
document.getElementById('sEmail').value = s.email || ''; document.getElementById('sEmail').value = s.email || '';
document.getElementById('sDesc').value = s.description || ''; document.getElementById('sDesc').value = s.description || '';
// Fechas (Cortar la parte de tiempo ISO)
if(s.scheduled_date) document.getElementById('sDate').value = s.scheduled_date.split('T')[0]; if(s.scheduled_date) document.getElementById('sDate').value = s.scheduled_date.split('T')[0];
if(s.scheduled_time) document.getElementById('sTime').value = s.scheduled_time; if(s.scheduled_time) document.getElementById('sTime').value = s.scheduled_time;
@@ -320,37 +376,24 @@
document.getElementById('sUrgent').checked = s.is_urgent; document.getElementById('sUrgent').checked = s.is_urgent;
document.getElementById('sIsCompany').checked = s.is_company; document.getElementById('sIsCompany').checked = s.is_company;
toggleCompanyFields(); // Mostrar/ocultar panel
// LÓGICA CRÍTICA PARA COMPAÑÍA
if(s.is_company) { if(s.is_company) {
document.getElementById('companyFields').classList.remove('hidden');
await loadCompanies(); // Esperamos a que el select se llene
document.getElementById('sCompanyId').value = s.company_id || ''; document.getElementById('sCompanyId').value = s.company_id || '';
document.getElementById('sCompanyRef').value = s.company_ref || ''; document.getElementById('sCompanyRef').value = s.company_ref || '';
} else {
document.getElementById('companyFields').classList.add('hidden');
} }
document.getElementById('sNotesInternal').value = s.internal_notes || ''; document.getElementById('sNotesInternal').value = s.internal_notes || '';
document.getElementById('sNotesClient').value = s.client_notes || ''; document.getElementById('sNotesClient').value = s.client_notes || '';
// El estado inicial no se edita aquí, se hace desde el panel de estados document.getElementById('sCreateStatus').disabled = true; // No editar estado aquí
document.getElementById('sCreateStatus').disabled = true;
lucide.createIcons(); lucide.createIcons();
} }
async function deleteService() {
if(!currentServiceId || !confirm("¿Estás seguro de que quieres borrar este servicio permanentemente?")) return;
try {
const res = await fetch(`${API_URL}/services/${currentServiceId}`, {
method: 'DELETE',
headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` }
});
if(res.ok) {
showToast("Servicio eliminado");
closeDetailPanel();
fetchServices();
} else { showToast("Error al borrar", true); }
} catch(e) { showToast("Error conexión", true); }
}
// --- SUBMIT UNIFICADO (CREAR / EDITAR) ---
async function handleFormSubmit(e) { async function handleFormSubmit(e) {
e.preventDefault(); e.preventDefault();
const btn = document.getElementById('btnSave'); const btn = document.getElementById('btnSave');
@@ -377,7 +420,7 @@
}; };
if (!isEdit) { if (!isEdit) {
data.status_id = document.getElementById('sCreateStatus').value; // Solo al crear data.status_id = document.getElementById('sCreateStatus').value;
} }
const url = isEdit ? `${API_URL}/services/${editId}` : `${API_URL}/services`; const url = isEdit ? `${API_URL}/services/${editId}` : `${API_URL}/services`;
@@ -392,66 +435,27 @@
const json = await res.json(); const json = await res.json();
if (json.ok) { if (json.ok) {
showToast(isEdit ? "✅ Servicio Actualizado" : "✅ Servicio Creado"); showToast(isEdit ? "✅ Actualizado" : "✅ Creado");
toggleView('list'); toggleView('list');
} else { } else {
showToast("❌ " + (json.error || "Error"), true); showToast("❌ " + (json.error || "Error"), true);
} }
} catch (e) { } catch (e) { showToast("Error conexión", true); }
showToast("Error de conexión", true); finally { btn.disabled = false; btn.innerText = "GUARDAR"; }
} finally {
btn.disabled = false; btn.innerText = "GUARDAR";
}
} }
// --- RESTO DE FUNCIONES (MAESTROS, LISTAR, ETC) --- async function deleteService() {
async function fetchStatuses() { if(!currentServiceId || !confirm("¿Borrar servicio permanentemente?")) return;
try { try {
const res = await fetch(`${API_URL}/statuses`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } }); const res = await fetch(`${API_URL}/services/${currentServiceId}`, {
const data = await res.json(); method: 'DELETE',
if(data.ok) { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` }
allStatuses = data.statuses;
const createSel = document.getElementById('sCreateStatus');
createSel.innerHTML = '';
allStatuses.forEach(s => {
const isDef = s.is_default ? 'selected' : '';
createSel.innerHTML += `<option value="${s.id}" ${isDef}>${s.name}</option>`;
});
}
} catch(e) {}
}
async function loadCompanies() {
try {
const res = await fetch(`${API_URL}/companies`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
const data = await res.json();
if (data.ok) {
const sel = document.getElementById('sCompanyId');
sel.innerHTML = '<option value="">-- Seleccionar --</option>';
data.companies.forEach(c => sel.innerHTML += `<option value="${c.id}">${c.name}</option>`);
}
} catch (e) {}
}
async function quickAddCompany() {
const name = prompt("Nombre de la nueva compañía:");
if(!name) return;
try {
const res = await fetch(`${API_URL}/companies`, {
method: 'POST',
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` },
body: JSON.stringify({ name })
}); });
if(res.ok) { showToast("Compañía añadida"); loadCompanies(); } if(res.ok) { showToast("Eliminado"); closeDetailPanel(); fetchServices(); }
} catch(e) { alert("Error"); } } catch(e) { showToast("Error", true); }
}
function toggleCompanyFields() {
const isChecked = document.getElementById('sIsCompany').checked;
const div = document.getElementById('companyFields');
if(isChecked) { div.classList.remove('hidden'); loadCompanies(); } else { div.classList.add('hidden'); }
} }
// --- BÚSQUEDA CLIENTE ---
async function searchClientByPhone() { async function searchClientByPhone() {
const phone = document.getElementById('sPhone').value; const phone = document.getElementById('sPhone').value;
if(phone.length < 8) return; if(phone.length < 8) return;
@@ -475,72 +479,53 @@
document.getElementById('clientFoundMsg').classList.add('hidden'); document.getElementById('clientFoundMsg').classList.add('hidden');
document.getElementById('sAddressSelect').classList.add('hidden'); document.getElementById('sAddressSelect').classList.add('hidden');
} }
} catch (e) { console.error(e); } } catch (e) {}
} }
function selectAddress(val) { if(val) document.getElementById('sAddress').value = val; } function selectAddress(val) { if(val) document.getElementById('sAddress').value = val; }
// --- LISTAR Y DETALLES --- // --- LISTAR SERVICIOS ---
async function fetchServices() { async function fetchServices() {
try { try {
const res = await fetch(`${API_URL}/services`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } }); const res = await fetch(`${API_URL}/services`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
const data = await res.json(); const data = await res.json();
const tbody = document.getElementById('servicesTableBody'); const tbody = document.getElementById('servicesTableBody');
tbody.innerHTML = ""; tbody.innerHTML = "";
if(!data.ok || data.services.length === 0) { if(!data.ok || data.services.length === 0) {
tbody.innerHTML = `<tr><td colspan="5" class="p-8 text-center text-gray-400 bg-white">No hay servicios registrados. <button onclick="toggleView('create')" class="text-blue-600 font-bold hover:underline">Crear el primero</button></td></tr>`; tbody.innerHTML = `<tr><td colspan="5" class="p-8 text-center text-gray-400 bg-white">No hay servicios. <button onclick="toggleView('create')" class="text-blue-600 font-bold hover:underline">Crear uno</button></td></tr>`;
return; return;
} }
data.services.forEach(s => { data.services.forEach(s => {
const color = s.status_color || 'gray'; const color = s.status_color || 'gray';
const date = new Date(s.created_at); const date = new Date(s.created_at);
const formattedDate = date.toLocaleDateString('es-ES', { day: '2-digit', month: 'short' }); const formattedDate = date.toLocaleDateString('es-ES', { day: '2-digit', month: 'short' });
tbody.innerHTML += ` tbody.innerHTML += `
<tr class="hover:bg-blue-50 cursor-pointer transition border-b last:border-0 bg-white" <tr class="hover:bg-blue-50 cursor-pointer transition border-b last:border-0 bg-white"
onclick="openDetail(${s.id}, '${s.contact_name}', '${s.title}', '${s.status_name}', '${color}', '${formattedDate}')"> onclick="openDetail(${s.id}, '${s.contact_name}', '${s.title}', '${s.status_name}', '${color}', '${formattedDate}')">
<td class="p-4 text-gray-500 whitespace-nowrap font-mono text-xs">${formattedDate}</td> <td class="p-4 text-gray-500 whitespace-nowrap font-mono text-xs">${formattedDate}</td>
<td class="p-4"> <td class="p-4"><p class="font-bold text-gray-900">${s.contact_name}</p><p class="text-xs text-gray-500 truncate max-w-[200px] flex items-center gap-1"><i data-lucide="map-pin" class="w-3 h-3"></i> ${s.address}</p></td>
<p class="font-bold text-gray-900">${s.contact_name}</p> <td class="p-4"><p class="text-sm text-gray-700 truncate max-w-[250px]">${s.description || 'Sin detalles'}</p>${s.is_urgent ? '<span class="text-[10px] font-bold text-red-600 bg-red-100 px-1 rounded uppercase">Urgente</span>' : ''}${s.is_company ? `<span class="text-[10px] font-bold text-blue-600 bg-blue-100 px-1 rounded uppercase ml-1">${s.company_name || 'Compañía'}</span>` : ''}</td>
<p class="text-xs text-gray-500 truncate max-w-[200px] flex items-center gap-1"><i data-lucide="map-pin" class="w-3 h-3"></i> ${s.address}</p> <td class="p-4"><span class="px-3 py-1 rounded-full text-xs font-bold text-white shadow-sm bg-${color}-500 whitespace-nowrap">${s.status_name || 'Nuevo'}</span></td>
</td>
<td class="p-4">
<p class="text-sm text-gray-700 truncate max-w-[250px]">${s.description || 'Sin detalles'}</p>
${s.is_urgent ? '<span class="text-[10px] font-bold text-red-600 bg-red-100 px-1 rounded uppercase">Urgente</span>' : ''}
${s.is_company ? `<span class="text-[10px] font-bold text-blue-600 bg-blue-100 px-1 rounded uppercase ml-1">${s.company_name || 'Compañía'}</span>` : ''}
</td>
<td class="p-4">
<span class="px-3 py-1 rounded-full text-xs font-bold text-white shadow-sm bg-${color}-500 whitespace-nowrap">
${s.status_name || 'Nuevo'}
</span>
</td>
<td class="p-4 text-right"><i data-lucide="chevron-right" class="w-5 h-5 text-gray-300"></i></td> <td class="p-4 text-right"><i data-lucide="chevron-right" class="w-5 h-5 text-gray-300"></i></td>
</tr> </tr>
`; `;
}); });
lucide.createIcons(); lucide.createIcons();
} catch (e) { console.error(e); } } catch (e) {}
} }
async function openDetail(id, client, title, statusName, statusColor, date) { async function openDetail(id, client, title, statusName, statusColor, date) {
currentServiceId = id; currentServiceId = id;
document.getElementById('serviceDetailPanel').classList.remove('hidden'); document.getElementById('serviceDetailPanel').classList.remove('hidden');
document.getElementById('detailTitle').innerText = title || 'Servicio General'; document.getElementById('detailTitle').innerText = title || 'Servicio General';
document.getElementById('detailClient').innerText = client; document.getElementById('detailClient').innerText = client;
document.getElementById('detailId').innerText = `#${id}`; document.getElementById('detailId').innerText = `#${id}`;
document.getElementById('detailDate').innerText = date; document.getElementById('detailDate').innerText = date;
const badge = document.getElementById('detailStatusBadge'); const badge = document.getElementById('detailStatusBadge');
badge.innerText = statusName; badge.innerText = statusName;
badge.className = `px-2 py-1 rounded text-[10px] font-bold text-white uppercase tracking-wide bg-${statusColor}-500`; badge.className = `px-2 py-1 rounded text-[10px] font-bold text-white uppercase tracking-wide bg-${statusColor}-500`;
const sel = document.getElementById('newStatusSelect'); const sel = document.getElementById('newStatusSelect');
sel.innerHTML = ""; sel.innerHTML = "";
allStatuses.forEach(s => sel.innerHTML += `<option value="${s.id}">${s.name}</option>`); allStatuses.forEach(s => sel.innerHTML += `<option value="${s.id}">${s.name}</option>`);
loadTimeline(id); loadTimeline(id);
} }
@@ -550,15 +535,15 @@
const res = await fetch(`${API_URL}/services/${id}/logs`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } }); const res = await fetch(`${API_URL}/services/${id}/logs`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
const data = await res.json(); const data = await res.json();
timeline.innerHTML = ""; timeline.innerHTML = "";
if(data.logs.length === 0) { timeline.innerHTML = '<p class="text-sm text-gray-400">Sin historial registrado.</p>'; return; } if(data.logs.length === 0) { timeline.innerHTML = '<p class="text-sm text-gray-400">Sin historial.</p>'; return; }
data.logs.forEach(log => { data.logs.forEach(log => {
const color = log.new_color || 'gray'; const color = log.new_color || 'gray';
const date = new Date(log.created_at); const date = new Date(log.created_at);
timeline.innerHTML += ` timeline.innerHTML += `
<div class="mb-6 relative group"> <div class="mb-6 relative group">
<span class="absolute -left-[33px] flex items-center justify-center w-6 h-6 bg-${color}-100 rounded-full ring-4 ring-white group-hover:scale-110 transition-transform"><div class="w-2 h-2 bg-${color}-600 rounded-full"></div></span> <span class="absolute -left-[33px] flex items-center justify-center w-6 h-6 bg-${color}-100 rounded-full ring-4 ring-white"><div class="w-2 h-2 bg-${color}-600 rounded-full"></div></span>
<div class="flex justify-between items-center mb-1"><h3 class="text-sm font-bold text-gray-800">${log.new_status}</h3><time class="text-[10px] font-medium text-gray-400 uppercase">${date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})} · ${date.toLocaleDateString()}</time></div> <div class="flex justify-between items-center mb-1"><h3 class="text-sm font-bold text-gray-800">${log.new_status}</h3><time class="text-[10px] font-medium text-gray-400 uppercase">${date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})} · ${date.toLocaleDateString()}</time></div>
<div class="bg-gray-50 p-3 rounded-lg border border-gray-100 text-sm text-gray-600 hover:bg-white hover:shadow-sm transition-all"><p>${log.comment || 'Cambio de estado'}</p><p class="text-[10px] text-gray-400 mt-2 text-right italic border-t border-gray-100 pt-1">Usuario: ${log.user_name || 'Sistema'}</p></div> <div class="bg-gray-50 p-3 rounded-lg border border-gray-100 text-sm text-gray-600"><p>${log.comment || 'Cambio de estado'}</p><p class="text-[10px] text-gray-400 mt-2 text-right italic border-t border-gray-100 pt-1">Usuario: ${log.user_name || 'Sistema'}</p></div>
</div> </div>
`; `;
}); });
@@ -567,7 +552,7 @@
async function updateStatus() { async function updateStatus() {
const statusId = document.getElementById('newStatusSelect').value; const statusId = document.getElementById('newStatusSelect').value;
if(!statusId) return; if(!statusId) return;
const comment = prompt("Añade un comentario sobre este cambio (opcional):"); const comment = prompt("Comentario (opcional):");
try { try {
await fetch(`${API_URL}/services/${currentServiceId}/status`, { await fetch(`${API_URL}/services/${currentServiceId}/status`, {
method: 'PUT', method: 'PUT',
@@ -577,7 +562,7 @@
showToast("Estado actualizado"); showToast("Estado actualizado");
loadTimeline(currentServiceId); loadTimeline(currentServiceId);
fetchServices(); fetchServices();
} catch(e) { showToast("Error al actualizar", true); } } catch(e) { showToast("Error", true); }
} }
function closeDetailPanel() { document.getElementById('serviceDetailPanel').classList.add('hidden'); } function closeDetailPanel() { document.getElementById('serviceDetailPanel').classList.add('hidden'); }