migrate/resources/views/additional/index.blade.php

359 lines
22 KiB
PHP

@extends('layouts.app')
@section('content')
<!-- Sync Frontpage Tabs Section -->
<div class="section">
<h2>Sync Frontpage Tabs</h2>
<div class="info-box" style="margin-bottom: 20px;">
<h3 style="margin-bottom: 10px; color: #1976D2; font-size: 1.1em;">Sync product frontpage tabs from Magento 1 to Magento 2</h3>
<p style="margin: 5px 0; color: #1976D2;">Scans all products by SKU in the Magento 1 database and ensures Magento 2 products have the same frontpage tabs. Missing values are added to M2; existing values are updated to match M1.</p>
<ul style="margin: 10px 0 0 20px; color: #1976D2; line-height: 1.8;">
<li><strong>Attribute:</strong> Uses the product attribute that stores frontpage tabs (default: <code>frontpage_tabs</code>). If your attribute has a different code, enter it below.</li>
<li><strong>Match by SKU:</strong> Only products that exist in both M1 and M2 are updated.</li>
<li><strong>Store scope:</strong> All store views (store_id) are synced.</li>
</ul>
@if(isset($frontpageTabsInfo['m1']) && $frontpageTabsInfo['m1'])
<p style="margin: 10px 0 0 0; color: #2e7d32; font-weight: 600;">Detected attribute: <code>{{ $frontpageTabsInfo['m1']->attribute_code }}</code> (M1: backend_type {{ $frontpageTabsInfo['m1']->backend_type }})@if(isset($frontpageTabsInfo['m2']) && $frontpageTabsInfo['m2']) M2 attribute present.@else M2 attribute not found; create it in Magento 2 first.@endif</p>
@elseif(!empty($frontpageTabsInfo['suggested_codes']))
<p style="margin: 10px 0 0 0; color: #ed6c02;">No default frontpage-tabs attribute found. Try attribute code: <code>{{ implode('</code>, <code>', $frontpageTabsInfo['suggested_codes']) }}</code></p>
@else
<p style="margin: 10px 0 0 0; color: #666;">Enter the product attribute code that stores frontpage tabs (e.g. <code>frontpage_tabs</code>).</p>
@endif
<p style="margin: 10px 0 0 0; color: #d32f2f; font-weight: 600;">⚠️ This will modify your Magento 2 database. Use dry run first if unsure.</p>
</div>
<div style="margin-top: 20px; display: flex; gap: 15px; align-items: center; flex-wrap: wrap;">
<div style="display: flex; gap: 10px; align-items: center;">
<label for="frontpageTabsAttributeCode" style="font-weight: 600; color: #333;">Attribute code:</label>
<input
type="text"
id="frontpageTabsAttributeCode"
value="{{ isset($frontpageTabsInfo['m1']) && $frontpageTabsInfo['m1'] ? $frontpageTabsInfo['m1']->attribute_code : 'frontpage_tabs' }}"
placeholder="frontpage_tabs"
style="padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 1em; min-width: 180px;"
/>
</div>
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
<input type="checkbox" id="frontpageTabsDryRun" checked />
<span style="color: #333;">Dry run (no changes)</span>
</label>
<button id="checkAttributeBtn" class="btn btn-secondary" onclick="checkAttribute()" type="button">
Check attribute
</button>
<button id="syncFrontpageTabsBtn" class="btn btn-primary" onclick="syncFrontpageTabs()">
Sync Frontpage Tabs
</button>
</div>
<div id="attributeDiagnosticResult" style="display: none; margin-top: 16px; padding: 16px; background: #f0f7ff; border: 1px solid #90caf9; border-radius: 6px;">
<h4 style="margin-bottom: 8px; color: #333;">Attribute diagnostic</h4>
<pre id="attributeDiagnosticContent" style="max-height: 280px; overflow-y: auto; font-size: 0.85em; margin: 0;"></pre>
</div>
<div id="syncFrontpageTabsResult" style="display: none; margin-top: 20px; padding: 16px; background: white; border: 1px solid #ddd; border-radius: 6px;">
<div id="syncFrontpageTabsMessage" style="font-weight: 600; margin-bottom: 10px;"></div>
<div id="syncFrontpageTabsSummary" style="margin-bottom: 10px; color: #666;"></div>
<pre id="syncFrontpageTabsLog" style="max-height: 300px; overflow-y: auto; font-size: 0.85em; background: #f5f5f5; padding: 12px; border-radius: 4px;"></pre>
</div>
</div>
<!-- Protabs Configuration Sync Section -->
<div class="section" style="margin-top: 30px;">
<h2>Sync Frontend Tabs Configuration (MGS Protabs)</h2>
<div class="info-box" style="margin-bottom: 20px;">
<h3 style="margin-bottom: 10px; color: #1976D2; font-size: 1.1em;">Find and add missing product tabs in Magento 2</h3>
<p style="margin: 5px 0; color: #1976D2;">Compares M1 product text attributes (those with actual product values) against M2's <code>mgs_protabs</code> configuration per website scope. Any attribute that has content in M1 but no corresponding Protabs entry in M2 is shown as missing.</p>
<ul style="margin: 10px 0 0 20px; color: #1976D2; line-height: 1.8;">
<li>Tabs are shown grouped by M2 website/store scope.</li>
<li>You can edit the tab title and position before adding.</li>
<li>Use "Add Selected" to insert only the tabs you check, or "Add All Missing" for a scope.</li>
</ul>
<p style="margin: 10px 0 0 0; color: #d32f2f; font-weight: 600;">⚠️ This will modify M2's <code>mgs_protabs</code> table. Flush the M2 cache after syncing.</p>
</div>
<div style="margin-bottom: 16px;">
<button id="compareProtabsBtn" class="btn btn-secondary" onclick="compareProtabs()" type="button">
Compare Tabs (M1 vs M2)
</button>
</div>
<div id="protabsCompareResult" style="display: none; margin-top: 10px;">
<div id="protabsCompareContent"></div>
</div>
<div id="protabsSyncResult" style="display: none; margin-top: 20px; padding: 16px; background: white; border: 1px solid #ddd; border-radius: 6px;">
<div id="protabsSyncMessage" style="font-weight: 600; margin-bottom: 10px;"></div>
<pre id="protabsSyncLog" style="max-height: 260px; overflow-y: auto; font-size: 0.85em; background: #f5f5f5; padding: 12px; border-radius: 4px;"></pre>
</div>
</div>
@endsection
@push('scripts')
<script>
window.additionalRoutes = {
syncFrontpageTabs: '{{ route("additional.sync-frontpage-tabs") }}',
attributeDiagnostic: '{{ route("additional.attribute-diagnostic") }}',
compareProtabs: '{{ route("additional.compare-protabs") }}',
syncProtabs: '{{ route("additional.sync-protabs") }}'
};
window.csrfToken = '{{ csrf_token() }}';
function syncFrontpageTabs() {
const btn = document.getElementById('syncFrontpageTabsBtn');
const resultEl = document.getElementById('syncFrontpageTabsResult');
const messageEl = document.getElementById('syncFrontpageTabsMessage');
const summaryEl = document.getElementById('syncFrontpageTabsSummary');
const logEl = document.getElementById('syncFrontpageTabsLog');
const attributeCode = document.getElementById('frontpageTabsAttributeCode').value.trim() || 'frontpage_tabs';
const dryRun = document.getElementById('frontpageTabsDryRun').checked;
btn.disabled = true;
resultEl.style.display = 'block';
messageEl.textContent = 'Running sync…';
summaryEl.textContent = '';
logEl.textContent = '';
const formData = new FormData();
formData.append('_token', window.csrfToken);
formData.append('attribute_code', attributeCode);
formData.append('dry_run', dryRun ? '1' : '0');
fetch(window.additionalRoutes.syncFrontpageTabs, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
}
})
.then(function (res) { return res.json(); })
.then(function (data) {
messageEl.textContent = data.message || (data.success ? 'Done.' : 'Error.');
messageEl.style.color = data.success ? '#2e7d32' : '#d32f2f';
if (data.updated !== undefined || data.skipped !== undefined || data.errors !== undefined) {
summaryEl.textContent = 'Updated: ' + (data.updated || 0) + ', Skipped: ' + (data.skipped || 0) + ', Errors: ' + (data.errors || 0);
}
if (data.diagnostic) {
summaryEl.textContent = (summaryEl.textContent ? summaryEl.textContent + ' | ' : '') + 'Diagnostic: ' + JSON.stringify(data.diagnostic);
}
if (data.log && data.log.length) {
logEl.textContent = data.log.join('\n');
} else if (data.diagnostic && !data.log) {
logEl.textContent = JSON.stringify(data.diagnostic, null, 2);
}
})
.catch(function (err) {
messageEl.textContent = 'Request failed: ' + err.message;
messageEl.style.color = '#d32f2f';
})
.finally(function () {
btn.disabled = false;
});
}
// ── Protabs Compare & Sync ────────────────────────────────────────────
function compareProtabs() {
const btn = document.getElementById('compareProtabsBtn');
const resultEl = document.getElementById('protabsCompareResult');
const contentEl = document.getElementById('protabsCompareContent');
btn.disabled = true;
btn.textContent = 'Loading…';
resultEl.style.display = 'block';
contentEl.innerHTML = '<p style="color:#666;">Comparing M1 attributes vs M2 Protabs configuration…</p>';
fetch(window.additionalRoutes.compareProtabs, {
headers: { 'Accept': 'application/json' }
})
.then(r => r.json())
.then(data => {
if (!data.success) {
contentEl.innerHTML = '<p style="color:#d32f2f;">Error: ' + (data.message || 'Unknown error') + '</p>';
return;
}
renderProtabsComparison(data.comparison, data.m1_text_attr_count);
})
.catch(err => {
contentEl.innerHTML = '<p style="color:#d32f2f;">Request failed: ' + err.message + '</p>';
})
.finally(() => {
btn.disabled = false;
btn.textContent = 'Compare Tabs (M1 vs M2)';
});
}
function renderProtabsComparison(comparison, m1Count) {
const contentEl = document.getElementById('protabsCompareContent');
let html = '<p style="color:#555; margin-bottom:16px;">M1 has <strong>' + m1Count + '</strong> text attribute(s) with product values.</p>';
comparison.forEach(scope => {
const missingCount = scope.missing.length;
const scopeColor = missingCount > 0 ? '#b71c1c' : '#2e7d32';
const badge = missingCount > 0
? '<span style="background:#d32f2f;color:#fff;border-radius:12px;padding:2px 10px;font-size:0.8em;margin-left:8px;">' + missingCount + ' missing</span>'
: '<span style="background:#2e7d32;color:#fff;border-radius:12px;padding:2px 10px;font-size:0.8em;margin-left:8px;">✓ complete</span>';
html += '<div style="border:1px solid #ddd;border-radius:6px;margin-bottom:20px;overflow:hidden;">';
html += '<div style="background:#f5f5f5;padding:12px 16px;display:flex;align-items:center;justify-content:space-between;">';
html += '<strong style="color:#333;">' + escHtml(scope.scope_label) + badge + '</strong>';
if (missingCount > 0) {
html += '<button class="btn btn-primary" style="font-size:0.85em;padding:5px 14px;" onclick="addAllMissing(' + JSON.stringify(scope.scope) + ',' + scope.scope_id + ')">Add All Missing</button>';
}
html += '</div>';
// Existing tabs table
if (scope.existing.length > 0) {
html += '<div style="padding:12px 16px 0;">';
html += '<p style="font-size:0.85em;color:#555;margin:0 0 6px;">Configured tabs (' + scope.existing.length + '):</p>';
html += '<table style="width:100%;border-collapse:collapse;font-size:0.85em;">';
html += '<thead><tr style="background:#f9f9f9;"><th style="padding:6px 10px;text-align:left;border-bottom:1px solid #eee;">Title</th><th style="padding:6px 10px;text-align:left;border-bottom:1px solid #eee;">Attribute</th><th style="padding:6px 10px;text-align:left;border-bottom:1px solid #eee;">Type</th><th style="padding:6px 10px;text-align:center;border-bottom:1px solid #eee;">Pos</th></tr></thead><tbody>';
scope.existing.forEach(tab => {
html += '<tr><td style="padding:5px 10px;border-bottom:1px solid #f0f0f0;">' + escHtml(tab.title || '') + '</td>';
html += '<td style="padding:5px 10px;border-bottom:1px solid #f0f0f0;"><code>' + escHtml(tab.value || '—') + '</code></td>';
html += '<td style="padding:5px 10px;border-bottom:1px solid #f0f0f0;">' + escHtml(tab.tab_type || '') + '</td>';
html += '<td style="padding:5px 10px;border-bottom:1px solid #f0f0f0;text-align:center;">' + (tab.position || '—') + '</td></tr>';
});
html += '</tbody></table></div>';
}
// Missing tabs
if (missingCount > 0) {
const missingId = 'missing_' + scope.scope + '_' + scope.scope_id;
html += '<div style="padding:12px 16px;background:#fff8f8;border-top:1px solid #ffcdd2;">';
html += '<p style="font-size:0.85em;color:#c62828;margin:0 0 8px;font-weight:600;">Missing from M2 Protabs (' + missingCount + ') — M1 has product values for these attributes:</p>';
html += '<table style="width:100%;border-collapse:collapse;font-size:0.85em;" id="' + missingId + '">';
html += '<thead><tr style="background:#ffebee;">';
html += '<th style="padding:6px 10px;text-align:left;border-bottom:1px solid #ffcdd2;"><input type="checkbox" onchange="toggleAll(this,\'' + missingId + '\')" title="Select all"> All</th>';
html += '<th style="padding:6px 10px;text-align:left;border-bottom:1px solid #ffcdd2;">Attribute Code</th>';
html += '<th style="padding:6px 10px;text-align:left;border-bottom:1px solid #ffcdd2;">Tab Title</th>';
html += '<th style="padding:6px 10px;text-align:center;border-bottom:1px solid #ffcdd2;">Position</th>';
html += '<th style="padding:6px 10px;text-align:right;border-bottom:1px solid #ffcdd2;">M1 Products</th>';
html += '</tr></thead><tbody>';
scope.missing.forEach((m, idx) => {
const rowId = missingId + '_' + idx;
html += '<tr>';
html += '<td style="padding:5px 10px;border-bottom:1px solid #ffebee;"><input type="checkbox" class="missing-tab-check" data-scope="' + escHtml(scope.scope) + '" data-scope-id="' + scope.scope_id + '" data-attr="' + escHtml(m.attribute_code) + '" id="chk_' + rowId + '" checked></td>';
html += '<td style="padding:5px 10px;border-bottom:1px solid #ffebee;"><code>' + escHtml(m.attribute_code) + '</code></td>';
html += '<td style="padding:5px 10px;border-bottom:1px solid #ffebee;"><input type="text" id="title_' + rowId + '" value="' + escHtml(m.suggested_title) + '" style="padding:4px 8px;border:1px solid #ddd;border-radius:3px;width:150px;" /></td>';
html += '<td style="padding:5px 10px;border-bottom:1px solid #ffebee;text-align:center;"><input type="number" id="pos_' + rowId + '" value="' + m.suggested_pos + '" style="padding:4px 6px;border:1px solid #ddd;border-radius:3px;width:60px;" /></td>';
html += '<td style="padding:5px 10px;border-bottom:1px solid #ffebee;text-align:right;color:#555;">' + m.value_count + '</td>';
html += '</tr>';
});
html += '</tbody></table>';
html += '<div style="margin-top:10px;">';
html += '<button class="btn btn-primary" style="font-size:0.85em;" onclick="addSelectedMissing(\'' + missingId + '\')">Add Selected to M2 Protabs</button>';
html += '</div></div>';
} else {
html += '<div style="padding:10px 16px;background:#f9fff9;border-top:1px solid #c8e6c9;color:#2e7d32;font-size:0.85em;">All M1 text attributes with values are configured in this scope.</div>';
}
html += '</div>'; // end scope card
});
contentEl.innerHTML = html;
}
function toggleAll(masterChk, tableId) {
document.querySelectorAll('#' + tableId + ' .missing-tab-check').forEach(chk => {
chk.checked = masterChk.checked;
});
}
function addAllMissing(scope, scopeId) {
const tableId = 'missing_' + scope + '_' + scopeId;
const checkboxes = document.querySelectorAll('#' + tableId + ' .missing-tab-check');
checkboxes.forEach(chk => { chk.checked = true; });
addSelectedMissing(tableId);
}
function addSelectedMissing(tableId) {
const checkboxes = document.querySelectorAll('#' + tableId + ' .missing-tab-check:checked');
if (checkboxes.length === 0) {
alert('No tabs selected.');
return;
}
const tabs = [];
checkboxes.forEach(chk => {
const idx = chk.id.replace('chk_', '');
const title = (document.getElementById('title_' + idx) || {}).value || chk.dataset.attr;
const pos = parseInt((document.getElementById('pos_' + idx) || {}).value || 99, 10);
tabs.push({
title: title,
tab_type: 'attribute',
value: chk.dataset.attr,
position: pos,
scope: chk.dataset.scope,
scope_id: parseInt(chk.dataset.scopeId, 10),
});
});
const resultEl = document.getElementById('protabsSyncResult');
const messageEl = document.getElementById('protabsSyncMessage');
const logEl = document.getElementById('protabsSyncLog');
resultEl.style.display = 'block';
messageEl.textContent = 'Adding ' + tabs.length + ' tab(s)…';
messageEl.style.color = '#555';
logEl.textContent = '';
fetch(window.additionalRoutes.syncProtabs, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': window.csrfToken,
'Accept': 'application/json',
},
body: JSON.stringify({ tabs }),
})
.then(r => r.json())
.then(data => {
messageEl.textContent = data.message || (data.success ? 'Done.' : 'Error.');
messageEl.style.color = data.success ? '#2e7d32' : '#d32f2f';
if (data.log && data.log.length) {
logEl.textContent = data.log.join('\n');
}
if (data.inserted > 0) {
// Re-run compare to refresh state
setTimeout(compareProtabs, 400);
}
})
.catch(err => {
messageEl.textContent = 'Request failed: ' + err.message;
messageEl.style.color = '#d32f2f';
});
}
function escHtml(str) {
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ── End Protabs ───────────────────────────────────────────────────────
function checkAttribute() {
const attributeCode = document.getElementById('frontpageTabsAttributeCode').value.trim() || 'frontpage_tabs';
const resultEl = document.getElementById('attributeDiagnosticResult');
const contentEl = document.getElementById('attributeDiagnosticContent');
resultEl.style.display = 'block';
contentEl.textContent = 'Loading…';
fetch(window.additionalRoutes.attributeDiagnostic + '?attribute_code=' + encodeURIComponent(attributeCode), {
headers: { 'Accept': 'application/json' }
})
.then(function (r) { return r.json(); })
.then(function (data) {
if (data.diagnostic) {
contentEl.textContent = JSON.stringify(data.diagnostic, null, 2);
} else {
contentEl.textContent = (data.message || 'Error') + '\n' + JSON.stringify(data, null, 2);
}
})
.catch(function (err) {
contentEl.textContent = 'Request failed: ' + err.message;
});
}
</script>
@endpush