import { S } from './state.js'; // ── Profiles (15c) ─────────────────────────────────────────────────────────── async function loadProfiles() { try { const r = await fetch('/api/profiles'); if (!r.ok) return; const d = await r.json(); S._profiles = d.profiles || []; _renderProfileSelect(); } catch(e) { /* profiles not critical */ } } function _renderProfileSelect() { const sel = document.getElementById('profileSelect'); if (!sel) return; const prev = sel.value; // Clear all except the placeholder option (first) while (sel.options.length > 1) sel.remove(1); for (const p of S._profiles) { const opt = document.createElement('option'); opt.value = p.id; const last = p.last_run ? ' — ' + p.last_run.slice(0, 10) : ''; opt.textContent = p.name + last; opt.title = p.description || ''; sel.appendChild(opt); } // Restore selection if the profile still exists; else fall back to placeholder if (prev && [...sel.options].some(o => o.value === prev)) { sel.value = prev; } else { sel.value = ''; S._activeProfileId = null; const clrBtn = document.getElementById('profileClearBtn'); if (clrBtn) clrBtn.style.display = 'none'; } } function _setProfileClearBtn(visible) { const btn = document.getElementById('profileClearBtn'); if (btn) btn.style.display = visible ? 'inline-block' : 'none'; } function onProfileChange() { const sel = document.getElementById('profileSelect'); const id = sel.value; if (!id) return; // placeholder can't be selected (disabled), guard anyway const profile = S._profiles.find(p => p.id === id); if (!profile) return; S._activeProfileId = id; _setProfileClearBtn(true); _applyProfile(profile); } // Clear the active profile label without touching sidebar settings. // The sidebar already reflects the loaded (or manually adjusted) state. function clearActiveProfile() { S._activeProfileId = null; const sel = document.getElementById('profileSelect'); if (sel) sel.value = ''; _setProfileClearBtn(false); } function _applyProfile(profile) { // ── Sources ────────────────────────────────────────────────────────────── // Restore source selections from profile — works for both M365 and file sources. // File sources may not be rendered yet (they load async), so store their IDs // in S._pendingProfileSources for renderSourcesPanel() to apply after re-render. const profileSources = profile.sources || []; // Ensure at least M365 source checkboxes are present before reading the DOM. // renderSourcesPanel() is idempotent and fast — safe to call here. if (!document.querySelector('#sourcesPanel input[data-source-id]') && typeof renderSourcesPanel === 'function') { renderSourcesPanel(); } document.querySelectorAll('#sourcesPanel input[data-source-id]').forEach(function(cb) { cb.checked = profileSources.includes(cb.dataset.sourceId); }); _updateAccountsVisibility(); // Deferred file sources — store IDs now, apply when _loadFileSources() resolves. // Don't filter against S._fileSources here — it may be empty at this point. const _knownSourceIds = new Set(['email', 'onedrive', 'sharepoint', 'teams', 'gmail', 'gdrive']); S._pendingProfileSources = (profile.file_sources && profile.file_sources.length) ? profile.file_sources.slice() : profileSources.filter(function(id) { return !_knownSourceIds.has(id); }); // Deferred Google sources — store IDs now, apply when smGoogleRefreshStatus() resolves. const googleIds = profile.google_sources || profileSources.filter(function(id) { return id === 'gmail' || id === 'gdrive'; }); S._pendingGoogleSources = googleIds.slice(); // ── Options ─────────────────────────────────────────────────────────────── const opts = profile.options || {}; if (opts.email_body !== undefined) { const el = document.getElementById('optEmailBody'); if (el) el.checked = opts.email_body; } if (opts.attachments !== undefined) { const el = document.getElementById('optAttachments'); if (el) { el.checked = opts.attachments; // Update the size row opacity directly const sizeRow = document.getElementById('attachSizeRow'); if (sizeRow) sizeRow.style.opacity = opts.attachments ? '1' : '0.4'; } } if (opts.max_attach_mb !== undefined) { const el = document.getElementById('optMaxAttachMB'); if (el) el.value = opts.max_attach_mb; } if (opts.max_emails !== undefined) { const el = document.getElementById('optMaxEmails'); if (el) el.value = opts.max_emails; } if (opts.delta !== undefined) { const el = document.getElementById('optDelta'); if (el) el.checked = opts.delta; } if (opts.scan_photos !== undefined) { const el = document.getElementById('optScanPhotos'); if (el) el.checked = opts.scan_photos; } if (opts.skip_gps_images !== undefined) { const el = document.getElementById('optSkipGps'); if (el) el.checked = opts.skip_gps_images; } if (opts.min_cpr_count !== undefined) { const el = document.getElementById('optMinCpr'); if (el) el.value = opts.min_cpr_count; } if (opts.ocr_lang !== undefined) { const el = document.getElementById('optOcrLang'); if (el) el.value = opts.ocr_lang; } if (opts.cpr_only !== undefined) { const el = document.getElementById('optCprOnly'); if (el) el.checked = opts.cpr_only; } if (opts.scan_emails !== undefined) { const el = document.getElementById('optScanEmails'); if (el) el.checked = opts.scan_emails; } if (opts.scan_phones !== undefined) { const el = document.getElementById('optScanPhones'); if (el) el.checked = opts.scan_phones; } // ── Date filter ─────────────────────────────────────────────────────────── const days = opts.older_than_days; if (days !== undefined) { const hidden = document.getElementById('olderThan'); const dateIn = document.getElementById('olderThanDate'); const presets = document.querySelectorAll('.date-preset'); if (hidden) hidden.value = days; if (dateIn) { if (!days) { dateIn.value = ''; } else { const d = new Date(); d.setDate(d.getDate() - days); dateIn.value = d.toISOString().slice(0, 10); } } // Highlight matching preset button presets.forEach(p => { const y = parseInt(p.dataset.years || '0'); const presetDays = y === 0 ? 0 : y * 365; if (y === 0) { p.classList.toggle('selected', !days); } else { p.classList.toggle('selected', days > 0 && presetDays === days); } }); } // ── Retention ───────────────────────────────────────────────────────────── const retEnabled = !!(opts.retention_enabled || profile.retention_years); const retEl = document.getElementById('optRetention'); if (retEl) { retEl.checked = retEnabled; // Show/hide panel directly const panel = document.getElementById('retentionPanel'); if (panel) panel.style.display = retEnabled ? 'block' : 'none'; } if (profile.retention_years) { const el = document.getElementById('optRetentionYears'); if (el) el.value = profile.retention_years; } if (profile.fiscal_year_end) { const el = document.getElementById('optFiscalYearEnd'); if (el) el.value = profile.fiscal_year_end; } updateRetentionCutoffHint && updateRetentionCutoffHint(); // ── User selection ──────────────────────────────────────────────────────── if (profile.user_ids === 'all') { if (S._allUsers.length) { S._allUsers.forEach(u => { u.selected = true; }); renderAccountList(); } else { // Users not loaded yet — defer until loadUsers() resolves window._pendingProfileAllUsers = true; } } else if (Array.isArray(profile.user_ids) && profile.user_ids.length) { window._pendingProfileUserIds = profile.user_ids.map(u => u.id || u); _addProfileManualUsers(profile.user_ids); _applyPendingProfileUsers(); } else if (Array.isArray(profile.user_ids) && profile.user_ids.length === 0) { // Explicitly empty list — deselect everyone so previous sidebar state doesn't persist S._allUsers.forEach(u => { u.selected = false; }); if (S._allUsers.length) renderAccountList(); } log(t('m365_profile_applied', 'Profile loaded') + ': ' + profile.name); } // Google-only users (id "google:") arrive later than M365 users, via the // un-awaited _mergeGoogleUsers(). Keep the pending IDs until every one is // matched or the user list is final, else those users stay unticked. function _userListFinal() { return S._m365UsersLoaded && (!window._googleConnected || S._googleUsersMerged); } function _applyPendingProfileUsers() { const ids = window._pendingProfileUserIds; if (!ids || !ids.length || !S._allUsers.length) return; const wanted = new Set(ids); // A saved google: may since have merged onto an M365 user (platform 'both') const matches = u => wanted.has(u.id) || (u.googleEmail && wanted.has('google:' + u.googleEmail)); S._allUsers.forEach(u => { u.selected = matches(u); }); renderAccountList(); const found = S._allUsers.filter(matches).length; if (found >= wanted.size || _userListFinal()) window._pendingProfileUserIds = null; } function _isManualEntry(u) { return typeof u === 'object' ? !!u.manual : String(u).startsWith('manual:'); } // Manual accounts aren't in the /api/users directory list, so they must be added // to S._allUsers (as sidebar manual users) or they can never be selected/scanned. // Legacy "manual:" entries are looked up to get their directory id. function _addProfileManualUsers(entries) { entries.filter(_isManualEntry).forEach(async function(u) { if (typeof u === 'object' && !String(u.id).startsWith('manual:')) { if (!S._allUsers.some(x => x.id === u.id)) { S._allUsers.push({id: u.id, displayName: u.displayName || u.email || u.id, email: u.email || '', userRole: u.userRole || 'other', platform: 'm365', manual: true, selected: false}); } return; // selection is handled by _applyPendingProfileUsers() } const legacyId = u.id || u; try { const email = legacyId.slice('manual:'.length); const d = await _lookupM365User(email); if (!d) { // Not connected / not found — scheduled scans retry the lookup at scan time log(t('m365_profile_manual_not_found', 'Manual account not found in M365') + ': ' + email, 'err'); return; } let user = S._allUsers.find(x => x.id === d.id); if (!user) { user = {...d, platform: 'm365', manual: true}; S._allUsers.push(user); } user.selected = true; // Pending apply may re-run later — make it select the resolved id, not the legacy one const pend = window._pendingProfileUserIds; if (pend) window._pendingProfileUserIds = pend.map(x => x === legacyId ? d.id : x); renderAccountList(); } catch(e) { /* leave for the scan-time fallback */ } }); } // Returns {id, displayName, email} or null when M365 isn't connected / the user doesn't exist. async function _lookupM365User(upn) { const r = await fetch('/api/users/lookup?upn=' + encodeURIComponent(upn)); if (!r.ok) return null; const d = await r.json(); return d && d.id ? d : null; } async function saveCurrentAsProfile() { const name = prompt(t('m365_profile_save_prompt', 'Profile name:'), S._activeProfileId ? (S._profiles.find(p => p.id === S._activeProfileId) || {}).name || '' : ''); if (!name) return; const { sources, fileSources, googleSources, allSources, user_ids, options } = buildScanPayload(); const existing = S._profiles.find(p => p.name.toLowerCase() === name.toLowerCase()); const profile = { id: existing?.id || '', name, description: existing?.description || '', sources: allSources, google_sources: googleSources, user_ids, options, retention_years: parseInt(document.getElementById('optRetentionYears')?.value) || null, fiscal_year_end: document.getElementById('optFiscalYearEnd')?.value || '', email_to: '', file_sources: fileSources, }; try { const r = await fetch('/api/profiles/save', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(profile) }); const d = await r.json(); if (d.error) { alert(d.error); return; } await loadProfiles(); // Select the newly saved profile const sel = document.getElementById('profileSelect'); if (sel) { sel.value = d.profile.id; S._activeProfileId = d.profile.id; _setProfileClearBtn(true); } log(t('m365_profile_saved', 'Profile saved') + ': ' + name); } catch(e) { alert('Save failed: ' + e.message); } } // ── Profile management modal (#15d) ────────────────────────────────────────── function openProfileMgmtModal() { try { _renderProfileMgmt(); } catch(e) { console.error('[profiles] _renderProfileMgmt threw:', e); } document.getElementById('pmgmtBackdrop').classList.add('open'); // Auto-open editor for the first profile if (S._profiles.length > 0) { try { _pmgmtOpenEditor(S._profiles[0].id); } catch(e) { console.error('[profiles] _pmgmtOpenEditor threw:', e); } } } function closeProfileMgmt() { document.getElementById('pmgmtBackdrop').classList.remove('open'); } function _sourceLabel(id) { const known = {email:'Outlook', onedrive:'OneDrive', sharepoint:'SharePoint', teams:'Teams', gmail:'Gmail', gdrive:'Google Drive'}; if (known[id]) return known[id]; const fs = S._fileSources.find(s => s.id === id); return fs ? (fs.label || fs.path || id) : id; } function _renderProfileMgmt() { const list = document.getElementById('pmgmtList'); if (!list) return; const saved = S._profiles.filter(p => p.name !== 'Default' || S._profiles.length === 1); if (!saved.length) { list.innerHTML = `
${t('m365_profile_no_profiles','No saved profiles yet. Use 💾 to save the current sidebar settings as a profile.')}
`; return; } list.innerHTML = ''; for (const p of S._profiles) { const sources = (p.sources || []).map(_sourceLabel).join(', ') || '—'; const lastRun = p.last_run ? p.last_run.slice(0,16).replace('T',' ') : t('m365_profile_never','never'); const isActive = p.id === S._activeProfileId; const row = document.createElement('div'); row.className = 'pmgmt-row'; row.dataset.id = p.id; row.onclick = function() { _pmgmtOpenEditor(p.id); }; row.innerHTML = `
${_esc(p.name)}${isActive ? ' ● activ' : ''}
${_esc(sources)}
${p.description ? `
${_esc(p.description)}
` : ''}
${t('m365_profile_last_run','Last run')}: ${lastRun}
`; list.appendChild(row); } } function _esc(s) { return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } function _pmgmtUse(id) { const profile = S._profiles.find(p => p.id === id); if (!profile) return; S._activeProfileId = id; _setProfileClearBtn(true); _applyProfile(profile); // Sync the topbar dropdown const sel = document.getElementById('profileSelect'); if (sel) sel.value = id; closeProfileMgmt(); } function _pmgmtOpenEditor(id) { const profile = S._profiles.find(p => p.id === id); if (!profile) return; _openEditorForProfile(profile); } function _openEditorForProfile(profile) { const id = profile.id || ''; window._pmgmtEditId = id; _pmgmtRoleActive = ''; // Highlight active row document.querySelectorAll('.pmgmt-row').forEach(r => r.classList.toggle('active', id && r.dataset.id === id)); document.getElementById('pmgmtEditorTitle').textContent = profile.name; const body = document.getElementById('pmgmtEditorBody'); const allSources = profile.sources || []; const opts = profile.options || {}; const srcCheck = (id) => allSources.includes(id) ? 'checked' : ''; // Build account list from S._allUsers const savedAll = profile.user_ids === 'all'; const savedList = Array.isArray(profile.user_ids) ? profile.user_ids : []; const savedIds = new Set(savedList.map(u => u.id || u)); const isSaved = u => savedAll || savedIds.has(u.id) || !!(u.googleEmail && savedIds.has('google:' + u.googleEmail)); // Remember "all" so saving before the user list has loaded doesn't turn it into [] window._pmgmtSavedAll = savedAll; const accountRows = S._allUsers.map(u => { // Only check if the user was explicitly saved — default to unchecked like the main window const checked = isSaved(u) ? 'checked' : ''; const manualAttrs = u.manual ? ` data-manual="1" data-name="${_esc(u.displayName || '')}" data-email="${_esc(u.email || '')}"` : ''; const platBadge = u.platform === 'both' ? 'M365+GWS' : (u.platform || 'm365') === 'google' ? 'GWS' : 'M365'; const roleBadge = u.userRole === 'student' ? t('role_student','Elev') : u.userRole === 'staff' ? t('role_staff','Ansat') : t('role_other','Anden'); const roleOverrideStyle = u.roleOverride ? 'color:var(--color-text-info);outline:1px solid var(--color-border-info);' : ''; return ``; }).join('') + _unresolvedAccountRows(savedList); body.innerHTML = `
Navn
Kilder
Konti
Indstillinger

${t('m365_opt_email_body','Scan e-mailindhold')}
${t('m365_opt_attachments','Scan vedhæftede filer')}
${t('m365_opt_max_attach','Maks. vedhæftet filstørrelse (MB)')}
${t('m365_opt_max_emails','Maks. e-mails pr. bruger')}
${t('m365_opt_delta','Delta-scanning')}
${t('m365_opt_scan_photos','Søg efter ansigter i billeder')}
${t('m365_opt_skip_gps','Ignorer GPS i billeder')}
${t('m365_opt_min_cpr','Min. CPR-antal pr. fil')}
${t('m365_opt_cpr_only','CPR-only mode')}
${t('m365_opt_ocr_lang','OCR-sprog')}
${t('m365_opt_scan_emails','Søg efter e-mailadresser')}
${t('m365_opt_scan_phones','Søg efter telefonnumre')}

${t('m365_opt_retention','Opbevaringspolitik')}
${t('m365_ret_years','Opbevaringsår')}
`;; document.getElementById('pmgmtEditorPlaceholder')?.remove(); document.getElementById('pmgmtEditor').classList.add('open'); _renderEditorSources((profile.sources || []).concat(profile.google_sources || []).concat(profile.file_sources || [])); } // Saved accounts that aren't in S._allUsers — manually added ("manual:"), // not loaded yet, or gone from the directory. Rendered as checked rows so the // next save keeps them instead of silently dropping them. function _unresolvedAccountRows(savedList) { const known = new Set(); S._allUsers.forEach(u => { known.add(u.id); if (u.googleEmail) known.add('google:' + u.googleEmail); }); const seen = new Set(); return savedList.map(u => { const id = u.id || u; if (!id || known.has(id) || seen.has(id)) return ''; seen.add(id); const isManual = _isManualEntry(u); const label = (typeof u === 'object' && u.displayName) || id.replace(/^(manual|google):/, ''); const badge = isManual ? t('m365_profile_acct_manual', 'Manual') : t('m365_profile_acct_unresolved', 'Not loaded'); const manualAttrs = _isManualEntry(u) && typeof u === 'object' ? ` data-manual="1" data-name="${_esc(u.displayName || '')}" data-email="${_esc(u.email || '')}"` : ''; return ``; }).join(''); } function _peSetDate(val) { if (!val) return; const ms = new Date() - new Date(val); const days = Math.round(ms / 86400000); const hidden = document.getElementById('peOptDays'); if (hidden) hidden.value = days; // Clear selected year buttons since user picked a custom date document.querySelectorAll('.peYearBtn').forEach(b => b.classList.remove('selected')); } function _peSetYear(years) { const days = years === 0 ? 0 : years * 365; const hidden = document.getElementById('peOptDays'); if (hidden) hidden.value = days; document.querySelectorAll('.peYearBtn').forEach(function(btn) { const y = parseInt(btn.dataset.years); const active = (years === 0 && y === 0) || (years > 0 && y === years); btn.classList.toggle('selected', active); }); // Sync the date input var dateInput = document.getElementById('peOptDate'); if (dateInput) { if (days === 0) { dateInput.value = ''; } else { var d = new Date(); d.setDate(d.getDate()-days); dateInput.value = d.toISOString().slice(0,10); } } } function _renderEditorSources(checkedIds) { const panel = document.getElementById('peSourcesPanel'); if (!panel) return; let html = ''; _M365_SOURCES.forEach(function(s) { const toggle = s.toggleId ? document.getElementById(s.toggleId) : null; if (toggle && !toggle.checked) return; const isChecked = checkedIds.includes(s.id); html += ''; }); if (window._googleConnected) { var gmailOn = !document.getElementById('smGoogleSrcGmail') || document.getElementById('smGoogleSrcGmail').checked; var driveOn = !document.getElementById('smGoogleSrcDrive') || document.getElementById('smGoogleSrcDrive').checked; if (gmailOn || driveOn) html += '
'; if (gmailOn) { html += ''; } if (driveOn) { html += ''; } } if (S._fileSources.length > 0) { html += '
'; S._fileSources.forEach(function(s) { const isSmb = s.path && (s.path.startsWith('//') || s.path.startsWith('\\\\')); html += ''; }); } panel.innerHTML = html; } function _pmgmtNewProfile() { // Create a blank profile shell and open the editor const blank = { id: '', name: '', description: '', sources: [], google_sources: [], user_ids: [], options: {}, file_sources: [], }; // Temporarily add to S._profiles so the editor can find it window._pmgmtNewDraft = blank; _openEditorForProfile(blank); } function _pmgmtCloseEditor() { document.getElementById('pmgmtEditor').classList.remove('open'); document.querySelectorAll('.pmgmt-row').forEach(r => r.classList.remove('active')); window._pmgmtEditId = null; closeProfileMgmt(); } async function _pmgmtCycleRole(uid, event) { event.stopPropagation(); if (typeof cycleUserRole !== 'function') return; await cycleUserRole(uid); // Refresh the badge inside the profile modal to reflect the new role const u = S._allUsers.find(function(u){ return u.id === uid; }); if (!u) return; const lbl = document.querySelector('#pmgmtAcctList label[data-uid="' + uid.replace(/"/g, '\\"') + '"]'); if (!lbl) return; const badge = lbl.querySelector('.pmgmt-role-badge'); if (!badge) return; const roleText = u.userRole === 'student' ? t('role_student','Elev') : u.userRole === 'staff' ? t('role_staff','Ansat') : t('role_other','Anden'); badge.textContent = roleText; lbl.dataset.role = u.userRole || 'other'; badge.style.color = u.roleOverride ? 'var(--color-text-info)' : ''; badge.style.outline = u.roleOverride ? '1px solid var(--color-border-info)' : ''; } function _pmgmtSelectAllAccounts(checked) { document.querySelectorAll('#pmgmtAcctList label input[type=checkbox]').forEach(function(cb) { if (cb.closest('label').style.display !== 'none') cb.checked = checked; }); } let _pmgmtRoleActive = ''; function _pmgmtRoleFilter(role) { _pmgmtRoleActive = role; // Update button styles ['peRoleAll','peRoleStaff','peRoleStudent'].forEach(function(id) { const btn = document.getElementById(id); if (!btn) return; const isActive = (id === 'peRoleAll' && role === '') || (id === 'peRoleStaff' && role === 'staff') || (id === 'peRoleStudent' && role === 'student'); btn.style.background = isActive ? 'var(--accent)' : 'none'; btn.style.color = isActive ? '#fff' : 'var(--muted)'; btn.style.border = isActive ? '1px solid var(--accent)' : '1px solid var(--border)'; }); // Apply filter combined with any active text search _pmgmtFilterAccounts(document.getElementById('pmgmtAcctSearch')?.value || ''); } async function _pmgmtAddManual() { const email = (prompt('E-mail adresse:') || '').trim(); if (!email) return; const list = document.getElementById('pmgmtAcctList'); if (!list) return; // Resolve to the directory id so the scan can use it. If M365 isn't reachable // right now, fall back to "manual:" — the scan engine resolves that later. let d = null; try { d = await _lookupM365User(email); } catch(e) { /* offline — use fallback */ } const id = d ? d.id : 'manual:' + email.toLowerCase(); const label = d ? (d.displayName || email) : email; const existing = [...list.querySelectorAll('input[data-uid]')].find(cb => cb.dataset.uid === id); if (existing) { existing.checked = true; return; } // already listed — just tick it const lbl = document.createElement('label'); lbl.className = 'pmgmt-acct-row'; lbl.dataset.uid = id; lbl.dataset.role = 'other'; lbl.innerHTML = `${_esc(label)}${_esc(t('m365_profile_acct_manual', 'Manual'))}`; list.appendChild(lbl); } function _pmgmtFilterAccounts(q) { q = (q || '').toLowerCase(); document.querySelectorAll('#pmgmtAcctList label').forEach(function(lbl) { var name = (lbl.querySelector('span') || {}).textContent || ''; var role = lbl.dataset.role || 'other'; var roleOk = !_pmgmtRoleActive || role === _pmgmtRoleActive; var nameOk = !q || name.toLowerCase().includes(q); lbl.style.display = (roleOk && nameOk) ? '' : 'none'; }); } async function _pmgmtSaveFullEdit() { const id = window._pmgmtEditId; const profile = (id ? S._profiles.find(p => p.id === id) : null) || window._pmgmtNewDraft || {}; const name = document.getElementById('pmgmtEditName')?.value?.trim(); if (!name) { alert(t('m365_profile_name_required','Profile name is required.')); return; } const peSources = Array.from(document.querySelectorAll('#peSourcesPanel input[type=checkbox]:checked')); const m365Sources = peSources.filter(cb => cb.dataset.sourceType === 'm365').map(cb => cb.dataset.sourceId); const googleSources = peSources.filter(cb => cb.dataset.sourceType === 'google').map(cb => cb.dataset.sourceId); const fileSources = peSources.filter(cb => cb.dataset.sourceType === 'file').map(cb => cb.dataset.sourceId); // Check whether the checkboxes were actually rendered in the editor DOM — // NOT whether Google is connected or file sources are loaded. Those are async // and may not have resolved when the editor first opened, leaving the panel // without checkboxes even though the connection exists. Using the DOM as the // source of truth avoids a race-condition that silently cleared google/file sources. const googleRendered = !!document.querySelector('#peSourcesPanel input[data-source-type="google"]'); const fileRendered = !!document.querySelector('#peSourcesPanel input[data-source-type="file"]'); const effectiveGoogleSources = googleRendered ? googleSources : (profile.google_sources || []); const effectiveFileSources = fileRendered ? fileSources : (profile.file_sources || []); const allSources = m365Sources.concat(effectiveGoogleSources).concat(effectiveFileSources); let user_ids = Array.from(document.querySelectorAll('#pmgmtAcctList input[type=checkbox]:checked')) .filter(cb => cb.dataset.uid) .map(cb => cb.dataset.manual ? {id: cb.dataset.uid, displayName: cb.dataset.name || '', email: cb.dataset.email || '', manual: true} : cb.dataset.uid); // "all" profile opened before users loaded: there were no rows to tick, so keep "all" if (window._pmgmtSavedAll && !S._allUsers.length && !user_ids.length) user_ids = 'all'; const updated = { ...profile, name, description: document.getElementById('pmgmtEditDesc')?.value?.trim() || '', sources: allSources, google_sources: effectiveGoogleSources, file_sources: effectiveFileSources, user_ids, options: { ...(profile.options || {}), older_than_days: parseInt(document.getElementById('peOptDays')?.value) || 0, email_body: document.getElementById('peOptBody')?.checked ?? true, attachments: document.getElementById('peOptAtt')?.checked ?? true, max_attach_mb: parseInt(document.getElementById('peOptMaxAttach')?.value) || 20, max_emails: parseInt(document.getElementById('peOptMaxEmails')?.value) || 2000, delta: document.getElementById('peOptDelta')?.checked ?? false, scan_photos: document.getElementById('peOptPhotos')?.checked ?? false, skip_gps_images: document.getElementById('peOptSkipGps')?.checked ?? false, min_cpr_count: parseInt(document.getElementById('peOptMinCpr')?.value) || 1, ocr_lang: document.getElementById('peOptOcrLang')?.value || 'dan+eng', cpr_only: document.getElementById('peOptCprOnly')?.checked ?? false, scan_emails: document.getElementById('peOptEmails')?.checked ?? false, scan_phones: document.getElementById('peOptPhones')?.checked ?? false, }, retention_years: document.getElementById('peOptRetention')?.checked ? (parseInt(document.getElementById('peOptRetYears')?.value) || 5) : null, fiscal_year_end: document.getElementById('peOptRetention')?.checked ? (document.getElementById('peOptFiscalYearEnd')?.value || '') : '', }; try { const r = await fetch('/api/profiles/save', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(updated) }); const d = await r.json(); if (d.error) { alert(d.error); return; } await loadProfiles(); _renderProfileMgmt(); window._pmgmtNewDraft = null; log(t('m365_profile_saved','Profile saved') + ': ' + name); // Show inline saved feedback without closing the modal const footer = document.querySelector('#pmgmtEditor > div:last-child'); if (footer) { const fb = document.createElement('span'); fb.textContent = '✓ ' + t('m365_profile_saved', 'Saved'); fb.style.cssText = 'font-size:11px;color:var(--success);margin-right:auto'; footer.prepend(fb); setTimeout(function() { fb.remove(); }, 2000); } // Re-open the editor for the saved profile so it reflects the saved state const saved = S._profiles.find(function(p) { return p.name === name; }); if (saved) { window._pmgmtEditId = saved.id; document.querySelectorAll('.pmgmt-row').forEach(r => r.classList.toggle('active', r.dataset.id === saved.id)); } } catch(e) { alert('Save failed: ' + e.message); } } async function _pmgmtSaveEdit(id) { const name = document.getElementById(`pmgmt-edit-name-${id}`)?.value?.trim(); const desc = document.getElementById(`pmgmt-edit-desc-${id}`)?.value?.trim(); if (!name) { alert(t('m365_profile_name_required','Profile name is required.')); return; } const profile = S._profiles.find(p => p.id === id); if (!profile) return; const updated = { ...profile, name, description: desc || '' }; try { const r = await fetch('/api/profiles/save', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(updated) }); const d = await r.json(); if (d.error) { alert(d.error); return; } await loadProfiles(); _renderProfileMgmt(); log(t('m365_profile_saved','Profile saved') + ': ' + name); } catch(e) { alert('Save failed: ' + e.message); } } async function _pmgmtDuplicate(id) { const profile = S._profiles.find(p => p.id === id); if (!profile) return; const base = profile.name.replace(/ \(copy( \d+)?\)$/, ''); // Find a unique name let n = 1, name = base + ' (copy)'; while (S._profiles.some(p => p.name === name)) { n++; name = `${base} (copy ${n})`; } const copy = { ...profile, id: '', name, last_run: null, last_scan_id: null }; try { const r = await fetch('/api/profiles/save', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(copy) }); const d = await r.json(); if (d.error) { alert(d.error); return; } await loadProfiles(); _renderProfileMgmt(); log(t('m365_profile_duplicated','Profile duplicated') + ': ' + name); } catch(e) { alert('Duplicate failed: ' + e.message); } } async function _pmgmtDelete(id, name) { if (!confirm(t('m365_profile_delete_confirm','Delete profile') + ' "' + name + '"?')) return; try { const r = await fetch('/api/profiles/delete', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ id }) }); const d = await r.json(); if (d.error) { alert(d.error); return; } if (S._activeProfileId === id) { S._activeProfileId = null; _setProfileClearBtn(false); } await loadProfiles(); _renderProfileMgmt(); log(t('m365_profile_deleted','Profile deleted') + ': ' + name); } catch(e) { alert('Delete failed: ' + e.message); } } // ── Window exports (HTML handlers + cross-module calls) ───────────────────── window.loadProfiles = loadProfiles; window._renderProfileSelect = _renderProfileSelect; window._setProfileClearBtn = _setProfileClearBtn; window.onProfileChange = onProfileChange; window.clearActiveProfile = clearActiveProfile; window._applyProfile = _applyProfile; window._applyPendingProfileUsers = _applyPendingProfileUsers; window.saveCurrentAsProfile = saveCurrentAsProfile; window.openProfileMgmtModal = openProfileMgmtModal; window.closeProfileMgmt = closeProfileMgmt; window._sourceLabel = _sourceLabel; window._renderProfileMgmt = _renderProfileMgmt; window._esc = _esc; window._pmgmtUse = _pmgmtUse; window._pmgmtOpenEditor = _pmgmtOpenEditor; window._openEditorForProfile = _openEditorForProfile; window._unresolvedAccountRows = _unresolvedAccountRows; window._addProfileManualUsers = _addProfileManualUsers; window._lookupM365User = _lookupM365User; window._userListFinal = _userListFinal; window._peSetDate = _peSetDate; window._peSetYear = _peSetYear; window._renderEditorSources = _renderEditorSources; window._pmgmtNewProfile = _pmgmtNewProfile; window._pmgmtCloseEditor = _pmgmtCloseEditor; window._pmgmtCycleRole = _pmgmtCycleRole; window._pmgmtSelectAllAccounts = _pmgmtSelectAllAccounts; window._pmgmtRoleFilter = _pmgmtRoleFilter; window._pmgmtAddManual = _pmgmtAddManual; window._pmgmtFilterAccounts = _pmgmtFilterAccounts; window._pmgmtSaveFullEdit = _pmgmtSaveFullEdit; window._pmgmtSaveEdit = _pmgmtSaveEdit; window._pmgmtDuplicate = _pmgmtDuplicate; window._pmgmtDelete = _pmgmtDelete; window._pmgmtRoleActive = _pmgmtRoleActive;