This commit is contained in:
Thomas Peterson 2026-03-20 13:40:01 +01:00
parent 60148d9c9e
commit a22df31b30
28 changed files with 677 additions and 154 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') {

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: "Europe/Berlin"
* default_timezone?: scalar|Param|null, // Default: "UTC"
* 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: "Europe/Berlin"
* default_timezone?: scalar|Param|null, // Default: "UTC"
* cdata?: scalar|Param|null, // Default: true
* },
* array_collection?: array{

View File

@ -12,6 +12,7 @@
</svg>
</a>
{% endif %}
{% if is_granted('ROLE_ADMIN') %}
<span class="hidden md:inline"><input type="checkbox" class=" border-gray-300 rounded-sm shadow-sm text-primary-600 outline-none focus:ring focus:ring-primary-200 focus:ring-opacity-50" onchange="window.location='{{ path('psc_backend_dashboard_toogle_deleted_shop') }}'" {% if displayDeletedShop %}checked="checked"{% endif %}/> Deaktive Shops zeigen</span>
{% endif %}

View File

@ -26,15 +26,15 @@ class Get extends AbstractController
#[Response(
response: 200,
description: 'my shops',
description: 'all shops',
content: new JsonContent(ref: new Model(type: PSC\Component\ApiBundle\Dto\Shop\Shops\Output::class)),
)]
#[Tag(name: 'Shops')]
#[Security(name: 'ApiKeyAuth')]
#[Security(name: 'Bearer')]
#[Route(path: '/my_shops', methods: ['GET'])]
#[Route(path: '/shops', methods: ['GET'])]
#[IsGranted('ROLE_SHOP')]
public function myShops(): JsonResponse
public function allShops(): JsonResponse
{
$shops = $this->entityManager->getRepository(\PSC\Shop\EntityBundle\Entity\Shop::class)->findAll();
@ -49,15 +49,15 @@ class Get extends AbstractController
#[Response(
response: 200,
description: 'shops',
description: 'my shops',
content: new JsonContent(ref: new Model(type: PSC\Component\ApiBundle\Dto\Shop\Shops\Output::class)),
)]
#[Tag(name: 'Shops')]
#[Security(name: 'ApiKeyAuth')]
#[Security(name: 'Bearer')]
#[Route(path: '/shops', methods: ['GET'])]
#[Route(path: '/my_shops', methods: ['GET'])]
#[IsGranted('ROLE_SHOP')]
public function allAction(): JsonResponse
public function myAction(): JsonResponse
{
$shops = $this->shopContactRepository->myEditableShops($this->getUser());

View File

@ -60,10 +60,12 @@ class Save extends AbstractController
$row->setLayouterModus($data->layouterModus);
// TODO: Store uploadedFiles in session data
$this->entityManager->persist($row);
$this->entityManager->flush();
return $this->json([
'success' => true,
'layouterUUId' => $row->getUuid(),
'layouterModus' => $row->getLayouterModus(),
'auth' => true,
'user' => $this->tokenStorage
->getToken()

View File

@ -23,7 +23,7 @@ class LayouterSession
#[ORM\Column(name: 'title', type: 'string', length: 255, nullable: true)]
protected string $title;
#[ORM\Column(name: 'org_article_id', type: 'integer')]
#[ORM\Column(name: 'org_article_id', type: 'string', length: 40, nullable: true)]
protected string $productId;
#[ORM\Column(name: 'contact_id', type: 'integer')]

View File

@ -120,6 +120,9 @@ class Orderpos
*/
#[ORM\Column(name: 'external_approval_status', type: 'integer', nullable: true)]
private ?int $externalApprovalStatus = null;
#[ORM\Column(name: 'external_approval_message', type: 'text', nullable: true)]
private ?string $externalApprovalMessage = null;
/**
* @var Product
*/
@ -219,17 +222,17 @@ class Orderpos
/**
* @var \DateTime
*/
#[ORM\Column(name: 'delivery_date', type: 'date', nullable: false)]
#[ORM\Column(name: 'delivery_date', type: 'date', nullable: true)]
private $deliveryDate;
/**
* @var integer
*/
#[ORM\Column(name: 'maschine', type: 'integer', nullable: false)]
#[ORM\Column(name: 'maschine', type: 'integer', nullable: true)]
private $maschine;
/**
* @var integer
*/
#[ORM\Column(name: 'papier', type: 'integer', nullable: false)]
#[ORM\Column(name: 'papier', type: 'integer', nullable: true)]
private $papier;
/**
* @var \DateTime
@ -1046,4 +1049,14 @@ class Orderpos
{
$this->externalApprovalStatus = $status;
}
public function getExternalApprovalMessage(): ?string
{
return $this->externalApprovalMessage;
}
public function setExternalApprovalMessage(?string $message): void
{
$this->externalApprovalMessage = $message;
}
}

View File

@ -2,9 +2,19 @@
namespace PSC\Shop\OrderBundle\Controller;
use Doctrine\ODM\MongoDB\DocumentManager;
use Doctrine\ORM\EntityManagerInterface;
use PSC\Library\Calc\Engine;
use PSC\Library\Calc\PaperContainer;
use PSC\Shop\EntityBundle\Document\Position;
use PSC\Shop\EntityBundle\Document\Product;
use PSC\Shop\EntityBundle\Entity\Orderpos;
use PSC\Shop\EntityBundle\Entity\Upload;
use PSC\Shop\OrderBundle\Model\Order\Position as AliasedPosition;
use PSC\Shop\OrderBundle\Transformer\Order\Position as PSCPosition;
use PSC\Shop\QueueBundle\Event\Position\ApprovalExternalAccept;
use PSC\Shop\QueueBundle\Event\Position\ApprovalExternalDeclined;
use PSC\System\SettingsBundle\Service\PaperDB;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Request;
@ -16,22 +26,49 @@ use Symfony\Component\Routing\Attribute\Route;
class ExternalApprovalController extends AbstractController
{
#[Route(path: '/{uuid}', name: 'psc_shop_order_external_approval_show', methods: ['GET', 'POST'])]
public function show(string $uuid, Request $request, EntityManagerInterface $em): Response
{
public function show(
string $uuid,
Request $request,
EntityManagerInterface $em,
DocumentManager $dm,
PaperDB $paperService,
\PSC\Shop\QueueBundle\Service\Event\Manager $eventManagerService,
PSCPosition $positionTransformer,
): Response {
/** @var Orderpos|null $orderpos */
$orderpos = $em->getRepository(Orderpos::class)->findOneBy(['uuid' => $uuid]);
$orderposDoc = $dm->getRepository(Position::class)->findOneBy(['uid' => $orderpos->getUid()]);
if (!$orderpos) {
throw $this->createNotFoundException('Position nicht gefunden.');
}
$position = new AliasedPosition();
$positionTransformer->fromDb($position, $orderpos, $orderposDoc);
if ($request->isMethod('POST')) {
$action = $request->request->get('action');
$message = trim($request->request->get('message', ''));
if ($action === 'approve') {
$orderpos->setExternalApprovalStatus(1);
$notify = new ApprovalExternalAccept();
$notify->setShop($orderpos->getOrder()->getShop()->getUID());
$notify->setOrder($orderpos->getOrder()->getUuid());
$notify->setPosition($orderpos->getUuid());
$notify->setMessage($message);
$eventManagerService->addJob($notify);
} elseif ($action === 'reject') {
$orderpos->setExternalApprovalStatus(-1);
$notify = new ApprovalExternalDeclined();
$notify->setShop($orderpos->getOrder()->getShop()->getUID());
$notify->setOrder($orderpos->getOrder()->getUuid());
$notify->setPosition($orderpos->getUuid());
$notify->setMessage($message);
$eventManagerService->addJob($notify);
}
if ($message !== '') {
$orderpos->setExternalApprovalMessage($message);
}
$em->flush();
@ -39,8 +76,12 @@ class ExternalApprovalController extends AbstractController
return $this->redirectToRoute('psc_shop_order_external_approval_show', ['uuid' => $uuid]);
}
$articleCalc = $this->buildCalc($orderpos, $dm, $paperService);
return $this->render('@PSCShopOrder/external_approval/show.html.twig', [
'orderpos' => $orderpos,
'articleCalc' => $articleCalc,
'position' => $position,
]);
}
@ -72,4 +113,50 @@ class ExternalApprovalController extends AbstractController
return $response;
}
private function buildCalc(Orderpos $orderpos, DocumentManager $dm, PaperDB $paperService): ?object
{
if (!$orderpos->hasCalcXml()) {
return null;
}
$shop = $orderpos->getOrder()->getShop();
/** @var Position|null $objDoc */
$objDoc = $dm->getRepository(Position::class)->findOneBy(['uid' => (string) $orderpos->getId()]);
$paperContainer = new PaperContainer();
$paperContainer->parse(simplexml_load_string($shop->getInstall()->getPaperContainer()));
$engine = new Engine();
$engine->setPaperRepository($paperService);
$engine->setPaperContainer($paperContainer);
if ($shop->getInstall()->getCalcTemplates()) {
$engine->setTemplates('<root>' . $shop->getInstall()->getCalcTemplates() . '</root>');
}
$engine->loadString($orderpos->getCalcXml());
$engine->setFormulas($shop->getFormel());
$engine->setParameters($shop->getParameter());
if ($objDoc && isset($objDoc->getSpecialProductTypeObject()['params'])) {
$engine->setVariables($objDoc->getSpecialProductTypeObject()['params']);
$engine->setSavedCalcReferences($objDoc->getCalcReferences());
if (isset($objDoc->getSpecialProductTypeObject()['kalk_artikel'])) {
$engine->setActiveArticle($objDoc->getSpecialProductTypeObject()['kalk_artikel']);
}
} elseif ($orderpos->getData()) {
$objPosition = unserialize($orderpos->getData());
$engine->setVariables($objPosition->getOptions());
if (isset($objPosition->getOptions()['kalk_artikel'])) {
$engine->setActiveArticle($objPosition->getOptions()['kalk_artikel']);
}
if ($objDoc && $objDoc->getXmlProduct() !== '') {
$engine->setActiveArticle($objDoc->getXmlProduct());
}
}
return $engine->getArticle();
}
}

View File

@ -13,9 +13,9 @@
namespace PSC\Shop\OrderBundle\Form\Backend;
use PSC\System\SettingsBundle\Service\Status;
use Spiriit\Bundle\FormFilterBundle\Filter\Form\Type\TextFilterType;
use Spiriit\Bundle\FormFilterBundle\Filter\Query\QueryInterface;
use PSC\System\SettingsBundle\Service\Status;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
@ -28,6 +28,7 @@ class SearchType extends AbstractType
protected $status = array();
/** @var Session */
protected $session;
public function __construct(RequestStack $requestStack, Status $status)
{
$this->status = $status->getOrderStatusAsArray();
@ -37,32 +38,55 @@ class SearchType extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$session = $this->session;
$builder
->setMethod('POST')
->add('term', TextFilterType::class, array(
$builder->setMethod('POST')->add('term', TextFilterType::class, array(
'data' => $session->get('order_search_term'),
'apply_filter' => function (QueryInterface $filterQuery, $field, $values) use ($session) {
$session->set('order_search_term', $values['value']);
if (empty($values['value'])) {
return null;
}
$filterQuery->getQueryBuilder()
->andWhere("(
orders.basketfield1 LIKE '%" . $values['value'] . "%' OR
orders.basketfield2 LIKE '%" . $values['value'] . "%' OR
contact.firstname LIKE '%" . $values['value'] . "%' OR
contact.lastname LIKE '%" . $values['value'] . "%' OR
contact.zip LIKE '%" . $values['value'] . "%' OR
contact.city LIKE '%" . $values['value'] . "%' OR
contact.houseNumber LIKE '%" . $values['value'] . "%' OR
contact.street LIKE '%" . $values['value'] . "%' OR
contact.uid = '" . $values['value'] . "' OR
orders.alias LIKE '%" . $values['value'] . "%')
");
}))
->add('status', ChoiceType::class, array(
$filterQuery
->getQueryBuilder()
->andWhere(
"(
orders.basketfield1 LIKE '%"
. $values['value']
. "%' OR
orders.basketfield2 LIKE '%"
. $values['value']
. "%' OR
contact.firstname LIKE '%"
. $values['value']
. "%' OR
contact.lastname LIKE '%"
. $values['value']
. "%' OR
contact.zip LIKE '%"
. $values['value']
. "%' OR
contact.city LIKE '%"
. $values['value']
. "%' OR
contact.houseNumber LIKE '%"
. $values['value']
. "%' OR
contact.street LIKE '%"
. $values['value']
. "%' OR
contact.username LIKE '%"
. $values['value']
. "%' OR
contact.uid = '"
. $values['value']
. "' OR
orders.alias LIKE '%"
. $values['value']
. "%')
",
);
},
))->add('status', ChoiceType::class, array(
'choices' => $this->status,
'required' => false,
'multiple' => true,
@ -70,17 +94,17 @@ class SearchType extends AbstractType
'label_attr' => ['class' => 'float-left'],
'data' => $session->get('order_search_status'),
'apply_filter' => function (QueryInterface $filterQuery, $field, $values) use ($session) {
$session->set('order_search_status', $values['value']);
if (empty($values['value'])) {
return null;
}
$filterQuery->getQueryBuilder()
->andWhere("
orders.status in (" . implode(",", $values['value']) . ")
");
}
$filterQuery->getQueryBuilder()->andWhere('
orders.status in ('
. implode(',', $values['value'])
. ')
');
},
));
}
@ -94,7 +118,7 @@ class SearchType extends AbstractType
$resolver->setDefaults(array(
'translation_domain' => 'status',
'choice_translation_domain' => 'status',
'csrf_protection' => false
'csrf_protection' => false,
));
}
}

View File

@ -1,14 +1,12 @@
psc_shop_order_backend:
resource: "@PSCShopOrderBundle/Controller/Backend"
type: attribute
prefix: /backend/order
psc_shop_order:
resource: "@PSCShopOrderBundle/Controller"
type: attribute
prefix: /order
psc_shop_order_backend:
resource: "@PSCShopOrderBundle/Controller/Backend"
type: attribute
prefix: /backend/order
psc_shop_order_api:
resource: "@PSCShopOrderBundle/Api"

View File

@ -61,41 +61,102 @@
{% endif %}
</div>
{# Auftragsdetails #}
{% set order = orderpos.order %}
<div class="rounded-lg border bg-white shadow-sm">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-base font-semibold text-gray-800">Auftragsdetails</h2>
</div>
<div class="divide-y divide-gray-100 text-sm">
<div class="grid grid-cols-2 px-6 py-3 gap-4">
<span class="text-gray-500">Auftragsnummer</span>
<span class="font-medium text-gray-800">{{ order.alias }}</span>
</div>
<div class="grid grid-cols-2 px-6 py-3 gap-4">
<span class="text-gray-500">Auftrag-ID</span>
<span class="font-medium text-gray-800">{{ order.uid }}</span>
</div>
<div class="grid grid-cols-2 px-6 py-3 gap-4">
<span class="text-gray-500">Datum</span>
<span class="font-medium text-gray-800">{{ order.created|date('d.m.Y H:i') }}</span>
</div>
<div class="grid grid-cols-2 px-6 py-3 gap-4">
<span class="text-gray-500">Kunde</span>
<div>
<p class="font-medium text-gray-800">{{ order.contact.firstname }} {{ order.contact.lastname }}</p>
<p class="text-gray-500 text-xs">{{ order.contact.username }}</p>
</div>
</div>
{% set inv = order.invoiceAddress %}
{% if inv %}
<div class="grid grid-cols-2 px-6 py-3 gap-4">
<span class="text-gray-500">Rechnungsadresse</span>
<div class="text-gray-800 leading-snug">
{% if inv.company %}<p class="font-medium">{{ inv.company }}</p>{% endif %}
<p>{{ inv.firstname }} {{ inv.lastname }}</p>
<p>{{ inv.street }} {{ inv.houseNumber }}</p>
<p>{{ inv.zip }} {{ inv.city }}</p>
</div>
</div>
{% endif %}
{% if order.basketfield1 or order.basketfield2 %}
<div class="grid grid-cols-2 px-6 py-3 gap-4">
<span class="text-gray-500">Kostenstelle</span>
<span class="font-medium text-gray-800">{{ order.basketfield1 }} {{ order.basketfield2 }}</span>
</div>
{% endif %}
</div>
</div>
{# Positionsdetails #}
<div class="rounded-lg border bg-white shadow-sm">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-base font-semibold text-gray-800">Positionsdetails</h2>
<h2 class="text-base font-semibold text-gray-800">Position</h2>
</div>
<div class="px-6 py-4 space-y-3 text-sm">
<div class="flex justify-between">
<div class="divide-y divide-gray-100 text-sm">
<div class="grid grid-cols-2 px-6 py-3 gap-4">
<span class="text-gray-500">Produkt</span>
<span class="font-medium text-gray-800">{{ orderpos.product.title }}</span>
</div>
<div class="flex justify-between">
<div class="grid grid-cols-2 px-6 py-3 gap-4">
<span class="text-gray-500">Menge</span>
<span class="font-medium text-gray-800">{{ orderpos.count }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">Preis (netto)</span>
<div class="grid grid-cols-2 px-6 py-3 gap-4">
<span class="text-gray-500">Preis netto</span>
<span class="font-medium text-gray-800">{{ orderpos.priceAllNetto|number_format(2, ',', '.') }} €</span>
</div>
{% if orderpos.order %}
<div class="flex justify-between">
<span class="text-gray-500">Auftragsnummer</span>
<span class="font-medium text-gray-800">{{ orderpos.order.alias }}</span>
<div class="grid grid-cols-2 px-6 py-3 gap-4">
<span class="text-gray-500">Preis brutto</span>
<span class="font-medium text-gray-800">{{ orderpos.priceAllBrutto|number_format(2, ',', '.') }} €</span>
</div>
{% if articleCalc %}
<div class="grid grid-cols-2 px-6 py-3 gap-4">
<span class="text-gray-500">Konfiguration</span>
<ul class="space-y-1 text-gray-800">
{% for opt in articleCalc.getOptions %}
{% if opt is not instanceof('\\PSC\\Library\\Calc\\Option\\Type\\Hidden') and opt.isValid() %}
<li class="flex gap-2">
<span class="text-gray-500">{{ opt.name }}:</span>
<span class="font-medium">{{ opt.value }}</span>
</li>
{% endif %}
{% endfor %}
</ul>
</div>
{% endif %}
</div>
</div>
{# Uploads #}
{% if orderpos.uploads|length > 0 %}
{% if position.uploadTypeObject %}
{% if position.uploadTypeObject.uploads|length > 0 %}
<div class="rounded-lg border bg-white shadow-sm">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-base font-semibold text-gray-800">Druckdaten ({{ orderpos.uploads|length }})</h2>
<h2 class="text-base font-semibold text-gray-800">Druckdaten ({{ position.uploadTypeObject.uploads|length }})</h2>
</div>
<ul class="divide-y divide-gray-100">
{% for upload in orderpos.uploads %}
{% for upload in position.uploadTypeObject.uploads %}
<li class="px-6 py-3 flex items-center justify-between gap-4">
<div class="flex items-center gap-3 min-w-0">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
@ -103,12 +164,12 @@
<path stroke-linecap="round" stroke-linejoin="round"
d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"/>
</svg>
<span class="text-sm text-gray-800 truncate">{{ upload.name }}</span>
<span class="text-sm text-gray-800 truncate">{{ upload.filename }}</span>
{% if upload.typ %}
<span class="text-xs text-gray-400 shrink-0">{{ upload.typ }}</span>
{% endif %}
</div>
<a href="{{ path('psc_shop_order_external_approval_download', {uuid: orderpos.uuid, uploadUuid: upload.uuid}) }}"
<a href="{{ upload.path }}"
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium text-white bg-green-600 hover:bg-green-700 transition-colors shrink-0">
<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">
@ -122,16 +183,24 @@
</ul>
</div>
{% endif %}
{% endif %}
{# Aktionen #}
{% if orderpos.externalApprovalStatus is null %}
<div class="rounded-lg border bg-white shadow-sm p-6">
<h2 class="text-base font-semibold text-gray-800 mb-4">Ihre Entscheidung</h2>
<form method="post" action="{{ path('psc_shop_order_external_approval_show', {uuid: orderpos.uuid}) }}">
<div class="mb-4">
<label for="approval-message" class="block text-sm font-medium text-gray-700 mb-1">
Mitteilung <span class="text-gray-400 font-normal">(optional)</span>
</label>
<textarea id="approval-message" name="message" rows="3"
placeholder="z.B. Grund für Ablehnung oder Anmerkung zur Freigabe …"
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-gray-400 resize-none"></textarea>
</div>
<div class="flex gap-4">
<form method="post" action="{{ path('psc_shop_order_external_approval_show', {uuid: orderpos.uuid}) }}" class="flex-1">
<input type="hidden" name="action" value="approve">
<button type="submit"
class="w-full inline-flex items-center justify-center gap-2 px-5 py-3 rounded-lg text-sm font-semibold text-white bg-green-600 hover:bg-green-700 transition-colors shadow-sm">
<button type="submit" name="action" value="approve"
class="flex-1 inline-flex items-center justify-center gap-2 px-5 py-3 rounded-lg text-sm font-semibold text-white bg-green-600 hover:bg-green-700 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-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round"
@ -139,11 +208,8 @@
</svg>
Freigeben
</button>
</form>
<form method="post" action="{{ path('psc_shop_order_external_approval_show', {uuid: orderpos.uuid}) }}" class="flex-1">
<input type="hidden" name="action" value="reject">
<button type="submit"
class="w-full inline-flex items-center justify-center gap-2 px-5 py-3 rounded-lg text-sm font-semibold text-white bg-red-600 hover:bg-red-700 transition-colors shadow-sm">
<button type="submit" name="action" value="reject"
class="flex-1 inline-flex items-center justify-center gap-2 px-5 py-3 rounded-lg text-sm font-semibold text-white bg-red-600 hover:bg-red-700 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-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round"
@ -151,8 +217,13 @@
</svg>
Ablehnen
</button>
</div>
</form>
</div>
{% elseif orderpos.externalApprovalMessage %}
<div class="rounded-lg border bg-white shadow-sm p-6">
<h2 class="text-base font-semibold text-gray-800 mb-2">Mitteilung</h2>
<p class="text-sm text-gray-700 whitespace-pre-line">{{ orderpos.externalApprovalMessage }}</p>
</div>
{% endif %}

View File

@ -41,6 +41,7 @@ use PSC\System\SettingsBundle\Service\Log;
use PSC\System\SettingsBundle\Service\Printing;
use PSC\System\SettingsBundle\Service\TemplateVars;
use Ramsey\Uuid\Uuid;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Bridge\Twig\Mime\WrappedTemplatedEmail;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
@ -394,7 +395,7 @@ class Mail implements QueueInterface, ConfigurableElementInterface
'shop' => $shop,
];
try {
$message = new Email()
$message = new TemplatedEmail()
->subject($subject->render($params))
->from($from->render($params))
->to(trim($to->render($params)));
@ -476,7 +477,7 @@ class Mail implements QueueInterface, ConfigurableElementInterface
'shop' => $shop,
];
try {
$message = new Email()
$message = new TemplatedEmail()
->subject($subject->render($params))
->from($from->render($params))
->to(trim($to->render($params)));
@ -551,7 +552,7 @@ class Mail implements QueueInterface, ConfigurableElementInterface
'shop' => $shop,
];
try {
$message = new Email()
$message = new TemplatedEmail()
->subject($subject->render($params))
->from($from->render($params))
->to(trim($to->render($params)));
@ -628,7 +629,7 @@ class Mail implements QueueInterface, ConfigurableElementInterface
'shop' => $shop,
];
try {
$message = new Email()
$message = new TemplatedEmail()
->subject($subject->render($params))
->from($from->render($params))
->to(trim($to->render($params)));
@ -702,7 +703,7 @@ class Mail implements QueueInterface, ConfigurableElementInterface
$params['doc'] = $objDoc;
$params['packageLink'] = '/apps/backend/order/detail/package/printpartner/' . $event->getPosition();
try {
$message = new Email()
$message = new TemplatedEmail()
->subject($subject->render($params))
->from($from->render($params))
->to($to->render($params));
@ -746,7 +747,7 @@ class Mail implements QueueInterface, ConfigurableElementInterface
$position = $positionRepo->findOneBy(['uuid' => $event->getPosition()]);
$templateVars->loadOrder($position->getOrder()->getUuid());
try {
$message = new Email()
$message = new TemplatedEmail()
->subject($subject->render($templateVars->getPosTwigVars($position->getUuid())))
->from($from->render($templateVars->getPosTwigVars($position->getUuid())))
->to($to->render($templateVars->getPosTwigVars($position->getUuid())));
@ -765,7 +766,7 @@ class Mail implements QueueInterface, ConfigurableElementInterface
if ($html) {
$message->html($html->render(array_merge($templateVars->getPosTwigVars($position->getUuid(), [
'email' => new WrappedTemplatedEmail($this->_template, $html),
'email' => new WrappedTemplatedEmail($this->_template, $message),
]))));
}
@ -916,7 +917,7 @@ class Mail implements QueueInterface, ConfigurableElementInterface
) {
$templateVars->loadOrder($event->getOrder());
try {
$message = new Email()
$message = new TemplatedEmail()
->subject($subject->render($templateVars->getTwigVars()))
->from($from->render($templateVars->getTwigVars()))
->to(trim($to->render($templateVars->getTwigVars())));
@ -935,7 +936,7 @@ class Mail implements QueueInterface, ConfigurableElementInterface
if ($html) {
$message->html($html->render(array_merge($templateVars->getTwigVars(), [
'email' => new WrappedTemplatedEmail($this->_template, $html),
'email' => new WrappedTemplatedEmail($this->_template, $message),
])));
}
@ -1265,7 +1266,6 @@ class Mail implements QueueInterface, ConfigurableElementInterface
],
);
$this->_mailer->send($message);
die();
} catch (\Exception $e) {
$this->_logService->createLogEntry(
$templateVars->getOrder()->getShop(),

View File

@ -0,0 +1,11 @@
<?php
namespace PSC\System\UpdateBundle\Migrations;
class Version20260311100002 extends Base
{
public function migrateDatabase(): void
{
$this->entityManager->getConnection()->executeQuery("ALTER TABLE orderspos ADD COLUMN IF NOT EXISTS external_approval_message TEXT NULL DEFAULT NULL;");
}
}

View File

@ -49,13 +49,18 @@
</ul>
{% if is_granted('ROLE_ADMIN') %}
{{ render(controller('\\PSC\\System\\SettingsBundle\\Controller\\FtpController::myDataTailwindAction')) }}
<hr/>
{% endif %}
<p class="text-center text-psc">{{ date("now")|date('d.m.Y H:i:s') }}</p>
<p class="mt-1 text-center">
<hr/>
<p class="text-left ml-5 text-psc">{{ date("now")|date('d.m.Y H:i:s') }}</p>
<p class="mt-1 ml-5 text-left">
{% if is_granted('ROLE_ADMIN') %}
<a href="{{ path('psc_system_version_backend_changelog') }}" class="text-xs text-gray-400 hover:text-psc-500 transition-colors">
v{{ versionService.release }} ({{ versionService.datum }})
</a>
{% else %}
<span class="text-xs text-gray-400 hover:text-psc-500 transition-colors">v{{ versionService.release }} ({{ versionService.datum }})</span>
{% endif %}
</p>
</nav>
</aside>
@ -72,8 +77,10 @@
<div class="mr-3">
{{ render(controller('PSC\\Backend\\DashboardBundle\\Controller\\ShopController::myEditableShopsTailwindAction', { tw: true })) }}
</div>
{% if is_granted('ROLE_ADMIN') %}
<div class="hidden md:block swarm">
</div>
{% endif %}
</div>
<div>
{% if app.request.locale == 'de_DE' %}

View File

@ -0,0 +1,31 @@
<?php
namespace Plugin\Custom\PSC\CollectLayouter\Model;
use PSC\Shop\OrderBundle\Model\Order\Position\Upload as BaseUpload;
use PSC\Shop\OrderBundle\Model\Order\Position\Upload\IUploadTypeObject;
class Upload implements IUploadTypeObject
{
public array $uploads = [];
public function canPreview(): bool
{
return false;
}
public function getCode(): string
{
return 102;
}
public function getName(): string
{
return 'CollectLayouter';
}
public function addUpload(BaseUpload $upload): void
{
$this->uploads[] = $upload;
}
}

View File

@ -18,7 +18,7 @@ use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormBuilderInterface;
require_once (__DIR__ .'/../../../../../../src/PSC/Shop/EntityBundle/Lagacy/TP_Basket_Item.php');
require_once __DIR__ . '/../../../../../../src/PSC/Shop/EntityBundle/Lagacy/TP_Basket_Item.php';
#[AutoconfigureTag('queues')]
class Render implements QueueInterface, ConfigurableElementInterface
@ -34,7 +34,6 @@ class Render implements QueueInterface, ConfigurableElementInterface
*/
private $_pdf;
function __construct(EntityManagerInterface $entityManager, Pdf $pdf)
{
$this->_entityManager = $entityManager;
@ -67,25 +66,13 @@ class Render implements QueueInterface, ConfigurableElementInterface
return 'Form Based Renderer';
}
public function getForm(FormBuilderInterface $builder, $form_options, EventInterface $event)
{
public function getForm(FormBuilderInterface $builder, $form_options, EventInterface $event) {}
}
public function injectDocument(Form $form, EventInterface $event, Queue $objQueue) {}
public function injectDocument(Form $form, EventInterface $event, Queue $objQueue)
{
public function setFormData(Form $form, EventInterface $event, Queue $queueObj) {}
}
public function setFormData(Form $form, EventInterface $event, Queue $queueObj)
{
}
public function getTemplate()
{
}
public function getTemplate() {}
/**
* @param EventInterface $event
@ -94,31 +81,46 @@ class Render implements QueueInterface, ConfigurableElementInterface
*/
public function execute(EventInterface $event, Queue $doc)
{
try {
set_time_limit(0);
$orderspos = $this->_entityManager->getRepository('PSC\Shop\EntityBundle\Entity\Orderpos')
$orderspos = $this->_entityManager
->getRepository('PSC\Shop\EntityBundle\Entity\Orderpos')
->findby(['layouterMode' => 102, 'renderPrint' => 1]);
/** @var Orderpos $pos */
foreach ($orderspos as $pos) {
/** @var \TP_Basket_Item $objPosition */
$objPosition = unserialize(($pos->getData()));
if($objPosition->getLayouterId() != "") {
if(!file_exists('/data/www/old/market/steplayouter/basket/' . $pos->getOrder()->getUid() . '/' . $pos->getPos())) {
mkdir('/data/www/old/market/steplayouter/basket/' . $pos->getOrder()->getUid() . '/' . $pos->getPos(), 0777, true);
$objPosition = unserialize($pos->getData());
if ($objPosition->getLayouterId() != '') {
if (!file_exists(
'/data/www/old/market/steplayouter/basket/' . $pos->getOrder()->getUid() . '/' . $pos->getPos(),
)) {
mkdir(
'/data/www/old/market/steplayouter/basket/' . $pos->getOrder()->getUid() . '/'
. $pos->getPos(),
0777,
true,
);
}
$layoutDesignData = $this->_entityManager
->getRepository(Layoutdesigndata::class)->findOneBy(array('uuid' => $objPosition->getLayouterId()));
->getRepository(Layoutdesigndata::class)
->findOneBy(array('uuid' => $objPosition->getLayouterId()));
if ($layoutDesignData) {
$data = $layoutDesignData->getDesign();
$outfile = '/data/www/old/market/steplayouter/basket/' . $pos->getOrder()->getUid() . '/' . $pos->getPos() . '/' . $pos->getOrder()->getAlias() . '_' . $pos->getPos() . '.pdf';
$outfile =
'/data/www/old/market/steplayouter/basket/'
. $pos->getOrder()->getUid()
. '/'
. $pos->getPos()
. '/'
. $pos->getOrder()->getAlias()
. '_'
. $pos->getPos()
. '.pdf';
copy($data['pdf'], $outfile);
}
@ -127,11 +129,7 @@ class Render implements QueueInterface, ConfigurableElementInterface
$this->_entityManager->persist($pos);
$this->_entityManager->flush();
}
}
} catch (\Exception $e) {
echo $e->getLine();
echo $e->getMessage();
@ -145,5 +143,5 @@ class Render implements QueueInterface, ConfigurableElementInterface
{
return $this->_error;
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace Plugin\Custom\PSC\CollectLayouter\Service;
use Plugin\Custom\PSC\CollectLayouter\Transformer\CollectLayouter;
use PSC\Shop\EntityBundle\Document\Product as ProductDoc;
use PSC\Shop\EntityBundle\Entity\Product as ProductEntity;
use PSC\Shop\ProductBundle\Interfaces\IUploadOption;
use PSC\Shop\ProductBundle\Transformer\Order\Position\IUploadOptionTransformer;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[AutoconfigureTag('product.upload.option')]
class CollectLayouterUpload implements IUploadOption
{
public function getType(): string
{
return 102;
}
public function getLabel(): string
{
return 'CollectLayouter Designer';
}
public function getTransformer(): IUploadOptionTransformer
{
return new CollectLayouter();
}
public function getContentUrl(): ?string
{
return null;
}
public function isEnabled(ProductEntity $entity, ?ProductDoc $document): bool
{
return (bool) $document->getPluginSettingModule('collectlayouter', 'uploadCollectLayouter');
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace Plugin\Custom\PSC\CollectLayouter\Transformer;
use Plugin\Custom\PSC\CollectLayouter\Model\Upload as PluginUpload;
use PSC\Shop\EntityBundle\Document\Position as PosDoc;
use PSC\Shop\EntityBundle\Entity\Orderpos;
use PSC\Shop\OrderBundle\Model\Order\Position;
use PSC\Shop\OrderBundle\Model\Order\Position\Upload;
use PSC\Shop\ProductBundle\Transformer\Order\Position\IUploadOptionTransformer;
class CollectLayouter implements IUploadOptionTransformer
{
public function fromDb(Position $position, Orderpos $posEntity, PosDoc $posDoc): void
{
$upload = new PluginUpload();
$outDir =
'/data/www/old/market/steplayouter/basket/' . $posEntity->getOrder()->getUid() . '/' . $posEntity->getPos();
$outFile = $outDir . '/' . $posEntity->getOrder()->getAlias() . '_' . $posEntity->getPos() . '.pdf';
if (file_exists($outFile)) {
$up = new Upload();
$up->setFileName($posEntity->getOrder()->getAlias() . '_' . $posEntity->getPos() . '.pdf');
$up->setTyp('pdf');
$up->setPath(
'/apps/market/steplayouter/basket/'
. $posEntity->getOrder()->getUid()
. '/'
. $posEntity->getPos()
. '/'
. $posEntity->getOrder()->getAlias()
. '_'
. $posEntity->getPos()
. '.pdf',
);
$upload->addUpload($up);
}
$position->setUploadTypeObject($upload);
}
public function toDb(Position $position, Orderpos $posEntity, PosDoc $posDoc): void {}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Plugin\Custom\PSC\LaufkartenLayouter\Model;
use PSC\Shop\OrderBundle\Model\Order\Position\Upload as BaseUpload;
use PSC\Shop\OrderBundle\Model\Order\Position\Upload\IUploadTypeObject;
class Upload implements IUploadTypeObject
{
public array $uploads = [];
public function canPreview(): bool
{
return false;
}
public function getCode(): string
{
return 106;
}
public function getName(): string
{
return 'Laufkartenlayouter';
}
public function addUpload(BaseUpload $upload): void
{
$this->uploads[] = $upload;
}
}

View File

@ -60,7 +60,6 @@ class Render implements QueueInterface, ConfigurableElementInterface
{
try {
set_time_limit(0);
$orderspos = $this->entityManager
->getRepository(Orderpos::class)
->findby(['layouterMode' => 106, 'renderPrint' => 1]);

View File

@ -0,0 +1,39 @@
<?php
namespace Plugin\Custom\PSC\LaufkartenLayouter\Service;
use Plugin\Custom\PSC\LaufkartenLayouter\Transformer\Laufkarten;
use PSC\Shop\EntityBundle\Document\Product as ProductDoc;
use PSC\Shop\EntityBundle\Entity\Product as ProductEntity;
use PSC\Shop\ProductBundle\Interfaces\IUploadOption;
use PSC\Shop\ProductBundle\Transformer\Order\Position\IUploadOptionTransformer;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[AutoconfigureTag('product.upload.option')]
class LaufkartenUpload implements IUploadOption
{
public function getType(): string
{
return 106;
}
public function getLabel(): string
{
return 'Laufkarten Designer';
}
public function getTransformer(): IUploadOptionTransformer
{
return new Laufkarten();
}
public function getContentUrl(): ?string
{
return null;
}
public function isEnabled(ProductEntity $entity, ?ProductDoc $document): bool
{
return (bool) $document->getPluginSettingModule('laufkartenlayouter', 'active');
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace Plugin\Custom\PSC\LaufkartenLayouter\Transformer;
use Plugin\Custom\PSC\LaufkartenLayouter\Model\Upload as PluginUpload;
use PSC\Shop\EntityBundle\Document\Position as PosDoc;
use PSC\Shop\EntityBundle\Entity\Orderpos;
use PSC\Shop\OrderBundle\Model\Order\Position;
use PSC\Shop\OrderBundle\Model\Order\Position\Upload;
use PSC\Shop\ProductBundle\Transformer\Order\Position\IUploadOptionTransformer;
class Laufkarten implements IUploadOptionTransformer
{
public function fromDb(Position $position, Orderpos $posEntity, PosDoc $posDoc): void
{
$upload = new PluginUpload();
$outDir =
'/data/www/old/market/steplayouter/basket/' . $posEntity->getOrder()->getUid() . '/' . $posEntity->getPos();
$outFile = $outDir . '/' . $posEntity->getOrder()->getAlias() . '_' . $posEntity->getPos() . '.pdf';
if (file_exists($outFile)) {
$up = new Upload();
$up->setFileName($posEntity->getOrder()->getAlias() . '_' . $posEntity->getPos() . '.pdf');
$up->setTyp('pdf');
$up->setPath(
'/apps/market/steplayouter/basket/'
. $posEntity->getOrder()->getUid()
. '/'
. $posEntity->getPos()
. '/'
. $posEntity->getOrder()->getAlias()
. '_'
. $posEntity->getPos()
. '.pdf',
);
$upload->addUpload($up);
}
$position->setUploadTypeObject($upload);
}
public function toDb(Position $position, Orderpos $posEntity, PosDoc $posDoc): void {}
}

View File

@ -57,10 +57,17 @@ class DesignController extends AbstractController
}
if (isset($output['data'])) {
foreach ($output['data']['__items'] as $item) {
if(strpos($item, 'folder')!== false) {
$data = $this->getItems($data, $item);
}
}
foreach ($output['data']['items'] as $item)
{
if (isset($item['unit'])) {
$data['designs'][] = $item;
}
}
}
return [
'designs' => $data,
'product' => $product,
@ -73,7 +80,7 @@ class DesignController extends AbstractController
if (isset($itemResult['data']['unit'])) {
$data['designs'][] = $itemResult['data'];
}
if (!isset($itemResult['data']['unit']) && isset($itemResult['data']['items'])) {
if (!isset($itemResult['data']['unit']) && isset($itemResult['data']['__items'])) {
$data['folders'][] = $itemResult['data'];
}
return $data;

View File

@ -104,6 +104,10 @@ class Producer implements IUiProducer, IProducerHydrateModel, ICalcNeedContact
*/
$specProd = $this->product->getSpecialProductTypeObject();
if (isset($specProd->getParams()['type'])) {
$this->engine->setActiveArticle($specProd->getParams()['type']);
}
$this->engine->setVariables($specProd->getParams());
if ($this->contact && $this->contact->getAccountType()->value > 1) {
$this->engine->setVariable('contact.accountType', $this->contact->getAccountType()->value);
@ -194,6 +198,21 @@ class Producer implements IUiProducer, IProducerHydrateModel, ICalcNeedContact
'required' => [],
];
if ($this->engine->getArticles()->count() > 1) {
$temp['properties']['type'] = [
'type' => 'string',
'title' => 'Typ',
'oneOf' => [],
'default' => $this->engine->getArticle()->getName(),
];
foreach ($this->engine->getArticles() as $article) {
$temp['properties']['type']['oneOf'][] = [
'title' => $article->getName(),
'const' => $article->getName(),
];
}
}
foreach ($this->engine->getArticle()->getOptions() as $option) {
if (!$option->isValid()) {
continue;

View File

@ -1037,6 +1037,10 @@ html {
margin-left: 1rem;
}
.ml-5{
margin-left: 1.25rem;
}
.ml-auto{
margin-left: auto;
}
@ -1190,6 +1194,10 @@ html {
height: 2.5rem;
}
.h-12{
height: 3rem;
}
.h-16{
height: 4rem;
}
@ -1274,6 +1282,10 @@ html {
width: 2.75rem;
}
.w-12{
width: 3rem;
}
.w-16{
width: 4rem;
}
@ -1521,6 +1533,10 @@ html {
cursor: pointer;
}
.resize-none{
resize: none;
}
.resize{
resize: both;
}
@ -1551,6 +1567,10 @@ html {
grid-template-columns: repeat(12, minmax(0, 1fr));
}
.grid-cols-2{
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grid-cols-4{
grid-template-columns: repeat(4, minmax(0, 1fr));
}
@ -1746,6 +1766,10 @@ html {
white-space: nowrap;
}
.whitespace-pre-line{
white-space: pre-line;
}
.whitespace-pre-wrap{
white-space: pre-wrap;
}
@ -1889,6 +1913,11 @@ html {
border-color: rgb(209 213 219 / var(--tw-border-opacity));
}
.border-green-200{
--tw-border-opacity: 1;
border-color: rgb(187 247 208 / var(--tw-border-opacity));
}
.border-psc-200{
--tw-border-opacity: 1;
border-color: rgb(226 128 119 / var(--tw-border-opacity));
@ -1988,6 +2017,11 @@ html {
background-color: rgb(220 252 231 / var(--tw-bg-opacity));
}
.bg-green-50{
--tw-bg-opacity: 1;
background-color: rgb(240 253 244 / var(--tw-bg-opacity));
}
.bg-green-600{
--tw-bg-opacity: 1;
background-color: rgb(22 163 74 / var(--tw-bg-opacity));
@ -2127,6 +2161,10 @@ html {
padding: 1.25rem;
}
.p-6{
padding: 1.5rem;
}
.p-8{
padding: 2rem;
}
@ -2346,6 +2384,10 @@ html {
line-height: 2.25rem;
}
.leading-snug{
line-height: 1.375;
}
.tracking-tight{
letter-spacing: -0.025em;
}
@ -2404,6 +2446,11 @@ html {
color: rgb(22 163 74 / var(--tw-text-opacity));
}
.text-green-700{
--tw-text-opacity: 1;
color: rgb(21 128 61 / var(--tw-text-opacity));
}
.text-green-800{
--tw-text-opacity: 1;
color: rgb(22 101 52 / var(--tw-text-opacity));
@ -3283,6 +3330,11 @@ html {
--tw-ring-color: rgb(229 231 235 / var(--tw-ring-opacity));
}
.focus\:ring-gray-400:focus{
--tw-ring-opacity: 1;
--tw-ring-color: rgb(156 163 175 / var(--tw-ring-opacity));
}
.focus\:ring-gray-500:focus{
--tw-ring-opacity: 1;
--tw-ring-color: rgb(107 114 128 / var(--tw-ring-opacity));

View File

@ -1,8 +1,15 @@
info:
datum: 10.03.2026
release: 2.3.3
datum: 19.03.2026
release: 2.3.4
changelog:
- version: 2.3.4
datum: 19.03.2026
changes:
- "Auftragssuche nach EMail"
- "Freigabeprozess für Externe implementiert (Aktion, Events)"
- "Kalkulation in Auftrag bearbeiten kann mehrere Produkte pro XML"
- "Kosmetische änderungen für eingeschränkte Benutzer"
- version: 2.3.3
datum: 10.03.2026
changes:

View File

@ -4807,7 +4807,7 @@ class BasketController extends TP_Controller_Action
$articleSession = new TP_Layoutersession();
$articleSess = $articleSession->getLayouterArticle($artikel->getLayouterId());
$art->layouter_mode = 4;
$art->layouter_mode = $articleSess->getLayouterModus();
if ($articleSess->getLayouterModus() == 2) {
$articleSess->copyToBasketStepLayouter($order, $art, $articleSess->getArticleId());
@ -5244,6 +5244,9 @@ class BasketController extends TP_Controller_Action
if ($article->confirmExternal) {
$art->status = 90;
$order->status = 90;
$order->save();
$art->save();
$dbMongo = TP_Mongo::getInstance();
$dbMongo->Job->insertOne(array(
'shop' => $this->shop->id,