960 lines
39 KiB
JavaScript
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;
|
|
|