feat: Agregar API para obtener roles de un gremio y recargar roles en el panel de control

This commit is contained in:
Shni
2025-10-15 11:08:40 -05:00
parent 4037569707
commit 51fa42c28a
3 changed files with 188 additions and 16 deletions

View File

@@ -18,22 +18,23 @@
<% const selectedStaff = (guildConfig && Array.isArray(guildConfig.staff) ? guildConfig.staff.map(String) : (guildConfig && guildConfig.staff ? String(guildConfig.staff).split(',') : [])) || []; %>
<% if (typeof guildRoles !== 'undefined' && guildRoles && guildRoles.length) { %>
<div class="mb-2">
<div id="staffTagsContainer" class="w-full rounded p-3 flex items-center gap-2 flex-wrap">
<div id="staffChips" class="flex items-center gap-2 flex-wrap"></div>
<button id="openRolePicker" type="button" class="ml-2 self-stretch inline-flex items-center justify-center px-3 py-1 rounded bg-white/6 hover:bg-white/10">
<div id="staffTagsContainer" class="w-full rounded p-3 flex flex-col sm:flex-row items-start sm:items-center gap-2">
<div id="staffChips" class="flex-1 min-w-0 flex items-center gap-2 flex-wrap"></div>
<button id="openRolePicker" type="button" class="ml-0 sm:ml-2 inline-flex items-center justify-center px-3 py-1 rounded bg-white/6 hover:bg-white/10">
<!-- plus svg -->
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 5v14m-7-7h14"/></svg>
</button>
<input type="hidden" id="staffHidden" name="staff" value="<%= selectedStaff.join(',') %>" />
</div>
<div class="text-xs text-slate-300 mt-1">Pulsa + para seleccionar roles del servidor.</div>
<!-- Role picker modal (hidden by default) -->
<div id="rolePickerModal" class="fixed inset-0 flex items-center justify-center z-50 hidden">
<div class="absolute inset-0" id="rolePickerBackdrop"></div>
<div class="relative w-full max-w-md bg-gray-800 rounded p-4 shadow-lg glass-card">
<div id="rolePickerModal" class="fixed inset-0 flex items-end sm:items-center justify-center z-50 hidden">
<div class="absolute inset-0 bg-black/50" id="rolePickerBackdrop"></div>
<div class="relative w-full sm:w-auto max-w-md bg-gray-800 rounded-t-lg sm:rounded p-4 shadow-lg glass-card mx-2 sm:mx-0" style="max-height:85vh; overflow:auto;">
<div class="flex items-center justify-between mb-2">
<strong class="text-sm">Seleccionar roles</strong>
<button id="closeRolePicker" type="button" class="text-slate-300">✕</button>
</div>
<input id="rolePickerSearch" class="w-full rounded p-2 bg-transparent border-2 border-dashed mb-2" placeholder="Filtrar roles..." />
<div id="rolePickerList" class="max-h-60 overflow-auto rounded bg-white/4 p-1"></div>
@@ -128,25 +129,59 @@
return { id: String(r.id), name: String(r.name || r.id), color: c };
});
%>
const ROLE_OPTIONS = <%- JSON.stringify(_roles) %>;
let ROLE_OPTIONS = <%- JSON.stringify(_roles) %>;
// Initial selected staff IDs (strings)
const INITIAL_STAFF = <%- JSON.stringify(selectedStaff || []) %>;
let selectedIds = Array.isArray(INITIAL_STAFF) ? INITIAL_STAFF.slice() : [];
// Fallback: if server didn't provide ROLE_OPTIONS (empty), try to read from any
// existing <select> on the page or request roles from server API at runtime.
(async function ensureRoleOptions(){
try {
if (!Array.isArray(ROLE_OPTIONS) || ROLE_OPTIONS.length === 0) {
// try to read an existing select element first
const sel = document.getElementById('staffSelect') || document.querySelector('select[name="staff"]');
if (sel && sel.options && sel.options.length) {
ROLE_OPTIONS = Array.from(sel.options).map(o => ({ id: String(o.value), name: String(o.textContent || o.innerText || o.label || o.value).trim(), color: (o.dataset && o.dataset.color) ? o.dataset.color : '#8b95a0' }));
renderChips();
return;
}
// otherwise, try fetching roles from server at runtime
try {
const guildId = '<%= selectedGuild %>';
if (guildId) {
const resp = await fetch(`/api/dashboard/${encodeURIComponent(guildId)}/roles`, { method: 'GET', headers: { 'Accept': 'application/json' } });
if (resp.ok) {
const j = await resp.json();
if (j && Array.isArray(j.roles) && j.roles.length) {
ROLE_OPTIONS = j.roles.map(r=>({ id: String(r.id), name: String(r.name||r.id), color: r.color || '#8b95a0' }));
renderChips();
return;
}
}
}
} catch (e){ /* ignore fetch errors */ }
}
} catch (e) {
// ignore
}
})();
function renderChips(){
if (!staffChips) return;
staffChips.innerHTML = '';
for(const id of selectedIds){
const role = ROLE_OPTIONS.find(r=>r.id===String(id));
const label = role ? role.name : String(id);
const chip = document.createElement('div');
chip.className = 'px-2 py-1 bg-white/6 rounded-full text-sm flex items-center gap-2';
const swatch = role && role.color ? `<span class="w-3 h-3 rounded-full inline-block" style="background:${role.color}"></span>` : '';
chip.innerHTML = `${swatch}<span class="truncate">${label}</span><button type="button" class="ml-2 text-xs text-slate-300 remove-chip" data-id="${id}">✕</button>`;
const label = role ? role.name : String(id);
const chip = document.createElement('div');
chip.className = 'px-2 py-1 bg-white/6 rounded-full text-sm flex items-center gap-2';
const swatch = role && role.color ? `<span class="w-3 h-3 rounded-full inline-block" style="background:${role.color}"></span>` : '';
chip.innerHTML = `${swatch}<span class="truncate">${label}</span><button type="button" class="ml-2 text-xs text-slate-300 remove-chip" data-id="${id}">✕</button>`;
staffChips.appendChild(chip);
}
// update hidden input
if(staffHidden) staffHidden.value = selectedIds.join(',');
if(typeof staffHidden !== 'undefined' && staffHidden) staffHidden.value = selectedIds.join(',');
}
function showSuggestions(query){
@@ -175,6 +210,17 @@
// initial render of chips
renderChips();
// Listen for runtime roles loaded events (from reload button in sidebar)
window.addEventListener('roles:loaded', (ev)=>{
try{
const data = ev && ev.detail && ev.detail.roles ? ev.detail.roles : null;
if(!data || !Array.isArray(data)) return;
ROLE_OPTIONS = data.map(r=>({ id: String(r.id), name: String(r.name||r.id), color: r.color || '#8b95a0' }));
renderChips();
if(typeof populateRolePicker === 'function') populateRolePicker('');
}catch(e){ /* ignore */ }
});
// Role picker modal logic
const openRolePicker = document.getElementById('openRolePicker');
const rolePickerModal = document.getElementById('rolePickerModal');
@@ -194,12 +240,16 @@
function openPicker(){
if(!rolePickerModal) return;
rolePickerModal.classList.remove('hidden');
// prevent body scroll on mobile when modal open
try{ document.documentElement.style.overflow = 'hidden'; }catch(e){}
populateRolePicker('');
setTimeout(()=>{ if(rolePickerSearch) rolePickerSearch.focus(); },50);
}
function closePicker(){
if(!rolePickerModal) return;
rolePickerModal.classList.add('hidden');
// restore body scroll
try{ document.documentElement.style.overflow = ''; }catch(e){}
if(rolePickerSearch) rolePickerSearch.value = '';
if(rolePickerList) rolePickerList.innerHTML = '';
}
@@ -224,6 +274,13 @@
});
}
// Close modal on Escape globally (only when open)
document.addEventListener('keydown', (ev)=>{
if(ev.key === 'Escape' && rolePickerModal && !rolePickerModal.classList.contains('hidden')){
closePicker();
}
});
if(rolePickerList){
rolePickerList.addEventListener('click', (ev)=>{
const it = ev.target.closest && ev.target.closest('.role-item');

View File

@@ -2,7 +2,33 @@
<nav class="bg-transparent rounded-xl p-4 sticky top-20 h-[80vh] overflow-auto">
<div class="mb-4 flex items-center gap-2">
<a href="/dashboard/<%= selectedGuild %>/overview" class="px-3 py-1 rounded-full bg-white/5 text-white text-xs">Home</a>
<button id="reloadRolesBtn" type="button" title=":" class="px-2 py-1 rounded-full bg-white/6 hover:bg-white/10 text-xs" aria-label="Recargar roles">
<!-- minimal two-dot icon -->
<span aria-hidden="true" class="text-sm">··</span>
</button>
</div>
<script>
(function(){
const btn = document.getElementById('reloadRolesBtn');
if(!btn) return;
btn.addEventListener('click', async function(e){
e.preventDefault();
const guildId = '<%= selectedGuild %>';
if(!guildId) return;
try{
const res = await fetch(`/api/dashboard/${encodeURIComponent(guildId)}/roles`, { method: 'GET', headers: { 'Accept': 'application/json' } });
if(!res.ok) {
return;
}
const j = await res.json();
if(j && Array.isArray(j.roles)){
window.dispatchEvent(new CustomEvent('roles:loaded', { detail: { roles: j.roles } }));
}
}catch(err){ /* ignore errors */ }
});
})();
</script>
<ul class="mt-2 space-y-3 text-slate-200">
<li class="text-sm"><a href="/dashboard/<%= selectedGuild %>/settings" class="flex items-center gap-3"><span class="opacity-80">⚙️</span> General Settings</a></li>
</ul>