pediatric-ai-scribe-v3/public/js/adminMilestones.js
Daniel 3d5b77721c Replace all browser dialogs with modern modal, add OIDC admin UI
- Add reusable showConfirm() modal component (supports plain confirm,
  input prompt, danger styling, Enter key)
- Replace ALL 18 confirm() and prompt() calls across 8 JS files with
  showConfirm() modal: admin user actions, session revoke, document
  delete, template delete, milestone management, transcription settings
- Fix broken admin reset-password (btn was undefined in scope)
- Add OIDC/SSO configuration UI to Admin Panel (issuer, client ID/secret,
  button label, disable local auth toggle, callback URL display)
2026-04-09 02:43:23 +02:00

326 lines
12 KiB
JavaScript

// ============================================================
// ADMIN: DEVELOPMENTAL MILESTONES MANAGEMENT
// ============================================================
(function() {
var _inited = false;
document.addEventListener('tabChanged', function(e) {
if (e.detail.tab !== 'admin' || _inited) return;
_inited = true;
initMilestonesAdmin();
});
var milestones = [];
var ageGroups = [];
var domains = [];
var editingId = null;
function initMilestonesAdmin() {
console.log('[AdminMilestones] Initializing...');
// Buttons
document.getElementById('btn-refresh-milestones')?.addEventListener('click', loadMilestones);
document.getElementById('btn-add-milestone')?.addEventListener('click', function() {
editingId = null;
openMilestoneModal();
});
document.getElementById('btn-close-milestone-modal')?.addEventListener('click', closeMilestoneModal);
document.getElementById('btn-cancel-milestone-modal')?.addEventListener('click', closeMilestoneModal);
document.getElementById('btn-save-milestone')?.addEventListener('click', saveMilestone);
document.getElementById('btn-bulk-import-milestones')?.addEventListener('click', bulkImportMilestones);
document.getElementById('btn-reimport-milestones')?.addEventListener('click', function() {
showConfirm('This will DELETE all existing milestones and re-import from static data. Continue?', function() {
bulkImportMilestones(true);
}, { danger: true, confirmText: 'Re-import' });
});
// Filter
document.getElementById('ms-filter-age')?.addEventListener('change', renderMilestones);
// Load data
loadMetadata();
loadMilestones();
}
function loadMetadata() {
fetch('/api/admin/milestones/meta', {
headers: getAuthHeaders()
})
.then(r => r.json())
.then(data => {
if (!data.success) return;
ageGroups = data.age_groups || [];
domains = data.domains || [];
// Populate filter dropdown
var filterSelect = document.getElementById('ms-filter-age');
if (filterSelect) {
filterSelect.innerHTML = '<option value="">All Age Groups</option>';
ageGroups.forEach(function(age) {
var opt = document.createElement('option');
opt.value = age;
opt.textContent = age;
filterSelect.appendChild(opt);
});
}
// Populate datalists for modal
var ageDatalist = document.getElementById('ms-age-list');
if (ageDatalist) {
ageDatalist.innerHTML = '';
ageGroups.forEach(function(age) {
var opt = document.createElement('option');
opt.value = age;
ageDatalist.appendChild(opt);
});
}
var domainDatalist = document.getElementById('ms-domain-list');
if (domainDatalist) {
domainDatalist.innerHTML = '';
domains.forEach(function(domain) {
var opt = document.createElement('option');
opt.value = domain;
domainDatalist.appendChild(opt);
});
}
})
.catch(err => console.error('[AdminMilestones] Failed to load metadata:', err));
}
function loadMilestones() {
var filterAge = document.getElementById('ms-filter-age')?.value;
var url = '/api/admin/milestones' + (filterAge ? '?age_group=' + encodeURIComponent(filterAge) : '');
fetch(url, {
headers: getAuthHeaders()
})
.then(r => r.json())
.then(data => {
if (!data.success) {
showToast(data.error || 'Failed to load milestones', 'error');
return;
}
milestones = data.milestones || [];
// Show/hide empty notice
var emptyNotice = document.getElementById('ms-empty-notice');
if (emptyNotice) {
emptyNotice.style.display = (milestones.length === 0 && !filterAge) ? 'block' : 'none';
}
renderMilestones();
})
.catch(err => {
console.error('[AdminMilestones] Failed to load:', err);
showToast('Failed to load milestones: ' + err.message, 'error');
});
}
function renderMilestones() {
var list = document.getElementById('milestones-list');
if (!list) return;
var filterAge = document.getElementById('ms-filter-age')?.value;
var filtered = filterAge ? milestones.filter(m => m.age_group === filterAge) : milestones;
if (filtered.length === 0) {
list.innerHTML = '<p style="text-align:center;color:var(--g400);padding:40px;">No milestones found. Click "Add Milestone" to create one.</p>';
return;
}
// Group by age_group and domain
var grouped = {};
filtered.forEach(function(m) {
if (!grouped[m.age_group]) grouped[m.age_group] = {};
if (!grouped[m.age_group][m.domain]) grouped[m.age_group][m.domain] = [];
grouped[m.age_group][m.domain].push(m);
});
var html = '';
Object.keys(grouped).forEach(function(age) {
html += '<div style="border:1px solid var(--g200);border-radius:8px;padding:12px;background:var(--g50);">';
html += '<h4 style="margin:0 0 12px 0;font-size:15px;color:var(--g700);">' + age + '</h4>';
Object.keys(grouped[age]).forEach(function(domain) {
html += '<div style="margin-bottom:10px;">';
html += '<div style="font-size:13px;font-weight:600;color:var(--g600);margin-bottom:6px;">' + domain + '</div>';
grouped[age][domain].forEach(function(m) {
html += '<div style="display:flex;align-items:center;gap:8px;padding:6px;border:1px solid var(--g200);border-radius:6px;background:white;margin-bottom:4px;">';
html += '<span style="flex:1;font-size:13px;color:var(--g700);">' + m.milestone_text + '</span>';
html += '<button class="btn-sm btn-ghost" style="padding:4px 8px;font-size:11px;" onclick="editMilestone(' + m.id + ')"><i class="fas fa-edit"></i></button>';
html += '<button class="btn-sm" style="padding:4px 8px;font-size:11px;background:var(--red);color:white;border:none;" onclick="deleteMilestone(' + m.id + ')"><i class="fas fa-trash"></i></button>';
html += '</div>';
});
html += '</div>';
});
html += '</div>';
});
list.innerHTML = html;
}
function openMilestoneModal(milestone) {
var modal = document.getElementById('milestone-modal');
var title = document.getElementById('milestone-modal-title');
if (!modal) return;
if (milestone) {
title.innerHTML = '<i class="fas fa-edit"></i> Edit Milestone';
document.getElementById('ms-edit-id').value = milestone.id;
document.getElementById('ms-edit-age').value = milestone.age_group;
document.getElementById('ms-edit-domain').value = milestone.domain;
document.getElementById('ms-edit-text').value = milestone.milestone_text;
document.getElementById('ms-edit-sort').value = milestone.sort_order;
} else {
title.innerHTML = '<i class="fas fa-plus"></i> Add Milestone';
document.getElementById('ms-edit-id').value = '';
document.getElementById('ms-edit-age').value = '';
document.getElementById('ms-edit-domain').value = '';
document.getElementById('ms-edit-text').value = '';
document.getElementById('ms-edit-sort').value = '0';
}
modal.style.display = 'flex';
}
function closeMilestoneModal() {
var modal = document.getElementById('milestone-modal');
if (modal) modal.style.display = 'none';
}
function saveMilestone() {
var id = document.getElementById('ms-edit-id').value;
var age_group = document.getElementById('ms-edit-age').value.trim();
var domain = document.getElementById('ms-edit-domain').value.trim();
var milestone_text = document.getElementById('ms-edit-text').value.trim();
var sort_order = parseInt(document.getElementById('ms-edit-sort').value) || 0;
if (!age_group || !domain || !milestone_text) {
showToast('Please fill in all fields', 'error');
return;
}
var method = id ? 'PUT' : 'POST';
var url = id ? '/api/admin/milestones/' + id : '/api/admin/milestones';
fetch(url, {
method: method,
headers: getAuthHeaders(),
body: JSON.stringify({ age_group, domain, milestone_text, sort_order })
})
.then(r => r.json())
.then(data => {
if (!data.success) {
showToast(data.error || 'Failed to save', 'error');
return;
}
showToast(id ? 'Milestone updated' : 'Milestone added', 'success');
closeMilestoneModal();
loadMetadata();
loadMilestones();
})
.catch(err => {
console.error('[AdminMilestones] Save error:', err);
showToast('Failed to save: ' + err.message, 'error');
});
}
window.editMilestone = function(id) {
var milestone = milestones.find(m => m.id === id);
if (!milestone) return;
editingId = id;
openMilestoneModal(milestone);
};
window.deleteMilestone = function(id) {
showConfirm('Delete this milestone? This action cannot be undone.', function() {
fetch('/api/admin/milestones/' + id, {
method: 'DELETE',
headers: getAuthHeaders()
})
.then(r => r.json())
.then(data => {
if (!data.success) {
showToast(data.error || 'Failed to delete', 'error');
return;
}
showToast('Milestone deleted', 'success');
loadMetadata();
loadMilestones();
})
.catch(err => {
console.error('[AdminMilestones] Delete error:', err);
showToast('Failed to delete: ' + err.message, 'error');
});
}, { danger: true, confirmText: 'Delete' });
};
function bulkImportMilestones(clearExisting) {
if (!window.MILESTONES_DATA_STATIC) {
showToast('Static milestones data not available', 'error');
return;
}
var btn = clearExisting ? document.getElementById('btn-reimport-milestones') : document.getElementById('btn-bulk-import-milestones');
if (btn) {
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Importing...';
}
// Convert static data to array format for bulk import
var milestonesToImport = [];
var sortOrder = 0;
Object.keys(window.MILESTONES_DATA_STATIC).forEach(function(ageGroup) {
Object.keys(window.MILESTONES_DATA_STATIC[ageGroup]).forEach(function(domain) {
var items = window.MILESTONES_DATA_STATIC[ageGroup][domain];
items.forEach(function(text) {
milestonesToImport.push({
age_group: ageGroup,
domain: domain,
milestone_text: text,
sort_order: sortOrder++
});
});
});
});
// Import directly (backend will handle existing data)
fetch('/api/admin/milestones/bulk-import', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ milestones: milestonesToImport, clearExisting: clearExisting })
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (btn) {
btn.disabled = false;
btn.innerHTML = clearExisting ? '<i class="fas fa-sync"></i> Re-import All' : '<i class="fas fa-download"></i> Import Default Milestones Data';
}
if (!data.success) {
showToast(data.error || 'Import failed', 'error');
return;
}
showToast('Successfully imported ' + data.imported + ' milestones', 'success');
loadMetadata();
loadMilestones();
})
.catch(function(err) {
if (btn) {
btn.disabled = false;
btn.innerHTML = clearExisting ? '<i class="fas fa-sync"></i> Re-import All' : '<i class="fas fa-download"></i> Import Default Milestones Data';
}
console.error('[AdminMilestones] Import error:', err);
showToast('Import failed: ' + err.message, 'error');
});
}
})();