diff --git a/public/components/admin.html b/public/components/admin.html index 1c69203..4d6e3e5 100644 --- a/public/components/admin.html +++ b/public/components/admin.html @@ -270,6 +270,13 @@
+ + +
@@ -277,6 +284,7 @@ +
diff --git a/public/js/adminMilestones.js b/public/js/adminMilestones.js index b355578..3a6e63d 100644 --- a/public/js/adminMilestones.js +++ b/public/js/adminMilestones.js @@ -28,6 +28,12 @@ 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() { + if (confirm('This will DELETE all existing milestones and re-import from static data. Continue?')) { + bulkImportMilestones(true); + } + }); // Filter document.getElementById('ms-filter-age')?.addEventListener('change', renderMilestones); @@ -97,6 +103,13 @@ 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 => { @@ -248,4 +261,66 @@ }); }; + 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 = ' 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 ? ' Re-import All' : ' 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 ? ' Re-import All' : ' Import Default Milestones Data'; + } + console.error('[AdminMilestones] Import error:', err); + showToast('Import failed: ' + err.message, 'error'); + }); + } + })(); diff --git a/src/routes/adminMilestones.js b/src/routes/adminMilestones.js index e407df2..8378f45 100644 --- a/src/routes/adminMilestones.js +++ b/src/routes/adminMilestones.js @@ -117,12 +117,17 @@ router.delete('/milestones/:id', adminMiddleware, async (req, res) => { // Bulk import milestones (from static data) router.post('/milestones/bulk-import', adminMiddleware, async (req, res) => { try { - const { milestones } = req.body; + const { milestones, clearExisting } = req.body; if (!Array.isArray(milestones)) { return res.status(400).json({ error: 'Expected array of milestones' }); } + // Clear existing if requested + if (clearExisting) { + await db.query('DELETE FROM developmental_milestones'); + } + let imported = 0; for (const m of milestones) { if (!m.age_group || !m.domain || !m.milestone_text) continue;