diff --git a/src/Infrastructure/Channel/Ebay/EbayTaxonomyService.php b/src/Infrastructure/Channel/Ebay/EbayTaxonomyService.php index 7aba7c2..002bca2 100644 --- a/src/Infrastructure/Channel/Ebay/EbayTaxonomyService.php +++ b/src/Infrastructure/Channel/Ebay/EbayTaxonomyService.php @@ -66,6 +66,45 @@ final class EbayTaxonomyService }); } + /** + * Search eBay categories by free-text query (e.g. "Notebook", "RAM", "Switch"). + * + * @return list + */ + public function getCategorySuggestions(string $query): array + { + $token = $this->oauthClient->getAccessToken(); + + $response = $this->httpClient->request( + 'GET', + $this->apiBaseUrl.'/commerce/taxonomy/v1/category_tree/'.$this->getTreeId().'/get_category_suggestions', + [ + 'headers' => [ + 'Authorization' => 'Bearer '.$token, + 'X-EBAY-C-MARKETPLACE-ID' => $this->marketplaceId, + ], + 'query' => ['q' => $query], + ], + ); + + /** @var array{categorySuggestions?: list}>} $data */ + $data = $response->toArray(); + + $results = []; + foreach ($data['categorySuggestions'] ?? [] as $suggestion) { + $ancestors = array_reverse($suggestion['categoryTreeNodeAncestors'] ?? []); + $path = implode(' › ', array_column($ancestors, 'categoryName')); + + $results[] = [ + 'id' => $suggestion['category']['categoryId'], + 'name' => $suggestion['category']['categoryName'], + 'path' => $path, + ]; + } + + return $results; + } + private function getTreeId(): string { return match ($this->marketplaceId) { diff --git a/src/Infrastructure/Http/Controller/Admin/ArticleCrudController.php b/src/Infrastructure/Http/Controller/Admin/ArticleCrudController.php index 552c1aa..9a4ad25 100644 --- a/src/Infrastructure/Http/Controller/Admin/ArticleCrudController.php +++ b/src/Infrastructure/Http/Controller/Admin/ArticleCrudController.php @@ -86,6 +86,7 @@ final class ArticleCrudController extends AbstractCrudController ) && !$this->jobRepository->hasActiveJobForArticle($a->getId())); return $actions + ->add(Crud::PAGE_INDEX, Action::DETAIL) ->add(Crud::PAGE_INDEX, $activate) ->add(Crud::PAGE_INDEX, $markDraft) ->add(Crud::PAGE_INDEX, $rerunAi) diff --git a/src/Infrastructure/Http/Controller/Admin/EbayAspectImportController.php b/src/Infrastructure/Http/Controller/Admin/EbayAspectImportController.php index 3603716..5c2be5f 100644 --- a/src/Infrastructure/Http/Controller/Admin/EbayAspectImportController.php +++ b/src/Infrastructure/Http/Controller/Admin/EbayAspectImportController.php @@ -11,13 +11,13 @@ use App\Domain\Article\Repository\ArticleTypeRepositoryInterface; use App\Infrastructure\Channel\Ebay\EbayTaxonomyService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Uid\Uuid; -#[Route('/admin/ebay/aspect-import/{id}', name: 'admin_ebay_aspect_import')] #[IsGranted('ROLE_USER')] final class EbayAspectImportController extends AbstractController { @@ -28,6 +28,25 @@ final class EbayAspectImportController extends AbstractController ) { } + #[Route('/admin/ebay/category-search', name: 'admin_ebay_category_search')] + public function searchCategories(Request $request): JsonResponse + { + $q = trim((string) $request->query->get('q', '')); + if (mb_strlen($q) < 2) { + return $this->json([]); + } + + try { + $results = $this->taxonomy->getCategorySuggestions($q); + } catch (\Throwable) { + return $this->json([]); + } + + return $this->json(array_slice($results, 0, 15)); + } + + #[Route('/admin/ebay/aspect-import/{id}', name: 'admin_ebay_aspect_import')] + #[IsGranted('ROLE_USER')] public function __invoke(string $id, Request $request): Response { $articleType = $this->articleTypeRepo->findById(Uuid::fromString($id)); @@ -35,39 +54,39 @@ final class EbayAspectImportController extends AbstractController throw $this->createNotFoundException(); } - $categoryId = $articleType->getEbayCategoryId(); - if (null === $categoryId) { - $this->addFlash('warning', 'Bitte zuerst die eBay Category ID am Artikel-Typ hinterlegen (Edit → eBay Category ID).'); - - return $this->redirectToRoute('easyadmin', [ - 'crudAction' => 'edit', - 'crudControllerFqcn' => ArticleTypeCrudController::class, - 'entityId' => $id, - ]); + // Save category selection (step 1 of the 2-step flow) + if ($request->isMethod('POST') && 'set-category' === $request->request->get('_action')) { + return $this->handleSetCategory($request, $articleType); } if ($request->isMethod('POST')) { return $this->handleImport($request, $articleType); } - $aspects = $this->taxonomy->getCategoryAspects($categoryId); - usort($aspects, static function (array $a, array $b): int { - $tierA = $a['required'] ? 0 : ('RECOMMENDED' === $a['usage'] ? 1 : 2); - $tierB = $b['required'] ? 0 : ('RECOMMENDED' === $b['usage'] ? 1 : 2); + $categoryId = $articleType->getEbayCategoryId(); - return $tierA <=> $tierB ?: strcmp($a['name'], $b['name']); - }); + $rows = []; + $allDefs = []; + $counts = ['required' => 0, 'recommended' => 0, 'optional' => 0]; - /** @var list $allDefs */ - $allDefs = $this->em->getRepository(AttributeDefinition::class)->findBy([], ['name' => 'ASC']); + if (null !== $categoryId) { + $aspects = $this->taxonomy->getCategoryAspects($categoryId); + usort($aspects, static function (array $a, array $b): int { + $tierA = $a['required'] ? 0 : ('RECOMMENDED' === $a['usage'] ? 1 : 2); + $tierB = $b['required'] ? 0 : ('RECOMMENDED' === $b['usage'] ? 1 : 2); - $rows = $this->buildRows($aspects, $allDefs, $articleType); + return $tierA <=> $tierB ?: strcmp($a['name'], $b['name']); + }); - $counts = [ - 'required' => count(array_filter($aspects, static fn (array $a) => $a['required'])), - 'recommended' => count(array_filter($aspects, static fn (array $a) => !$a['required'] && 'RECOMMENDED' === $a['usage'])), - 'optional' => count(array_filter($aspects, static fn (array $a) => !$a['required'] && 'OPTIONAL' === $a['usage'])), - ]; + /** @var list $allDefs */ + $allDefs = $this->em->getRepository(AttributeDefinition::class)->findBy([], ['name' => 'ASC']); + $rows = $this->buildRows($aspects, $allDefs, $articleType); + $counts = [ + 'required' => count(array_filter($aspects, static fn (array $a) => $a['required'])), + 'recommended' => count(array_filter($aspects, static fn (array $a) => !$a['required'] && 'RECOMMENDED' === $a['usage'])), + 'optional' => count(array_filter($aspects, static fn (array $a) => !$a['required'] && 'OPTIONAL' === $a['usage'])), + ]; + } return $this->render('admin/ebay/aspect_import.html.twig', [ 'articleType' => $articleType, @@ -75,9 +94,21 @@ final class EbayAspectImportController extends AbstractController 'allDefs' => $allDefs, 'counts' => $counts, 'categoryId' => $categoryId, + 'searchUrl' => $this->generateUrl('admin_ebay_category_search'), ]); } + private function handleSetCategory(Request $request, ArticleType $articleType): Response + { + $categoryId = trim((string) $request->request->get('categoryId', '')); + if ('' !== $categoryId) { + $articleType->setEbayCategoryId($categoryId); + $this->em->flush(); + } + + return $this->redirectToRoute('admin_ebay_aspect_import', ['id' => $articleType->getId()->toRfc4122()]); + } + private function handleImport(Request $request, ArticleType $articleType): Response { if (!$this->isCsrfTokenValid('ebay_aspect_import', (string) $request->request->get('_token'))) { diff --git a/templates/admin/ebay/aspect_import.html.twig b/templates/admin/ebay/aspect_import.html.twig index 798ec5b..43dad13 100644 --- a/templates/admin/ebay/aspect_import.html.twig +++ b/templates/admin/ebay/aspect_import.html.twig @@ -2,13 +2,46 @@ {% block page_title %} eBay Aspects importieren — {{ articleType.name }} - Kategorie {{ categoryId }} {% endblock %} {% block main %} -
- +{# ── Category search / picker ─────────────────────────────────────── #} +
+
+
eBay-Kategorie
+ {% if categoryId %} + ID: {{ categoryId }} + {% endif %} +
+
+ + + + +
+ + +
+ + {% if not categoryId %} +
+ Kategorie wählen, um eBay-Aspekte zu laden. +
+ {% endif %} + +
+
+ +{% if categoryId %} {# ── Summary bar ──────────────────────────────────────────────────── #}
@@ -16,7 +49,11 @@ {{ counts.required }} Required {{ counts.recommended }} Recommended {{ counts.optional }} Optional - 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 }} + + 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 }} +
@@ -25,13 +62,16 @@ class="btn btn-sm btn-outline-secondary"> Abbrechen -
-{# ── Main table ───────────────────────────────────────────────────── #} +{# ── Aspect import form ───────────────────────────────────────────── #} +
+ + @@ -48,7 +88,6 @@ {% set aspect = row.aspect %} - {# Aspect name #} - {# Tier badge #} - {# eBay values preview #} - {# Action selector #} - {# Attribute input (match or create) #} - {# Required checkbox #}
{{ aspect.name }} {% if row.alreadyAssigned %} @@ -56,7 +95,6 @@ {% endif %} {% if aspect.required %} Required @@ -67,18 +105,16 @@ {% endif %} {% if aspect.values %} - {{ aspect.values|slice(0, 6)|join(', ') }}{% if aspect.values|length > 6 %} (+{{ aspect.values|length - 6 }} weitere){% endif %} + {{ aspect.values|slice(0, 6)|join(', ') }}{% if aspect.values|length > 6 %} (+{{ aspect.values|length - 6 }}){% endif %} {% else %} Freitext {% endif %} - {# Match: pick existing definition #}
- - {# Create: name + type #}
- - {# Skip: placeholder #}
+{% else %} + +
+ Kategorie oben suchen und auswählen, um die eBay-Aspekte zu laden. +
+ +{% endif %} +