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>
<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">
<i data-lucide="user" class="w-5 h-5 text-gray-400"></i> Datos del Cliente
</h3>
@@ -245,7 +247,6 @@
let allStatuses = [];
let currentServiceId = null;
let autocomplete;
let currentServiceData = null; // Guardamos datos del servicio actual para edición
document.addEventListener("DOMContentLoaded", () => {
const token = localStorage.getItem("token");
@@ -272,12 +273,12 @@
document.getElementById('servicesListView').classList.remove('hidden');
fetchServices();
} else {
// RESETEAR FORMULARIO SI ES NUEVO
if(document.getElementById('editServiceId').value !== "") {
document.getElementById('editServiceId').value = ""; // Limpiar ID
document.getElementById('editServiceId').value = "";
document.querySelector('form').reset();
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();
}
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() {
if(!currentServiceId) return;
// Cargar datos completos si no los tenemos
try {
const res = await fetch(`${API_URL}/services/${currentServiceId}`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
const json = await res.json();
if(json.ok) {
fillEditForm(json.service);
await fillEditForm(json.service); // AWAIT IMPORTANTE
closeDetailPanel();
document.getElementById('createServiceView').classList.remove('hidden');
document.getElementById('servicesListView').classList.add('hidden');
@@ -302,7 +358,8 @@
} 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('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('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_time) document.getElementById('sTime').value = s.scheduled_time;
@@ -320,37 +376,24 @@
document.getElementById('sUrgent').checked = s.is_urgent;
document.getElementById('sIsCompany').checked = s.is_company;
toggleCompanyFields(); // Mostrar/ocultar panel
// LÓGICA CRÍTICA PARA COMPAÑÍA
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('sCompanyRef').value = s.company_ref || '';
} else {
document.getElementById('companyFields').classList.add('hidden');
}
document.getElementById('sNotesInternal').value = s.internal_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;
document.getElementById('sCreateStatus').disabled = true; // No editar estado aquí
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) {
e.preventDefault();
const btn = document.getElementById('btnSave');
@@ -377,7 +420,7 @@
};
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`;
@@ -392,66 +435,27 @@
const json = await res.json();
if (json.ok) {
showToast(isEdit ? "✅ Servicio Actualizado" : "✅ Servicio Creado");
showToast(isEdit ? "✅ Actualizado" : "✅ Creado");
toggleView('list');
} else {
showToast("❌ " + (json.error || "Error"), true);
}
} catch (e) {
showToast("Error de conexión", true);
} finally {
btn.disabled = false; btn.innerText = "GUARDAR";
}
} catch (e) { showToast("Error conexión", true); }
finally { btn.disabled = false; btn.innerText = "GUARDAR"; }
}
// --- RESTO DE FUNCIONES (MAESTROS, LISTAR, ETC) ---
async function fetchStatuses() {
async function deleteService() {
if(!currentServiceId || !confirm("¿Borrar servicio permanentemente?")) return;
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) {}
}
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 })
const res = await fetch(`${API_URL}/services/${currentServiceId}`, {
method: 'DELETE',
headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` }
});
if(res.ok) { showToast("Compañía añadida"); loadCompanies(); }
} catch(e) { alert("Error"); }
}
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'); }
if(res.ok) { showToast("Eliminado"); closeDetailPanel(); fetchServices(); }
} catch(e) { showToast("Error", true); }
}
// --- BÚSQUEDA CLIENTE ---
async function searchClientByPhone() {
const phone = document.getElementById('sPhone').value;
if(phone.length < 8) return;
@@ -475,72 +479,53 @@
document.getElementById('clientFoundMsg').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; }
// --- LISTAR Y DETALLES ---
// --- LISTAR SERVICIOS ---
async function fetchServices() {
try {
const res = await fetch(`${API_URL}/services`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
const data = await res.json();
const tbody = document.getElementById('servicesTableBody');
tbody.innerHTML = "";
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;
}
data.services.forEach(s => {
const color = s.status_color || 'gray';
const date = new Date(s.created_at);
const formattedDate = date.toLocaleDateString('es-ES', { day: '2-digit', month: 'short' });
tbody.innerHTML += `
<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}')">
<td class="p-4 text-gray-500 whitespace-nowrap font-mono text-xs">${formattedDate}</td>
<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>
<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"><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>
<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>
</tr>
`;
});
lucide.createIcons();
} catch (e) { console.error(e); }
} catch (e) {}
}
async function openDetail(id, client, title, statusName, statusColor, date) {
currentServiceId = id;
document.getElementById('serviceDetailPanel').classList.remove('hidden');
document.getElementById('detailTitle').innerText = title || 'Servicio General';
document.getElementById('detailClient').innerText = client;
document.getElementById('detailId').innerText = `#${id}`;
document.getElementById('detailDate').innerText = date;
const badge = document.getElementById('detailStatusBadge');
badge.innerText = statusName;
badge.className = `px-2 py-1 rounded text-[10px] font-bold text-white uppercase tracking-wide bg-${statusColor}-500`;
const sel = document.getElementById('newStatusSelect');
sel.innerHTML = "";
allStatuses.forEach(s => sel.innerHTML += `<option value="${s.id}">${s.name}</option>`);
loadTimeline(id);
}
@@ -550,15 +535,15 @@
const res = await fetch(`${API_URL}/services/${id}/logs`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
const data = await res.json();
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 => {
const color = log.new_color || 'gray';
const date = new Date(log.created_at);
timeline.innerHTML += `
<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="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>
`;
});
@@ -567,7 +552,7 @@
async function updateStatus() {
const statusId = document.getElementById('newStatusSelect').value;
if(!statusId) return;
const comment = prompt("Añade un comentario sobre este cambio (opcional):");
const comment = prompt("Comentario (opcional):");
try {
await fetch(`${API_URL}/services/${currentServiceId}/status`, {
method: 'PUT',
@@ -577,7 +562,7 @@
showToast("Estado actualizado");
loadTimeline(currentServiceId);
fetchServices();
} catch(e) { showToast("Error al actualizar", true); }
} catch(e) { showToast("Error", true); }
}
function closeDetailPanel() { document.getElementById('serviceDetailPanel').classList.add('hidden'); }