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 @@
+
+
+
No milestones in database
+
Import the default developmental milestones data (birth to 11 years) to enable admin editing.
+
+
+
@@ -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;