Actualizar usuarios.html
This commit is contained in:
301
usuarios.html
301
usuarios.html
@@ -175,126 +175,50 @@
|
||||
});
|
||||
}
|
||||
|
||||
// 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;
|
||||
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');
|
||||
|
||||
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();
|
||||
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) {
|
||||
const grouped = data.municipios.reduce((acc, m) => {
|
||||
if(!acc[m.municipio]) acc[m.municipio] = [];
|
||||
acc[m.municipio].push(m.codigo_postal);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
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);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
selMun.innerHTML = '<option value="">-- Selecciona Pueblo --</option>';
|
||||
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;
|
||||
}
|
||||
} catch (e) {
|
||||
showToast("Error al conectar con la 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++;
|
||||
selMun.innerHTML = '<option value="">-- Selecciona Pueblo --</option>';
|
||||
Object.keys(grouped).sort().forEach(mun => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = mun;
|
||||
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) {
|
||||
console.error("Error geo:", e);
|
||||
showToast("Error base de datos", true);
|
||||
} finally {
|
||||
if (loader) loader.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
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,64 +388,47 @@ 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>` : "";
|
||||
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(", ");
|
||||
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;
|
||||
}, {});
|
||||
|
||||
currentUsers.forEach(u => {
|
||||
const uGuildNames = (u.guilds || []).map(gid => availableGuilds.find(ag => ag.id === gid)?.name).filter(Boolean).join(", ");
|
||||
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>`;
|
||||
}).join("") || '<span class="text-[9px] text-gray-300 italic">Sin zona</span>';
|
||||
|
||||
// --- 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);
|
||||
}
|
||||
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>';
|
||||
|
||||
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>
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
}
|
||||
tbody.innerHTML += `
|
||||
<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>`;
|
||||
});
|
||||
}
|
||||
|
||||
function showToast(msg, err=false){
|
||||
const t=document.getElementById('toast'), m=document.getElementById('toastMsg');
|
||||
|
||||
Reference in New Issue
Block a user