SuperSeller3000/templates/admin/ebay/aspect_import.html.twig
Simon Kuehn d26c534c34 feat: eBay aspect import — match/create attributes from eBay taxonomy
ArticleType gains ebayCategoryId (migration 20260520080000).

New admin action "Import eBay Aspects" on ArticleType list and detail:
  - Fetches aspects via EbayTaxonomyService (cached 7d)
  - Sorts: Required → Recommended → Optional
  - Auto-matches by case-insensitive name to existing AttributeDefinitions
  - Pre-selects "Create new" for required/recommended with no match
  - Pre-selects "Skip" for optional with no match
  - Already-assigned definitions highlighted green
  - Per-row: override to Skip / Match existing / Create new
  - Type auto-detected: Select (≤30 eBay values) or String (freetext)
  - User can override type in create form
  - Required checkbox pre-checked for eBay-required aspects
  - "All → Create" / "All → Skip" bulk buttons
  - On submit: creates new AttributeDefinitions, links all to ArticleType,
    deduplicates, calls applyAttributeAssignments(), flushes

PHPStan level 9 clean throughout.

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

190 lines
8.6 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 }}
<small class="text-muted fw-normal ms-2 fs-6">Kategorie {{ categoryId }}</small>
{% endblock %}
{% block main %}
<form method="post" id="aspect-import-form">
<input type="hidden" name="_token" value="{{ csrf_token('ebay_aspect_import') }}">
{# ── 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" class="btn btn-primary btn-sm">
<i class="fa fa-check me-1"></i>Importieren
</button>
</div>
</div>
{# ── Main table ───────────────────────────────────────────────────── #}
<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 }}">
{# Aspect name #}
<td>
<span class="fw-medium">{{ aspect.name }}</span>
{% if row.alreadyAssigned %}
<span class="badge bg-success ms-1" title="Bereits zugewiesen">✓</span>
{% endif %}
</td>
{# Tier badge #}
<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>
{# eBay values preview #}
<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 }} weitere)</em>{% endif %}
</span>
{% else %}
<span class="text-muted small fst-italic">Freitext</span>
{% endif %}
</td>
{# Action selector #}
<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>
{# Attribute input (match or create) #}
<td>
{# Match: pick existing definition #}
<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 }}" {% if row.preMatchId == def.id|toString %}selected{% endif %}>
{{ def.name }} ({{ def.type.value }})
</option>
{% endfor %}
</select>
</div>
{# Create: name + type #}
<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 }} Werte)</option>
<option value="int">Int</option>
<option value="float">Float</option>
</select>
</div>
</div>
{# Skip: placeholder #}
<div class="section-skip-{{ i }}" style="display:{% if row.action == 'skip' %}block{% else %}none{% endif %};">
<span class="text-muted small">—</span>
</div>
</td>
{# Required checkbox #}
<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>
<script>
(function () {
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 => {
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 %}