Actualizar usuarios.html

This commit is contained in:
2026-02-15 17:24:54 +00:00
parent eb1570ff90
commit e87b0bd8aa

View File

@@ -175,24 +175,20 @@
});
}
// 1. Cargamos municipios AGRUPADOS por nombre
// 1. Carga municipios agrupados para facilitar la selección masiva
async function fetchMunicipios(provName) {
const selMun = document.getElementById('selMunicipio');
const loader = document.getElementById('loaderMun');
selMun.innerHTML = '<option value="">-- Cargando... --</option>';
selMun.disabled = true;
if(!provName) return;
if (loader) loader.classList.remove('hidden');
try {
const res = await fetch(`${API_URL}/api/geo/municipios/${provName}`, {
headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` }
});
const data = await res.json();
if (data.ok && data.municipios.length > 0) {
// Agrupamos por nombre de municipio para el selector
const grouped = data.municipios.reduce((acc, m) => {
if(!acc[m.municipio]) acc[m.municipio] = [];
acc[m.municipio].push(m.codigo_postal);
@@ -203,98 +199,26 @@ async function fetchMunicipios(provName) {
Object.keys(grouped).sort().forEach(mun => {
const opt = document.createElement('option');
opt.value = mun;
// Guardamos la lista real de CPs para que el sistema los procese
opt.dataset.cps = JSON.stringify(grouped[mun]);
opt.innerText = `${mun.toUpperCase()} (${grouped[mun].length} CPs)`;
selMun.appendChild(opt);
});
selMun.innerHTML += '<option value="OTRO">-- NO ESTÁ EN LA LISTA (MANUAL) --</option>';
selMun.disabled = false;
} else {
selMun.innerHTML = '<option value="OTRO">-- ESCRIBIR MANUALMENTE --</option>';
selMun.disabled = false;
}
} catch (e) {
showToast("Error al conectar con la base de datos", true);
console.error("Error geo:", e);
showToast("Error base de datos", true);
} finally {
if (loader) loader.classList.add('hidden');
}
}
// 2. Añadir zonas al usuario (CORREGIDO: Sin error de validación)
function addZoneToUser() {
const prov = document.getElementById('selProvince').value;
const selMun = document.getElementById('selMunicipio');
const munName = selMun.value;
// Validación corregida
if (!prov || !munName) {
showToast("Selecciona Provincia y Municipio primero", true);
return;
}
if (munName === "OTRO") {
const manualCP = document.getElementById('cpInput').value.trim();
if(!manualCP) { showToast("Escribe el CP manual en el cuadro de la derecha", true); return; }
tempUserZones.push({ province: prov, city: "MANUAL", cps: manualCP });
} else {
// Extraemos la lista de CPs reales del municipio
const selectedOption = selMun.options[selMun.selectedIndex];
const cpList = JSON.parse(selectedOption.dataset.cps);
let addedCount = 0;
cpList.forEach(cp => {
// Verificamos si el CP individual ya está para no duplicar
const alreadyHasIt = tempUserZones.some(z => z.city === munName && z.cps === cp);
if (!alreadyHasIt) {
tempUserZones.push({ province: prov, city: munName, cps: cp });
addedCount++;
}
});
if(addedCount > 0) {
showToast(`✅ Añadidos ${addedCount} códigos de ${munName}`);
} else {
showToast("Esta zona ya estaba completa", true);
}
}
renderTempZones();
document.getElementById('cpInput').value = ""; // Limpiamos campo manual
}
// 3. Renderizado con agrupación visual (Mantiene la tabla limpia)
function renderTempZones() {
const area = document.getElementById('userZonesArea');
if (!area) return;
area.innerHTML = tempUserZones.length === 0 ? '<p class="text-[10px] text-gray-300 italic p-1">Sin zonas añadidas...</p>' : "";
// Agrupamos solo para mostrarlo bonito en los chips
const visualGroup = tempUserZones.reduce((acc, curr) => {
if(!acc[curr.city]) acc[curr.city] = [];
acc[curr.city].push(curr.cps);
return acc;
}, {});
Object.keys(visualGroup).forEach(city => {
const cps = visualGroup[city].join(", ");
area.innerHTML += `
<span class="bg-blue-100 text-blue-700 px-3 py-1.5 rounded-lg text-[10px] font-black border border-blue-200 flex items-center gap-2">
${city} (${visualGroup[city].length} CPs)
<button type="button" onclick="removeTempCity('${city}')" class="text-blue-400 hover:text-red-500">
<i data-lucide="x" class="w-3 h-3"></i>
</button>
</span>`;
});
lucide.createIcons();
}
// Función auxiliar para borrar un pueblo entero de la lista temporal
function removeTempCity(cityName) {
tempUserZones = tempUserZones.filter(z => z.city !== cityName);
renderTempZones();
}
function autoFillCP(munName) {
const sel = document.getElementById('selMunicipio');
const selectedOpt = sel.options[sel.selectedIndex];
if (munName === "OTRO") {
const customMun = prompt("Nombre del Municipio:");
if (customMun) {
@@ -303,43 +227,54 @@ function removeTempCity(cityName) {
newOpt.innerText = customMun.toUpperCase();
newOpt.selected = true;
sel.appendChild(newOpt);
document.getElementById('cpInput').focus();
}
return;
}
if(selectedOpt && selectedOpt.dataset.cp) {
document.getElementById('cpInput').value = selectedOpt.dataset.cp;
}
}
function addZoneToUser() {
const prov = document.getElementById('selProvince').value;
const mun = document.getElementById('selMunicipio').value;
const cp = document.getElementById('cpInput').value.trim();
if (!prov || !mun || !cp || mun === "OTRO") { showToast("Completa los datos de zona", true); return; }
const selMun = document.getElementById('selMunicipio');
const munName = selMun.value;
if (tempUserZones.some(z => z.city === mun && z.cps === cp)) {
showToast("Esta zona ya está añadida", true); return;
if (!prov || !munName) { showToast("Selecciona Provincia y Municipio", true); return; }
if (munName === "OTRO" || selMun.options[selMun.selectedIndex].text.includes("MANUAL")) {
const manualCP = document.getElementById('cpInput').value.trim();
if(!manualCP) { showToast("Escribe el CP manual", true); return; }
tempUserZones.push({ province: prov, city: munName === "OTRO" ? "MANUAL" : munName, cps: manualCP });
} else {
const cpList = JSON.parse(selMun.options[selMun.selectedIndex].dataset.cps);
cpList.forEach(cp => {
if (!tempUserZones.some(z => z.city === munName && z.cps === cp)) {
tempUserZones.push({ province: prov, city: munName, cps: cp });
}
});
showToast(`Añadidos códigos de ${munName}`);
}
tempUserZones.push({ province: prov, city: mun, cps: cp });
renderTempZones();
document.getElementById('cpInput').value = "";
}
function removeTempZone(idx) {
tempUserZones.splice(idx, 1);
function removeTempCity(cityName) {
tempUserZones = tempUserZones.filter(z => z.city !== cityName);
renderTempZones();
}
function renderTempZones() {
const area = document.getElementById('userZonesArea');
area.innerHTML = tempUserZones.length === 0 ? '<p class="text-[10px] text-gray-300 italic p-1">Sin zonas añadidas...</p>' : "";
tempUserZones.forEach((z, i) => {
const visualGroup = tempUserZones.reduce((acc, curr) => {
if(!acc[curr.city]) acc[curr.city] = [];
acc[curr.city].push(curr.cps);
return acc;
}, {});
Object.keys(visualGroup).forEach(city => {
area.innerHTML += `
<span class="bg-blue-100 text-blue-700 px-3 py-1.5 rounded-lg text-[10px] font-black border border-blue-200 flex items-center gap-2">
${z.city} (${z.cps})
<button type="button" onclick="removeTempZone(${i})" class="text-blue-400 hover:text-red-500"><i data-lucide="x" class="w-3 h-3"></i></button>
${city} (${visualGroup[city].length} CPs)
<button type="button" onclick="removeTempCity('${city}')" class="text-blue-400 hover:text-red-500"><i data-lucide="x" class="w-3 h-3"></i></button>
</span>`;
});
lucide.createIcons();
@@ -349,11 +284,7 @@ function removeTempCity(cityName) {
try {
const res = await fetch(`${API_URL}/guilds`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
const data = await res.json();
if (data.ok) {
availableGuilds = data.guilds;
renderGuildsList();
renderGuildsCheckboxes();
}
if (data.ok) { availableGuilds = data.guilds; renderGuildsList(); renderGuildsCheckboxes(); }
} catch (e) {}
}
@@ -378,10 +309,7 @@ function removeTempCity(cityName) {
try {
const res = await fetch(`${API_URL}/admin/users`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
const data = await res.json();
if (data.ok) {
currentUsers = data.users;
renderUsersTable();
}
if (data.ok) { currentUsers = data.users; renderUsersTable(); }
} catch (e) {}
}
@@ -411,11 +339,8 @@ function removeTempCity(cityName) {
body: JSON.stringify(data)
});
const json = await res.json();
if (json.ok) {
showToast(isEdit ? "Datos actualizados" : "Usuario creado");
resetUserForm();
fetchUsers();
} else { showToast("❌ " + (json.error || "Error"), true); }
if (json.ok) { showToast(isEdit ? "Actualizado" : "Creado"); resetUserForm(); fetchUsers(); }
else { showToast("Error: " + json.error, true); }
} catch (e) { showToast("Error conexión", true); }
finally { btn.disabled = false; btn.innerText = isEdit ? "Actualizar Datos" : "Guardar Usuario y Zonas"; }
}
@@ -429,26 +354,20 @@ function removeTempCity(cityName) {
document.getElementById('uPhone').value = user.phone;
document.getElementById('uRole').value = user.role;
document.getElementById('uPass').value = "";
tempUserZones = user.zones || [];
renderTempZones();
const userGuilds = user.guilds || [];
document.querySelectorAll('.guild-checkbox').forEach(cb => {
cb.checked = userGuilds.includes(parseInt(cb.value));
});
document.querySelectorAll('.guild-checkbox').forEach(cb => { cb.checked = userGuilds.includes(parseInt(cb.value)); });
document.getElementById('formTitle').innerText = "2. Editando Usuario";
document.getElementById('btnSubmitUser').classList.replace('bg-green-600', 'bg-blue-600');
document.getElementById('btnCancelEdit').classList.remove('hidden');
window.scrollTo({ top: 0, behavior: 'smooth' });
}
async function deleteUser(id) {
if(!confirm("¿Seguro que quieres borrar a este usuario?")) return;
if(!confirm("¿Borrar usuario?")) return;
try {
await fetch(`${API_URL}/admin/users/${id}`, { method: 'DELETE', headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
fetchUsers(); showToast("Usuario borrado");
fetchUsers(); showToast("Borrado");
} catch(e) { showToast("Error", true); }
}
@@ -461,7 +380,6 @@ function removeTempCity(cityName) {
tempUserZones = [];
renderTempZones();
document.getElementById('formTitle').innerText = "2. Nuevo Usuario";
document.getElementById('btnSubmitUser').classList.replace('bg-blue-600', 'bg-green-600');
document.getElementById('btnCancelEdit').classList.add('hidden');
document.querySelectorAll('.guild-checkbox').forEach(cb => cb.checked = false);
}
@@ -470,60 +388,43 @@ function removeTempCity(cityName) {
const list = document.getElementById('guildsList');
list.innerHTML = "";
availableGuilds.forEach(g => {
list.innerHTML += `<div class="flex justify-between items-center bg-white p-2 rounded border shadow-sm"><span class="text-xs font-black uppercase text-slate-700">${g.name}</span><button type="button" onclick="deleteGuild(${g.id})" class="text-gray-400 hover:text-red-500 transition-colors text-left"><i data-lucide="trash-2" class="w-3.5 h-3.5 text-left"></i></button></div>`;
list.innerHTML += `<div class="flex justify-between items-center bg-white p-2 rounded border shadow-sm"><span class="text-xs font-black uppercase text-slate-700">${g.name}</span><button onclick="deleteGuild(${g.id})" class="text-gray-400 hover:text-red-500 transition-colors"><i data-lucide="trash-2" class="w-3.5 h-3.5"></i></button></div>`;
});
lucide.createIcons();
}
function renderGuildsCheckboxes() {
const area = document.getElementById('guildsCheckboxArea');
area.innerHTML = availableGuilds.length === 0 ? '<p class="text-xs text-gray-300 italic">No hay gremios registrados...</p>' : "";
area.innerHTML = availableGuilds.length === 0 ? '<p class="text-xs text-gray-300 italic">No hay gremios...</p>' : "";
availableGuilds.forEach(g => {
area.innerHTML += `<label class="flex items-center space-x-2 cursor-pointer p-2 hover:bg-white rounded border border-transparent hover:border-green-200 transition-all text-left"><input type="checkbox" value="${g.id}" class="guild-checkbox h-4 w-4 text-green-600 border-gray-300 rounded text-left"><span class="text-[10px] font-black uppercase text-slate-500 text-left">${g.name}</span></label>`;
area.innerHTML += `<label class="flex items-center space-x-2 cursor-pointer p-2 hover:bg-white rounded border border-transparent hover:border-green-200 transition-all"><input type="checkbox" value="${g.id}" class="guild-checkbox h-4 w-4 text-green-600 border-gray-300 rounded"><span class="text-[10px] font-black uppercase text-slate-500">${g.name}</span></label>`;
});
}
function renderUsersTable() {
const tbody = document.getElementById('usersListBody');
tbody.innerHTML = currentUsers.length === 0 ? `<tr><td colspan="5" class="p-8 text-center text-gray-300 font-bold uppercase tracking-widest text-left">Cargando equipo...</td></tr>` : "";
currentUsers.forEach(u => {
const uGuildNames = (u.guilds || []).map(gid => availableGuilds.find(ag => ag.id === gid)?.name).filter(Boolean).join(", ");
// --- LÓGICA DE AGRUPACIÓN POR MUNICIPIO ---
const groupedZones = (u.zones || []).reduce((acc, current) => {
if (!acc[current.city]) {
acc[current.city] = [];
}
// Evitamos duplicados de CP en la misma ciudad
if (!acc[current.city].includes(current.cps)) {
acc[current.city].push(current.cps);
}
const groupedZones = (u.zones || []).reduce((acc, curr) => {
if (!acc[curr.city]) acc[curr.city] = [];
if (!acc[curr.city].includes(curr.cps)) acc[curr.city].push(curr.cps);
return acc;
}, {});
// Convertimos el objeto agrupado en HTML elegante
const uZonesHtml = Object.keys(groupedZones).map(city => {
const cps = groupedZones[city].join(", ");
return `<div class="mb-1 last:mb-0">
<span class="font-black text-blue-600 text-[10px] uppercase">📍 ${city}</span>
<span class="text-gray-500 text-[9px] font-medium">(${cps})</span>
</div>`;
}).join("") || '<span class="text-[9px] text-gray-300 italic text-left">Sin zona</span>';
return `<div class="mb-1 last:mb-0"><span class="font-black text-blue-600 text-[10px] uppercase">📍 ${city}</span> <span class="text-gray-500 text-[9px]">(${groupedZones[city].join(", ")})</span></div>`;
}).join("") || '<span class="text-[9px] text-gray-300 italic">Sin zona</span>';
tbody.innerHTML += `
<tr class="bg-white hover:bg-gray-50 transition border-b text-left">
<td class="p-4"><div class="flex flex-col text-left"><span class="font-black text-slate-800 uppercase text-left">${u.full_name}</span><span class="text-[9px] text-gray-400 font-bold tracking-tighter text-left">${u.email}</span></div></td>
<td class="p-4 font-black text-green-600 text-left">${u.phone}</td>
<td class="p-4 text-left"><span class="bg-slate-100 text-slate-600 px-2 py-0.5 rounded text-[10px] font-black uppercase text-left">${u.role}</span><p class="text-[9px] text-gray-400 mt-1 uppercase font-bold text-left">${uGuildNames || '-'}</p></td>
<td class="p-4 text-left">
<div class="max-h-24 overflow-y-auto no-scrollbar">
${uZonesHtml}
</div>
</td>
<td class="p-4 text-right space-x-3 text-left">
<button onclick="editUser(${u.id})" class="text-blue-500 hover:text-blue-700 font-black text-xs uppercase tracking-widest text-left">Editar</button>
<button onclick="deleteUser(${u.id})" class="text-red-400 hover:text-red-700 font-black text-xs uppercase tracking-widest text-left">Baja</button>
<tr class="bg-white hover:bg-gray-50 transition border-b">
<td class="p-4"><div class="flex flex-col"><span class="font-black text-slate-800 uppercase">${u.full_name}</span><span class="text-[9px] text-gray-400 font-bold">${u.email}</span></div></td>
<td class="p-4 font-black text-green-600">${u.phone}</td>
<td class="p-4"><span class="bg-slate-100 text-slate-600 px-2 py-0.5 rounded text-[10px] font-black uppercase">${u.role}</span><p class="text-[9px] text-gray-400 mt-1 uppercase font-bold">${uGuildNames || '-'}</p></td>
<td class="p-4 text-left"><div class="max-h-24 overflow-y-auto no-scrollbar">${uZonesHtml}</div></td>
<td class="p-4 text-right space-x-3">
<button onclick="editUser(${u.id})" class="text-blue-500 hover:text-blue-700 font-black text-xs uppercase">Editar</button>
<button onclick="deleteUser(${u.id})" class="text-red-400 hover:text-red-700 font-black text-xs">Baja</button>
</td>
</tr>`;
});