diff --git a/src/Application/Article/PhotoService.php b/src/Application/Article/PhotoService.php index ec49aa7..cab8722 100644 --- a/src/Application/Article/PhotoService.php +++ b/src/Application/Article/PhotoService.php @@ -9,6 +9,7 @@ use App\Application\Storage\StoredFile; use App\Domain\Article\ArticlePhoto; use App\Domain\Article\Repository\ArticlePhotoRepositoryInterface; use App\Domain\Article\Repository\ArticleRepositoryInterface; +use App\Domain\Storage\Repository\StoragePathRepositoryInterface; use Symfony\Component\Uid\Uuid; final class PhotoService @@ -17,6 +18,7 @@ final class PhotoService private readonly ArticleRepositoryInterface $articleRepository, private readonly ArticlePhotoRepositoryInterface $photoRepository, private readonly StorageManagerInterface $storageManager, + private readonly StoragePathRepositoryInterface $storagePathRepository, ) { } @@ -58,6 +60,23 @@ final class PhotoService /** * @param list $orderedPhotoIds UUIDs in the desired display order */ + /** + * Attach a file that was already stored (e.g. uploaded at ingest time) to an article. + * + * @param string $storagePathId UUID of the StoragePath entity + */ + public function attachExtra(Uuid $articleId, string $storagePathId, string $filename, int $sortOrder): void + { + $article = $this->articleRepository->findById($articleId) + ?? throw new \DomainException("Article {$articleId->toRfc4122()} not found"); + + $storagePath = $this->storagePathRepository->findById(Uuid::fromString($storagePathId)) + ?? throw new \DomainException("StoragePath {$storagePathId} not found"); + + $photo = new ArticlePhoto($article, $storagePath, $filename, false, $sortOrder); + $this->photoRepository->save($photo); + } + public function reorder(Uuid $articleId, array $orderedPhotoIds): void { $photos = $this->photoRepository->findByArticle($articleId); diff --git a/src/Infrastructure/Http/Controller/Admin/ManualIngestController.php b/src/Infrastructure/Http/Controller/Admin/ManualIngestController.php index 2bf43e7..6bbd468 100644 --- a/src/Infrastructure/Http/Controller/Admin/ManualIngestController.php +++ b/src/Infrastructure/Http/Controller/Admin/ManualIngestController.php @@ -57,6 +57,28 @@ final class ManualIngestController extends AbstractController $storedPath = $stored->storagePath->resolveFilePath($stored->filename); + // Upload any extra photos and store references for DraftArticleHandler + $extraPhotos = []; + /** @var UploadedFile[] $additionalFiles */ + $additionalFiles = array_filter( + (array) $request->files->get('additional_photos', []), + static fn ($f) => $f instanceof UploadedFile, + ); + foreach ($additionalFiles as $extra) { + try { + $extraStored = $this->photoService->uploadRaw( + $extra->getRealPath(), + $extra->getClientOriginalName(), + ); + $extraPhotos[] = [ + 'storagePathId' => $extraStored->storagePath->getId()->toRfc4122(), + 'filename' => $extraStored->filename, + ]; + } catch (\RuntimeException) { + // skip photos that fail to store — don't abort the whole job + } + } + $job = new AIPipelineJob(AIPipelineJobType::Photo, [ 'inventoryNumber' => $catalogNumber, 'articleTypeId' => $articleType->getId()->toRfc4122(), @@ -64,6 +86,7 @@ final class ManualIngestController extends AbstractController 'conditionNotes' => $form->get('conditionNotes')->getData(), 'originalFilename' => $image->getClientOriginalName(), 'storedPhotoPath' => $storedPath, + 'extraPhotos' => $extraPhotos, ]); $this->jobRepository->save($job); diff --git a/src/Infrastructure/Messenger/Handler/DraftArticleHandler.php b/src/Infrastructure/Messenger/Handler/DraftArticleHandler.php index f6e96b5..e294261 100644 --- a/src/Infrastructure/Messenger/Handler/DraftArticleHandler.php +++ b/src/Infrastructure/Messenger/Handler/DraftArticleHandler.php @@ -5,8 +5,10 @@ declare(strict_types=1); namespace App\Infrastructure\Messenger\Handler; use App\Application\Article\ArticleService; +use App\Application\Article\PhotoService; use App\Domain\Article\ArticleCondition; use App\Domain\Article\ArticleStatus; +use App\Domain\Article\Repository\ArticlePhotoRepositoryInterface; use App\Domain\Article\Repository\ArticleRepositoryInterface; use App\Domain\Pipeline\Repository\AIPipelineJobRepositoryInterface; use App\Infrastructure\Messenger\Message\DraftArticleMessage; @@ -23,6 +25,8 @@ final class DraftArticleHandler private readonly ArticleRepositoryInterface $articleRepository, private readonly AIPipelineJobRepositoryInterface $jobRepository, private readonly MessageBusInterface $bus, + private readonly PhotoService $photoService, + private readonly ArticlePhotoRepositoryInterface $photoRepository, ) { } @@ -80,6 +84,21 @@ final class DraftArticleHandler $job->markCompleted(['articleId' => $article->getId()->toRfc4122()]); $this->jobRepository->save($job); + // Attach any extra photos uploaded during ingest + $existingCount = \count($this->photoRepository->findByArticle($article->getId())); + foreach ($job->getInputData()['extraPhotos'] ?? [] as $i => $photoData) { + try { + $this->photoService->attachExtra( + $article->getId(), + (string) $photoData['storagePathId'], + (string) $photoData['filename'], + $existingCount + $i, + ); + } catch (\DomainException) { + // best-effort — don't fail the whole pipeline for a missing extra photo + } + } + $this->bus->dispatch(new EbayTextMessage( jobId: $message->jobId, articleId: $article->getId()->toRfc4122(), diff --git a/templates/admin/manual_ingest.html.twig b/templates/admin/manual_ingest.html.twig index a5ef8a2..4e65b02 100644 --- a/templates/admin/manual_ingest.html.twig +++ b/templates/admin/manual_ingest.html.twig @@ -1,6 +1,6 @@ {% extends '@EasyAdmin/page/content.html.twig' %} -{% block page_title %}Ingest Article{% endblock %} +{% block page_title %}Artikel einlesen{% endblock %} {% block main %} @@ -8,109 +8,193 @@ {% endif %} +{# ── Camera Modal ─────────────────────────────────────────────────── #} + +
- {# ── Left: ingest form ─────────────────────────────────── #} + {# ── Left: form ───────────────────────────────────────────────── #}
-
-
-
Scan Nameplate
-
-
- {{ form_start(form, {'attr': {'enctype': 'multipart/form-data', 'novalidate': 'novalidate'}}) }} + {{ form_start(form, {'attr': {'enctype': 'multipart/form-data', 'id': 'ingest-form', 'novalidate': 'novalidate'}}) }} +
+
Artikelinfo
+
{{ form_label(form.articleType) }} {{ form_widget(form.articleType) }} {{ form_errors(form.articleType) }}
- -
- {{ form_label(form.condition) }} - {{ form_widget(form.condition) }} - {{ form_errors(form.condition) }} -
- -
- {{ form_label(form.image) }} -
+ + {# ── Search photo (mandatory) ─────────────────────────────── #} +
+
+
+ Erkennungs-Foto + Pflicht +
+
Typenschild / Aufkleber mit Modell & Seriennummer
+
+
+ {# Hidden actual file input #} + {{ form_widget(form.image, {'attr': {'class': 'd-none', 'id': 'search-photo-input'}}) }} + {{ form_errors(form.image) }} + + {# Preview #} + + + {# Placeholder when empty #} +
+ + Noch kein Foto ausgewählt +
+ +
+ + +
+
+
+ + {# ── Additional photos (optional) ─────────────────────────── #} +
+
+
Weitere Fotos (optional)
+
+
+ {# Hidden input that holds extra files #} + + +
+
+ Keine weiteren Fotos hinzugefügt +
+
+ +
+ + +
+
+
+ + + + {{ form_end(form) }}
- {# ── Right: AI config panel ────────────────────────────── #} + {# ── Right: AI config ─────────────────────────────────────────── #}
-
AI Configuration
+
KI-Konfiguration
- {{ aiConfig.backend }} active + {{ aiConfig.backend }} aktiv
- + - + - + - + - - - -
Vision modelVision-Modell {{ aiConfig.vision_model }}
Text modelText-Modell {{ aiConfig.text_model }}
Mistral endpointMistral Endpoint {{ aiConfig.mistral_base_url }}
Mistral API keyMistral API-Key {% if aiConfig.mistral_key_set %} - set + gesetzt {{ aiConfig.mistral_key_hint }} {% else %} - not set + nicht gesetzt {% endif %}
Ollama endpoint{{ aiConfig.ollama_base_url }}
@@ -120,68 +204,227 @@
{% endblock %}