Initial commit
This commit is contained in:
commit
9c7df76fbd
91 files changed
+36572
No files matched your search
@@ -0,0 +1,475 @@
|
||||
import { S } from './state.js';
|
||||
// ── Accounts ──────────────────────────────────────────────────────────────────
|
||||
|
||||
async function loadUsers() {
|
||||
const list = document.getElementById('accountsList');
|
||||
const loading = document.getElementById('accountsLoading');
|
||||
if (!list) return;
|
||||
if (loading) loading.textContent = t('lbl_loading', 'Loading…');
|
||||
// Ensure source panel checkboxes exist before we render the account list
|
||||
if (!document.querySelector('#sourcesPanel input') && typeof renderSourcesPanel === 'function') {
|
||||
renderSourcesPanel();
|
||||
}
|
||||
try {
|
||||
const r = await fetch('/api/users');
|
||||
if (!r.ok) { if (loading) loading.textContent = 'Could not load users'; return; }
|
||||
const d = await r.json();
|
||||
if (d.error) { if (loading) loading.textContent = d.error; return; }
|
||||
// Merge with any manually-added users, preserving them
|
||||
const fetched = d.users || [];
|
||||
fetched.forEach(u => { u.platform = 'm365'; });
|
||||
const existingManual = S._allUsers.filter(u => u.manual);
|
||||
const fetchedIds = new Set(fetched.map(u => u.id));
|
||||
const toAdd = existingManual.filter(u => !fetchedIds.has(u.id));
|
||||
// Preserve existing selected state for users already in S._allUsers;
|
||||
// new users default to selected=true
|
||||
const prevSelected = new Map(S._allUsers.map(u => [u.id, u.selected]));
|
||||
fetched.forEach(u => {
|
||||
u.selected = prevSelected.has(u.id) ? prevSelected.get(u.id) : false;
|
||||
});
|
||||
S._allUsers = [...fetched, ...toAdd];
|
||||
renderAccountList(fetched.length <= 1);
|
||||
// Merge Google users separately so they're not blocked by M365 auth timing
|
||||
_mergeGoogleUsers();
|
||||
checkCheckpoint();
|
||||
checkDeltaStatus();
|
||||
_applyPendingProfileUsers();
|
||||
|
||||
// Show warning banner when no users could be classified
|
||||
const warn = document.getElementById('skuWarnBanner');
|
||||
if (warn) {
|
||||
const allOther = fetched.length > 0 && fetched.every(u => u.userRole === 'other');
|
||||
warn.style.display = allOther ? 'block' : 'none';
|
||||
}
|
||||
} catch(e) {
|
||||
if (loading) loading.textContent = 'Could not load users';
|
||||
}
|
||||
}
|
||||
|
||||
async function _mergeGoogleUsers() {
|
||||
if (!window._googleConnected) return;
|
||||
try {
|
||||
var gr = await fetch('/api/google/scan/users');
|
||||
if (!gr.ok) return;
|
||||
var gd = await gr.json();
|
||||
if (gd.error) return;
|
||||
var prevSelected = new Map(S._allUsers.map(function(u){ return [u.id, u.selected]; }));
|
||||
|
||||
// Build displayName → Google user map for cross-platform matching
|
||||
// Both M365 and GWS are maintained from AD — full name is identical
|
||||
var googleByName = {};
|
||||
(gd.users || []).forEach(function(gu) {
|
||||
var name = (gu.displayName || '').trim().toLowerCase();
|
||||
if (name) googleByName[name] = gu;
|
||||
});
|
||||
|
||||
// Merge onto M365 users where display name matches
|
||||
var matchedNames = new Set();
|
||||
S._allUsers.forEach(function(u) {
|
||||
if ((u.platform || 'm365') !== 'm365') return;
|
||||
var name = (u.displayName || '').trim().toLowerCase();
|
||||
var gu = googleByName[name];
|
||||
if (gu) {
|
||||
u.platform = 'both';
|
||||
u.googleEmail = gu.email;
|
||||
// Keep M365 displayName (from AD, authoritative)
|
||||
matchedNames.add(name);
|
||||
} else {
|
||||
// Clear previous merge if Google disconnected
|
||||
delete u.googleEmail;
|
||||
u.platform = 'm365';
|
||||
}
|
||||
});
|
||||
|
||||
// Add unmatched Google users as standalone entries
|
||||
var googleUsers = [];
|
||||
(gd.users || []).forEach(function(gu) {
|
||||
var name = (gu.displayName || '').trim().toLowerCase();
|
||||
if (matchedNames.has(name)) return; // already merged
|
||||
var uid = 'google:' + gu.email;
|
||||
googleUsers.push({
|
||||
id: uid,
|
||||
displayName: gu.displayName || gu.email,
|
||||
email: gu.email,
|
||||
userRole: gu.userRole || 'other',
|
||||
platform: 'google',
|
||||
selected: prevSelected.has(uid) ? prevSelected.get(uid) : false,
|
||||
});
|
||||
});
|
||||
|
||||
// Remove stale standalone Google users, add fresh unmatched ones
|
||||
S._allUsers = S._allUsers.filter(function(u){ return (u.platform||'m365') !== 'google'; });
|
||||
S._allUsers = S._allUsers.concat(googleUsers);
|
||||
renderAccountList();
|
||||
} catch(e) { /* Google users unavailable */ }
|
||||
}
|
||||
|
||||
let _activeRoleFilter = ''; // '' = all, 'staff', 'student'
|
||||
|
||||
// ── Sidebar section collapse ──────────────────────────────────────────────────
|
||||
const _COLLAPSE_SECTIONS = ['sourcesPanelSection', 'optionsSection', 'accountsSection', 'logSection'];
|
||||
|
||||
function toggleSection(id) {
|
||||
const body = document.getElementById(id + 'Body');
|
||||
if (!body) return;
|
||||
const collapsing = body.style.display !== 'none';
|
||||
body.style.display = collapsing ? 'none' : '';
|
||||
const btn = document.getElementById(id + '-btn');
|
||||
if (btn) btn.textContent = collapsing ? '▸' : '▾';
|
||||
if (id === 'accountsSection') {
|
||||
const sec = document.getElementById('accountsSection');
|
||||
if (sec) sec.style.flex = collapsing ? '0 0 auto' : '1';
|
||||
}
|
||||
try { localStorage.setItem('sc_' + id, collapsing ? '1' : '0'); } catch(e) {}
|
||||
}
|
||||
|
||||
function restoreSectionStates() {
|
||||
_COLLAPSE_SECTIONS.forEach(function(id) {
|
||||
try {
|
||||
if (localStorage.getItem('sc_' + id) === '1') {
|
||||
const body = document.getElementById(id + 'Body');
|
||||
if (body) body.style.display = 'none';
|
||||
const btn = document.getElementById(id + '-btn');
|
||||
if (btn) btn.textContent = '▸';
|
||||
if (id === 'accountsSection') {
|
||||
const sec = document.getElementById('accountsSection');
|
||||
if (sec) sec.style.flex = '0 0 auto';
|
||||
}
|
||||
}
|
||||
} catch(e) {}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Role filter with counts ───────────────────────────────────────────────────
|
||||
function updateRoleFilterCounts() {
|
||||
const total = S._allUsers.filter(function(u){ return !u.manual; }).length;
|
||||
const staff = S._allUsers.filter(function(u){ return !u.manual && u.userRole === 'staff'; }).length;
|
||||
const student = S._allUsers.filter(function(u){ return !u.manual && u.userRole === 'student'; }).length;
|
||||
const btnAll = document.getElementById('rfAll');
|
||||
const btnStaff = document.getElementById('rfStaff');
|
||||
const btnStudent = document.getElementById('rfStudent');
|
||||
if (btnAll) btnAll.textContent = t('m365_role_all','All') + (total ? ' (' + total + ')' : '');
|
||||
if (btnStaff) btnStaff.textContent = t('role_staff','Ansat') + (staff ? ' (' + staff + ')' : '');
|
||||
if (btnStudent) btnStudent.textContent = t('role_student','Elev') + (student ? ' (' + student + ')' : '');
|
||||
}
|
||||
|
||||
function setRoleFilter(role) {
|
||||
_activeRoleFilter = role;
|
||||
[['rfAll',''],['rfStaff','staff'],['rfStudent','student']].forEach(function(pair) {
|
||||
const btn = document.getElementById(pair[0]);
|
||||
if (!btn) return;
|
||||
const active = role === pair[1];
|
||||
btn.style.background = active ? 'var(--accent)' : 'none';
|
||||
btn.style.color = active ? '#fff' : 'var(--muted)';
|
||||
});
|
||||
updateRoleFilterCounts();
|
||||
filterUsers();
|
||||
}
|
||||
|
||||
// ── Last scan summary (empty state) ──────────────────────────────────────────
|
||||
async function loadLastScanSummary() {
|
||||
try {
|
||||
const r = await fetch('/api/db/stats');
|
||||
const d = await r.json();
|
||||
if (!d.scan_id || S.flaggedData.length > 0) return;
|
||||
const panel = document.getElementById('lastScanSummary');
|
||||
const empty = document.getElementById('emptyState');
|
||||
if (!panel || !empty) return;
|
||||
|
||||
const dateStr = d.finished_at
|
||||
? new Date(d.finished_at * 1000).toLocaleDateString('da-DK', {day:'numeric', month:'short', year:'numeric'})
|
||||
: '—';
|
||||
const sources = Object.keys(d.by_source || {});
|
||||
const srcLabels = {'email':'Outlook','onedrive':'OneDrive','sharepoint':'SharePoint','teams':'Teams',
|
||||
'gmail':'Gmail','gdrive':'Drive','local':'Lokale filer','smb':'SMB'};
|
||||
const srcStr = sources.map(function(s){ return srcLabels[s] || s; }).join(' · ') || '—';
|
||||
|
||||
panel.innerHTML =
|
||||
'<div class="last-scan-card">' +
|
||||
'<h3>' + t('last_scan_title', 'Seneste scanning') + '</h3>' +
|
||||
'<div class="last-scan-stats">' +
|
||||
'<div class="last-scan-stat"><span class="val">' + (d.flagged_count || 0) + '</span><span class="lbl">' + t('last_scan_hits', 'Fund') + '</span></div>' +
|
||||
'<div class="last-scan-stat"><span class="val">' + (d.unique_subjects || 0) + '</span><span class="lbl">' + t('last_scan_subjects', 'Unikke CPR') + '</span></div>' +
|
||||
'<div class="last-scan-stat"><span class="val">' + (d.total_scanned || 0) + '</span><span class="lbl">' + t('last_scan_scanned', 'Scannet') + '</span></div>' +
|
||||
'</div>' +
|
||||
'<div style="margin-top:12px;font-size:11px;color:var(--muted)">' + dateStr + ' · ' + srcStr + '</div>' +
|
||||
'</div>' +
|
||||
'<div class="empty-text" style="font-size:12px">' + t('m365_empty_hint', 'Vælg kilder og klik på <strong>Scan</strong><br>for at starte en ny scanning') + '</div>';
|
||||
|
||||
empty.style.display = 'none';
|
||||
panel.style.display = 'flex';
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
function renderAccountList(showAdminNote = false) {
|
||||
updateRoleFilterCounts();
|
||||
const list = document.getElementById('accountsList');
|
||||
if (!list) return;
|
||||
const q = (document.getElementById('userSearch')?.value || '').toLowerCase().trim();
|
||||
|
||||
let visible = S._allUsers;
|
||||
|
||||
// Filter by platform: only show accounts relevant to checked sources
|
||||
// If the sources panel hasn't been rendered yet (no checkboxes at all), treat M365 as active
|
||||
var panelHasAny = !!document.querySelector('#sourcesPanel input[data-source-type]');
|
||||
var hasM365Src = panelHasAny
|
||||
? !!document.querySelector('#sourcesPanel input[data-source-type="m365"]:checked')
|
||||
: S._allUsers.some(function(u){ return !u.platform || u.platform === 'm365' || u.platform === 'both'; });
|
||||
var hasGoogleSrc = !!document.querySelector('#sourcesPanel input[data-source-type="google"]:checked');
|
||||
// Always filter — if neither is active, show nothing
|
||||
// Check if Google is enabled in Source Management (not just selected in KILDER)
|
||||
var googleEnabled = !!(document.getElementById('smGoogleSrcGmail') && document.getElementById('smGoogleSrcGmail').checked)
|
||||
|| !!(document.getElementById('smGoogleSrcDrive') && document.getElementById('smGoogleSrcDrive').checked);
|
||||
var effectiveGws = hasGoogleSrc && googleEnabled;
|
||||
visible = visible.filter(function(u) {
|
||||
var plat = u.platform || 'm365';
|
||||
if (plat === 'both') return hasM365Src || effectiveGws;
|
||||
return (plat === 'm365' && hasM365Src) || (plat === 'google' && effectiveGws);
|
||||
});
|
||||
|
||||
// Apply role filter first
|
||||
if (_activeRoleFilter) {
|
||||
visible = visible.filter(u => (u.userRole || 'other') === _activeRoleFilter);
|
||||
}
|
||||
|
||||
// Then apply text search
|
||||
if (q) {
|
||||
visible = visible.filter(u =>
|
||||
(u.displayName || '').toLowerCase().includes(q) ||
|
||||
(u.email || '').toLowerCase().includes(q));
|
||||
}
|
||||
|
||||
_updateUserCountBadge(visible.length, S._allUsers.length);
|
||||
|
||||
const note = (!q && !_activeRoleFilter && showAdminNote)
|
||||
? `<div style="font-size:10px;color:var(--muted);padding:4px 0 6px;line-height:1.4">${t('m365_admin_note','Only showing your account. To list all users, an admin must grant <strong>User.Read.All</strong> consent.')}</div>`
|
||||
: '';
|
||||
|
||||
const noMatch = (q || _activeRoleFilter) && !visible.length
|
||||
? `<div style="padding:4px 0;color:var(--muted);font-size:11px">${t('m365_no_users_match','No users match')} "${q || _activeRoleFilter}"</div>`
|
||||
: '';
|
||||
|
||||
list.innerHTML = note + noMatch + visible.map(u => `
|
||||
<label style="display:flex;align-items:center;gap:7px;padding:2px 0;cursor:pointer">
|
||||
<input type="checkbox" class="account-check" data-id="${u.id}" data-name="${u.displayName}" data-role="${u.userRole || 'other'}"
|
||||
${u.selected !== false ? 'checked' : ''}
|
||||
onchange="onAccountCheckChange('${u.id}', this.checked)">
|
||||
<span style="flex:1;overflow:hidden">
|
||||
<span style="display:block;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${u.displayName}${u.isMe ? ' <span style=\'color:var(--accent);font-size:10px\'>(you)</span>' : ''}${u.manual ? ' <span style=\'color:var(--muted);font-size:10px\'>(manual)</span>' : ''}</span>
|
||||
<span style="color:var(--muted);font-size:10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block">${u.email}</span>
|
||||
</span>
|
||||
<span style="font-size:9px;padding:1px 5px;border-radius:10px;flex-shrink:0;"
|
||||
class="${u.platform==='both' ? (hasM365Src && effectiveGws ? 'plat-badge-both' : effectiveGws ? 'plat-badge-google' : 'plat-badge-m365') : (u.platform||'m365')==='google' ? 'plat-badge-google' : 'plat-badge-m365'}">
|
||||
${u.platform==='both' ? (hasM365Src && effectiveGws ? 'M365 + GWS' : effectiveGws ? 'GWS' : 'M365') : (u.platform||'m365')==='google' ? 'GWS' : 'M365'}
|
||||
</span>
|
||||
<button type="button" onclick="cycleUserRole(this.getAttribute('data-uid'))"
|
||||
data-uid="${u.id.replace(/&/g,'&').replace(/'/g,''').replace(/"/g,'"')}"
|
||||
title="${t('m365_role_cycle_tip','Click to change role')}"
|
||||
class="role-badge" style="font-size:9px;padding:1px 5px;cursor:pointer;flex-shrink:0;white-space:nowrap;border:none;${u.roleOverride ? 'color:var(--color-text-info);outline:1px solid var(--color-border-info)' : ''}">
|
||||
${u.userRole === 'student' ? t('role_student','Elev') : u.userRole === 'staff' ? t('role_staff','Ansat') : t('role_other','Anden')}${u.roleOverride ? ' ✎' : ''}
|
||||
</button>
|
||||
${u.manual ? `<button onclick="removeUser(this.getAttribute('data-uid'))" data-uid="${u.id.replace(/&/g,'&').replace(/'/g,''').replace(/"/g,'"')}" style="background:none;border:none;color:var(--muted);cursor:pointer;font-size:13px;padding:0;flex-shrink:0" title="Remove">×</button>` : ''}
|
||||
</label>`).join('');
|
||||
}
|
||||
|
||||
function _updateUserCountBadge(visible, total) {
|
||||
const badge = document.getElementById('userCountBadge');
|
||||
if (!badge) return;
|
||||
if (total === 0) { badge.textContent = ''; return; }
|
||||
badge.textContent = visible < total ? `(${visible} / ${total})` : `(${total})`;
|
||||
}
|
||||
|
||||
// ── SKU debug — surface unknown tenant SKU IDs so they can be added to m365_skus.json ──
|
||||
async function showSkuDebug() {
|
||||
let modal = document.getElementById('skuDebugModal');
|
||||
if (!modal) {
|
||||
modal = document.createElement('div');
|
||||
modal.id = 'skuDebugModal';
|
||||
modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:1000;display:flex;align-items:center;justify-content:center';
|
||||
modal.onclick = e => { if (e.target === modal) modal.remove(); };
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
modal.innerHTML = `<div style="background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:22px 26px;width:min(520px,95vw);max-height:80vh;display:flex;flex-direction:column;gap:12px;font-size:12px">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between">
|
||||
<strong style="font-size:13px">${t('m365_sku_debug_title','🔍 Tenant SKU IDs')}</strong>
|
||||
<button onclick="document.getElementById('skuDebugModal').remove()" style="background:none;border:none;color:var(--muted);cursor:pointer;font-size:16px">×</button>
|
||||
</div>
|
||||
<div style="color:var(--muted);font-size:11px;line-height:1.5">${t('m365_sku_debug_desc','These are the raw SKU IDs assigned to your users. Any marked <b>❓ unknown</b> are not in <code>classification/m365_skus.json</code> — copy them in under <code>student_ids</code> or <code>staff_ids</code> and restart.')}</div>
|
||||
<div id="skuDebugList" style="overflow-y:auto;flex:1;font-family:var(--mono);font-size:11px">Loading…</div>
|
||||
<div style="display:flex;justify-content:flex-end;gap:8px;padding-top:4px;border-top:1px solid var(--border)">
|
||||
<button onclick="document.getElementById('skuDebugModal').remove()" style="background:none;border:1px solid var(--border);color:var(--muted);padding:4px 14px;border-radius:6px;cursor:pointer">${t('btn_close','Close')}</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const listEl = document.getElementById('skuDebugList');
|
||||
try {
|
||||
const r = await fetch('/api/users/license_debug');
|
||||
const d = await r.json();
|
||||
if (d.error) { listEl.textContent = 'Error: ' + d.error; return; }
|
||||
|
||||
// Collect unique SKUs across all users
|
||||
const skuSeen = {}; // skuId → {name, role, count, known}
|
||||
for (const u of (d.users || [])) {
|
||||
for (let i = 0; i < (u.skuIds || []).length; i++) {
|
||||
const id = u.skuIds[i];
|
||||
const nm = (u.skuNames || [])[i] || '';
|
||||
if (!skuSeen[id]) skuSeen[id] = { name: nm, role: u.role, count: 0 };
|
||||
skuSeen[id].count++;
|
||||
}
|
||||
}
|
||||
|
||||
const rows = Object.entries(skuSeen).sort((a,b) => b[1].count - a[1].count);
|
||||
if (!rows.length) { listEl.textContent = t('m365_sku_debug_none','No license data returned — check that the app has User.Read.All permission.'); return; }
|
||||
|
||||
const knownStudent = new Set((d.student_ids || []));
|
||||
const knownStaff = new Set((d.staff_ids || []));
|
||||
|
||||
listEl.innerHTML = rows.map(([id, info]) => {
|
||||
const known = knownStudent.has(id) ? '🎓 student'
|
||||
: knownStaff.has(id) ? '👔 staff'
|
||||
: '❓ unknown';
|
||||
const color = known.startsWith('❓') ? 'var(--danger)' : 'var(--accent)';
|
||||
return `<div style="display:flex;align-items:baseline;gap:8px;padding:3px 0;border-bottom:1px solid var(--border)">
|
||||
<code style="flex:1;color:var(--text);user-select:all">${id}</code>
|
||||
<span style="color:var(--muted);font-size:10px;white-space:nowrap">${info.name || '—'}</span>
|
||||
<span style="color:${color};font-size:10px;white-space:nowrap;flex-shrink:0">${known} (${info.count})</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
} catch(e) {
|
||||
listEl.textContent = 'Error: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
function filterUsers() {
|
||||
const showAdminNote = S._allUsers.filter(u => !u.manual).length <= 1;
|
||||
renderAccountList(showAdminNote);
|
||||
}
|
||||
|
||||
async function cycleUserRole(id) {
|
||||
// Cycle: student → staff → other → (clear override, back to auto)
|
||||
if (!id) { console.warn('cycleUserRole: no id'); return; }
|
||||
const u = S._allUsers.find(u => u.id === id);
|
||||
if (!u) { console.warn('cycleUserRole: user not found for id', id); return; }
|
||||
const cycle = ['student', 'staff', 'other'];
|
||||
let next;
|
||||
if (!u.roleOverride) {
|
||||
// First click: remember auto role, pin to next in cycle
|
||||
u._autoRole = u.userRole;
|
||||
u._cycleSteps = 0;
|
||||
const cur = cycle.indexOf(u.userRole);
|
||||
next = cycle[(cur + 1) % cycle.length];
|
||||
} else {
|
||||
u._cycleSteps = (u._cycleSteps || 0) + 1;
|
||||
if (u._cycleSteps >= cycle.length) {
|
||||
next = ''; // full cycle completed — clear override
|
||||
} else {
|
||||
const cur = cycle.indexOf(u.userRole);
|
||||
next = cycle[(cur + 1) % cycle.length];
|
||||
}
|
||||
}
|
||||
try {
|
||||
const r = await fetch('/api/users/role_override', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({user_id: id, role: next})
|
||||
});
|
||||
const d = await r.json();
|
||||
if (d.error) { log('Role override failed: ' + d.error, 'err'); return; }
|
||||
// Update local state
|
||||
if (next) {
|
||||
if (!u.roleOverride) u._autoRole = u.userRole; // remember original for clear
|
||||
u.userRole = next;
|
||||
u.roleOverride = true;
|
||||
} else {
|
||||
u.userRole = u._autoRole || u.userRole;
|
||||
u.roleOverride = false;
|
||||
u._autoRole = undefined;
|
||||
}
|
||||
// Update the role filter count badges and re-render
|
||||
renderAccountList(S._allUsers.filter(u => !u.manual).length <= 1);
|
||||
log((next ? t('m365_role_set', 'Role set') + ': ' + next : t('m365_role_cleared', 'Role override cleared')) + ' — ' + (u.displayName || id));
|
||||
} catch(e) {
|
||||
log('Role override error: ' + e.message, 'err');
|
||||
}
|
||||
}
|
||||
|
||||
function removeUser(id) {
|
||||
S._allUsers = S._allUsers.filter(u => u.id !== id);
|
||||
renderAccountList(S._allUsers.filter(u => !u.manual).length <= 1);
|
||||
}
|
||||
|
||||
async function addUserManually() {
|
||||
const input = document.getElementById('addUserInput');
|
||||
const upn = input.value.trim();
|
||||
if (!upn) return;
|
||||
// Look up the user via server
|
||||
const btn = input.nextElementSibling;
|
||||
btn.disabled = true; btn.textContent = '…';
|
||||
try {
|
||||
const r = await fetch('/api/users/lookup?upn=' + encodeURIComponent(upn));
|
||||
const d = await r.json();
|
||||
if (d.error) { alert('User not found: ' + d.error); return; }
|
||||
if (S._allUsers.find(u => u.id === d.id)) { alert('User already in list.'); return; }
|
||||
S._allUsers.push({...d, manual: true});
|
||||
input.value = '';
|
||||
renderAccountList(S._allUsers.filter(u => !u.manual).length <= 1);
|
||||
} catch(e) {
|
||||
alert('Lookup failed: ' + e.message);
|
||||
} finally {
|
||||
btn.disabled = false; btn.textContent = '+';
|
||||
}
|
||||
}
|
||||
|
||||
function onAccountCheckChange(id, checked) {
|
||||
const user = S._allUsers.find(u => u.id === id);
|
||||
if (user) user.selected = checked;
|
||||
}
|
||||
|
||||
function selectAllAccounts(checked) {
|
||||
// Toggle all visible users (respects search + role filter)
|
||||
const visible = new Set(
|
||||
Array.from(document.querySelectorAll('#accountsList .account-check')).map(cb => cb.dataset.id)
|
||||
);
|
||||
S._allUsers.forEach(u => { if (visible.has(u.id)) u.selected = checked; });
|
||||
document.querySelectorAll('#accountsList .account-check').forEach(cb => cb.checked = checked);
|
||||
}
|
||||
|
||||
function getSelectedUsers() {
|
||||
// Only return M365 users — Google users are handled separately via selectedGoogleEmails
|
||||
let selected = S._allUsers.filter(u => u.selected !== false && (u.platform === 'm365' || u.platform === 'both'));
|
||||
// Respect the active role filter — hidden users must not sneak into the scan
|
||||
// even if they were checked before the filter was applied.
|
||||
if (_activeRoleFilter) {
|
||||
selected = selected.filter(u => (u.userRole || 'other') === _activeRoleFilter);
|
||||
}
|
||||
if (selected.length) {
|
||||
return selected.map(u => ({
|
||||
id: u.id, displayName: u.displayName, userRole: u.userRole || 'other'
|
||||
}));
|
||||
}
|
||||
// Fallback to DOM if S._allUsers not yet populated
|
||||
return Array.from(document.querySelectorAll('.account-check:checked')).map(cb => ({
|
||||
id: cb.dataset.id, displayName: cb.dataset.name, userRole: cb.dataset.role || 'other'
|
||||
}));
|
||||
}
|
||||
|
||||
// ── Window exports (HTML handlers + cross-module calls) ─────────────────────
|
||||
window.loadUsers = loadUsers;
|
||||
window._mergeGoogleUsers = _mergeGoogleUsers;
|
||||
window.toggleSection = toggleSection;
|
||||
window.restoreSectionStates = restoreSectionStates;
|
||||
window.updateRoleFilterCounts = updateRoleFilterCounts;
|
||||
window.setRoleFilter = setRoleFilter;
|
||||
window.loadLastScanSummary = loadLastScanSummary;
|
||||
window.renderAccountList = renderAccountList;
|
||||
window._updateUserCountBadge = _updateUserCountBadge;
|
||||
window.showSkuDebug = showSkuDebug;
|
||||
window.filterUsers = filterUsers;
|
||||
window.cycleUserRole = cycleUserRole;
|
||||
window.removeUser = removeUser;
|
||||
window.addUserManually = addUserManually;
|
||||
window.onAccountCheckChange = onAccountCheckChange;
|
||||
window.selectAllAccounts = selectAllAccounts;
|
||||
window.getSelectedUsers = getSelectedUsers;
|
||||
window._activeRoleFilter = _activeRoleFilter;
|
||||
window._COLLAPSE_SECTIONS = _COLLAPSE_SECTIONS;
|
||||
Reference in new issue
Block a user