/** * Duplicate Management Module * * Handles duplicate asset management (merge, delete) */ export class DuplicateManagement { constructor() { this.init(); } init() { this.setupDeleteDuplicate(); this.setupDeleteGroup(); this.setupMergeGroup(); this.setupRefresh(); } setupDeleteDuplicate() { document.addEventListener('click', async (e) => { const button = e.target.closest('[data-delete-duplicate]'); if (!button) return; e.preventDefault(); const assetId = button.dataset.assetId; if (!confirm(`Are you sure you want to delete this duplicate asset?`)) { return; } await this.deleteAsset(assetId, button); }); } setupDeleteGroup() { document.addEventListener('click', async (e) => { const button = e.target.closest('[data-delete-group]'); if (!button) return; e.preventDefault(); const sha256 = button.dataset.sha256; const groupCard = button.closest('.duplicate-group'); const assetCount = groupCard?.querySelectorAll('.duplicate-asset-card').length || 0; if (!confirm(`Are you sure you want to delete all ${assetCount} duplicate assets in this group?`)) { return; } await this.deleteGroup(sha256, button); }); } setupMergeGroup() { document.addEventListener('click', async (e) => { const button = e.target.closest('[data-merge-group]'); if (!button) return; e.preventDefault(); const sha256 = button.dataset.sha256; const groupCard = button.closest('.duplicate-group'); const assets = Array.from(groupCard?.querySelectorAll('[data-asset-id]') || []) .map(el => el.dataset.assetId); if (assets.length < 2) { if (window.Toast) { window.Toast.warning('Need at least 2 assets to merge'); } else { alert('Need at least 2 assets to merge'); } return; } // Show merge dialog const keepAssetId = await this.showMergeDialog(assets); if (!keepAssetId) { return; } await this.mergeGroup(sha256, assets, keepAssetId, button); }); } setupRefresh() { document.addEventListener('click', (e) => { const button = e.target.closest('[data-refresh-duplicates]'); if (!button) return; e.preventDefault(); window.location.reload(); }); } async deleteAsset(assetId, button) { const originalText = button.textContent; button.disabled = true; button.textContent = 'Deleting...'; try { const response = await fetch(`/admin/api/assets/${assetId}`, { method: 'DELETE', headers: { 'X-Requested-With': 'XMLHttpRequest', }, }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } if (window.Toast) { window.Toast.success('Duplicate asset deleted'); } // Remove asset card button.closest('.duplicate-asset-card')?.remove(); // Check if group is now empty or has only one asset const groupCard = button.closest('.duplicate-group'); const remainingAssets = groupCard?.querySelectorAll('.duplicate-asset-card').length || 0; if (remainingAssets <= 1) { groupCard?.remove(); } } catch (error) { console.error('Failed to delete asset:', error); if (window.Toast) { window.Toast.error(`Failed to delete asset: ${error.message}`); } else { alert(`Failed to delete asset: ${error.message}`); } } finally { button.disabled = false; button.textContent = originalText; } } async deleteGroup(sha256, button) { const groupCard = button.closest('.duplicate-group'); const assetCards = groupCard?.querySelectorAll('.duplicate-asset-card') || []; const assetIds = Array.from(assetCards).map(card => card.querySelector('[data-asset-id]')?.dataset.assetId ).filter(Boolean); button.disabled = true; button.textContent = 'Deleting...'; try { // Delete all assets in parallel const deletePromises = assetIds.map(id => fetch(`/admin/api/assets/${id}`, { method: 'DELETE', headers: { 'X-Requested-With': 'XMLHttpRequest', }, }) ); await Promise.all(deletePromises); if (window.Toast) { window.Toast.success(`Deleted ${assetIds.length} duplicate assets`); } // Remove group card groupCard?.remove(); } catch (error) { console.error('Failed to delete group:', error); if (window.Toast) { window.Toast.error(`Failed to delete group: ${error.message}`); } else { alert(`Failed to delete group: ${error.message}`); } } finally { button.disabled = false; button.textContent = 'Delete All'; } } async showMergeDialog(assetIds) { return new Promise((resolve) => { const dialog = document.createElement('div'); dialog.className = 'merge-dialog'; dialog.innerHTML = `

Merge Duplicate Assets

Select which asset to keep. All other duplicates will be deleted.

`; document.body.appendChild(dialog); const assetsList = dialog.querySelector('#merge-assets-list'); let selectedAssetId = null; // Load asset details and create radio buttons assetIds.forEach(assetId => { const assetCard = document.querySelector(`[data-asset-id="${assetId}"]`)?.closest('.duplicate-asset-card'); if (!assetCard) return; const assetIdEl = assetCard.querySelector('.asset-id code'); const assetBucket = assetCard.querySelector('.asset-bucket'); const assetPreview = assetCard.querySelector('.asset-preview-small')?.innerHTML; const radio = document.createElement('div'); radio.className = 'merge-asset-option'; radio.innerHTML = ` `; radio.querySelector('input').addEventListener('change', (e) => { if (e.target.checked) { selectedAssetId = e.target.value; dialog.querySelector('[data-merge-confirm]').disabled = false; } }); assetsList.appendChild(radio); }); const cleanup = () => { document.body.removeChild(dialog); }; dialog.querySelector('[data-merge-confirm]').addEventListener('click', () => { cleanup(); resolve(selectedAssetId); }); dialog.querySelector('[data-merge-cancel]').addEventListener('click', () => { cleanup(); resolve(null); }); dialog.querySelector('.merge-dialog__overlay').addEventListener('click', () => { cleanup(); resolve(null); }); }); } async mergeGroup(sha256, assetIds, keepAssetId, button) { button.disabled = true; button.textContent = 'Merging...'; try { // Delete all assets except the one to keep const deleteIds = assetIds.filter(id => id !== keepAssetId); const deletePromises = deleteIds.map(id => fetch(`/admin/api/assets/${id}`, { method: 'DELETE', headers: { 'X-Requested-With': 'XMLHttpRequest', }, }) ); await Promise.all(deletePromises); if (window.Toast) { window.Toast.success(`Merged ${deleteIds.length} duplicates into one asset`); } // Remove group card button.closest('.duplicate-group')?.remove(); } catch (error) { console.error('Failed to merge group:', error); if (window.Toast) { window.Toast.error(`Failed to merge group: ${error.message}`); } else { alert(`Failed to merge group: ${error.message}`); } } finally { button.disabled = false; button.textContent = 'Merge Group'; } } } // Auto-initialize document.addEventListener('DOMContentLoaded', () => { if (document.querySelector('.duplicates-list')) { new DuplicateManagement(); } });