2026-05-18 18:30:45 +00:00
|
|
|
{% extends '@EasyAdmin/page/content.html.twig' %}
|
|
|
|
|
|
|
|
|
|
{% block page_title %}
|
|
|
|
|
<i class="fa fa-cloud-download-alt me-2"></i>eBay Aspects importieren — {{ articleType.name }}
|
|
|
|
|
{% endblock %}
|
|
|
|
|
|
|
|
|
|
{% block main %}
|
|
|
|
|
|
2026-05-18 20:10:07 +00:00
|
|
|
{# ── Category search / picker ─────────────────────────────────────── #}
|
|
|
|
|
<div class="card mb-4">
|
|
|
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
|
|
|
<h5 class="mb-0"><i class="fa fa-search me-2"></i>eBay-Kategorie</h5>
|
|
|
|
|
{% if categoryId %}
|
|
|
|
|
<span class="badge bg-info fs-6 font-monospace">ID: {{ categoryId }}</span>
|
|
|
|
|
{% endif %}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
<form method="post" id="category-select-form">
|
|
|
|
|
<input type="hidden" name="_action" value="set-category">
|
|
|
|
|
<input type="hidden" name="categoryId" id="selected-category-id" value="{{ categoryId ?? '' }}">
|
|
|
|
|
|
|
|
|
|
<div class="position-relative">
|
|
|
|
|
<input type="text"
|
|
|
|
|
id="category-search-input"
|
|
|
|
|
class="form-control"
|
|
|
|
|
placeholder="Kategorie suchen, z.B. „Notebook", „RAM", „Switch" …"
|
|
|
|
|
value="{{ categoryId ? '(ID ' ~ categoryId ~ ' — tippen um zu ändern)' : '' }}"
|
|
|
|
|
autocomplete="off">
|
|
|
|
|
<div id="category-dropdown"
|
|
|
|
|
class="position-absolute w-100 border rounded bg-white shadow-sm"
|
|
|
|
|
style="z-index:1000;display:none;max-height:320px;overflow-y:auto;top:100%;left:0;">
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{% if not categoryId %}
|
|
|
|
|
<div class="text-muted small mt-2">
|
|
|
|
|
<i class="fa fa-info-circle me-1"></i>Kategorie wählen, um eBay-Aspekte zu laden.
|
|
|
|
|
</div>
|
|
|
|
|
{% endif %}
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{% if categoryId %}
|
2026-05-18 18:30:45 +00:00
|
|
|
|
|
|
|
|
{# ── Summary bar ──────────────────────────────────────────────────── #}
|
|
|
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
|
|
|
<div class="d-flex gap-2 align-items-center">
|
|
|
|
|
<span class="badge bg-danger fs-6">{{ counts.required }} Required</span>
|
|
|
|
|
<span class="badge bg-warning text-dark fs-6">{{ counts.recommended }} Recommended</span>
|
|
|
|
|
<span class="badge bg-secondary fs-6">{{ counts.optional }} Optional</span>
|
2026-05-18 20:10:07 +00:00
|
|
|
<span class="text-muted small ms-2">
|
|
|
|
|
Auto-matched {{ rows|filter(r => r.action == 'match')|length }} ·
|
|
|
|
|
Create {{ rows|filter(r => r.action == 'create')|length }} ·
|
|
|
|
|
Skip {{ rows|filter(r => r.action == 'skip')|length }}
|
|
|
|
|
</span>
|
2026-05-18 18:30:45 +00:00
|
|
|
</div>
|
|
|
|
|
<div class="d-flex gap-2">
|
|
|
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="btn-select-all-create">All → Create</button>
|
|
|
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="btn-select-all-skip">All → Skip</button>
|
|
|
|
|
<a href="{{ ea_url().setController('App\\Infrastructure\\Http\\Controller\\Admin\\ArticleTypeCrudController').setAction('index').generateUrl() }}"
|
|
|
|
|
class="btn btn-sm btn-outline-secondary">
|
|
|
|
|
<i class="fa fa-arrow-left me-1"></i>Abbrechen
|
|
|
|
|
</a>
|
2026-05-18 20:10:07 +00:00
|
|
|
<button type="submit" form="aspect-import-form" class="btn btn-primary btn-sm">
|
2026-05-18 18:30:45 +00:00
|
|
|
<i class="fa fa-check me-1"></i>Importieren
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-05-18 20:10:07 +00:00
|
|
|
{# ── Aspect import form ───────────────────────────────────────────── #}
|
|
|
|
|
<form method="post" id="aspect-import-form">
|
|
|
|
|
<input type="hidden" name="_token" value="{{ csrf_token('ebay_aspect_import') }}">
|
|
|
|
|
|
2026-05-18 18:30:45 +00:00
|
|
|
<table class="table table-hover align-middle" id="aspects-table">
|
|
|
|
|
<thead class="table-light">
|
|
|
|
|
<tr>
|
|
|
|
|
<th style="width:22%">eBay Aspect</th>
|
|
|
|
|
<th style="width:10%">Tier</th>
|
|
|
|
|
<th style="width:28%">eBay-Werte</th>
|
|
|
|
|
<th style="width:13%">Aktion</th>
|
|
|
|
|
<th>Attribut / Name + Typ</th>
|
|
|
|
|
<th style="width:8%" class="text-center">Pflicht?</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{% for i, row in rows %}
|
|
|
|
|
{% set aspect = row.aspect %}
|
|
|
|
|
<tr class="aspect-row{% if row.alreadyAssigned %} table-success{% endif %}" data-index="{{ i }}">
|
|
|
|
|
|
|
|
|
|
<td>
|
|
|
|
|
<span class="fw-medium">{{ aspect.name }}</span>
|
|
|
|
|
{% if row.alreadyAssigned %}
|
|
|
|
|
<span class="badge bg-success ms-1" title="Bereits zugewiesen">✓</span>
|
|
|
|
|
{% endif %}
|
|
|
|
|
</td>
|
|
|
|
|
|
|
|
|
|
<td>
|
|
|
|
|
{% if aspect.required %}
|
|
|
|
|
<span class="badge bg-danger">Required</span>
|
|
|
|
|
{% elseif aspect.usage == 'RECOMMENDED' %}
|
|
|
|
|
<span class="badge bg-warning text-dark">Recommended</span>
|
|
|
|
|
{% else %}
|
|
|
|
|
<span class="badge bg-secondary">Optional</span>
|
|
|
|
|
{% endif %}
|
|
|
|
|
</td>
|
|
|
|
|
|
|
|
|
|
<td>
|
|
|
|
|
{% if aspect.values %}
|
|
|
|
|
<span class="text-muted small">
|
2026-05-18 20:10:07 +00:00
|
|
|
{{ aspect.values|slice(0, 6)|join(', ') }}{% if aspect.values|length > 6 %} <em>(+{{ aspect.values|length - 6 }})</em>{% endif %}
|
2026-05-18 18:30:45 +00:00
|
|
|
</span>
|
|
|
|
|
{% else %}
|
|
|
|
|
<span class="text-muted small fst-italic">Freitext</span>
|
|
|
|
|
{% endif %}
|
|
|
|
|
</td>
|
|
|
|
|
|
|
|
|
|
<td>
|
|
|
|
|
<select name="aspects[{{ i }}][action]"
|
|
|
|
|
class="form-select form-select-sm aspect-action"
|
|
|
|
|
data-index="{{ i }}">
|
|
|
|
|
<option value="skip" {% if row.action == 'skip' %}selected{% endif %}>— Überspringen</option>
|
|
|
|
|
<option value="match" {% if row.action == 'match' %}selected{% endif %}>Vorhandenes verknüpfen</option>
|
|
|
|
|
<option value="create" {% if row.action == 'create' %}selected{% endif %}>Neu anlegen</option>
|
|
|
|
|
</select>
|
|
|
|
|
<input type="hidden" name="aspects[{{ i }}][ebayName]" value="{{ aspect.name }}">
|
|
|
|
|
<input type="hidden" name="aspects[{{ i }}][ebayValues]" value="{{ aspect.values|join(',') }}">
|
|
|
|
|
<input type="hidden" name="aspects[{{ i }}][ebayRequired]" value="{{ aspect.required ? '1' : '0' }}">
|
|
|
|
|
</td>
|
|
|
|
|
|
|
|
|
|
<td>
|
|
|
|
|
<div class="section-match-{{ i }}" style="display:{% if row.action == 'match' %}block{% else %}none{% endif %};">
|
|
|
|
|
<select name="aspects[{{ i }}][definitionId]" class="form-select form-select-sm">
|
|
|
|
|
{% for def in allDefs %}
|
2026-05-18 20:12:17 +00:00
|
|
|
<option value="{{ def.id.toRfc4122() }}" {% if row.preMatchId == def.id.toRfc4122() %}selected{% endif %}>
|
2026-05-18 18:30:45 +00:00
|
|
|
{{ def.name }} ({{ def.type.value }})
|
|
|
|
|
</option>
|
|
|
|
|
{% endfor %}
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="section-create-{{ i }}" style="display:{% if row.action == 'create' %}block{% else %}none{% endif %};">
|
|
|
|
|
<div class="d-flex gap-1">
|
|
|
|
|
<input type="text"
|
|
|
|
|
name="aspects[{{ i }}][name]"
|
|
|
|
|
value="{{ aspect.name }}"
|
|
|
|
|
class="form-control form-control-sm flex-grow-1"
|
|
|
|
|
placeholder="Attribut-Name">
|
|
|
|
|
<select name="aspects[{{ i }}][type]" class="form-select form-select-sm" style="width:auto;">
|
2026-05-18 20:10:07 +00:00
|
|
|
<option value="string" {% if row.suggestedType == 'string' %}selected{% endif %}>Text</option>
|
|
|
|
|
<option value="select" {% if row.suggestedType == 'select' %}selected{% endif %}>Select ({{ aspect.values|length }})</option>
|
2026-05-18 18:30:45 +00:00
|
|
|
<option value="int">Int</option>
|
|
|
|
|
<option value="float">Float</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="section-skip-{{ i }}" style="display:{% if row.action == 'skip' %}block{% else %}none{% endif %};">
|
|
|
|
|
<span class="text-muted small">—</span>
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
|
|
|
|
|
<td class="text-center">
|
|
|
|
|
<div class="section-req-{{ i }}" style="display:{% if row.action != 'skip' %}block{% else %}none{% endif %};">
|
|
|
|
|
<input type="checkbox"
|
|
|
|
|
name="aspects[{{ i }}][required]"
|
|
|
|
|
value="1"
|
|
|
|
|
class="form-check-input"
|
|
|
|
|
{% if aspect.required %}checked{% endif %}>
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
|
|
|
|
|
</tr>
|
|
|
|
|
{% endfor %}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
|
|
|
|
|
<div class="d-flex justify-content-end gap-2 mt-3">
|
|
|
|
|
<a href="{{ ea_url().setController('App\\Infrastructure\\Http\\Controller\\Admin\\ArticleTypeCrudController').setAction('index').generateUrl() }}"
|
|
|
|
|
class="btn btn-outline-secondary">Abbrechen</a>
|
|
|
|
|
<button type="submit" class="btn btn-primary">
|
|
|
|
|
<i class="fa fa-check me-1"></i>Importieren
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
</form>
|
|
|
|
|
|
2026-05-18 20:10:07 +00:00
|
|
|
{% else %}
|
|
|
|
|
|
|
|
|
|
<div class="alert alert-info">
|
|
|
|
|
<i class="fa fa-arrow-up me-2"></i>Kategorie oben suchen und auswählen, um die eBay-Aspekte zu laden.
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
2026-05-18 18:30:45 +00:00
|
|
|
<script>
|
|
|
|
|
(function () {
|
2026-05-18 20:10:07 +00:00
|
|
|
const searchInput = document.getElementById('category-search-input');
|
|
|
|
|
const dropdown = document.getElementById('category-dropdown');
|
|
|
|
|
const hiddenId = document.getElementById('selected-category-id');
|
|
|
|
|
const selectForm = document.getElementById('category-select-form');
|
|
|
|
|
const searchUrl = {{ searchUrl|json_encode|raw }};
|
|
|
|
|
|
|
|
|
|
let debounceTimer = null;
|
|
|
|
|
|
|
|
|
|
searchInput.addEventListener('focus', function () {
|
|
|
|
|
if (hiddenId.value) {
|
|
|
|
|
this.value = '';
|
|
|
|
|
hiddenId.value = '';
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
searchInput.addEventListener('input', function () {
|
|
|
|
|
clearTimeout(debounceTimer);
|
|
|
|
|
const q = this.value.trim();
|
|
|
|
|
if (q.length < 2) { hideDropdown(); return; }
|
|
|
|
|
debounceTimer = setTimeout(() => fetchSuggestions(q), 250);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.addEventListener('click', e => {
|
|
|
|
|
if (!searchInput.contains(e.target) && !dropdown.contains(e.target)) {
|
|
|
|
|
hideDropdown();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
async function fetchSuggestions(q) {
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(searchUrl + '?q=' + encodeURIComponent(q));
|
|
|
|
|
const items = await res.json();
|
|
|
|
|
renderDropdown(items);
|
|
|
|
|
} catch { hideDropdown(); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderDropdown(items) {
|
|
|
|
|
if (!items.length) { hideDropdown(); return; }
|
|
|
|
|
|
|
|
|
|
dropdown.innerHTML = items.map(item => {
|
|
|
|
|
const path = item.path ? `<div class="text-muted small">${e(item.path)}</div>` : '';
|
|
|
|
|
return `<div class="px-3 py-2 border-bottom category-option" style="cursor:pointer;"
|
|
|
|
|
data-id="${e(item.id)}" data-name="${e(item.name)}">
|
|
|
|
|
<span class="fw-medium">${e(item.name)}</span>
|
|
|
|
|
<span class="badge bg-light text-dark border ms-1">${e(item.id)}</span>
|
|
|
|
|
${path}
|
|
|
|
|
</div>`;
|
|
|
|
|
}).join('');
|
|
|
|
|
|
|
|
|
|
dropdown.querySelectorAll('.category-option').forEach(opt => {
|
|
|
|
|
opt.addEventListener('mouseenter', () => opt.style.background = '#f8f9fa');
|
|
|
|
|
opt.addEventListener('mouseleave', () => opt.style.background = '');
|
|
|
|
|
opt.addEventListener('click', () => selectCategory(opt.dataset.id, opt.dataset.name));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
dropdown.style.display = 'block';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectCategory(id, name) {
|
|
|
|
|
searchInput.value = name + ' (ID: ' + id + ')';
|
|
|
|
|
hiddenId.value = id;
|
|
|
|
|
hideDropdown();
|
|
|
|
|
selectForm.submit();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hideDropdown() { dropdown.style.display = 'none'; }
|
|
|
|
|
function e(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|
|
|
|
|
|
|
|
|
/* ── Aspect table JS ─────────────────────────────────────────── */
|
2026-05-18 18:30:45 +00:00
|
|
|
function syncRow(index, action) {
|
|
|
|
|
['match', 'create', 'skip'].forEach(s => {
|
|
|
|
|
const el = document.querySelector(`.section-${s}-${index}`);
|
|
|
|
|
if (el) el.style.display = s === action ? 'block' : 'none';
|
|
|
|
|
});
|
|
|
|
|
const req = document.querySelector(`.section-req-${index}`);
|
|
|
|
|
if (req) req.style.display = action !== 'skip' ? 'block' : 'none';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.querySelectorAll('.aspect-action').forEach(sel => {
|
2026-05-18 20:10:07 +00:00
|
|
|
sel.addEventListener('change', function () { syncRow(this.dataset.index, this.value); });
|
2026-05-18 18:30:45 +00:00
|
|
|
});
|
|
|
|
|
|
2026-05-18 20:10:07 +00:00
|
|
|
document.getElementById('btn-select-all-create')?.addEventListener('click', () => {
|
2026-05-18 18:30:45 +00:00
|
|
|
document.querySelectorAll('.aspect-action').forEach(sel => {
|
2026-05-18 20:10:07 +00:00
|
|
|
sel.value = 'create'; syncRow(sel.dataset.index, 'create');
|
2026-05-18 18:30:45 +00:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-18 20:10:07 +00:00
|
|
|
document.getElementById('btn-select-all-skip')?.addEventListener('click', () => {
|
2026-05-18 18:30:45 +00:00
|
|
|
document.querySelectorAll('.aspect-action').forEach(sel => {
|
2026-05-18 20:10:07 +00:00
|
|
|
sel.value = 'skip'; syncRow(sel.dataset.index, 'skip');
|
2026-05-18 18:30:45 +00:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
})();
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
{% endblock %}
|