migrate/resources/js/products.js

960 lines
39 KiB
JavaScript

// Products page JavaScript
let routes = {};
let csrfToken = '';
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
if (window.productRoutes) {
routes = window.productRoutes;
csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
}
// Attach event listeners to buttons
const fixCategoryProductsBtn = document.getElementById('fixCategoryProductsBtn');
if (fixCategoryProductsBtn) {
fixCategoryProductsBtn.addEventListener('click', fixCategoryProducts);
}
});
function startProductMigration(dryRun) {
// Check if routes are available
if (!routes || !routes.migrateProducts) {
console.error('Product routes not available', routes);
alert('Error: Routes not initialized. Please refresh the page.');
return;
}
const button = dryRun ? document.getElementById('dryRunProductMigrationBtn') : document.getElementById('startProductMigrationBtn');
const otherButton = dryRun ? document.getElementById('startProductMigrationBtn') : document.getElementById('dryRunProductMigrationBtn');
if (!button) {
console.error('Button not found');
alert('Error: Button not found. Please refresh the page.');
return;
}
const originalText = button.textContent;
button.disabled = true;
if (otherButton) {
otherButton.disabled = true;
}
button.textContent = dryRun ? 'Running Dry Run...' : 'Migrating...';
button.style.cursor = 'not-allowed';
const logContent = document.getElementById('productMigrationLogContent');
const progressContainer = document.getElementById('migrationProgressContainer');
if (!logContent) {
console.error('Log content element not found');
button.disabled = false;
if (otherButton) {
otherButton.disabled = false;
}
button.textContent = originalText;
return;
}
// Show progress bar
if (progressContainer && !dryRun) {
progressContainer.style.display = 'block';
updateProgressBar(0, 0, 0, 0, 0, 'Starting migration...', '');
}
logContent.innerHTML = '<div class="log-entry">' + (dryRun ? 'Running dry run...' : 'Starting migration...') + '</div>';
// Generate progress key for tracking (only for non-dry-run)
let progressKey = null;
let progressInterval = null;
if (!dryRun) {
progressKey = 'product_migration_progress_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
// Start polling immediately
progressInterval = pollMigrationProgress(progressKey, button, otherButton, originalText, logContent);
}
fetch(routes.migrateProducts, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
},
body: JSON.stringify({ dry_run: dryRun, progress_key: progressKey })
})
.then(response => response.json())
.then(data => {
// Stop polling when migration completes
if (progressInterval) {
clearInterval(progressInterval);
}
// Handle final result
handleMigrationComplete(data, button, otherButton, originalText, logContent, dryRun);
})
.catch(error => {
// Stop polling on error
if (progressInterval) {
clearInterval(progressInterval);
}
button.disabled = false;
if (otherButton) {
otherButton.disabled = false;
}
button.textContent = originalText;
button.style.cursor = 'pointer';
const progressContainer = document.getElementById('migrationProgressContainer');
if (progressContainer) {
progressContainer.style.display = 'none';
}
const errorEntry = document.createElement('div');
errorEntry.className = 'log-entry error';
errorEntry.textContent = '✗ Error: ' + error.message;
logContent.appendChild(errorEntry);
});
}
function pollMigrationProgress(progressKey, button, otherButton, originalText, logContent) {
return setInterval(() => {
if (!routes.migrationProgress) {
console.error('Migration progress route not available');
return;
}
fetch(routes.migrationProgress + '?progress_key=' + encodeURIComponent(progressKey), {
method: 'GET',
headers: {
'X-CSRF-TOKEN': csrfToken
}
})
.then(response => response.json())
.then(data => {
if (data.success && data.progress) {
const progress = data.progress;
const percentage = progress.total > 0 ? Math.round((progress.current / progress.total) * 100) : 0;
updateProgressBar(
percentage,
progress.current,
progress.total,
progress.added,
progress.updated,
progress.errors,
progress.status,
progress.current_sku
);
} else if (data.success === false && data.message === 'Progress not found') {
// Progress not found yet, migration might not have started
// This is okay, just continue polling
}
})
.catch(error => {
console.error('Error polling progress:', error);
});
}, 500); // Poll every 500ms for smoother updates
}
function updateProgressBar(percentage, current, total, added, updated, errors, status, currentSku) {
const progressBar = document.getElementById('progressBar');
const progressPercentage = document.getElementById('progressPercentage');
const progressStatus = document.getElementById('progressStatus');
const currentProduct = document.getElementById('currentProduct');
const progressAdded = document.getElementById('progressAdded');
const progressUpdated = document.getElementById('progressUpdated');
const progressErrors = document.getElementById('progressErrors');
if (progressBar) {
progressBar.style.width = percentage + '%';
}
if (progressPercentage) {
progressPercentage.textContent = percentage + '%';
}
if (progressStatus) {
const statusText = status === 'running' ? 'Migrating...' :
status === 'completed' ? 'Migration completed!' :
status === 'failed' ? 'Migration failed!' : 'Starting...';
progressStatus.textContent = statusText;
}
if (currentProduct) {
currentProduct.textContent = currentSku ? `Current: ${currentSku}` : `Processing ${current} of ${total}`;
}
if (progressAdded) {
progressAdded.textContent = added;
}
if (progressUpdated) {
progressUpdated.textContent = updated;
}
if (progressErrors) {
progressErrors.textContent = errors;
}
}
function handleMigrationComplete(data, button, otherButton, originalText, logContent, dryRun) {
button.disabled = false;
if (otherButton) {
otherButton.disabled = false;
}
button.textContent = originalText;
button.style.cursor = 'pointer';
const progressContainer = document.getElementById('migrationProgressContainer');
if (progressContainer) {
if (data.success) {
updateProgressBar(100, data.added + data.updated, data.added + data.updated, data.added, data.updated, data.errors, 'completed', '');
}
}
if (data.success) {
// Clear log content and show only errors
logContent.innerHTML = '';
if (data.log && data.log.length > 0) {
// Filter to show only error logs (case-insensitive check)
const errorLogs = data.log.filter(log => {
const logUpper = log.toUpperCase();
return logUpper.includes('ERROR');
});
if (errorLogs.length > 0) {
// Show summary with error count
const summaryEntry = document.createElement('div');
summaryEntry.className = 'log-entry';
summaryEntry.style.cssText = 'font-weight: 600; margin-bottom: 10px; color: #dc3545;';
summaryEntry.textContent = `⚠️ Migration completed with ${errorLogs.length} error(s). Added: ${data.added || 0}, Updated: ${data.updated || 0}`;
logContent.appendChild(summaryEntry);
// Show each error
errorLogs.forEach(log => {
const entry = document.createElement('div');
entry.className = 'log-entry error';
entry.textContent = log;
logContent.appendChild(entry);
});
} else {
// No errors found
const noErrorsEntry = document.createElement('div');
noErrorsEntry.className = 'log-entry';
noErrorsEntry.style.cssText = 'color: #28a745; font-weight: 600;';
noErrorsEntry.textContent = `${dryRun ? 'Dry run' : 'Migration'} completed successfully! Added: ${data.added || 0}, Updated: ${data.updated || 0}, Errors: 0`;
logContent.appendChild(noErrorsEntry);
}
} else {
// No logs at all
const noLogsEntry = document.createElement('div');
noLogsEntry.className = 'log-entry';
noLogsEntry.style.cssText = 'color: #28a745; font-weight: 600;';
noLogsEntry.textContent = `${dryRun ? 'Dry run' : 'Migration'} completed! Added: ${data.added || 0}, Updated: ${data.updated || 0}, Errors: ${data.errors || 0}`;
logContent.appendChild(noLogsEntry);
}
if (data.missing_attributes && data.missing_attributes.length > 0) {
const missingSection = document.getElementById('missingAttributesSection');
const tbody = document.getElementById('missingAttributesTableBody');
tbody.innerHTML = '';
data.missing_attributes.forEach(attr => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${attr.attribute_code}</td>
<td>${attr.frontend_label || 'N/A'}</td>
<td>${attr.backend_type || 'N/A'}</td>
`;
tbody.appendChild(row);
});
missingSection.style.display = 'block';
}
const logContainer = document.getElementById('productMigrationLogContainer');
logContainer.scrollTop = logContainer.scrollHeight;
} else {
const errorEntry = document.createElement('div');
errorEntry.className = 'log-entry error';
errorEntry.textContent = '✗ ' + (dryRun ? 'Dry run' : 'Migration') + ' failed: ' + (data.message || 'Unknown error');
logContent.appendChild(errorEntry);
}
}
function deleteM2Product(productId, productName, productSku) {
if (!confirm(`Are you sure you want to delete product "${productName}" (SKU: ${productSku})?`)) {
return;
}
const url = routes.deleteProduct.replace(':id', productId);
fetch(url, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Product deleted successfully!');
location.reload();
} else {
alert('Error: ' + (data.message || 'Failed to delete product'));
}
})
.catch(error => {
alert('Error: ' + error.message);
});
}
function deleteProductsAboveM1Max() {
if (!confirm('Are you sure you want to delete all Magento 2 products with entity_id greater than the maximum Magento 1 product ID? This action cannot be undone.')) {
return;
}
const button = document.getElementById('deleteProductsAboveM1MaxBtn');
const originalText = button.textContent;
button.disabled = true;
button.textContent = 'Deleting...';
fetch(routes.deleteProductsAboveM1Max, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
}
})
.then(response => response.json())
.then(data => {
button.disabled = false;
button.textContent = originalText;
if (data.success) {
alert(`Successfully deleted ${data.deleted || 0} products.`);
location.reload();
} else {
alert('Error: ' + (data.message || 'Failed to delete products'));
}
})
.catch(error => {
button.disabled = false;
button.textContent = originalText;
alert('Error: ' + error.message);
});
}
function syncProductCategories() {
if (!confirm('Are you sure you want to sync product category assignments from Magento 1 to Magento 2?')) {
return;
}
const button = document.getElementById('syncProductCategoriesBtn');
const originalText = button.textContent;
button.disabled = true;
button.textContent = 'Syncing...';
fetch(routes.syncProductCategories, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
}
})
.then(response => response.json())
.then(data => {
button.disabled = false;
button.textContent = originalText;
if (data.success) {
alert(`Sync completed! Updated: ${data.updated || 0}, Skipped: ${data.skipped || 0}, Errors: ${data.errors || 0}`);
} else {
alert('Error: ' + (data.message || 'Sync failed'));
}
})
.catch(error => {
button.disabled = false;
button.textContent = originalText;
alert('Error: ' + error.message);
});
}
function fixCategoryProducts() {
if (!confirm('Are you sure you want to fix category products? This will add missing products to the catalog_category_product table based on their category_ids attribute in Magento 2.')) {
return;
}
const button = document.getElementById('fixCategoryProductsBtn');
const logContainer = document.getElementById('fixCategoryProductsLogContainer');
const logContent = document.getElementById('fixCategoryProductsLogContent');
if (!button) {
console.error('fixCategoryProductsBtn not found');
return;
}
if (!routes.fixCategoryProducts) {
console.error('routes.fixCategoryProducts not found', routes);
alert('Error: Route not configured. Please refresh the page.');
return;
}
const originalText = button.textContent;
button.disabled = true;
button.textContent = 'Fixing...';
logContainer.style.display = 'block';
logContent.innerHTML = '<div class="log-entry">Starting fix process...</div>';
fetch(routes.fixCategoryProducts, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
}
})
.then(response => response.json())
.then(data => {
button.disabled = false;
button.textContent = originalText;
if (data.success) {
const successEntry = document.createElement('div');
successEntry.className = 'log-entry success';
successEntry.textContent = `✓ Fix completed! Added: ${data.added || 0}, Skipped: ${data.skipped || 0}, Errors: ${data.errors || 0}`;
logContent.appendChild(successEntry);
if (data.log && data.log.length > 0) {
data.log.forEach(log => {
const entry = document.createElement('div');
entry.className = 'log-entry ' + (log.includes('ERROR') ? 'error' : (log.includes('ADDED') ? 'success' : 'info'));
entry.textContent = log;
logContent.appendChild(entry);
});
}
logContent.scrollTop = logContent.scrollHeight;
} else {
const errorEntry = document.createElement('div');
errorEntry.className = 'log-entry error';
errorEntry.textContent = '✗ Fix failed: ' + (data.message || 'Unknown error');
logContent.appendChild(errorEntry);
}
})
.catch(error => {
button.disabled = false;
button.textContent = originalText;
const errorEntry = document.createElement('div');
errorEntry.className = 'log-entry error';
errorEntry.textContent = '✗ Error: ' + error.message;
logContent.appendChild(errorEntry);
});
}
function loadM1CategoryTreeWithProducts() {
const container = document.getElementById('m1-category-products-tree-container');
if (!container) return;
container.innerHTML = '<div class="tree-loading">Loading categories and products...</div>';
fetch(routes.magento1CategoryTreeWithProducts)
.then(response => response.json())
.then(data => {
if (data.success) {
container.innerHTML = '';
if (data.tree && data.tree.length > 0) {
renderCategoryTreeWithProducts(container, data.tree);
} else {
container.innerHTML = '<div class="tree-loading">No categories found</div>';
}
} else {
container.innerHTML = `<div class="tree-error">Error: ${data.message || 'Failed to load categories'}</div>`;
}
})
.catch(error => {
container.innerHTML = `<div class="tree-error">Error: ${error.message}</div>`;
});
}
function loadM2CategoryTreeWithProducts() {
const container = document.getElementById('m2-category-products-tree-container');
if (!container) return;
container.innerHTML = '<div class="tree-loading">Loading categories and products...</div>';
fetch(routes.magento2CategoryTreeWithProducts)
.then(response => response.json())
.then(data => {
if (data.success) {
container.innerHTML = '';
if (data.tree && data.tree.length > 0) {
renderCategoryTreeWithProducts(container, data.tree);
} else {
container.innerHTML = '<div class="tree-loading">No categories found</div>';
}
} else {
container.innerHTML = `<div class="tree-error">Error: ${data.message || 'Failed to load categories'}</div>`;
}
})
.catch(error => {
container.innerHTML = `<div class="tree-error">Error: ${error.message}</div>`;
});
}
function renderCategoryTreeWithProducts(container, tree) {
tree.forEach(node => {
const nodeElement = createCategoryWithProductsNode(node);
container.appendChild(nodeElement);
});
}
function toggleNode(toggleElement) {
const nodeItem = toggleElement.parentElement;
const nodeDiv = nodeItem.parentElement;
const childrenDivs = nodeDiv.querySelectorAll('.tree-children');
if (childrenDivs.length > 0) {
let isExpanded = false;
childrenDivs.forEach(div => {
if (div.style.display !== 'none') {
isExpanded = true;
}
});
if (isExpanded) {
childrenDivs.forEach(div => {
div.style.display = 'none';
});
toggleElement.classList.remove('expanded');
toggleElement.classList.add('collapsed');
} else {
childrenDivs.forEach(div => {
div.style.display = 'block';
});
toggleElement.classList.remove('collapsed');
toggleElement.classList.add('expanded');
}
}
}
function createCategoryWithProductsNode(node) {
const nodeDiv = document.createElement('div');
nodeDiv.className = 'tree-node';
const hasChildren = node.children && node.children.length > 0;
const hasProducts = node.products && node.products.length > 0;
const productCount = node.product_count !== undefined ? node.product_count : (node.products ? node.products.length : 0);
const itemDiv = document.createElement('div');
itemDiv.className = 'tree-node-item';
const toggle = document.createElement('span');
toggle.className = (hasChildren || hasProducts) ? 'tree-toggle collapsed' : 'tree-toggle leaf';
if (hasChildren || hasProducts) {
toggle.onclick = function(e) {
e.stopPropagation();
toggleNode(this);
};
}
const label = document.createElement('div');
label.className = 'tree-label';
const labelText = document.createElement('span');
labelText.className = 'tree-label-text';
labelText.textContent = `[${node.id}] ${node.name || 'Unnamed Category'} (${productCount} products)`;
const badge = document.createElement('span');
badge.className = `tree-badge ${node.is_active ? 'active' : 'inactive'}`;
badge.textContent = node.is_active ? 'Active' : 'Inactive';
label.appendChild(labelText);
label.appendChild(badge);
itemDiv.appendChild(toggle);
itemDiv.appendChild(label);
nodeDiv.appendChild(itemDiv);
// Add products section if products exist
if (hasProducts) {
const productsDiv = document.createElement('div');
productsDiv.className = 'tree-children';
productsDiv.style.display = 'none';
const productsHeader = document.createElement('div');
productsHeader.style.cssText = 'padding: 8px 12px; font-weight: 600; color: #667eea; background: #f0f0f0; border-radius: 4px; margin: 5px 0;';
productsHeader.textContent = `Products (${node.products.length}):`;
productsDiv.appendChild(productsHeader);
node.products.forEach(product => {
const productDiv = document.createElement('div');
productDiv.style.cssText = 'padding: 6px 12px 6px 30px; font-size: 0.9em; color: #666; border-left: 2px solid #e0e0e0; margin-left: 20px;';
productDiv.textContent = `ID: ${product.id || product.product_id || 'N/A'} | SKU: ${product.sku || 'N/A'} | Name: ${product.name || 'Unnamed Product'}`;
productsDiv.appendChild(productDiv);
});
nodeDiv.appendChild(productsDiv);
}
// Add children
if (hasChildren) {
const childrenDiv = document.createElement('div');
childrenDiv.className = 'tree-children';
childrenDiv.style.display = 'none';
node.children.forEach(child => {
childrenDiv.appendChild(createCategoryWithProductsNode(child));
});
nodeDiv.appendChild(childrenDiv);
}
return nodeDiv;
}
function startProductOptionsMigration(dryRun) {
// Check if routes are available
if (!routes || !routes.migrateProductOptions) {
console.error('Product options routes not available', routes);
alert('Error: Routes not initialized. Please refresh the page.');
return;
}
const button = dryRun ? document.getElementById('dryRunProductOptionsMigrationBtn') : document.getElementById('startProductOptionsMigrationBtn');
const otherButton = dryRun ? document.getElementById('startProductOptionsMigrationBtn') : document.getElementById('dryRunProductOptionsMigrationBtn');
const logContainer = document.getElementById('productOptionsMigrationLogContainer');
const logContent = document.getElementById('productOptionsMigrationLogContent');
if (!button) {
console.error('Button not found');
alert('Error: Button not found. Please refresh the page.');
return;
}
const originalText = button.textContent;
button.disabled = true;
if (otherButton) {
otherButton.disabled = true;
}
button.textContent = dryRun ? 'Running Dry Run...' : 'Migrating...';
button.style.cursor = 'not-allowed';
logContainer.style.display = 'block';
logContent.innerHTML = '<div class="log-entry">' + (dryRun ? 'Running dry run...' : 'Starting migration...') + '</div>';
fetch(routes.migrateProductOptions, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
},
body: JSON.stringify({ dry_run: dryRun })
})
.then(response => response.json())
.then(data => {
button.disabled = false;
if (otherButton) {
otherButton.disabled = false;
}
button.textContent = originalText;
button.style.cursor = 'pointer';
if (data.success) {
const successEntry = document.createElement('div');
successEntry.className = 'log-entry success';
successEntry.textContent = `${dryRun ? 'Dry run' : 'Migration'} completed! Migrated: ${data.migrated || 0}, Skipped: ${data.skipped || 0}, Errors: ${data.errors || 0}`;
logContent.appendChild(successEntry);
if (data.log && data.log.length > 0) {
data.log.forEach(log => {
const entry = document.createElement('div');
entry.className = 'log-entry ' + (log.includes('ERROR') ? 'error' : (log.includes('WARNING') ? 'warning' : (log.includes('INFO') ? 'info' : 'success')));
entry.textContent = log;
logContent.appendChild(entry);
});
}
logContent.scrollTop = logContent.scrollHeight;
} else {
const errorEntry = document.createElement('div');
errorEntry.className = 'log-entry error';
errorEntry.textContent = '✗ ' + (dryRun ? 'Dry run' : 'Migration') + ' failed: ' + (data.message || 'Unknown error');
logContent.appendChild(errorEntry);
if (data.log && data.log.length > 0) {
data.log.forEach(log => {
const entry = document.createElement('div');
entry.className = 'log-entry ' + (log.includes('ERROR') ? 'error' : 'warning');
entry.textContent = log;
logContent.appendChild(entry);
});
}
}
})
.catch(error => {
button.disabled = false;
if (otherButton) {
otherButton.disabled = false;
}
button.textContent = originalText;
button.style.cursor = 'pointer';
const errorEntry = document.createElement('div');
errorEntry.className = 'log-entry error';
errorEntry.textContent = '✗ Error: ' + error.message;
logContent.appendChild(errorEntry);
});
}
function compareProductBySku() {
const skuInput = document.getElementById('productSkuInput');
const compareBtn = document.getElementById('compareProductBtn');
const comparisonSection = document.getElementById('productComparisonSection');
const comparisonMessage = document.getElementById('productComparisonMessage');
if (!skuInput || !compareBtn || !comparisonSection) {
console.error('Required elements not found');
alert('Error: Required elements not found. Please refresh the page.');
return;
}
const sku = skuInput.value.trim();
if (!sku) {
alert('Please enter a product SKU');
return;
}
if (!routes || !routes.compareProductBySku) {
console.error('Route not available', routes);
alert('Error: Route not configured. Please refresh the page.');
return;
}
const originalText = compareBtn.textContent;
compareBtn.disabled = true;
compareBtn.textContent = 'Comparing...';
comparisonSection.style.display = 'none';
fetch(routes.compareProductBySku, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
},
body: JSON.stringify({ sku: sku })
})
.then(response => response.json())
.then(data => {
compareBtn.disabled = false;
compareBtn.textContent = originalText;
comparisonSection.style.display = 'block';
const tablesContainer = document.getElementById('productComparisonTablesContainer');
if (!tablesContainer) {
console.error('Tables container not found');
return;
}
// Clear previous results
tablesContainer.innerHTML = '';
if (data.success) {
// Display message
comparisonMessage.textContent = data.message || 'Comparison completed';
comparisonMessage.style.background = '#d4edda';
comparisonMessage.style.color = '#155724';
comparisonMessage.style.border = '1px solid #c3e6cb';
if (data.m1_product_id) {
const infoText = document.createElement('div');
infoText.style.marginTop = '5px';
infoText.style.fontSize = '0.9em';
infoText.textContent = `M1 Product ID: ${data.m1_product_id}${data.m2_product_id ? ` | M2 Product ID: ${data.m2_product_id}` : ''}`;
comparisonMessage.appendChild(infoText);
}
// Display tables for each backend type
if (data.changes_by_table) {
const backendTypes = ['varchar', 'int', 'text', 'decimal', 'datetime'];
let hasAnyChanges = false;
backendTypes.forEach(backendType => {
const tableData = data.changes_by_table[backendType];
if (!tableData) return;
const changes = tableData.changes || [];
if (changes.length > 0) {
hasAnyChanges = true;
}
// Create table container
const tableWrapper = document.createElement('div');
tableWrapper.style.cssText = 'background: white; border: 1px solid #ddd; border-radius: 6px; padding: 20px; margin-bottom: 20px; max-height: 600px; overflow-y: auto;';
// Table title
const tableTitle = document.createElement('h4');
tableTitle.style.cssText = 'margin: 0 0 15px 0; color: #333; font-size: 1.1em;';
tableTitle.textContent = `Table: ${tableData.table_name}`;
tableWrapper.appendChild(tableTitle);
// Table info
const tableInfo = document.createElement('div');
tableInfo.style.cssText = 'margin-bottom: 15px; padding: 8px; background: #f8f9fa; border-radius: 4px; font-size: 0.9em; color: #666;';
tableInfo.innerHTML = `
<strong>M1 Table:</strong> ${tableData.m1_table}<br>
<strong>M2 Table:</strong> ${tableData.m2_table}<br>
<strong>Changes:</strong> ${changes.length}
`;
tableWrapper.appendChild(tableInfo);
// Create table
const table = document.createElement('table');
table.className = 'products-table';
table.style.width = '100%';
// Table header
const thead = document.createElement('thead');
thead.innerHTML = `
<tr>
<th>Attribute Code</th>
<th>Attribute Label</th>
<th>Store ID</th>
<th>M1 Value (To Update)</th>
<th>M2 Current Value</th>
<th>Action</th>
</tr>
`;
table.appendChild(thead);
// Table body
const tbody = document.createElement('tbody');
if (changes.length > 0) {
changes.forEach(change => {
const row = document.createElement('tr');
// Determine row style based on action
if (change.action === 'INSERT') {
row.style.backgroundColor = '#d4edda';
} else if (change.action === 'UPDATE') {
row.style.backgroundColor = '#fff3cd';
} else if (change.action === 'DELETE') {
row.style.backgroundColor = '#f8d7da';
}
// Format values for display
const m1Value = change.m1_value !== null && change.m1_value !== undefined
? (change.m1_value.toString().length > 100
? change.m1_value.toString().substring(0, 100) + '...'
: change.m1_value.toString())
: '(empty)';
const m2Value = change.m2_value !== null && change.m2_value !== undefined
? (change.m2_value.toString().length > 100
? change.m2_value.toString().substring(0, 100) + '...'
: change.m2_value.toString())
: '(empty)';
// Determine action badge color
let actionBadgeClass = '';
let actionText = change.action;
if (change.action === 'INSERT') {
actionBadgeClass = 'style="background-color: #28a745; color: white; padding: 2px 8px; border-radius: 3px; font-size: 0.85em;"';
} else if (change.action === 'UPDATE') {
actionBadgeClass = 'style="background-color: #ffc107; color: #000; padding: 2px 8px; border-radius: 3px; font-size: 0.85em;"';
} else if (change.action === 'DELETE') {
actionBadgeClass = 'style="background-color: #dc3545; color: white; padding: 2px 8px; border-radius: 3px; font-size: 0.85em;"';
}
row.innerHTML = `
<td>${change.attribute_code || 'N/A'}</td>
<td>${change.attribute_label || 'N/A'}</td>
<td>${change.store_id || 0}</td>
<td style="word-break: break-word;">${m1Value}</td>
<td style="word-break: break-word;">${m2Value}</td>
<td><span ${actionBadgeClass}>${actionText}</span></td>
`;
tbody.appendChild(row);
});
} else {
const row = document.createElement('tr');
row.innerHTML = `
<td colspan="6" style="text-align: center; padding: 20px; color: #666;">
No changes in this table. Values are identical in M1 and M2.
</td>
`;
tbody.appendChild(row);
}
table.appendChild(tbody);
tableWrapper.appendChild(table);
tablesContainer.appendChild(tableWrapper);
});
if (!hasAnyChanges) {
const noChangesMsg = document.createElement('div');
noChangesMsg.style.cssText = 'text-align: center; padding: 40px; color: #666; background: white; border: 1px solid #ddd; border-radius: 6px;';
noChangesMsg.textContent = 'No differences found. The product values are identical in M1 and M2 across all tables.';
tablesContainer.appendChild(noChangesMsg);
}
} else {
const noDataMsg = document.createElement('div');
noDataMsg.style.cssText = 'text-align: center; padding: 40px; color: #666; background: white; border: 1px solid #ddd; border-radius: 6px;';
noDataMsg.textContent = 'No comparison data available.';
tablesContainer.appendChild(noDataMsg);
}
} else {
// Error case
comparisonMessage.textContent = data.message || 'Comparison failed';
comparisonMessage.style.background = '#f8d7da';
comparisonMessage.style.color = '#721c24';
comparisonMessage.style.border = '1px solid #f5c6cb';
const errorMsg = document.createElement('div');
errorMsg.style.cssText = 'text-align: center; padding: 40px; color: #dc3545; background: white; border: 1px solid #ddd; border-radius: 6px;';
errorMsg.textContent = data.message || 'Failed to compare product';
tablesContainer.appendChild(errorMsg);
}
})
.catch(error => {
compareBtn.disabled = false;
compareBtn.textContent = originalText;
comparisonSection.style.display = 'block';
const tablesContainer = document.getElementById('productComparisonTablesContainer');
if (tablesContainer) {
tablesContainer.innerHTML = '';
}
comparisonMessage.textContent = 'Error: ' + error.message;
comparisonMessage.style.background = '#f8d7da';
comparisonMessage.style.color = '#721c24';
comparisonMessage.style.border = '1px solid #f5c6cb';
if (tablesContainer) {
const errorMsg = document.createElement('div');
errorMsg.style.cssText = 'text-align: center; padding: 40px; color: #dc3545; background: white; border: 1px solid #ddd; border-radius: 6px;';
errorMsg.textContent = 'Error: ' + error.message;
tablesContainer.appendChild(errorMsg);
}
});
}
// Add Enter key support for SKU input
document.addEventListener('DOMContentLoaded', function() {
const skuInput = document.getElementById('productSkuInput');
if (skuInput) {
skuInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
compareProductBySku();
}
});
}
});
// Make functions available globally
window.startProductMigration = startProductMigration;
window.startProductOptionsMigration = startProductOptionsMigration;
window.deleteM2Product = deleteM2Product;
window.deleteProductsAboveM1Max = deleteProductsAboveM1Max;
window.syncProductCategories = syncProductCategories;
window.fixCategoryProducts = fixCategoryProducts;
window.loadM1CategoryTreeWithProducts = loadM1CategoryTreeWithProducts;
window.loadM2CategoryTreeWithProducts = loadM2CategoryTreeWithProducts;
window.compareProductBySku = compareProductBySku;