Finish
Some checks failed
Gitea Actions / Run-Tests-On-Amd64 (push) Failing after 38m40s
Gitea Actions / Merge (push) Successful in 8m29s
Gitea Actions / Run-Tests-On-Arm64 (push) Has been cancelled

This commit is contained in:
Thomas Peterson 2026-03-11 09:21:48 +01:00
parent 4cf1203fad
commit acae4b4843
21 changed files with 573 additions and 125 deletions

View File

@ -64,10 +64,10 @@ server {
try_files $uri @sfFront;
}
# location /w2p/ {
# proxy_pass http://tp:8080/w2p/;
# proxy_temp_path /tmp/proxy;
# }
location /w2p/ {
proxy_pass http://tp:8080/w2p/;
proxy_temp_path /tmp/proxy;
}
location @sfFront { # Symfony
if ($request_method = 'OPTIONS') {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

View File

@ -33,12 +33,14 @@ const Order_List_Preview = (uploadTypeObject) => {
const Order_List_Detail = ({ uuid, basketField1, customerInfo, basketField2, pos, price, product, status, allNet, reOrder, reOrderOrder, reOrderPos, uploadTypeObject }, orderUuid) => `
<div class="px-6 py-4 bg-gray-50" style="${psc.order.get_pos_bg_color(status)}">
<div class="flex gap-4 items-start" id="row-${uuid}">
${Order_List_Preview(uploadTypeObject)}
<div class="grid grid-cols-12 gap-4 flex-1">
<div class="col-span-1">
<span class="text-xs font-semibold text-gray-700">Pos:</span>
<div class="font-medium">${pos}</div>
</div>
<div class="col-span-1">
${Order_List_Preview(uploadTypeObject)}
</div>
<div class="col-span-4">
<span class="text-xs font-semibold text-gray-700">Produkt:</span>
<div>
@ -48,7 +50,7 @@ const Order_List_Detail = ({ uuid, basketField1, customerInfo, basketField2, pos
${psc.order.get_special_product_options(product.specialProductTypeObject)}
</div>
</div>
<div class="col-span-2">
<div class="col-span-1">
<span class="text-xs font-semibold text-gray-700">Auflage:</span>
<div>${price.count}</div>
</div>
@ -56,14 +58,14 @@ const Order_List_Detail = ({ uuid, basketField1, customerInfo, basketField2, pos
<span class="text-xs font-semibold text-gray-700">Kunden Info:</span>
<div class="text-sm">${customerInfo}${(reOrder ? `<br/><strong class="text-yellow-600">Ist eine Nachbestellung</strong>` : ``)}</div>
</div>
<div class="col-span-2">
<div class="col-span-1">
<span class="text-xs font-semibold text-gray-700">Preis:</span>
<div class="text-right whitespace-nowrap">
${new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(price.allNet / 100)}<br/>
<strong>(${new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(price.allGross / 100)})</strong>
</div>
</div>
<div class="col-span-1">
<div class="col-span-2">
<span class="text-xs font-semibold text-gray-700">Status:</span>
<div class="relative inline-block text-left w-full">
<button type="button" class="inline-flex justify-between items-center w-full px-3 py-1.5 text-xs font-medium text-white bg-psc-500 hover:bg-psc-600 rounded-sm shadow-sm">

View File

@ -476,7 +476,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* datetime?: array{
* default_format?: scalar|Param|null, // Default: "Y-m-d\\TH:i:sP"
* default_deserialization_formats?: list<scalar|Param|null>,
* default_timezone?: scalar|Param|null, // Default: "UTC"
* default_timezone?: scalar|Param|null, // Default: "Europe/Berlin"
* cdata?: scalar|Param|null, // Default: true
* },
* array_collection?: array{
@ -576,7 +576,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* datetime?: array{
* default_format?: scalar|Param|null, // Default: "Y-m-d\\TH:i:sP"
* default_deserialization_formats?: list<scalar|Param|null>,
* default_timezone?: scalar|Param|null, // Default: "UTC"
* default_timezone?: scalar|Param|null, // Default: "Europe/Berlin"
* cdata?: scalar|Param|null, // Default: true
* },
* array_collection?: array{
@ -2433,7 +2433,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* length?: scalar|Param|null, // Default: 5
* width?: scalar|Param|null, // Default: 130
* height?: scalar|Param|null, // Default: 50
* font?: scalar|Param|null, // Default: "/application/src/new/vendor/gregwar/captcha-bundle/DependencyInjection/../Generator/Font/captcha.ttf"
* font?: scalar|Param|null, // Default: "/data/www/new/vendor/gregwar/captcha-bundle/DependencyInjection/../Generator/Font/captcha.ttf"
* keep_value?: scalar|Param|null, // Default: false
* charset?: scalar|Param|null, // Default: "abcdefhjkmnprstuvwxyz23456789"
* as_file?: scalar|Param|null, // Default: false

View File

@ -16,7 +16,6 @@ namespace PSC\Shop\OrderBundle\Controller\Backend;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Knp\Component\Pager\PaginatorInterface;
use Spiriit\Bundle\FormFilterBundle\Filter\FilterBuilderUpdaterInterface;
use PSC\Shop\EntityBundle\Document\Order;
use PSC\Shop\EntityBundle\Entity\Motiv;
use PSC\Shop\EntityBundle\Entity\Shop;
@ -27,16 +26,18 @@ use PSC\System\PluginBundle\Form\Chain\Section;
use PSC\System\SettingsBundle\Document\LogEntry;
use PSC\System\SettingsBundle\Service\Log;
use PSC\System\SettingsBundle\Service\Status;
use Spiriit\Bundle\FormFilterBundle\Filter\FilterBuilderUpdaterInterface;
use Symfony\Bridge\Twig\Attribute\Template;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Bridge\Twig\Attribute\Template;
use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Component\HttpFoundation\Request;
/**
* ListController fürs Backend
@ -46,9 +47,11 @@ use Symfony\Component\HttpFoundation\Request;
*/
class ListController extends AbstractController
{
public function __construct(private Log $logService)
{
}
public function __construct(
private Log $logService,
#[AutowireIterator('backend.order.list.button')]
private iterable $listButtons,
) {}
/**
* Default Seite
@ -79,18 +82,18 @@ class ListController extends AbstractController
Status $statusService,
Section $sectionService,
\PSC\Shop\OrderBundle\Service\Order $orderService,
) {
$customSections = $sectionService->get(\PSC\System\PluginBundle\Form\Interfaces\Section::Order);
$selectedShop = $shop->getSelectedShop();
$userRepository = $em->getRepository('PSC\Shop\EntityBundle\Entity\Order');
$qb = $userRepository->createQueryBuilder('orders')
$qb = $userRepository
->createQueryBuilder('orders')
->leftJoin('orders.contact', 'contact')
->leftJoin('orders.invoiceAddress', 'invoiceAddress')
->orderBy('orders.uid', 'desc');
$qbCount = $userRepository->createQueryBuilder('orders')
$qbCount = $userRepository
->createQueryBuilder('orders')
->select('count(orders.uid)')
->leftJoin('orders.contact', 'contact')
->leftJoin('orders.invoiceAddress', 'invoiceAddress')
@ -111,24 +114,28 @@ class ListController extends AbstractController
$session->set('order_sort_page', $request->get('page', 1));
}
$qb->andWhere('orders.shop = :shop_id')
->setParameter("shop_id", $selectedShop->getUid());
$qbCount->andWhere('orders.shop = :shop_id')
->setParameter("shop_id", $selectedShop->getUid());
$qb->andWhere('orders.shop = :shop_id')->setParameter('shop_id', $selectedShop->getUid());
$qbCount->andWhere('orders.shop = :shop_id')->setParameter('shop_id', $selectedShop->getUid());
$query = $qb->getQuery();
$count = $qbCount->getQuery()->getSingleScalarResult();
$query->setHint('knp_paginator.count', $count);
$pagination = $paginator->paginate($query, $request->query->getInt('page', $session->get('order_sort_page', 1)), 30, [
$pagination = $paginator->paginate(
$query,
$request->query->getInt('page', $session->get('order_sort_page', 1)),
30,
[
'defaultSortFieldName' => $session->get('order_sort_field', 'orders.uid'),
'defaultSortDirection' => $session->get('order_sort_direction', 'desc'),
'distinct' => false
]);
'distinct' => false,
],
);
return array(
'pagination' => $pagination,
'orderStatuse' => $statusService,
'orderService' => $orderService,
'form' => $form->createView(),
'customSections' => $customSections
'customSections' => $customSections,
'customListButtons' => $this->listButtons,
);
}
@ -147,8 +154,14 @@ class ListController extends AbstractController
*/
#[Route(path: '/list/switchstatus/{order}/{status}', name: 'psc_shop_order_backend_list_switchstatus')]
#[Template('@PSCShopOrder/backend/list/switchstatus.html.twig')]
public function switchStatusAction(\PSC\System\SettingsBundle\Service\Shop $shopService, SessionInterface $session, EntityManagerInterface $emService, \PSC\Shop\QueueBundle\Service\Event\Manager $eventManagerService, $order = "", $status = 10)
{
public function switchStatusAction(
\PSC\System\SettingsBundle\Service\Shop $shopService,
SessionInterface $session,
EntityManagerInterface $emService,
\PSC\Shop\QueueBundle\Service\Event\Manager $eventManagerService,
$order = '',
$status = 10,
) {
$selectedShop = $shopService->getSelectedShop();
/**
* @var \PSC\Shop\EntityBundle\Entity\Order $order
@ -159,7 +172,14 @@ class ListController extends AbstractController
$order->setStatus($status);
$emService->persist($order);
$emService->flush();
$this->logService->createLogEntry($selectedShop, $this->getUser(), LogEntry::INFO, PSCShopOrderBundle::class, $order->getUuid(), "Order Status In List Changed To: ". $order->getStatus());
$this->logService->createLogEntry(
$selectedShop,
$this->getUser(),
LogEntry::INFO,
PSCShopOrderBundle::class,
$order->getUuid(),
'Order Status In List Changed To: ' . $order->getStatus(),
);
$session->getFlashBag()->add('success', 'Status erfolgreich gesetzt');
$notify = new \PSC\Shop\QueueBundle\Event\Order\Status\Change();
$notify->setShop($selectedShop->getUID());
@ -185,8 +205,14 @@ class ListController extends AbstractController
*/
#[Route(path: '/list/switchstatuspos/{position}/{status}', name: 'psc_shop_order_backend_list_switchstatus_pos')]
#[Template('@PSCShopOrder/backend/list/switchstatuspos.html.twig')]
public function switchStatusPosAction(\PSC\System\SettingsBundle\Service\Shop $shopService, SessionInterface $session, EntityManagerInterface $emService, \PSC\Shop\QueueBundle\Service\Event\Manager $eventManagerService, $position = "", $status = 10)
{
public function switchStatusPosAction(
\PSC\System\SettingsBundle\Service\Shop $shopService,
SessionInterface $session,
EntityManagerInterface $emService,
\PSC\Shop\QueueBundle\Service\Event\Manager $eventManagerService,
$position = '',
$status = 10,
) {
$selectedShop = $shopService->getSelectedShop();
/**
* @var \PSC\Shop\EntityBundle\Entity\Orderpos $orderPos
@ -197,7 +223,14 @@ class ListController extends AbstractController
$orderPos->setStatus($status);
$emService->persist($orderPos);
$emService->flush();
$this->logService->createLogEntry($selectedShop, $this->getUser(), LogEntry::INFO, PSCShopOrderBundle::class, $orderPos->getUuid(), "Orderpos In List Status Changed To: " . $orderPos->getStatus());
$this->logService->createLogEntry(
$selectedShop,
$this->getUser(),
LogEntry::INFO,
PSCShopOrderBundle::class,
$orderPos->getUuid(),
'Orderpos In List Status Changed To: ' . $orderPos->getStatus(),
);
$session->getFlashBag()->add('success', 'Status erfolgreich gesetzt');
$notify = new \PSC\Shop\QueueBundle\Event\Position\Status\Change();
$notify->setShop($selectedShop->getUID());

View File

@ -13,19 +13,26 @@
namespace PSC\Shop\OrderBundle\Controller\Backend;
use Doctrine\ODM\MongoDB\DocumentManager;
use Doctrine\ORM\EntityManagerInterface;
use PSC\Shop\EntityBundle\Entity\Motiv;
use PSC\Shop\EntityBundle\Entity\Orderpos;
use PSC\Shop\EntityBundle\Entity\Upload;
use PSC\Shop\MediaBundle\Helper\Transformer\PdfTransformer;
use PSC\Shop\OrderBundle\Form\Backend\Upload\DeleteType;
use PSC\Shop\ProductBundle\Model\Upload\Setting;
use Ramsey\Uuid\Uuid;
use Symfony\Bridge\Twig\Attribute\Template;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\File\UploadedFile;
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\Core\SecurityContext;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Component\Serializer\SerializerInterface;
/**
* UploadController fürs Backend
@ -35,6 +42,29 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
*/
class UploadController extends AbstractController
{
public function __construct(
private readonly DocumentManager $documentManager,
private readonly SerializerInterface $serializer,
) {}
#[IsGranted('ROLE_ORDER_VIEW')]
#[Route(path: '/upload/center-modal/delete/{uploadUuid}', name: 'psc_shop_order_backend_upload_center_modal_delete', methods: ['POST'])]
public function centerModalDeleteAction(string $uploadUuid, Request $request, EntityManagerInterface $entityManager): Response
{
$upload = $entityManager->getRepository(Upload::class)->findOneBy(['uuid' => $uploadUuid]);
if (!$upload) {
return new Response('Upload nicht gefunden', 404);
}
$posUuid = $upload->getOrderPos()->getUuid();
$entityManager->remove($upload);
$entityManager->flush();
return $this->redirectToRoute('psc_shop_order_backend_upload_center_modal', ['pos' => $posUuid]);
}
#[IsGranted('ROLE_ORDER_VIEW')]
#[Route(path: '/upload/preview', name: 'psc_shop_order_backend_upload_preview', methods: ['GET'])]
public function previewAction(Request $request, PdfTransformer $pdfTransformer): BinaryFileResponse
@ -82,6 +112,80 @@ class UploadController extends AbstractController
return $response;
}
#[IsGranted('ROLE_ORDER_VIEW')]
#[Route(path: '/upload/center-modal', name: 'psc_shop_order_backend_upload_center_modal', methods: ['GET', 'POST'])]
public function centerModalAction(Request $request, EntityManagerInterface $entityManager): Response
{
$posUuid = $request->query->get('pos', '');
/** @var Orderpos|null $orderpos */
$orderpos = $entityManager->getRepository(Orderpos::class)->findOneBy(['uuid' => $posUuid]);
if (!$orderpos) {
return new Response('Position nicht gefunden', 404);
}
// Load upload areas defined on the product
$uploadAreas = [];
$product = $orderpos->getProduct();
if ($product) {
$productDoc = $this->documentManager
->getRepository(\PSC\Shop\EntityBundle\Document\Product::class)
->findOneBy(['uid' => $product->getUID()]);
if ($productDoc) {
$raw = $productDoc->getPluginSettingModule('uploads', 'config');
if ($raw) {
/** @var Setting $setting */
$setting = $this->serializer->deserialize($raw, Setting::class, 'json');
$uploadAreas = $setting->uploads;
}
}
}
$error = null;
if ($request->isMethod('POST')) {
$file = $request->files->get('file');
$typ = trim($request->request->get('typ', ''));
if (!$file) {
$error = 'Bitte eine Datei auswählen.';
} else {
$shopUuid = $orderpos->getOrder()->getShop()->getUuid();
$uploadDir = $this->getParameter('kernel.project_dir') . '/web/uploads/' . $shopUuid . '/article/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$originalName = $file->getClientOriginalName();
$file->move($uploadDir, $originalName);
$upload = new Upload();
$upload->setUuid(Uuid::uuid4()->toString());
$upload->setOrderPos($orderpos);
$upload->setName($originalName);
$upload->setPath($originalName);
$upload->setTyp($typ);
$upload->setExport(false);
$upload->setCreated(new \DateTime());
$upload->setUpdated(new \DateTime());
$entityManager->persist($upload);
$entityManager->flush();
return $this->redirectToRoute('psc_shop_order_backend_upload_center_modal', ['pos' => $posUuid]);
}
}
return $this->render('@PSCShopOrder/backend/upload/center_modal.html.twig', [
'orderpos' => $orderpos,
'uploadAreas' => $uploadAreas,
'error' => $error,
]);
}
/**
* Delete Seite
*

View File

@ -157,6 +157,10 @@
</svg>
Details
</a>
{% for customListButton in customListButtons %}
{{ customListButton.render(orderObj)|raw }}
{% endfor %}
</div>
</div>
</td>

View File

@ -0,0 +1,114 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Upload Center</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-white p-4 text-sm font-sans">
{% if error %}
<div class="mb-4 px-4 py-3 bg-red-50 border border-red-200 text-red-700 rounded-lg text-sm">
{{ error }}
</div>
{% endif %}
{# Vorhandene Uploads #}
{% set uploads = orderpos.uploads %}
{% if uploads|length > 0 %}
<div class="mb-6">
<h3 class="text-sm font-semibold text-gray-700 mb-2">Vorhandene Dateien</h3>
<ul class="divide-y divide-gray-100 border border-gray-200 rounded-lg overflow-hidden">
{% for upload in uploads %}
<li class="flex items-center justify-between px-4 py-3 bg-white hover:bg-gray-50">
<div class="flex items-center gap-3 min-w-0">
<svg class="w-5 h-5 text-gray-400 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
<span class="truncate text-gray-700">{{ upload.name }}</span>
{% if upload.typ %}
<span class="shrink-0 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-orange-100 text-orange-700">
{{ upload.typ }}
</span>
{% endif %}
</div>
<div class="ml-4 flex items-center gap-3 shrink-0">
<a href="/apps/{{ upload.path }}"
target="_blank"
class="text-blue-600 hover:underline flex items-center gap-1">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
</svg>
Download
</a>
<form method="POST"
action="{{ path('psc_shop_order_backend_upload_center_modal_delete', {uploadUuid: upload.uuid}) }}"
onsubmit="return confirm('Datei wirklich löschen?')">
<button type="submit"
class="text-red-500 hover:text-red-700 flex items-center gap-1">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
Löschen
</button>
</form>
</div>
</li>
{% endfor %}
</ul>
</div>
{% else %}
<p class="mb-6 text-sm text-gray-500 text-center py-4">Noch keine Dateien hochgeladen.</p>
{% endif %}
{# Upload-Formular #}
<div class="border border-gray-200 rounded-lg p-4">
<h3 class="text-sm font-semibold text-gray-700 mb-3">Neue Datei hochladen</h3>
<form method="POST" enctype="multipart/form-data" class="space-y-3">
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">Bereich</label>
{% if uploadAreas|length > 0 %}
<select name="typ"
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-orange-400 focus:border-transparent bg-white">
<option value="">— Bitte wählen —</option>
{% for area in uploadAreas %}
<option value="{{ area.id }}">{{ area.name }}</option>
{% endfor %}
</select>
{% else %}
<input type="text"
name="typ"
placeholder="Bereich eingeben …"
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-orange-400 focus:border-transparent">
<p class="mt-1 text-xs text-gray-400">Keine Bereiche am Produkt konfiguriert.</p>
{% endif %}
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">Datei</label>
<input type="file"
name="file"
required
class="block w-full text-sm text-gray-500
file:mr-3 file:py-1.5 file:px-3
file:rounded file:border-0
file:text-sm file:font-medium
file:bg-orange-50 file:text-orange-700
hover:file:bg-orange-100 cursor-pointer">
</div>
<button type="submit"
class="inline-flex items-center gap-2 px-4 py-2 bg-orange-500 text-white text-sm font-medium rounded-md hover:bg-orange-600 transition-colors">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/>
</svg>
Hochladen
</button>
</form>
</div>
</body>
</html>

View File

@ -29,7 +29,7 @@ class CenterUpload implements IUploadOption
public function getContentUrl(): ?string
{
return null;
return '/apps/backend/order/upload/center-modal';
}
public function isEnabled(ProductEntity $entity, ?ProductDoc $document): bool

View File

@ -29,8 +29,12 @@ interface Section
public const Shop = 9;
public const Order = 10;
public const OrderPositionDetail = 11;
public function getTitle();
public function getId();
public function getModule();
public function getController();
}

View File

@ -41,6 +41,7 @@ class Preview
array $ocrMarkers,
array $configData = [],
array $currentFile = [],
array $tab = [],
): void {
if (empty($ocrMarkers)) {
return;
@ -78,7 +79,9 @@ class Preview
// Draw text with configurable font size
$pdf->SetFont('helvetica', 'B', $size); // size is font size in pt
$pdf->SetTextColor(0, 0, 0);
$pdf->Text($x, $y, $text);
if (isset($tab['tabNumber'])) {
$pdf->Text($x, $y, $text . ' Tab: ' . $tab['tabNumber']);
}
// Restore transformation
$pdf->StopTransform();
@ -167,7 +170,7 @@ class Preview
});
$tab = array_shift($tab);
// Render OCR markers on this page
$this->renderOcrMarkers($pdf, $configData['ocrMarkers'] ?? [], $configData, $file);
$this->renderOcrMarkers($pdf, $configData['ocrMarkers'] ?? [], $configData, $file, $tab);
// Draw colored background rectangle
$bgColor = $this->getBackgroundColor($file['color']);

View File

@ -48,21 +48,13 @@ class Render implements QueueInterface, ConfigurableElementInterface
return 'LaufkartenLayouter Renderer';
}
public function getForm(FormBuilderInterface $builder, $form_options, EventInterface $event): void
{
}
public function getForm(FormBuilderInterface $builder, $form_options, EventInterface $event): void {}
public function injectDocument(Form $form, EventInterface $event, Queue $objQueue): void
{
}
public function injectDocument(Form $form, EventInterface $event, Queue $objQueue): void {}
public function setFormData(Form $form, EventInterface $event, Queue $queueObj): void
{
}
public function setFormData(Form $form, EventInterface $event, Queue $queueObj): void {}
public function getTemplate(): void
{
}
public function getTemplate(): void {}
public function execute(EventInterface $event, Queue $doc): bool
{
@ -154,7 +146,7 @@ class Render implements QueueInterface, ConfigurableElementInterface
$configData['contentAreaHeight'],
false,
);
$this->renderOcrMarkers($pdf, $configData['ocrMarkers'] ?? []);
$this->renderOcrMarkers($pdf, $configData['ocrMarkers'] ?? [], $tab);
$this->drawTab($pdf, $tab, $tab['x'], $tab['y'], $file);
// Page 2 (back, mirrored)
@ -168,7 +160,7 @@ class Render implements QueueInterface, ConfigurableElementInterface
$configData['contentAreaHeight'],
false,
);
$this->renderOcrMarkers($pdf, $configData['ocrMarkers'] ?? []);
$this->renderOcrMarkers($pdf, $configData['ocrMarkers'] ?? [], $tab);
$xPos = $configData['width'] - $tab['width'] - $tab['x'];
$this->drawTab($pdf, $tab, $xPos - 1, $tab['y'] - 1, $file, $tab['width'] + 2, $tab['height'] + 1);
} else {
@ -182,7 +174,7 @@ class Render implements QueueInterface, ConfigurableElementInterface
$configData['contentAreaHeight'],
false,
);
$this->renderOcrMarkers($pdf, $configData['ocrMarkers'] ?? []);
$this->renderOcrMarkers($pdf, $configData['ocrMarkers'] ?? [], $tab);
if ($frontSide) {
$this->drawTab($pdf, $tab, $tab['x'], $tab['y'], $file);
@ -245,7 +237,7 @@ class Render implements QueueInterface, ConfigurableElementInterface
);
}
private function renderOcrMarkers(Fpdi $pdf, array $ocrMarkers): void
private function renderOcrMarkers(Fpdi $pdf, array $ocrMarkers, array $tab = []): void
{
if (empty($ocrMarkers)) {
return;
@ -272,7 +264,7 @@ class Render implements QueueInterface, ConfigurableElementInterface
$pdf->Rotate(90, $x, $y);
$pdf->SetFont('helvetica', 'B', $size);
$pdf->SetTextColor(0, 0, 0);
$pdf->Text($x, $y, $marker['text'] ?? '');
$pdf->Text($x, $y, $marker['text'] ?? '' . ' Tab: ' . $tab['tabNumber']);
$pdf->StopTransform();
break;

View File

@ -0,0 +1,36 @@
<?php
namespace Plugin\System\PSC\Invoice\Button;
use PSC\Shop\OrderBundle\Model\Base;
use PSC\Shop\OrderBundle\Model\Order\Position;
use PSC\System\PluginBundle\Form\Section;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
#[AutoconfigureTag('backend.order.list.button')]
class OrderListButton
{
public function __construct(
private UrlGeneratorInterface $urlGenerator,
) {}
public function render(Base $order)
{
$url = $this->urlGenerator->generate('psc_backend_invoice_index_create') . '#/' . $order->getUuid();
return (
'<a href="'
. $url
. '#/'
. $order->getUuid()
. '" target="_blank" title="In Invoice bearbeiten"'
. ' class="inline-flex items-center justify-center gap-1 px-2.5 py-1.5 rounded-md text-xs font-medium text-white bg-orange-500 hover:bg-orange-600 transition-colors shadow-sm">'
. '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">'
. '<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" />'
. '</svg>'
. 'Bearbeiten'
. '</a>'
);
}
}

View File

@ -19,7 +19,7 @@ const ItemsComponent = ({positions, delPos, shop, changePos}) => {
<th className="text-right py-4 px-4 text-xs font-bold uppercase tracking-wider text-[#EA641B] dark:text-orange-300">Netto</th>
<th className="text-right py-4 px-4 text-xs font-bold uppercase tracking-wider text-[#EA641B] dark:text-orange-300">MwSt</th>
<th className="text-right py-4 px-4 text-xs font-bold uppercase tracking-wider text-[#EA641B] dark:text-orange-300">Brutto</th>
<th className="text-right py-4 px-4 text-xs font-bold uppercase tracking-wider text-[#EA641B] dark:text-orange-300">Status</th>
<th className="text-right py-4 px-4 text-xs font-bold uppercase tracking-wider text-[#EA641B] dark:text-orange-300">Druckdaten</th>
<th className="py-4 px-4 text-xs font-bold uppercase tracking-wider text-[#EA641B] dark:text-orange-300"></th>
</tr>
</thead>

View File

@ -5,7 +5,7 @@ import Button from '../base/Button'
import EditPositionComponent from './EditPositionComponent'
import { Shop } from '../../model/shop'
import Currency from '../base/Currency'
import { Button as FlowbiteButton } from "flowbite-react"
import { Button as FlowbiteButton, Spinner } from "flowbite-react"
import ProductService from '../../services/product'
import {UploadOption} from '../../model/uploadOption'
import UploadOptionModal from './UploadOptionModal'
@ -13,14 +13,20 @@ import UploadOptionModal from './UploadOptionModal'
const PosComponent = ({index, pos, delPos, changePos, shop}) => {
const [uploadOptions, setUploadOptions] = useState<UploadOption[]>([])
const [activeOption, setActiveOption] = useState<UploadOption | null>(null)
const [loadingOption, setLoadingOption] = useState(false)
useEffect(() => {
if (pos.product.uuid) {
if (pos.product.uuid && pos.uploadMode) {
setLoadingOption(true)
const productService = new ProductService()
productService.getUploadOptions(pos.product).then(setUploadOptions)
productService.getUploadOptions(pos.product).then(options => {
setActiveOption(options.find(o => o.type === pos.uploadMode) ?? null)
}).finally(() => setLoadingOption(false))
} else {
setActiveOption(null)
}
}, [pos.product.uuid])
}, [pos.product.uuid, pos.uploadMode])
const deletePos = (uuid: String) => {
delPos(uuid)
@ -49,13 +55,13 @@ const PosComponent = ({index, pos, delPos, changePos, shop}) => {
<Currency price={pos.price.allGross} />
</td>
<td className="py-4 px-4 text-sm text-right">
{uploadOptions.length > 0 && (
<div className="flex flex-wrap gap-1 justify-end">
{uploadOptions.map(option => (
<UploadOptionModal key={option.type} option={option} pos={pos} />
))}
{loadingOption ? (
<div className="flex justify-end">
<Spinner size="sm" />
</div>
)}
) : activeOption ? (
<UploadOptionModal option={activeOption} pos={pos} />
) : null}
</td>
<td className="py-4 px-4 text-sm text-right">
<div className="flex justify-end">

View File

@ -1,10 +1,35 @@
import React, {useState} from 'react'
import {Modal, Button as FlowbiteButton} from 'flowbite-react'
import {createPortal} from 'react-dom'
import {Button as FlowbiteButton} from 'flowbite-react'
import {UploadOption} from '../../model/uploadOption'
import {UploadFile} from '../../model/upload'
import {Pos} from '../../model/pos'
import {getUploadOptionRenderer} from '../../lib/uploadOptionRegistry'
const BigModal = ({title, onClose, children}: {title: string, onClose: () => void, children: React.ReactNode}) =>
createPortal(
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
onClick={onClose}>
<div className="bg-white rounded-lg shadow-xl flex flex-col"
style={{width: '90vw', height: '90vh'}}
onClick={e => e.stopPropagation()}>
<div className="flex items-center justify-between px-5 py-3 border-b border-gray-200 shrink-0">
<h3 className="text-base font-semibold text-gray-800">{title}</h3>
<button onClick={onClose}
className="text-gray-400 hover:text-gray-600 p-1 rounded hover:bg-gray-100">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div className="flex-1 overflow-auto p-4">
{children}
</div>
</div>
</div>,
document.body
)
const UploadOptionModal = ({option, pos}: {option: UploadOption, pos: Pos}) => {
const [show, setShow] = useState(false)
const [files, setFiles] = useState<UploadFile[]>(pos.uploads)
@ -17,14 +42,12 @@ const UploadOptionModal = ({option, pos}: {option: UploadOption, pos: Pos}) => {
{option.label}
</FlowbiteButton>
<Modal show={show} onClose={() => setShow(false)}>
<Modal.Header>{option.label} {pos.product.title}</Modal.Header>
<Modal.Body>
{show && (
<BigModal title={`${option.label}${pos.product.title}`} onClose={() => setShow(false)}>
{option.contentUrl ? (
<iframe
src={option.contentUrl + '?pos=' + pos.uuid}
className="w-full border-0 rounded"
style={{minHeight: '400px'}}
className="w-full h-full border-0 rounded"
title={option.label}
/>
) : Renderer ? (
@ -32,8 +55,8 @@ const UploadOptionModal = ({option, pos}: {option: UploadOption, pos: Pos}) => {
) : (
<p className="text-sm text-gray-500">Kein Renderer für Typ {option.type}" registriert.</p>
)}
</Modal.Body>
</Modal>
</BigModal>
)}
</>
)
}

View File

@ -9,6 +9,7 @@ import ProductService from "../../services/product"
import {useEffect, useRef, useState} from "react"
import {Price} from "../../model/price"
import {Pos} from "../../model/pos"
import {UploadOption} from "../../model/uploadOption"
import { useDebouncedCallback } from 'use-debounce'
import Button from '../base/Button'
import Currency from '../base/Currency'
@ -24,6 +25,8 @@ const ProductForm = ({shop, pos, handleClose, handleChange}) => {
const [type, setType] = useState<number>(0)
const [price, setPrice] = useState<Price>(new Price())
const [uploadOptions, setUploadOptions] = useState<UploadOption[]>([])
const [uploadMode, setUploadMode] = useState<string>(pos.uploadMode ?? '')
const loadSchema = (loadData: any) => {
if(pos.product.uuid == "") {
@ -41,6 +44,8 @@ const ProductForm = ({shop, pos, handleClose, handleChange}) => {
setPrice(value['price'])
setType(value['typ'])
})
product_api.getUploadOptions(pos.product).then(setUploadOptions)
}
const changeCalc = (formData) => {
@ -72,6 +77,7 @@ const ProductForm = ({shop, pos, handleClose, handleChange}) => {
}
pos.price = price
pos.uploadMode = uploadMode
handleChange(pos)
handleClose()
}
@ -96,6 +102,38 @@ const ProductForm = ({shop, pos, handleClose, handleChange}) => {
formData={formData}
onChange={(e) => changeCalc(e.formData)}
validator={validator}/>
{uploadOptions.length > 0 && (
<div className='mt-4 pt-4 border-t border-gray-200'>
<label className='block text-sm font-medium text-gray-700 mb-2'>Druckdaten</label>
<div className='flex flex-wrap gap-2'>
<label className='flex items-center gap-2 cursor-pointer'>
<input
type='radio'
name='uploadMode'
value=''
checked={uploadMode === ''}
onChange={() => setUploadMode('')}
className='text-orange-500'
/>
<span className='text-sm text-gray-600'>Keine</span>
</label>
{uploadOptions.map(opt => (
<label key={opt.type} className='flex items-center gap-2 cursor-pointer'>
<input
type='radio'
name='uploadMode'
value={opt.type}
checked={uploadMode === opt.type}
onChange={() => setUploadMode(opt.type)}
className='text-orange-500'
/>
<span className='text-sm text-gray-600'>{opt.label}</span>
</label>
))}
</div>
</div>
)}
</div>
<div className='w-48 shrink-0 border-l border-gray-200 pl-6 pt-2'>
<h5 className='text-sm text-gray-600 mb-1'>Netto: <Currency price={ price.allNet} /></h5>
@ -104,7 +142,6 @@ const ProductForm = ({shop, pos, handleClose, handleChange}) => {
<Button onClick={addProduct} type={3} variant={"success"} />
</div>
</div>
</>
)
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1342,6 +1342,10 @@ html {
width: 100vw;
}
.min-w-0{
min-width: 0px;
}
.min-w-\[120px\]{
min-width: 120px;
}
@ -1991,6 +1995,16 @@ html {
background-color: rgb(101 163 13 / var(--tw-bg-opacity));
}
.bg-orange-100{
--tw-bg-opacity: 1;
background-color: rgb(255 237 213 / var(--tw-bg-opacity));
}
.bg-orange-500{
--tw-bg-opacity: 1;
background-color: rgb(249 115 22 / var(--tw-bg-opacity));
}
.bg-psc-50{
--tw-bg-opacity: 1;
background-color: rgb(238 180 175 / var(--tw-bg-opacity));
@ -2246,6 +2260,10 @@ html {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
.font-sans{
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
.text-2xl{
font-size: 1.5rem;
line-height: 2rem;
@ -2383,6 +2401,11 @@ html {
color: rgb(22 101 52 / var(--tw-text-opacity));
}
.text-orange-700{
--tw-text-opacity: 1;
color: rgb(194 65 12 / var(--tw-text-opacity));
}
.text-psc{
--tw-text-opacity: 1;
color: rgb(234 100 27 / var(--tw-text-opacity));
@ -2408,6 +2431,11 @@ html {
color: rgb(147 51 234 / var(--tw-text-opacity));
}
.text-red-500{
--tw-text-opacity: 1;
color: rgb(239 68 68 / var(--tw-text-opacity));
}
.text-red-600{
--tw-text-opacity: 1;
color: rgb(220 38 38 / var(--tw-text-opacity));
@ -2826,6 +2854,52 @@ html {
text-transform: uppercase;
}
.file\:mr-3::file-selector-button{
margin-right: 0.75rem;
}
.file\:rounded::file-selector-button{
border-radius: 0.25rem;
}
.file\:border-0::file-selector-button{
border-width: 0px;
}
.file\:bg-orange-50::file-selector-button{
--tw-bg-opacity: 1;
background-color: rgb(255 247 237 / var(--tw-bg-opacity));
}
.file\:px-3::file-selector-button{
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.file\:py-1::file-selector-button{
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.file\:py-1\.5::file-selector-button{
padding-top: 0.375rem;
padding-bottom: 0.375rem;
}
.file\:text-sm::file-selector-button{
font-size: 0.875rem;
line-height: 1.25rem;
}
.file\:font-medium::file-selector-button{
font-weight: 500;
}
.file\:text-orange-700::file-selector-button{
--tw-text-opacity: 1;
color: rgb(194 65 12 / var(--tw-text-opacity));
}
.after\:absolute::after{
content: var(--tw-content);
position: absolute;
@ -2962,6 +3036,11 @@ html {
background-color: rgb(79 70 229 / var(--tw-bg-opacity));
}
.hover\:bg-orange-600:hover{
--tw-bg-opacity: 1;
background-color: rgb(234 88 12 / var(--tw-bg-opacity));
}
.hover\:bg-psc-50:hover{
--tw-bg-opacity: 1;
background-color: rgb(238 180 175 / var(--tw-bg-opacity));
@ -3105,6 +3184,11 @@ html {
--tw-ring-offset-width: 2px;
}
.hover\:file\:bg-orange-100::file-selector-button:hover{
--tw-bg-opacity: 1;
background-color: rgb(255 237 213 / var(--tw-bg-opacity));
}
.focus\:z-10:focus{
z-index: 10;
}
@ -3196,6 +3280,11 @@ html {
--tw-ring-color: rgb(107 114 128 / var(--tw-ring-opacity));
}
.focus\:ring-orange-400:focus{
--tw-ring-opacity: 1;
--tw-ring-color: rgb(251 146 60 / var(--tw-ring-opacity));
}
.focus\:ring-psc-500:focus{
--tw-ring-opacity: 1;
--tw-ring-color: rgb(234 100 27 / var(--tw-ring-opacity));

View File

@ -1,11 +1,12 @@
info:
datum: 25.02.2026
datum: 10.03.2026
release: 2.3.3
changelog:
- version: 2.3.3
datum: 25.02.2026
datum: 10.03.2026
changes:
- "Auftrag bearbeiten jetzt mit Dateiupload"
- "Auftragsdetails in Tailwind"
- "Bereich Aufträge hat jetzt eigene Benutzergruppe"
- "In EMails können Bilder eingebettet werden: {{ email.image('@images/logo.png') }}"