feat: Agregar API para obtener roles de un gremio y recargar roles en el panel de control
This commit is contained in:
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user