feat: add category assignment column, modal, and JS to marketplace offers view

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 19:07:07 +01:00
parent 72b9de7ad2
commit f5b9f81077

View File

@@ -1,5 +1,6 @@
<?php $integrationData = is_array($integration ?? null) ? $integration : []; ?>
<?php $rows = is_array($offers ?? null) ? $offers : []; ?>
<?php $integrationId = (int) ($integrationData['id'] ?? 0); ?>
<section class="card">
<h1><?= $e($t('marketplace.offers_title', ['name' => (string) ($integrationData['name'] ?? '')])) ?></h1>
@@ -29,6 +30,7 @@
<th>SKU</th>
<th>EAN</th>
<th><?= $e($t('marketplace.fields.updated_at')) ?></th>
<th>Kategorie</th>
</tr>
</thead>
<tbody>
@@ -48,6 +50,14 @@
<td><?= $e((string) ($row['product_sku'] ?? '')) ?></td>
<td><?= $e((string) ($row['product_ean'] ?? '')) ?></td>
<td><?= $e((string) ($row['updated_at'] ?? '')) ?></td>
<td>
<button
type="button"
class="btn btn--secondary js-assign-categories"
data-integration-id="<?= $e((string) $integrationId) ?>"
data-product-id="<?= $e((string) ($row['external_product_id'] ?? '')) ?>"
>Przypisz kategorie</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
@@ -56,3 +66,212 @@
<?php endif; ?>
</section>
<!-- Modal: przypisywanie kategorii shopPRO -->
<div id="cat-modal-backdrop" class="jq-alert-modal-backdrop" style="display:none" aria-hidden="true">
<div class="jq-alert-modal" role="dialog" aria-modal="true" aria-labelledby="cat-modal-title" style="max-width:520px;width:100%">
<div class="jq-alert-modal__header">
<h3 id="cat-modal-title">Przypisz kategorie</h3>
</div>
<div class="jq-alert-modal__body" style="max-height:420px;overflow-y:auto">
<div id="cat-modal-loading">Ładowanie kategorii...</div>
<div id="cat-modal-error" class="alert alert--danger" style="display:none"></div>
<div id="cat-modal-tree" style="display:none"></div>
</div>
<div class="jq-alert-modal__footer">
<button type="button" class="btn btn--secondary" id="cat-modal-cancel">Anuluj</button>
<button type="button" class="btn btn--primary" id="cat-modal-save" style="display:none">Zapisz</button>
</div>
</div>
</div>
<script>
(function () {
'use strict';
var CSRF = <?= json_encode($csrfToken ?? '') ?>;
var backdrop = document.getElementById('cat-modal-backdrop');
var treeEl = document.getElementById('cat-modal-tree');
var loadingEl = document.getElementById('cat-modal-loading');
var errorEl = document.getElementById('cat-modal-error');
var saveBtn = document.getElementById('cat-modal-save');
var cancelBtn = document.getElementById('cat-modal-cancel');
var state = { integrationId: 0, productId: 0, cachedCategories: null, cachedIntegrationId: 0 };
// Open modal on button click
document.addEventListener('click', function (e) {
var btn = e.target.closest('.js-assign-categories');
if (!btn) return;
state.integrationId = parseInt(btn.dataset.integrationId, 10) || 0;
state.productId = parseInt(btn.dataset.productId, 10) || 0;
if (state.integrationId <= 0 || state.productId <= 0) return;
openModal();
loadData();
});
function openModal() {
backdrop.style.display = '';
backdrop.setAttribute('aria-hidden', 'false');
backdrop.classList.add('is-visible');
loadingEl.style.display = '';
treeEl.style.display = 'none';
errorEl.style.display = 'none';
saveBtn.style.display = 'none';
treeEl.innerHTML = '';
}
function closeModal() {
backdrop.classList.remove('is-visible');
backdrop.style.display = 'none';
backdrop.setAttribute('aria-hidden', 'true');
}
cancelBtn.addEventListener('click', closeModal);
backdrop.addEventListener('click', function (e) { if (e.target === backdrop) closeModal(); });
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && backdrop.style.display !== 'none') closeModal();
});
// Load categories + current product categories in parallel
function loadData() {
var iid = state.integrationId;
var pid = state.productId;
var categoriesPromise;
if (state.cachedIntegrationId === iid && state.cachedCategories !== null) {
categoriesPromise = Promise.resolve(state.cachedCategories);
} else {
categoriesPromise = fetch('/marketplace/' + iid + '/categories', {
headers: { 'Accept': 'application/json' }
})
.then(function (r) { return r.json(); })
.then(function (d) {
if (!d.ok) throw new Error(d.message || 'Błąd pobierania kategorii');
state.cachedCategories = d.categories;
state.cachedIntegrationId = iid;
return d.categories;
});
}
var currentPromise = fetch('/marketplace/' + iid + '/product/' + pid + '/categories', {
headers: { 'Accept': 'application/json' }
})
.then(function (r) { return r.json(); })
.then(function (d) {
if (!d.ok) throw new Error(d.message || 'Błąd pobierania kategorii produktu');
return d.current_category_ids || [];
});
Promise.all([categoriesPromise, currentPromise])
.then(function (res) { renderTree(res[0], res[1]); })
.catch(function (err) { showError(err.message || 'Nieznany błąd'); });
}
// Build tree from flat list
function buildTree(flat) {
var map = {}, roots = [];
flat.forEach(function (c) { map[c.id] = { id: c.id, parent_id: c.parent_id, title: c.title, children: [] }; });
flat.forEach(function (c) {
if (c.parent_id && map[c.parent_id]) {
map[c.parent_id].children.push(map[c.id]);
} else {
roots.push(map[c.id]);
}
});
return roots;
}
function renderNode(node, checked) {
var li = document.createElement('li');
li.style.cssText = 'list-style:none;padding:0';
var row = document.createElement('div');
row.style.cssText = 'display:flex;align-items:center;gap:6px;padding:3px 0';
if (node.children.length > 0) {
var toggle = document.createElement('button');
toggle.type = 'button';
toggle.textContent = '▶';
toggle.style.cssText = 'background:none;border:none;cursor:pointer;font-size:10px;padding:0 2px;color:#666';
toggle.addEventListener('click', function () {
var sub = li.querySelector('ul');
if (sub) { sub.hidden = !sub.hidden; toggle.textContent = sub.hidden ? '▶' : '▼'; }
});
row.appendChild(toggle);
} else {
var sp = document.createElement('span');
sp.style.display = 'inline-block'; sp.style.width = '16px';
row.appendChild(sp);
}
var label = document.createElement('label');
label.style.cssText = 'display:flex;align-items:center;gap:5px;cursor:pointer';
var cb = document.createElement('input');
cb.type = 'checkbox'; cb.value = String(node.id);
cb.checked = checked.indexOf(node.id) !== -1;
label.appendChild(cb);
label.appendChild(document.createTextNode(node.title));
row.appendChild(label);
li.appendChild(row);
if (node.children.length > 0) {
var ul = document.createElement('ul');
ul.style.cssText = 'padding-left:20px;margin:0';
node.children.forEach(function (ch) { ul.appendChild(renderNode(ch, checked)); });
li.appendChild(ul);
}
return li;
}
function renderTree(flat, checked) {
var roots = buildTree(flat);
var ul = document.createElement('ul');
ul.style.cssText = 'padding:0;margin:0';
if (roots.length === 0) {
treeEl.textContent = 'Brak dostępnych kategorii.';
} else {
roots.forEach(function (r) { ul.appendChild(renderNode(r, checked)); });
treeEl.appendChild(ul);
}
loadingEl.style.display = 'none';
treeEl.style.display = '';
saveBtn.style.display = '';
}
function showError(msg) {
loadingEl.style.display = 'none';
errorEl.textContent = msg;
errorEl.style.display = '';
}
// Save
saveBtn.addEventListener('click', function () {
var cbs = treeEl.querySelectorAll('input[type=checkbox]:checked');
var ids = [];
cbs.forEach(function (cb) { var id = parseInt(cb.value, 10); if (id > 0) ids.push(id); });
saveBtn.disabled = true;
saveBtn.textContent = 'Zapisuję...';
fetch('/marketplace/' + state.integrationId + '/product/' + state.productId + '/categories', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify({ _token: CSRF, category_ids: ids })
})
.then(function (r) { return r.json(); })
.then(function (d) {
saveBtn.disabled = false; saveBtn.textContent = 'Zapisz';
if (d.ok) {
closeModal();
if (window.OrderProAlerts) window.OrderProAlerts.show({ type: 'success', message: 'Kategorie zapisane.', timeout: 3000 });
} else {
if (window.OrderProAlerts) window.OrderProAlerts.show({ type: 'danger', message: d.message || 'Błąd zapisu.', timeout: 5000 });
}
})
.catch(function (err) {
saveBtn.disabled = false; saveBtn.textContent = 'Zapisz';
if (window.OrderProAlerts) window.OrderProAlerts.show({ type: 'danger', message: 'Błąd sieci: ' + err.message, timeout: 5000 });
});
});
})();
</script>