SuperSeller3000/templates/admin/ebay/aspect_import.html.twig
Simon Kuehn bf1af0a0bf feat: replace JSON ebay mappings with ArticleTypeEbayMapping entity
Introduces a proper key-value table (article_type_ebay_mappings) that
explicitly maps each eBay aspect name to either an Article field
(manufacturer, modelNumber, …) or an AttributeDefinition, with a
required flag per mapping entry.

- New entity ArticleTypeEbayMapping with SOURCE_ARTICLE_FIELD / SOURCE_ATTRIBUTE
- ArticleType gains OneToMany ebayMappings collection with upsertEbayMapping()
- EbayAdapter.buildAspects() reads from the mapping table instead of implicit name-matching
- Import controller persists mappings via upsertEbayMapping() and syncs required attribute assignments
- Template shows active mappings card and article_field action option
- Migration 20260520100000 creates the new table, drops old JSON column

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 20:52:25 +00:00

337 lines
15 KiB
Twig

{% 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 %}
{# ── 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 %}
{# ── Existing mappings table ──────────────────────────────────────── #}
{% if existingMappings|length > 0 %}
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="fa fa-table me-2"></i>Aktive Mappings <span class="badge bg-secondary ms-1">{{ existingMappings|length }}</span></h5>
</div>
<div class="card-body p-0">
<table class="table table-sm mb-0">
<thead class="table-light">
<tr>
<th>eBay-Aspekt (Ziel)</th>
<th>Quelle</th>
<th class="text-center" style="width:6rem">Pflicht?</th>
</tr>
</thead>
<tbody>
{% for mapping in existingMappings %}
<tr>
<td class="fw-medium">{{ mapping.ebayAspectName }}</td>
<td class="text-muted">{{ mapping.sourceLabel }}</td>
<td class="text-center">
{% if mapping.required %}
<span class="badge bg-danger">Ja</span>
{% else %}
<span class="text-muted">—</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
{# ── 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>
<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>
</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>
<button type="submit" form="aspect-import-form" class="btn btn-primary btn-sm">
<i class="fa fa-check me-1"></i>Importieren
</button>
</div>
</div>
{# ── Aspect import form ───────────────────────────────────────────── #}
<form method="post" id="aspect-import-form">
<input type="hidden" name="_token" value="{{ csrf_token('ebay_aspect_import') }}">
<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">
{{ aspect.values|slice(0, 6)|join(', ') }}{% if aspect.values|length > 6 %} <em>(+{{ aspect.values|length - 6 }})</em>{% endif %}
</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="article_field" {% if row.action == 'article_field' %}selected{% endif %}>→ Artikelfeld</option>
<option value="match" {% if row.action == 'match' %}selected{% endif %}>Attribut verknüpfen</option>
<option value="create" {% if row.action == 'create' %}selected{% endif %}>Attribut 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-article_field-{{ i }}" style="display:{% if row.action == 'article_field' %}block{% else %}none{% endif %};">
<select name="aspects[{{ i }}][articleField]" class="form-select form-select-sm">
{% for fieldKey, fieldLabel in articleFields %}
<option value="{{ fieldKey }}" {% if row.preFieldKey == fieldKey %}selected{% endif %}>
{{ fieldLabel }}
</option>
{% endfor %}
</select>
</div>
<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 %}
<option value="{{ def.id.toRfc4122() }}" {% if row.preMatchId == def.id.toRfc4122() %}selected{% endif %}>
{{ 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;">
<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>
<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' and row.action != 'article_field' %}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>
{% 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 %}
<script>
(function () {
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
/* ── Aspect table JS ─────────────────────────────────────────── */
function syncRow(index, action) {
['article_field', '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' && action !== 'article_field') ? 'block' : 'none';
}
document.querySelectorAll('.aspect-action').forEach(sel => {
sel.addEventListener('change', function () { syncRow(this.dataset.index, this.value); });
});
document.getElementById('btn-select-all-create')?.addEventListener('click', () => {
document.querySelectorAll('.aspect-action').forEach(sel => {
sel.value = 'create'; syncRow(sel.dataset.index, 'create');
});
});
document.getElementById('btn-select-all-skip')?.addEventListener('click', () => {
document.querySelectorAll('.aspect-action').forEach(sel => {
sel.value = 'skip'; syncRow(sel.dataset.index, 'skip');
});
});
})();
</script>
{% endblock %}