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
d86625c7e6
commit
d82a900878
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>
|
<button id="btn-refresh-milestones" class="btn-sm btn-ghost"><i class="fas fa-rotate"></i> Refresh</button>
|
||||||
</div>
|
</div>
|
||||||
<div style="padding:16px;display:flex;flex-direction:column;gap:16px;">
|
<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 -->
|
<!-- Filters -->
|
||||||
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
|
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
|
||||||
<label style="font-size:13px;font-weight:600;">Age Group:</label>
|
<label style="font-size:13px;font-weight:600;">Age Group:</label>
|
||||||
|
|
@ -277,6 +284,7 @@
|
||||||
<option value="">All Age Groups</option>
|
<option value="">All Age Groups</option>
|
||||||
</select>
|
</select>
|
||||||
<button id="btn-add-milestone" class="btn-sm btn-primary"><i class="fas fa-plus"></i> Add Milestone</button>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Milestones List -->
|
<!-- Milestones List -->
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,12 @@
|
||||||
document.getElementById('btn-close-milestone-modal')?.addEventListener('click', closeMilestoneModal);
|
document.getElementById('btn-close-milestone-modal')?.addEventListener('click', closeMilestoneModal);
|
||||||
document.getElementById('btn-cancel-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-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
|
// Filter
|
||||||
document.getElementById('ms-filter-age')?.addEventListener('change', renderMilestones);
|
document.getElementById('ms-filter-age')?.addEventListener('change', renderMilestones);
|
||||||
|
|
@ -97,6 +103,13 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
milestones = data.milestones || [];
|
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();
|
renderMilestones();
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.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)
|
// Bulk import milestones (from static data)
|
||||||
router.post('/milestones/bulk-import', adminMiddleware, async (req, res) => {
|
router.post('/milestones/bulk-import', adminMiddleware, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { milestones } = req.body;
|
const { milestones, clearExisting } = req.body;
|
||||||
|
|
||||||
if (!Array.isArray(milestones)) {
|
if (!Array.isArray(milestones)) {
|
||||||
return res.status(400).json({ error: 'Expected array of 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;
|
let imported = 0;
|
||||||
for (const m of milestones) {
|
for (const m of milestones) {
|
||||||
if (!m.age_group || !m.domain || !m.milestone_text) continue;
|
if (!m.age_group || !m.domain || !m.milestone_text) continue;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue