Actualizar usuarios.html

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

View File

@@ -175,126 +175,50 @@
}); });
} }
// 1. Cargamos municipios AGRUPADOS por nombre async function fetchMunicipios(provName) {
// 1. Carga municipios agrupados para facilitar la selección masiva const selMun = document.getElementById('selMunicipio');
async function fetchMunicipios(provName) { const loader = document.getElementById('loaderMun');
const selMun = document.getElementById('selMunicipio'); selMun.innerHTML = '<option value="">-- Cargando... --</option>';
const loader = document.getElementById('loaderMun'); selMun.disabled = true;
selMun.innerHTML = '<option value="">-- Cargando... --</option>'; if(!provName) return;
selMun.disabled = true; if (loader) loader.classList.remove('hidden');
if(!provName) return;
if (loader) loader.classList.remove('hidden'); try {
try { const res = await fetch(`${API_URL}/api/geo/municipios/${provName}`, {
const res = await fetch(`${API_URL}/api/geo/municipios/${provName}`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` }
headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
}); const data = await res.json();
const data = await res.json(); if (data.ok && data.municipios.length > 0) {
const grouped = data.municipios.reduce((acc, m) => {
if (data.ok && data.municipios.length > 0) { if(!acc[m.municipio]) acc[m.municipio] = [];
// Agrupamos por nombre de municipio para el selector acc[m.municipio].push(m.codigo_postal);
const grouped = data.municipios.reduce((acc, m) => { return acc;
if(!acc[m.municipio]) acc[m.municipio] = []; }, {});
acc[m.municipio].push(m.codigo_postal);
return acc;
}, {});
selMun.innerHTML = '<option value="">-- Selecciona Pueblo --</option>'; selMun.innerHTML = '<option value="">-- Selecciona Pueblo --</option>';
Object.keys(grouped).sort().forEach(mun => { Object.keys(grouped).sort().forEach(mun => {
const opt = document.createElement('option'); const opt = document.createElement('option');
opt.value = mun; opt.value = mun;
// Guardamos la lista real de CPs para que el sistema los procese opt.dataset.cps = JSON.stringify(grouped[mun]);
opt.dataset.cps = JSON.stringify(grouped[mun]); opt.innerText = `${mun.toUpperCase()} (${grouped[mun].length} CPs)`;
opt.innerText = `${mun.toUpperCase()} (${grouped[mun].length} CPs)`; selMun.appendChild(opt);
selMun.appendChild(opt); });
}); selMun.innerHTML += '<option value="OTRO">-- NO ESTÁ EN LA LISTA (MANUAL) --</option>';
selMun.innerHTML += '<option value="OTRO">-- NO ESTÁ EN LA LISTA (MANUAL) --</option>'; selMun.disabled = false;
selMun.disabled = false; } else {
} selMun.innerHTML = '<option value="OTRO">-- ESCRIBIR MANUALMENTE --</option>';
} catch (e) { selMun.disabled = false;
showToast("Error al conectar con la base de datos", true); }
} finally { } catch (e) {
if (loader) loader.classList.add('hidden'); 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) { function autoFillCP(munName) {
const sel = document.getElementById('selMunicipio'); const sel = document.getElementById('selMunicipio');
const selectedOpt = sel.options[sel.selectedIndex];
if (munName === "OTRO") { if (munName === "OTRO") {
const customMun = prompt("Nombre del Municipio:"); const customMun = prompt("Nombre del Municipio:");
if (customMun) { if (customMun) {
@@ -303,43 +227,54 @@ function removeTempCity(cityName) {
newOpt.innerText = customMun.toUpperCase(); newOpt.innerText = customMun.toUpperCase();
newOpt.selected = true; newOpt.selected = true;
sel.appendChild(newOpt); sel.appendChild(newOpt);
document.getElementById('cpInput').focus();
} }
return;
}
if(selectedOpt && selectedOpt.dataset.cp) {
document.getElementById('cpInput').value = selectedOpt.dataset.cp;
} }
} }
function addZoneToUser() { function addZoneToUser() {
const prov = document.getElementById('selProvince').value; const prov = document.getElementById('selProvince').value;
const mun = document.getElementById('selMunicipio').value; const selMun = document.getElementById('selMunicipio');
const cp = document.getElementById('cpInput').value.trim(); const munName = selMun.value;
if (!prov || !mun || !cp || mun === "OTRO") { showToast("Completa los datos de zona", true); return; }
if (!prov || !munName) { showToast("Selecciona Provincia y Municipio", true); return; }
if (tempUserZones.some(z => z.city === mun && z.cps === cp)) { if (munName === "OTRO" || selMun.options[selMun.selectedIndex].text.includes("MANUAL")) {
showToast("Esta zona ya está añadida", true); return; 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(); renderTempZones();
document.getElementById('cpInput').value = ""; document.getElementById('cpInput').value = "";
} }
function removeTempZone(idx) { function removeTempCity(cityName) {
tempUserZones.splice(idx, 1); tempUserZones = tempUserZones.filter(z => z.city !== cityName);
renderTempZones(); renderTempZones();
} }
function renderTempZones() { function renderTempZones() {
const area = document.getElementById('userZonesArea'); 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>' : ""; 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 += ` 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"> <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}) ${city} (${visualGroup[city].length} 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> <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>`; </span>`;
}); });
lucide.createIcons(); lucide.createIcons();
@@ -349,11 +284,7 @@ function removeTempCity(cityName) {
try { try {
const res = await fetch(`${API_URL}/guilds`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } }); const res = await fetch(`${API_URL}/guilds`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
const data = await res.json(); const data = await res.json();
if (data.ok) { if (data.ok) { availableGuilds = data.guilds; renderGuildsList(); renderGuildsCheckboxes(); }
availableGuilds = data.guilds;
renderGuildsList();
renderGuildsCheckboxes();
}
} catch (e) {} } catch (e) {}
} }
@@ -378,10 +309,7 @@ function removeTempCity(cityName) {
try { try {
const res = await fetch(`${API_URL}/admin/users`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } }); const res = await fetch(`${API_URL}/admin/users`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
const data = await res.json(); const data = await res.json();
if (data.ok) { if (data.ok) { currentUsers = data.users; renderUsersTable(); }
currentUsers = data.users;
renderUsersTable();
}
} catch (e) {} } catch (e) {}
} }
@@ -411,11 +339,8 @@ function removeTempCity(cityName) {
body: JSON.stringify(data) body: JSON.stringify(data)
}); });
const json = await res.json(); const json = await res.json();
if (json.ok) { if (json.ok) { showToast(isEdit ? "Actualizado" : "Creado"); resetUserForm(); fetchUsers(); }
showToast(isEdit ? "Datos actualizados" : "Usuario creado"); else { showToast("Error: " + json.error, true); }
resetUserForm();
fetchUsers();
} else { showToast("❌ " + (json.error || "Error"), true); }
} catch (e) { showToast("Error conexión", true); } } catch (e) { showToast("Error conexión", true); }
finally { btn.disabled = false; btn.innerText = isEdit ? "Actualizar Datos" : "Guardar Usuario y Zonas"; } 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('uPhone').value = user.phone;
document.getElementById('uRole').value = user.role; document.getElementById('uRole').value = user.role;
document.getElementById('uPass').value = ""; document.getElementById('uPass').value = "";
tempUserZones = user.zones || []; tempUserZones = user.zones || [];
renderTempZones(); renderTempZones();
const userGuilds = user.guilds || []; const userGuilds = user.guilds || [];
document.querySelectorAll('.guild-checkbox').forEach(cb => { document.querySelectorAll('.guild-checkbox').forEach(cb => { cb.checked = userGuilds.includes(parseInt(cb.value)); });
cb.checked = userGuilds.includes(parseInt(cb.value));
});
document.getElementById('formTitle').innerText = "2. Editando Usuario"; document.getElementById('formTitle').innerText = "2. Editando Usuario";
document.getElementById('btnSubmitUser').classList.replace('bg-green-600', 'bg-blue-600');
document.getElementById('btnCancelEdit').classList.remove('hidden'); document.getElementById('btnCancelEdit').classList.remove('hidden');
window.scrollTo({ top: 0, behavior: 'smooth' }); window.scrollTo({ top: 0, behavior: 'smooth' });
} }
async function deleteUser(id) { async function deleteUser(id) {
if(!confirm("¿Seguro que quieres borrar a este usuario?")) return; if(!confirm("¿Borrar usuario?")) return;
try { try {
await fetch(`${API_URL}/admin/users/${id}`, { method: 'DELETE', headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } }); 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); } } catch(e) { showToast("Error", true); }
} }
@@ -461,7 +380,6 @@ function removeTempCity(cityName) {
tempUserZones = []; tempUserZones = [];
renderTempZones(); renderTempZones();
document.getElementById('formTitle').innerText = "2. Nuevo Usuario"; 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.getElementById('btnCancelEdit').classList.add('hidden');
document.querySelectorAll('.guild-checkbox').forEach(cb => cb.checked = false); document.querySelectorAll('.guild-checkbox').forEach(cb => cb.checked = false);
} }
@@ -470,64 +388,47 @@ function removeTempCity(cityName) {
const list = document.getElementById('guildsList'); const list = document.getElementById('guildsList');
list.innerHTML = ""; list.innerHTML = "";
availableGuilds.forEach(g => { 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(); lucide.createIcons();
} }
function renderGuildsCheckboxes() { function renderGuildsCheckboxes() {
const area = document.getElementById('guildsCheckboxArea'); 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 => { 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() { function renderUsersTable() {
const tbody = document.getElementById('usersListBody'); 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>` : ""; 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 => {
currentUsers.forEach(u => { const uGuildNames = (u.guilds || []).map(gid => availableGuilds.find(ag => ag.id === gid)?.name).filter(Boolean).join(", ");
const uGuildNames = (u.guilds || []).map(gid => availableGuilds.find(ag => ag.id === gid)?.name).filter(Boolean).join(", "); const groupedZones = (u.zones || []).reduce((acc, curr) => {
if (!acc[curr.city]) acc[curr.city] = [];
// --- LÓGICA DE AGRUPACIÓN POR MUNICIPIO --- if (!acc[curr.city].includes(curr.cps)) acc[curr.city].push(curr.cps);
const groupedZones = (u.zones || []).reduce((acc, current) => { return acc;
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);
}
return acc;
}, {});
// Convertimos el objeto agrupado en HTML elegante const uZonesHtml = Object.keys(groupedZones).map(city => {
const uZonesHtml = Object.keys(groupedZones).map(city => { 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>`;
const cps = groupedZones[city].join(", "); }).join("") || '<span class="text-[9px] text-gray-300 italic">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] font-medium">(${cps})</span>
</div>`;
}).join("") || '<span class="text-[9px] text-gray-300 italic text-left">Sin zona</span>';
tbody.innerHTML += ` tbody.innerHTML += `
<tr class="bg-white hover:bg-gray-50 transition border-b text-left"> <tr class="bg-white hover:bg-gray-50 transition border-b">
<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"><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 text-left">${u.phone}</td> <td class="p-4 font-black text-green-600">${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"><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"> <td class="p-4 text-left"><div class="max-h-24 overflow-y-auto no-scrollbar">${uZonesHtml}</div></td>
<div class="max-h-24 overflow-y-auto no-scrollbar"> <td class="p-4 text-right space-x-3">
${uZonesHtml} <button onclick="editUser(${u.id})" class="text-blue-500 hover:text-blue-700 font-black text-xs uppercase">Editar</button>
</div> <button onclick="deleteUser(${u.id})" class="text-red-400 hover:text-red-700 font-black text-xs">Baja</button>
</td> </td>
<td class="p-4 text-right space-x-3 text-left"> </tr>`;
<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> }
</td>
</tr>`;
});
}
function showToast(msg, err=false){ function showToast(msg, err=false){
const t=document.getElementById('toast'), m=document.getElementById('toastMsg'); const t=document.getElementById('toast'), m=document.getElementById('toastMsg');