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:
parent
75a3a47598
commit
2bccd4e374
3 changed files with 89 additions and 1 deletions
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue