diff --git a/src/Controller/ScanController.php b/src/Controller/ScanController.php index 55f6429b..271ced5d 100644 --- a/src/Controller/ScanController.php +++ b/src/Controller/ScanController.php @@ -92,7 +92,6 @@ class ScanController extends AbstractController $input = $form['input']->getData(); } - $infoModeData = null; if ($input !== null && $input !== '') { $mode = $form->isSubmitted() ? $form['mode']->getData() : null; @@ -119,7 +118,18 @@ class ScanController extends AbstractController } // Info mode fallback: render page with prefilled result - $infoModeData = $scan->getDecodedForInfoMode(); + $decoded = $scan->getDecodedForInfoMode(); + + //Try to resolve to an entity, to enhance info mode with entity-specific data + $dbEntity = $this->resultHandler->resolveEntity($scan); + $resolvedPart = $this->resultHandler->resolvePart($scan); + $openUrl = $this->resultHandler->getInfoURL($scan); + + //If no entity is found, try to create an URL for creating a new part (only for vendor codes) + $createUrl = null; + if ($dbEntity === null) { + $createUrl = $this->buildCreateUrlForScanResult($scan); + } } catch (\Throwable $e) { // Keep fallback user-friendly; avoid 500 @@ -129,7 +139,13 @@ class ScanController extends AbstractController return $this->render('label_system/scanner/scanner.html.twig', [ 'form' => $form, - 'infoModeData' => $infoModeData, + + //Info mode + 'decoded' => $decoded ?? null, + 'entity' => $dbEntity ?? null, + 'part' => $resolvedPart ?? null, + 'openUrl' => $openUrl ?? null, + 'createUrl' => $createUrl ?? null, ]); } @@ -181,51 +197,6 @@ class ScanController extends AbstractController return null; } - private function buildLocationsForPart(Part $part): array - { - $byLocationId = []; - - foreach ($part->getPartLots() as $lot) { - $loc = $lot->getStorageLocation(); - if ($loc === null) { - continue; - } - - $locId = $loc->getID(); - $qty = $lot->getAmount(); - - if (!isset($byLocationId[$locId])) { - $byLocationId[$locId] = [ - 'breadcrumb' => $this->buildStorageBreadcrumb($loc), - 'qty' => $qty, - ]; - } else { - $byLocationId[$locId]['qty'] += $qty; - } - } - - return array_values($byLocationId); - } - - private function buildStorageBreadcrumb(StorageLocation $loc): array - { - $items = []; - $cur = $loc; - - // 20 is the overflow limit in src/Entity/Base/AbstractStructuralDBElement.php line ~273 - for ($i = 0; $i < 20 && $cur !== null; $i++) { - $items[] = [ - 'name' => $cur->getName(), - 'url' => $this->generateUrl('part_list_store_location', ['id' => $cur->getID()]), - ]; - - $parent = $cur->getParent(); // inherited from AbstractStructuralDBElement - $cur = ($parent instanceof StorageLocation) ? $parent : null; - } - - return array_reverse($items); - } - /** * Provides XHR endpoint for looking up barcode information and return JSON response * @param Request $request @@ -261,53 +232,31 @@ class ScanController extends AbstractController $decoded = $scan->getDecodedForInfoMode(); - // Determine if this barcode resolves to *anything* (part, lot->part, storelocation) - $redirectUrl = null; - $targetFound = false; - try { - $redirectUrl = $this->resultHandler->getInfoURL($scan); - $targetFound = true; - } catch (EntityNotFoundException) { - } + //Try to resolve to an entity, to enhance info mode with entity-specific data + $dbEntity = $this->resultHandler->resolveEntity($scan); + $resolvedPart = $this->resultHandler->resolvePart($scan); + $openUrl = $this->resultHandler->getInfoURL($scan); - // Only resolve Part for part-like targets. Storelocation scans should remain null here. - $part = null; - $partName = null; - $partUrl = null; - $locations = []; - - if ($targetFound) { - $part = $this->resultHandler->resolvePart($scan); - - if ($part instanceof Part) { - $partName = $part->getName(); - $partUrl = $this->generateUrl('app_part_show', ['id' => $part->getID()]); - $locations = $this->buildLocationsForPart($part); - } - } - - // Create link only when NOT found (vendor codes) + //If no entity is found, try to create an URL for creating a new part (only for vendor codes) $createUrl = null; - if (!$targetFound) { + if ($dbEntity === null) { $createUrl = $this->buildCreateUrlForScanResult($scan); } // Render fragment (use openUrl for universal "Open" link) - $html = $this->renderView('label_system/scanner/augmented_result.html.twig', [ + $html = $this->renderView('label_system/scanner/_info_mode.html.twig', [ 'decoded' => $decoded, - 'found' => $targetFound, - 'openUrl' => $redirectUrl, - 'partName' => $partName, - 'partUrl' => $partUrl, - 'locations' => $locations, + 'entity' => $dbEntity, + 'part' => $resolvedPart, + 'openUrl' => $openUrl, 'createUrl' => $createUrl, ]); return new JsonResponse([ 'ok' => true, - 'found' => $targetFound, - 'redirectUrl' => $redirectUrl, // client redirects only when infoMode=false + 'found' => $openUrl !== null, // we consider the code "found", if we can at least show an info page (even if the part is not found, but we can show the decoded data and a "create" button) + 'redirectUrl' => $openUrl, // client redirects only when infoMode=false 'createUrl' => $createUrl, 'html' => $html, 'infoMode' => $infoMode, diff --git a/src/Services/LabelSystem/BarcodeScanner/BarcodeScanResultHandler.php b/src/Services/LabelSystem/BarcodeScanner/BarcodeScanResultHandler.php index 3f868cf7..372e976e 100644 --- a/src/Services/LabelSystem/BarcodeScanner/BarcodeScanResultHandler.php +++ b/src/Services/LabelSystem/BarcodeScanner/BarcodeScanResultHandler.php @@ -71,17 +71,15 @@ final readonly class BarcodeScanResultHandler * Determines the URL to which the user should be redirected, when scanning a QR code. * * @param BarcodeScanResultInterface $barcodeScan The result of the barcode scan - * @return string the URL to which should be redirected - * - * @throws EntityNotFoundException + * @return string|null the URL to which should be redirected, or null if no suitable URL could be determined for the given barcode scan result */ - public function getInfoURL(BarcodeScanResultInterface $barcodeScan): string + public function getInfoURL(BarcodeScanResultInterface $barcodeScan): ?string { //For other barcodes try to resolve the part first and then redirect to the part page $entity = $this->resolveEntity($barcodeScan); if ($entity === null) { - throw new EntityNotFoundException("No entity could be resolved for the given barcode scan result"); + return null; } if ($entity instanceof Part) { diff --git a/src/Twig/AttachmentExtension.php b/src/Twig/AttachmentExtension.php index 3d5ec611..23ab7d6e 100644 --- a/src/Twig/AttachmentExtension.php +++ b/src/Twig/AttachmentExtension.php @@ -23,7 +23,10 @@ declare(strict_types=1); namespace App\Twig; use App\Entity\Attachments\Attachment; +use App\Entity\Attachments\AttachmentContainingDBElement; +use App\Entity\Parts\Part; use App\Services\Attachments\AttachmentURLGenerator; +use App\Services\Attachments\PartPreviewGenerator; use App\Services\Misc\FAIconGenerator; use Twig\Attribute\AsTwigFunction; use Twig\Extension\AbstractExtension; @@ -31,7 +34,7 @@ use Twig\TwigFunction; final readonly class AttachmentExtension { - public function __construct(private AttachmentURLGenerator $attachmentURLGenerator, private FAIconGenerator $FAIconGenerator) + public function __construct(private AttachmentURLGenerator $attachmentURLGenerator, private FAIconGenerator $FAIconGenerator, private PartPreviewGenerator $partPreviewGenerator) { } @@ -44,6 +47,26 @@ final readonly class AttachmentExtension return $this->attachmentURLGenerator->getThumbnailURL($attachment, $filter_name); } + /** + * Returns the URL of the thumbnail of the given element. Returns null if no thumbnail is available. + * For parts, a special preview image is generated, for other entities, the master picture is used as preview (if available). + */ + #[AsTwigFunction("entity_thumbnail")] + public function entityThumbnail(AttachmentContainingDBElement $element, string $filter_name = 'thumbnail_sm'): ?string + { + if ($element instanceof Part) { + $preview_attachment = $this->partPreviewGenerator->getTablePreviewAttachment($element); + } else { // For other entities, we just use the master picture as preview, if available + $preview_attachment = $element->getMasterPictureAttachment(); + } + + if ($preview_attachment === null) { + return null; + } + + return $this->attachmentURLGenerator->getThumbnailURL($preview_attachment, $filter_name); + } + /** * Return the font-awesome icon type for the given file extension. Returns "file" if no specific icon is available. * Null is allowed for files withot extension diff --git a/templates/label_system/scanner/_info_mode.html.twig b/templates/label_system/scanner/_info_mode.html.twig new file mode 100644 index 00000000..aa72c38b --- /dev/null +++ b/templates/label_system/scanner/_info_mode.html.twig @@ -0,0 +1,119 @@ +{% import "helper.twig" as helper %} + +{% if decoded is not empty %} +
+ + {% if part %} {# Show detailed info when it is a part #} +
+
+ {% trans %}label_scanner.db_part_found{% endtrans %} + {% if openUrl %} +
+ + + +
+ {% endif %} + +
+
+
+ +
+ + +
+

{{ part.name }}

+
{{ part.description | format_markdown(true) }}
+
+
+ {% trans %}category.label{% endtrans %} + +
+
+ {{ helper.structural_entity_link(part.category) }} +
+
+ +
+
+ {% trans %}footprint.label{% endtrans %} + +
+
+ {{ helper.structural_entity_link(part.footprint) }} +
+
+ + {# Show part lots / locations #} + {% if part.partLots is not empty %} + + + + + + + + + {% for lot in part.partLots %} + + + + + {% endfor %} + +
{% trans %}part_lots.storage_location{% endtrans %} + {% trans %}part_lots.amount{% endtrans %} +
+ {% if lot.storageLocation %} + {{ helper.structural_entity_link(lot.storageLocation) }} + {% else %} + + {% endif %} + + {% if lot.instockUnknown %} + ? + {% else %} + {{ lot.amount | format_amount(part.partUnit, {'decimals': 5}) }} + {% endif %} +
+ {% else %} +
{% trans %}label_scanner.no_locations{% endtrans %}
+ {% endif %} + +
+
+
+ {% endif %} + + {% if createUrl %} +
+

{% trans %}label_scanner.part_can_be_created{% endtrans %}

+

{% trans %}label_scanner.part_can_be_created.help{% endtrans %}

+
+ {% trans %}label_scanner.part_create_btn{% endtrans %} +
+ {% endif %} + +

+ {% trans %}label_scanner.scan_result.title{% endtrans %} +

+ + {# Decoded barcode fields #} + + + {% for key, value in decoded %} + + + + + {% endfor %} + +
{{ key }}{{ value }}
+ + {# Whitespace under table and Input form fields #} +
+ +{% endif %} diff --git a/templates/label_system/scanner/augmented_result.html.twig b/templates/label_system/scanner/augmented_result.html.twig deleted file mode 100644 index ad57881e..00000000 --- a/templates/label_system/scanner/augmented_result.html.twig +++ /dev/null @@ -1,97 +0,0 @@ -{% if decoded is not empty %} -
- -
-

- {% if found and partName %} - {% trans %}label_scanner.part_info.title{% endtrans %} - {% else %} - {% trans %}label_scanner.scan_result.title{% endtrans %} - {% endif %} -

- - - {% if createUrl %} - - - - {% endif %} -
- - {% if found %} -
-
- {% if partName %} - {{ partName }} - {% else %} - {% trans %}label_scanner.target_found{% endtrans %} - {% endif %} -
- - {% if openUrl %} - - {% trans %}open{% endtrans %} - - {% endif %} -
- - {% if partName %} - {% if locations is not empty %} - - - - - - - - - {% for loc in locations %} - - - - - {% endfor %} - -
{% trans %}part_lots.storage_location{% endtrans %} - {% trans %}part_lots.amount{% endtrans %} -
- - - {% if loc.qty is not null %}{{ loc.qty }}{% else %}{% endif %} -
- {% else %} -
{% trans %}label_scanner.no_locations{% endtrans %}
- {% endif %} - {% endif %} - {% else %} -
- {% trans %}label_scanner.qr_part_no_found{% endtrans %} -
- {% endif %} - - {# Decoded barcode fields #} - - - {% for key, value in decoded %} - - - - - {% endfor %} - -
{{ key }}{{ value }}
- - {# Whitespace under table and Input form fields #} -
-
-
-
- -{% endif %} diff --git a/templates/label_system/scanner/scanner.html.twig b/templates/label_system/scanner/scanner.html.twig index afc4a2be..7275f89d 100644 --- a/templates/label_system/scanner/scanner.html.twig +++ b/templates/label_system/scanner/scanner.html.twig @@ -20,37 +20,11 @@ -
+
+ {% include "label_system/scanner/_info_mode.html.twig" %} +
{{ form_start(form, {'attr': {'id': 'scan_dialog_form'}}) }} {{ form_end(form) }} - - - {% if infoModeData %} -
-
-

{% trans %}label_scanner.decoded_info.title{% endtrans %}

- - {% if createUrl is defined and createUrl %} - - - - {% endif %} -
- - - - {% for key, value in infoModeData %} - - - - - {% endfor %} - -
{{ key }}{{ value }}
- - {% endif %} - {% endblock %} diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index f9790883..5b97f32b 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -9539,7 +9539,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g label_scanner.no_locations - Part is not stored at any locations + Part is not stored at any location. @@ -12545,5 +12545,35 @@ Buerklin-API Authentication server: Last stocktake + + + label_scanner.open + View details + + + + + label_scanner.db_part_found + Database [part] found for barcode + + + + + label_scanner.part_can_be_created + [Part] can be created + + + + + label_scanner.part_can_be_created.help + No matching [part] was found in the database, but you can create a new [part] based of this barcode. + + + + + label_scanner.part_create_btn + Create [part] from barcode + +