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 %}
+
{% trans %}label_scanner.decoded_info.title{% endtrans %}
-
- {% if createUrl is defined and createUrl %}
-
-
-
- {% endif %}
-
-
-
-
- {% for key, value in infoModeData %}
-
-
{{ key }}
-
{{ value }}
-
- {% endfor %}
-
-
-
- {% 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
+
+