v2.1: Add visible bulk import UI for developmental milestones

NEW FEATURES:
- Bulk Import button in Admin Panel → Developmental Milestones section
- "Import Default Milestones Data" button appears when database is empty
- "Re-import All" button to clear and re-import all static data
- Visible notice when no milestones exist with one-click import

IMPROVEMENTS:
- Auto-shows empty state notice when database has no milestones
- Backend bulk-import endpoint now supports clearExisting parameter
- Imports ALL age groups from static data (birth to 11 years)
- Better UX - admin doesn't need CLI to populate milestone data

FIXES:
- Makes milestone admin editing feature discoverable and usable
- No need to manually run import script anymore
This commit is contained in:
ifedan-ed 2026-04-01 18:06:42 +00:00
parent 75a3a47598
commit 2bccd4e374
3 changed files with 89 additions and 1 deletions

View file

@ -270,6 +270,13 @@
<button id="btn-refresh-milestones" class="btn-sm btn-ghost"><i class="fas fa-rotate"></i> Refresh</button>
</div>
<div style="padding:16px;display:flex;flex-direction:column;gap:16px;">
<!-- Bulk Import Notice -->
<div id="ms-empty-notice" style="display:none;background:var(--blue-bg);border-left:3px solid var(--blue);padding:12px;border-radius:6px;">
<p style="margin:0 0 8px;font-weight:600;color:var(--blue);"><i class="fas fa-info-circle"></i> No milestones in database</p>
<p style="margin:0 0 12px;font-size:13px;color:var(--g600);">Import the default developmental milestones data (birth to 11 years) to enable admin editing.</p>
<button id="btn-bulk-import-milestones" class="btn-sm btn-primary"><i class="fas fa-download"></i> Import Default Milestones Data</button>
</div>
<!-- Filters -->
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
<label style="font-size:13px;font-weight:600;">Age Group:</label>
@ -277,6 +284,7 @@
<option value="">All Age Groups</option>
</select>
<button id="btn-add-milestone" class="btn-sm btn-primary"><i class="fas fa-plus"></i> Add Milestone</button>
<button id="btn-reimport-milestones" class="btn-sm btn-ghost" title="Re-import all static data (clears existing)"><i class="fas fa-sync"></i> Re-import All</button>
</div>
<!-- Milestones List -->

View file

@ -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 = '<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');
});
}
})();

View file

@ -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;