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; try_files $uri @sfFront;
} }
# location /w2p/ { location /w2p/ {
# proxy_pass http://tp:8080/w2p/; proxy_pass http://tp:8080/w2p/;
# proxy_temp_path /tmp/proxy; proxy_temp_path /tmp/proxy;
# } }
location @sfFront { # Symfony location @sfFront { # Symfony
if ($request_method = 'OPTIONS') { 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) => ` 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="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}"> <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="grid grid-cols-12 gap-4 flex-1">
<div class="col-span-1"> <div class="col-span-1">
<span class="text-xs font-semibold text-gray-700">Pos:</span> <span class="text-xs font-semibold text-gray-700">Pos:</span>
<div class="font-medium">${pos}</div> <div class="font-medium">${pos}</div>
</div> </div>
<div class="col-span-1">
${Order_List_Preview(uploadTypeObject)}
</div>
<div class="col-span-4"> <div class="col-span-4">
<span class="text-xs font-semibold text-gray-700">Produkt:</span> <span class="text-xs font-semibold text-gray-700">Produkt:</span>
<div> <div>
@ -48,7 +50,7 @@ const Order_List_Detail = ({ uuid, basketField1, customerInfo, basketField2, pos
${psc.order.get_special_product_options(product.specialProductTypeObject)} ${psc.order.get_special_product_options(product.specialProductTypeObject)}
</div> </div>
</div> </div>
<div class="col-span-2"> <div class="col-span-1">
<span class="text-xs font-semibold text-gray-700">Auflage:</span> <span class="text-xs font-semibold text-gray-700">Auflage:</span>
<div>${price.count}</div> <div>${price.count}</div>
</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> <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 class="text-sm">${customerInfo}${(reOrder ? `<br/><strong class="text-yellow-600">Ist eine Nachbestellung</strong>` : ``)}</div>
</div> </div>
<div class="col-span-2"> <div class="col-span-1">
<span class="text-xs font-semibold text-gray-700">Preis:</span> <span class="text-xs font-semibold text-gray-700">Preis:</span>
<div class="text-right whitespace-nowrap"> <div class="text-right whitespace-nowrap">
${new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(price.allNet / 100)}<br/> ${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> <strong>(${new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(price.allGross / 100)})</strong>
</div> </div>
</div> </div>
<div class="col-span-1"> <div class="col-span-2">
<span class="text-xs font-semibold text-gray-700">Status:</span> <span class="text-xs font-semibold text-gray-700">Status:</span>
<div class="relative inline-block text-left w-full"> <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"> <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{ * datetime?: array{
* default_format?: scalar|Param|null, // Default: "Y-m-d\\TH:i:sP" * default_format?: scalar|Param|null, // Default: "Y-m-d\\TH:i:sP"
* default_deserialization_formats?: list<scalar|Param|null>, * 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 * cdata?: scalar|Param|null, // Default: true
* }, * },
* array_collection?: array{ * array_collection?: array{
@ -576,7 +576,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* datetime?: array{ * datetime?: array{
* default_format?: scalar|Param|null, // Default: "Y-m-d\\TH:i:sP" * default_format?: scalar|Param|null, // Default: "Y-m-d\\TH:i:sP"
* default_deserialization_formats?: list<scalar|Param|null>, * 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 * cdata?: scalar|Param|null, // Default: true
* }, * },
* array_collection?: array{ * array_collection?: array{
@ -2433,7 +2433,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* length?: scalar|Param|null, // Default: 5 * length?: scalar|Param|null, // Default: 5
* width?: scalar|Param|null, // Default: 130 * width?: scalar|Param|null, // Default: 130
* height?: scalar|Param|null, // Default: 50 * 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 * keep_value?: scalar|Param|null, // Default: false
* charset?: scalar|Param|null, // Default: "abcdefhjkmnprstuvwxyz23456789" * charset?: scalar|Param|null, // Default: "abcdefhjkmnprstuvwxyz23456789"
* as_file?: scalar|Param|null, // Default: false * 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\EntityManager;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Knp\Component\Pager\PaginatorInterface; use Knp\Component\Pager\PaginatorInterface;
use Spiriit\Bundle\FormFilterBundle\Filter\FilterBuilderUpdaterInterface;
use PSC\Shop\EntityBundle\Document\Order; use PSC\Shop\EntityBundle\Document\Order;
use PSC\Shop\EntityBundle\Entity\Motiv; use PSC\Shop\EntityBundle\Entity\Motiv;
use PSC\Shop\EntityBundle\Entity\Shop; 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\Document\LogEntry;
use PSC\System\SettingsBundle\Service\Log; use PSC\System\SettingsBundle\Service\Log;
use PSC\System\SettingsBundle\Service\Status; 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\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Component\Routing\Attribute\Route; 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\Security\Http\Attribute\IsGranted;
use Symfony\Component\HttpFoundation\Request;
/** /**
* ListController fürs Backend * ListController fürs Backend
@ -46,9 +47,11 @@ use Symfony\Component\HttpFoundation\Request;
*/ */
class ListController extends AbstractController 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 * Default Seite
@ -79,18 +82,18 @@ class ListController extends AbstractController
Status $statusService, Status $statusService,
Section $sectionService, Section $sectionService,
\PSC\Shop\OrderBundle\Service\Order $orderService, \PSC\Shop\OrderBundle\Service\Order $orderService,
) { ) {
$customSections = $sectionService->get(\PSC\System\PluginBundle\Form\Interfaces\Section::Order); $customSections = $sectionService->get(\PSC\System\PluginBundle\Form\Interfaces\Section::Order);
$selectedShop = $shop->getSelectedShop(); $selectedShop = $shop->getSelectedShop();
$userRepository = $em->getRepository('PSC\Shop\EntityBundle\Entity\Order'); $userRepository = $em->getRepository('PSC\Shop\EntityBundle\Entity\Order');
$qb = $userRepository->createQueryBuilder('orders') $qb = $userRepository
->createQueryBuilder('orders')
->leftJoin('orders.contact', 'contact') ->leftJoin('orders.contact', 'contact')
->leftJoin('orders.invoiceAddress', 'invoiceAddress') ->leftJoin('orders.invoiceAddress', 'invoiceAddress')
->orderBy('orders.uid', 'desc'); ->orderBy('orders.uid', 'desc');
$qbCount = $userRepository->createQueryBuilder('orders') $qbCount = $userRepository
->createQueryBuilder('orders')
->select('count(orders.uid)') ->select('count(orders.uid)')
->leftJoin('orders.contact', 'contact') ->leftJoin('orders.contact', 'contact')
->leftJoin('orders.invoiceAddress', 'invoiceAddress') ->leftJoin('orders.invoiceAddress', 'invoiceAddress')
@ -111,24 +114,28 @@ class ListController extends AbstractController
$session->set('order_sort_page', $request->get('page', 1)); $session->set('order_sort_page', $request->get('page', 1));
} }
$qb->andWhere('orders.shop = :shop_id') $qb->andWhere('orders.shop = :shop_id')->setParameter('shop_id', $selectedShop->getUid());
->setParameter("shop_id", $selectedShop->getUid()); $qbCount->andWhere('orders.shop = :shop_id')->setParameter('shop_id', $selectedShop->getUid());
$qbCount->andWhere('orders.shop = :shop_id')
->setParameter("shop_id", $selectedShop->getUid());
$query = $qb->getQuery(); $query = $qb->getQuery();
$count = $qbCount->getQuery()->getSingleScalarResult(); $count = $qbCount->getQuery()->getSingleScalarResult();
$query->setHint('knp_paginator.count', $count); $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'), 'defaultSortFieldName' => $session->get('order_sort_field', 'orders.uid'),
'defaultSortDirection' => $session->get('order_sort_direction', 'desc'), 'defaultSortDirection' => $session->get('order_sort_direction', 'desc'),
'distinct' => false 'distinct' => false,
]); ],
);
return array( return array(
'pagination' => $pagination, 'pagination' => $pagination,
'orderStatuse' => $statusService, 'orderStatuse' => $statusService,
'orderService' => $orderService, 'orderService' => $orderService,
'form' => $form->createView(), 'form' => $form->createView(),
'customSections' => $customSections 'customSections' => $customSections,
'customListButtons' => $this->listButtons,
); );
} }
@ -147,10 +154,16 @@ class ListController extends AbstractController
*/ */
#[Route(path: '/list/switchstatus/{order}/{status}', name: 'psc_shop_order_backend_list_switchstatus')] #[Route(path: '/list/switchstatus/{order}/{status}', name: 'psc_shop_order_backend_list_switchstatus')]
#[Template('@PSCShopOrder/backend/list/switchstatus.html.twig')] #[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(); $selectedShop = $shopService->getSelectedShop();
/** /**
* @var \PSC\Shop\EntityBundle\Entity\Order $order * @var \PSC\Shop\EntityBundle\Entity\Order $order
*/ */
$order = $emService $order = $emService
@ -159,7 +172,14 @@ class ListController extends AbstractController
$order->setStatus($status); $order->setStatus($status);
$emService->persist($order); $emService->persist($order);
$emService->flush(); $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'); $session->getFlashBag()->add('success', 'Status erfolgreich gesetzt');
$notify = new \PSC\Shop\QueueBundle\Event\Order\Status\Change(); $notify = new \PSC\Shop\QueueBundle\Event\Order\Status\Change();
$notify->setShop($selectedShop->getUID()); $notify->setShop($selectedShop->getUID());
@ -185,10 +205,16 @@ class ListController extends AbstractController
*/ */
#[Route(path: '/list/switchstatuspos/{position}/{status}', name: 'psc_shop_order_backend_list_switchstatus_pos')] #[Route(path: '/list/switchstatuspos/{position}/{status}', name: 'psc_shop_order_backend_list_switchstatus_pos')]
#[Template('@PSCShopOrder/backend/list/switchstatuspos.html.twig')] #[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(); $selectedShop = $shopService->getSelectedShop();
/** /**
* @var \PSC\Shop\EntityBundle\Entity\Orderpos $orderPos * @var \PSC\Shop\EntityBundle\Entity\Orderpos $orderPos
*/ */
$orderPos = $emService $orderPos = $emService
@ -197,7 +223,14 @@ class ListController extends AbstractController
$orderPos->setStatus($status); $orderPos->setStatus($status);
$emService->persist($orderPos); $emService->persist($orderPos);
$emService->flush(); $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'); $session->getFlashBag()->add('success', 'Status erfolgreich gesetzt');
$notify = new \PSC\Shop\QueueBundle\Event\Position\Status\Change(); $notify = new \PSC\Shop\QueueBundle\Event\Position\Status\Change();
$notify->setShop($selectedShop->getUID()); $notify->setShop($selectedShop->getUID());

View File

@ -13,19 +13,26 @@
namespace PSC\Shop\OrderBundle\Controller\Backend; namespace PSC\Shop\OrderBundle\Controller\Backend;
use Doctrine\ODM\MongoDB\DocumentManager;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use PSC\Shop\EntityBundle\Entity\Motiv; 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\MediaBundle\Helper\Transformer\PdfTransformer;
use PSC\Shop\OrderBundle\Form\Backend\Upload\DeleteType; 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\Bridge\Twig\Attribute\Template;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Core\SecurityContext; use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Component\Serializer\SerializerInterface;
/** /**
* UploadController fürs Backend * UploadController fürs Backend
@ -35,6 +42,29 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
*/ */
class UploadController extends AbstractController 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')] #[IsGranted('ROLE_ORDER_VIEW')]
#[Route(path: '/upload/preview', name: 'psc_shop_order_backend_upload_preview', methods: ['GET'])] #[Route(path: '/upload/preview', name: 'psc_shop_order_backend_upload_preview', methods: ['GET'])]
public function previewAction(Request $request, PdfTransformer $pdfTransformer): BinaryFileResponse public function previewAction(Request $request, PdfTransformer $pdfTransformer): BinaryFileResponse
@ -82,6 +112,80 @@ class UploadController extends AbstractController
return $response; 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 * Delete Seite
* *

View File

@ -157,6 +157,10 @@
</svg> </svg>
Details Details
</a> </a>
{% for customListButton in customListButtons %}
{{ customListButton.render(orderObj)|raw }}
{% endfor %}
</div> </div>
</div> </div>
</td> </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 public function getContentUrl(): ?string
{ {
return null; return '/apps/backend/order/upload/center-modal';
} }
public function isEnabled(ProductEntity $entity, ?ProductDoc $document): bool public function isEnabled(ProductEntity $entity, ?ProductDoc $document): bool

View File

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

View File

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

View File

@ -48,21 +48,13 @@ class Render implements QueueInterface, ConfigurableElementInterface
return 'LaufkartenLayouter Renderer'; 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 public function execute(EventInterface $event, Queue $doc): bool
{ {
@ -154,7 +146,7 @@ class Render implements QueueInterface, ConfigurableElementInterface
$configData['contentAreaHeight'], $configData['contentAreaHeight'],
false, false,
); );
$this->renderOcrMarkers($pdf, $configData['ocrMarkers'] ?? []); $this->renderOcrMarkers($pdf, $configData['ocrMarkers'] ?? [], $tab);
$this->drawTab($pdf, $tab, $tab['x'], $tab['y'], $file); $this->drawTab($pdf, $tab, $tab['x'], $tab['y'], $file);
// Page 2 (back, mirrored) // Page 2 (back, mirrored)
@ -168,7 +160,7 @@ class Render implements QueueInterface, ConfigurableElementInterface
$configData['contentAreaHeight'], $configData['contentAreaHeight'],
false, false,
); );
$this->renderOcrMarkers($pdf, $configData['ocrMarkers'] ?? []); $this->renderOcrMarkers($pdf, $configData['ocrMarkers'] ?? [], $tab);
$xPos = $configData['width'] - $tab['width'] - $tab['x']; $xPos = $configData['width'] - $tab['width'] - $tab['x'];
$this->drawTab($pdf, $tab, $xPos - 1, $tab['y'] - 1, $file, $tab['width'] + 2, $tab['height'] + 1); $this->drawTab($pdf, $tab, $xPos - 1, $tab['y'] - 1, $file, $tab['width'] + 2, $tab['height'] + 1);
} else { } else {
@ -182,7 +174,7 @@ class Render implements QueueInterface, ConfigurableElementInterface
$configData['contentAreaHeight'], $configData['contentAreaHeight'],
false, false,
); );
$this->renderOcrMarkers($pdf, $configData['ocrMarkers'] ?? []); $this->renderOcrMarkers($pdf, $configData['ocrMarkers'] ?? [], $tab);
if ($frontSide) { if ($frontSide) {
$this->drawTab($pdf, $tab, $tab['x'], $tab['y'], $file); $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)) { if (empty($ocrMarkers)) {
return; return;
@ -272,7 +264,7 @@ class Render implements QueueInterface, ConfigurableElementInterface
$pdf->Rotate(90, $x, $y); $pdf->Rotate(90, $x, $y);
$pdf->SetFont('helvetica', 'B', $size); $pdf->SetFont('helvetica', 'B', $size);
$pdf->SetTextColor(0, 0, 0); $pdf->SetTextColor(0, 0, 0);
$pdf->Text($x, $y, $marker['text'] ?? ''); $pdf->Text($x, $y, $marker['text'] ?? '' . ' Tab: ' . $tab['tabNumber']);
$pdf->StopTransform(); $pdf->StopTransform();
break; 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">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">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">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> <th className="py-4 px-4 text-xs font-bold uppercase tracking-wider text-[#EA641B] dark:text-orange-300"></th>
</tr> </tr>
</thead> </thead>

View File

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

View File

@ -1,10 +1,35 @@
import React, {useState} from 'react' 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 {UploadOption} from '../../model/uploadOption'
import {UploadFile} from '../../model/upload' import {UploadFile} from '../../model/upload'
import {Pos} from '../../model/pos' import {Pos} from '../../model/pos'
import {getUploadOptionRenderer} from '../../lib/uploadOptionRegistry' 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 UploadOptionModal = ({option, pos}: {option: UploadOption, pos: Pos}) => {
const [show, setShow] = useState(false) const [show, setShow] = useState(false)
const [files, setFiles] = useState<UploadFile[]>(pos.uploads) const [files, setFiles] = useState<UploadFile[]>(pos.uploads)
@ -17,14 +42,12 @@ const UploadOptionModal = ({option, pos}: {option: UploadOption, pos: Pos}) => {
{option.label} {option.label}
</FlowbiteButton> </FlowbiteButton>
<Modal show={show} onClose={() => setShow(false)}> {show && (
<Modal.Header>{option.label} {pos.product.title}</Modal.Header> <BigModal title={`${option.label}${pos.product.title}`} onClose={() => setShow(false)}>
<Modal.Body>
{option.contentUrl ? ( {option.contentUrl ? (
<iframe <iframe
src={option.contentUrl + '?pos=' + pos.uuid} src={option.contentUrl + '?pos=' + pos.uuid}
className="w-full border-0 rounded" className="w-full h-full border-0 rounded"
style={{minHeight: '400px'}}
title={option.label} title={option.label}
/> />
) : Renderer ? ( ) : 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> <p className="text-sm text-gray-500">Kein Renderer für Typ {option.type}" registriert.</p>
)} )}
</Modal.Body> </BigModal>
</Modal> )}
</> </>
) )
} }

View File

@ -9,6 +9,7 @@ import ProductService from "../../services/product"
import {useEffect, useRef, useState} from "react" import {useEffect, useRef, useState} from "react"
import {Price} from "../../model/price" import {Price} from "../../model/price"
import {Pos} from "../../model/pos" import {Pos} from "../../model/pos"
import {UploadOption} from "../../model/uploadOption"
import { useDebouncedCallback } from 'use-debounce' import { useDebouncedCallback } from 'use-debounce'
import Button from '../base/Button' import Button from '../base/Button'
import Currency from '../base/Currency' import Currency from '../base/Currency'
@ -24,6 +25,8 @@ const ProductForm = ({shop, pos, handleClose, handleChange}) => {
const [type, setType] = useState<number>(0) const [type, setType] = useState<number>(0)
const [price, setPrice] = useState<Price>(new Price()) const [price, setPrice] = useState<Price>(new Price())
const [uploadOptions, setUploadOptions] = useState<UploadOption[]>([])
const [uploadMode, setUploadMode] = useState<string>(pos.uploadMode ?? '')
const loadSchema = (loadData: any) => { const loadSchema = (loadData: any) => {
if(pos.product.uuid == "") { if(pos.product.uuid == "") {
@ -41,6 +44,8 @@ const ProductForm = ({shop, pos, handleClose, handleChange}) => {
setPrice(value['price']) setPrice(value['price'])
setType(value['typ']) setType(value['typ'])
}) })
product_api.getUploadOptions(pos.product).then(setUploadOptions)
} }
const changeCalc = (formData) => { const changeCalc = (formData) => {
@ -72,6 +77,7 @@ const ProductForm = ({shop, pos, handleClose, handleChange}) => {
} }
pos.price = price pos.price = price
pos.uploadMode = uploadMode
handleChange(pos) handleChange(pos)
handleClose() handleClose()
} }
@ -96,6 +102,38 @@ const ProductForm = ({shop, pos, handleClose, handleChange}) => {
formData={formData} formData={formData}
onChange={(e) => changeCalc(e.formData)} onChange={(e) => changeCalc(e.formData)}
validator={validator}/> 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>
<div className='w-48 shrink-0 border-l border-gray-200 pl-6 pt-2'> <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> <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"} /> <Button onClick={addProduct} type={3} variant={"success"} />
</div> </div>
</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; width: 100vw;
} }
.min-w-0{
min-width: 0px;
}
.min-w-\[120px\]{ .min-w-\[120px\]{
min-width: 120px; min-width: 120px;
} }
@ -1991,6 +1995,16 @@ html {
background-color: rgb(101 163 13 / var(--tw-bg-opacity)); 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{ .bg-psc-50{
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(238 180 175 / var(--tw-bg-opacity)); 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-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{ .text-2xl{
font-size: 1.5rem; font-size: 1.5rem;
line-height: 2rem; line-height: 2rem;
@ -2383,6 +2401,11 @@ html {
color: rgb(22 101 52 / var(--tw-text-opacity)); 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{ .text-psc{
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(234 100 27 / var(--tw-text-opacity)); color: rgb(234 100 27 / var(--tw-text-opacity));
@ -2408,6 +2431,11 @@ html {
color: rgb(147 51 234 / var(--tw-text-opacity)); 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{ .text-red-600{
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(220 38 38 / var(--tw-text-opacity)); color: rgb(220 38 38 / var(--tw-text-opacity));
@ -2826,6 +2854,52 @@ html {
text-transform: uppercase; 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{ .after\:absolute::after{
content: var(--tw-content); content: var(--tw-content);
position: absolute; position: absolute;
@ -2962,6 +3036,11 @@ html {
background-color: rgb(79 70 229 / var(--tw-bg-opacity)); 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{ .hover\:bg-psc-50:hover{
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(238 180 175 / var(--tw-bg-opacity)); background-color: rgb(238 180 175 / var(--tw-bg-opacity));
@ -3105,6 +3184,11 @@ html {
--tw-ring-offset-width: 2px; --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{ .focus\:z-10:focus{
z-index: 10; z-index: 10;
} }
@ -3196,6 +3280,11 @@ html {
--tw-ring-color: rgb(107 114 128 / var(--tw-ring-opacity)); --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{ .focus\:ring-psc-500:focus{
--tw-ring-opacity: 1; --tw-ring-opacity: 1;
--tw-ring-color: rgb(234 100 27 / var(--tw-ring-opacity)); --tw-ring-color: rgb(234 100 27 / var(--tw-ring-opacity));

View File

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