359 lines
22 KiB
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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
}
|
|
|
|
// ── 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
|