`
: '';
list.innerHTML = note + noMatch + visible.map(u => `
`).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 = `
${t('m365_sku_debug_title','🔍 Tenant SKU IDs')}
${t('m365_sku_debug_desc','These are the raw SKU IDs assigned to your users. Any marked ❓ unknown are not in classification/m365_skus.json — copy them in under student_ids or staff_ids and restart.')}
Loading…
`;
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 `
${id}${info.name || '—'}${known} (${info.count})
`;
}).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;