registerPage('dedupe', async (content, params = {}) => { document.getElementById('topbar-title').textContent = 'Duplicate Employees'; let groups = []; content.innerHTML = `
${loadingHTML()}
`; window.loadDupes = async function loadDupes() { document.getElementById('dedupe-content').innerHTML = loadingHTML(); document.getElementById('dedupe-summary')?.setAttribute('data-text', ''); const r = await api('dedupe.php', { action: 'list' }); if (!r.success) { document.getElementById('dedupe-content').innerHTML = emptyHTML('Error: ' + r.error); return; } groups = r.groups; if (!groups.length) { document.getElementById('merge-all-btn').style.display = 'none'; document.getElementById('dedupe-content').innerHTML = `

βœ“ No duplicate employees found.

`; return; } const sumEl2 = document.getElementById('dedupe-summary'); if (sumEl2) sumEl2.textContent = `${r.total_groups} duplicate groups found`; document.getElementById('merge-all-btn').style.display = groups.length ? '' : 'none'; document.getElementById('dedupe-content').innerHTML = groups.map((g, gi) => `

${g.records[0].i_doc_passport} ${[...new Set(g.records.map(r => r.client_employees_name + ' ' + r.surname))].join(' / ')}

${g.records.length} records ${g.total_assessments} assessments ${g.total_tests} tests ${g.total_bookings} bookings
${g.records.map(row => ` `).join('')}
Record IDNameSurnameClient Company NoOccupationAssessmentsTestsBookingsStatus
${row.record_id} ${row.client_employees_name} ${row.surname} ${row.clients_name || 'β€”'} ${row.company_number || 'β€”'} ${row.occupation || 'β€”'} ${row.assessments_count} ${row.tests_count} ${row.bookings_count} ${row.is_primary ? 'KEEP (Primary)' : 'DUPLICATE β€” will be removed'}
`).join(''); } window.previewMerge = async (gi) => { const g = groups[gi]; const dupIds = g.records.filter(r => !r.is_primary).map(r => r.record_id); document.getElementById('preview-title').textContent = `Merge: ${g.records[0].client_employees_name} ${g.records[0].surname}`; document.getElementById('preview-body').innerHTML = loadingHTML(); document.getElementById('preview-footer').innerHTML = ''; openModal('preview-modal'); const r = await api('dedupe.php', { action: 'preview', primary_id: g.primary_id, dup_ids: JSON.stringify(dupIds) }, 'POST'); if (!r.success) { document.getElementById('preview-body').innerHTML = emptyHTML('Error: ' + r.error); return; } const p = r.preview; const assRows = p.assessments.map(a => `${a.record_id}${a.assesses_name}${a.date || 'β€”'}${a.results || 'β€”'}` ).join('') || 'None'; const testRows = p.tests.map(t => `${t.record_id}${t.test_name}${t.date || 'β€”'}${t.results || 'β€”'}` ).join('') || 'None'; const bookRows = p.bookings.map(b => `${b.booking_number || b.record_id}${b.date_booked || 'β€”'}${statusBadge(b.status)}${b.affected_dup_ids.join(', ')}` ).join('') || 'None'; const primary = g.records.find(r => r.is_primary); const dups = g.records.filter(r => !r.is_primary); document.getElementById('preview-body').innerHTML = `

Primary record (kept): ID ${primary.record_id} β€” ${primary.client_employees_name} ${primary.surname} @ ${primary.clients_name || '?'}

Duplicate records (deleted): IDs ${dups.map(d => d.record_id).join(', ')}

Assessments that will be re-linked to ID ${g.primary_id}:

${assRows}
IDNameDateResult

Tests that will be re-linked to ID ${g.primary_id}:

${testRows}
IDNameDateResult

Bookings that will be updated:

${bookRows}
Booking #DateStatusDup IDs affected

A full backup will be saved automatically before any changes are made. You can rollback from the Backups panel.

`; document.getElementById('preview-footer').innerHTML = ` `; }; window.doMerge = async (gi, primaryId, dupIds) => { if (!confirm('Are you sure? Duplicate employee records will be deleted. A backup will be saved first.')) return; const g = groups[gi]; const desc = `Merge ${g.records[0].client_employees_name} ${g.records[0].surname} (${g.records[0].i_doc_passport}) β€” primary ID ${primaryId}, removed IDs ${dupIds.join(',')}`; const r = await api('dedupe.php', { action: 'merge', primary_id: primaryId, dup_ids: JSON.stringify(dupIds), description: desc, }, 'POST'); if (r.success) { toast(`βœ“ Merged β€” Backup #${r.backup_id} saved`, 'success'); closeModal('preview-modal'); loadDupes(); } else { toast('Error: ' + r.error, 'error'); } }; window.mergeAll = async () => { if (!groups.length) { toast('No duplicates to merge', 'error'); return; } if (!confirm(`Merge ALL ${groups.length} duplicate groups automatically?\n\nA backup will be created for each group. You can rollback individual merges from the Backups panel.`)) return; document.getElementById('merge-all-btn').disabled = true; document.getElementById('merge-all-btn').textContent = 'Merging…'; let done = 0, failed = 0; const total = groups.length; // Progress bar document.getElementById('dedupe-content').innerHTML = `

Merging 0 / ${total} groups…

`; for (const g of groups) { const dupIds = g.records.filter(r => !r.is_primary).map(r => r.record_id); const desc = `Auto-merge ID ${g.records[0].i_doc_passport} β€” primary ${g.primary_id}, removed ${dupIds.join(',')}`; const r = await api('dedupe.php', { action: 'merge', primary_id: g.primary_id, dup_ids: JSON.stringify(dupIds), description: desc, }, 'POST'); done++; const pct = Math.round((done / total) * 100); const progBar = document.getElementById('prog-bar'); const progDone = document.getElementById('prog-done'); const progLog = document.getElementById('prog-log'); if (progBar) progBar.style.width = pct + '%'; if (progDone) progDone.textContent = done; if (progLog) { const line = document.createElement('div'); if (r.success) { line.style.color = 'var(--success)'; line.textContent = `βœ“ ${g.records[0].i_doc_passport} β€” merged ${dupIds.length} duplicate(s) β†’ ID ${g.primary_id} (backup #${r.backup_id})`; } else { failed++; line.style.color = 'var(--danger)'; line.textContent = `βœ• ${g.records[0].i_doc_passport} β€” Error: ${r.error}`; } progLog.appendChild(line); progLog.scrollTop = progLog.scrollHeight; } } document.getElementById('merge-all-btn').disabled = false; document.getElementById('merge-all-btn').textContent = '⚑ Merge All Duplicates'; document.getElementById('merge-all-btn').style.display = 'none'; toast(`Done β€” ${done - failed} merged, ${failed} failed`, failed ? 'error' : 'success'); // Reload after short delay setTimeout(() => loadDupes(), 1200); }; window.showBackups = async () => { document.getElementById('backups-body').innerHTML = loadingHTML(); openModal('backups-modal'); const r = await api('dedupe.php', { action: 'list_backups' }); if (!r.success) { document.getElementById('backups-body').innerHTML = emptyHTML('Error'); return; } const rows = r.backups; document.getElementById('backups-body').innerHTML = rows.length === 0 ? emptyHTML('No backups yet') : `
${rows.map(b => ``).join('')}
#DateDescriptionBy
${b.record_id} ${b.created_at} ${b.description} ${b.safesure_users_name || 'β€”'}
`; }; window.doRollback = async (backupId) => { if (!confirm(`Rollback backup #${backupId}? This will restore deleted employee records and undo all reassignments.`)) return; const r = await api('dedupe.php', { action: 'rollback', backup_id: backupId }, 'POST'); if (r.success) { toast('Rollback complete', 'success'); closeModal('backups-modal'); loadDupes(); } else { toast('Error: ' + r.error, 'error'); } }; loadDupes(); });