This commit is contained in:
Thomas Peterson 2025-08-07 21:35:49 +02:00
parent 28f1b5e36a
commit 059c62459e
41 changed files with 1685 additions and 287 deletions

View File

@ -1,8 +0,0 @@
PSC\System\PluginBundle\Document\Plugin:
plugin_1:
name: XML Kalkulations Produkt
namespace: \Plugin\System\PSC\XmlCalc\Plugin
path: System/PSC/XmlCalc
pluginId: 19ff3fd21de9dbd7452fd0a67c928758
type: 0
installed: true

View File

@ -1,26 +1,10 @@
<?php <?php
/**
* PrintshopCreator Suite
*
* PHP Version 5.3
*
* @author Thomas Peterson <info@thomas-peterson.de>
* @copyright 2012-2013 PrintshopCreator GmbH
* @license Private
* @link http://www.printshopcreator.de
*/
namespace PSC\Shop\MediaBundle\Document; namespace PSC\Shop\MediaBundle\Document;
use Doctrine\ODM\MongoDB\Mapping\Annotations\Document;
use Doctrine\ODM\MongoDB\Mapping\Annotations\Field; use Doctrine\ODM\MongoDB\Mapping\Annotations\Field;
use Doctrine\ODM\MongoDB\Mapping\Annotations\Id; use Doctrine\ODM\MongoDB\Mapping\Annotations\Id;
use Doctrine\ODM\MongoDB\Mapping\Annotations\Document;
use Doctrine\ODM\MongoDB\Mapping\Annotations\EmbeddedDocument;
use Doctrine\ODM\MongoDB\Mapping\Annotations\EmbedOne;
use Doctrine\ODM\MongoDB\Mapping\Annotations\Index;
use Doctrine\ODM\MongoDB\Mapping\Annotations\ReferenceMany;
use Doctrine\ODM\MongoDB\Mapping\Annotations\ReferenceOne;
#[Document(collection: 'Media_Folder')] #[Document(collection: 'Media_Folder')]
class Folder class Folder
@ -28,23 +12,23 @@ class Folder
#[Id] #[Id]
protected $id; protected $id;
/** /**
* @var \DateTime $created; * @var \DateTime $created;
*/ */
#[Field(type: 'date')] #[Field(type: 'date')]
protected $created; protected $created;
/** /**
* @var \DateTime $updated; * @var \DateTime $updated;
*/ */
#[Field(type: 'date')] #[Field(type: 'date')]
protected $updated; protected $updated;
/** /**
* @var string $title * @var string $title
*/ */
#[Field(type: 'string')] #[Field(type: 'string')]
protected $title; protected $title;
/** /**
* @var string $icon * @var string $icon
*/ */
#[Field(type: 'string')] #[Field(type: 'string')]
protected $icon; protected $icon;
@ -54,6 +38,7 @@ class Folder
private $subFolders = []; private $subFolders = [];
private $media = 0; private $media = 0;
public function __construct() public function __construct()
{ {
$this->created = new \DateTime(); $this->created = new \DateTime();

View File

@ -13,17 +13,28 @@ class Help
$this->mongoManager = $mongoManager; $this->mongoManager = $mongoManager;
} }
public function getHelp(string $id, string $name): ?\PSC\System\SettingsBundle\Model\Help public function getHelp(null|string $id, string $name): null|\PSC\System\SettingsBundle\Model\Help
{ {
/** @var \PSC\System\SettingsBundle\Document\Help $doc */ /** @var \PSC\System\SettingsBundle\Document\Help $doc */
$doc = $this->mongoManager->getRepository(\PSC\System\SettingsBundle\Document\Help::class)->findOneBy(['name' => sprintf("%s_%s", $id, $name)]); $doc = null;
if ($id !== null) {
$doc = $this->mongoManager
->getRepository(\PSC\System\SettingsBundle\Document\Help::class)
->findOneBy(['name' => sprintf('%s_%s', $id, $name)]);
}
if (null === $doc) { if (null === $doc) {
$doc = $this->mongoManager->getRepository(\PSC\System\SettingsBundle\Document\Help::class)->findOneBy(['name' => $name]); $doc = $this->mongoManager
->getRepository(\PSC\System\SettingsBundle\Document\Help::class)
->findOneBy(['name' => $name]);
if ($doc === null) { if ($doc === null) {
return null; return null;
} }
} }
return new \PSC\System\SettingsBundle\Model\Help($doc->id, $doc->name, (string)$doc->helpText, (string)$doc->helpTitle); return new \PSC\System\SettingsBundle\Model\Help(
$doc->id,
$doc->name,
(string) $doc->helpText,
(string) $doc->helpTitle,
);
} }
} }

View File

@ -0,0 +1,62 @@
<?php
namespace Plugins\Custom\PSC\FormBuilder\Api;
use Faker\Factory;
use Faker\Generator;
use PSC\Shop\ContactBundle\Repository\ContactRepository;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Tests\RefreshDatabaseTrait;
class LayoutTest extends WebTestCase
{
use RefreshDatabaseTrait;
private Generator $faker;
public function setUp(): void
{
$this->faker = Factory::create(locale: 'de_DE');
}
public function testCreateLayout(): void
{
$client = static::createClient();
$userRepository = static::getContainer()->get(ContactRepository::class);
$testUser = $userRepository->loadUserByUsername('admin@shop.de');
$client->loginUser($testUser, 'api');
$name = $this->faker->slug();
$client->jsonRequest(
'POST',
'/api/plugin/custom/psc/formbuilder/layouts/add',
[
'title' => $name,
'shop' => 'shop1234',
'data' => [
'uuid' => '5678987656789',
'name' => 'testlayout',
'options' => [
[
'type' => 1,
'name' => 'Auflage',
'id' => 'auflage',
],
],
],
],
[],
);
$this->assertResponseIsSuccessful();
$data = json_decode($client->getResponse()->getContent(), true);
self::assertSame($name, $data['title']);
$client->jsonRequest('GET', '/api/plugin/custom/psc/formbuilder/layouts/all/shop1234', [], []);
$this->assertResponseIsSuccessful();
$data = json_decode($client->getResponse()->getContent(), true);
}
}

View File

@ -19,6 +19,7 @@ class DesignTest extends WebTestCase
'/api/plugin/system/psc/xmlcalc/product/design', '/api/plugin/system/psc/xmlcalc/product/design',
[ [
'product' => '01938686-0e4d-7da9-bae3-b2e1b1681f9f', 'product' => '01938686-0e4d-7da9-bae3-b2e1b1681f9f',
'shop' => '771a1176-d531-48ed-93b8-eec1fd4b917f',
'jsonProduct' => json_decode( 'jsonProduct' => json_decode(
'[{"uuid":"df2df718-b28e-482d-bf0c-67d246f05d32","name":"Test Artikel","options":[{"id":"auflage","name":"Auflage","default":"100","dependencys":[],"placeHolder":"Placeholder","required":false,"type":2},{"id":"seiten_umschlag","name":"Seiten Umschlag","default":"2","dependencys":[],"placeHolder":"Placeholder","required":false,"type":2},{"id":"seiten_anzahl_inhalt","name":"Seiten Anzahl Inhalt","default":"10","dependencys":[{"relation":"auflage","formula":"","borders":[{"calcValue":"","calcValue1":"","calcValue2":"","calcValue3":"","calcValue4":"","calcValue5":"","calcValue6":"","calcValue7":"","calcValue8":"","calcValue9":"","calcValue10":"","flatRate":"","formula":"$Vauflage$V*0.12","price":0,"value":"1-10","dependencys":[{"relation":"seiten_umschlag","formula":"","borders":[{"calcValue":"","calcValue1":"","calcValue2":"","calcValue3":"","calcValue4":"","calcValue5":"","calcValue6":"","calcValue7":"","calcValue8":"","calcValue9":"","calcValue10":"","flatRate":"","formula":"$Vseiten_umschlag$V*0.24","price":0,"value":"1-2","dependencys":[]},{"calcValue":"","calcValue1":"","calcValue2":"","calcValue3":"","calcValue4":"","calcValue5":"","calcValue6":"","calcValue7":"","calcValue8":"","calcValue9":"","calcValue10":"","flatRate":"","formula":"$Vseiten_umschlag$V*0.23","price":0,"value":"3-","dependencys":[]}]}]},{"calcValue":"","calcValue1":"","calcValue2":"","calcValue3":"","calcValue4":"","calcValue5":"","calcValue6":"","calcValue7":"","calcValue8":"","calcValue9":"","calcValue10":"","flatRate":"","formula":"$Vauflage$V*0.11","price":0,"value":"11-","dependencys":[{"relation":"seiten_umschlag","formula":"","borders":[{"calcValue":"","calcValue1":"","calcValue2":"","calcValue3":"","calcValue4":"","calcValue5":"","calcValue6":"","calcValue7":"","calcValue8":"","calcValue9":"","calcValue10":"","flatRate":"","formula":"$Vseiten_umschlag$V*0.21","price":0,"value":"1-2","dependencys":[]},{"calcValue":"","calcValue1":"","calcValue2":"","calcValue3":"","calcValue4":"","calcValue5":"","calcValue6":"","calcValue7":"","calcValue8":"","calcValue9":"","calcValue10":"","flatRate":"","formula":"$Vseiten_umschlag$V*0.20","price":0,"value":"3-","dependencys":[]}]}]}]}],"placeHolder":"Placeholder","required":true,"type":2},{"id":"farbigkeit","name":"Farbigkeit","default":"10","dependencys":[],"type":3,"options":[{"id":"10","name":"1\/0 farbig","dependencys":[{"relation":"auflage","formula":"","borders":[{"calcValue":"","calcValue1":"","calcValue2":"","calcValue3":"","calcValue4":"","calcValue5":"","calcValue6":"","calcValue7":"","calcValue8":"","calcValue9":"","calcValue10":"","flatRate":"","formula":"","price":0,"value":"1-101","dependencys":[]}]}]},{"id":"11","name":"1\/1 farbig","dependencys":[]},{"id":"20","name":"2\/0 farbig","dependencys":[]},{"id":"21","name":"2\/1 farbig","dependencys":[]},{"id":"22","name":"2\/2 farbig","dependencys":[{"relation":"auflage","formula":"","borders":[{"calcValue":"","calcValue1":"","calcValue2":"","calcValue3":"","calcValue4":"","calcValue5":"","calcValue6":"","calcValue7":"","calcValue8":"","calcValue9":"","calcValue10":"","flatRate":"","formula":"","price":0,"value":"11-50","dependencys":[]}]}]}],"mode":"normal"},{"id":"calc","name":"calc","dependencys":[{"relation":"auflage","formula":"","borders":[{"calcValue":"","calcValue1":"","calcValue2":"","calcValue3":"","calcValue4":"","calcValue5":"","calcValue6":"","calcValue7":"","calcValue8":"","calcValue9":"","calcValue10":"","flatRate":"","formula":"$Vauflage$V*$Vseiten_anzahl_inhalt$V","price":0,"value":"1-","dependencys":[]}]}],"type":1}]}]', '[{"uuid":"df2df718-b28e-482d-bf0c-67d246f05d32","name":"Test Artikel","options":[{"id":"auflage","name":"Auflage","default":"100","dependencys":[],"placeHolder":"Placeholder","required":false,"type":2},{"id":"seiten_umschlag","name":"Seiten Umschlag","default":"2","dependencys":[],"placeHolder":"Placeholder","required":false,"type":2},{"id":"seiten_anzahl_inhalt","name":"Seiten Anzahl Inhalt","default":"10","dependencys":[{"relation":"auflage","formula":"","borders":[{"calcValue":"","calcValue1":"","calcValue2":"","calcValue3":"","calcValue4":"","calcValue5":"","calcValue6":"","calcValue7":"","calcValue8":"","calcValue9":"","calcValue10":"","flatRate":"","formula":"$Vauflage$V*0.12","price":0,"value":"1-10","dependencys":[{"relation":"seiten_umschlag","formula":"","borders":[{"calcValue":"","calcValue1":"","calcValue2":"","calcValue3":"","calcValue4":"","calcValue5":"","calcValue6":"","calcValue7":"","calcValue8":"","calcValue9":"","calcValue10":"","flatRate":"","formula":"$Vseiten_umschlag$V*0.24","price":0,"value":"1-2","dependencys":[]},{"calcValue":"","calcValue1":"","calcValue2":"","calcValue3":"","calcValue4":"","calcValue5":"","calcValue6":"","calcValue7":"","calcValue8":"","calcValue9":"","calcValue10":"","flatRate":"","formula":"$Vseiten_umschlag$V*0.23","price":0,"value":"3-","dependencys":[]}]}]},{"calcValue":"","calcValue1":"","calcValue2":"","calcValue3":"","calcValue4":"","calcValue5":"","calcValue6":"","calcValue7":"","calcValue8":"","calcValue9":"","calcValue10":"","flatRate":"","formula":"$Vauflage$V*0.11","price":0,"value":"11-","dependencys":[{"relation":"seiten_umschlag","formula":"","borders":[{"calcValue":"","calcValue1":"","calcValue2":"","calcValue3":"","calcValue4":"","calcValue5":"","calcValue6":"","calcValue7":"","calcValue8":"","calcValue9":"","calcValue10":"","flatRate":"","formula":"$Vseiten_umschlag$V*0.21","price":0,"value":"1-2","dependencys":[]},{"calcValue":"","calcValue1":"","calcValue2":"","calcValue3":"","calcValue4":"","calcValue5":"","calcValue6":"","calcValue7":"","calcValue8":"","calcValue9":"","calcValue10":"","flatRate":"","formula":"$Vseiten_umschlag$V*0.20","price":0,"value":"3-","dependencys":[]}]}]}]}],"placeHolder":"Placeholder","required":true,"type":2},{"id":"farbigkeit","name":"Farbigkeit","default":"10","dependencys":[],"type":3,"options":[{"id":"10","name":"1\/0 farbig","dependencys":[{"relation":"auflage","formula":"","borders":[{"calcValue":"","calcValue1":"","calcValue2":"","calcValue3":"","calcValue4":"","calcValue5":"","calcValue6":"","calcValue7":"","calcValue8":"","calcValue9":"","calcValue10":"","flatRate":"","formula":"","price":0,"value":"1-101","dependencys":[]}]}]},{"id":"11","name":"1\/1 farbig","dependencys":[]},{"id":"20","name":"2\/0 farbig","dependencys":[]},{"id":"21","name":"2\/1 farbig","dependencys":[]},{"id":"22","name":"2\/2 farbig","dependencys":[{"relation":"auflage","formula":"","borders":[{"calcValue":"","calcValue1":"","calcValue2":"","calcValue3":"","calcValue4":"","calcValue5":"","calcValue6":"","calcValue7":"","calcValue8":"","calcValue9":"","calcValue10":"","flatRate":"","formula":"","price":0,"value":"11-50","dependencys":[]}]}]}],"mode":"normal"},{"id":"calc","name":"calc","dependencys":[{"relation":"auflage","formula":"","borders":[{"calcValue":"","calcValue1":"","calcValue2":"","calcValue3":"","calcValue4":"","calcValue5":"","calcValue6":"","calcValue7":"","calcValue8":"","calcValue9":"","calcValue10":"","flatRate":"","formula":"$Vauflage$V*$Vseiten_anzahl_inhalt$V","price":0,"value":"1-","dependencys":[]}]}],"type":1}]}]',
), ),

View File

@ -0,0 +1,35 @@
<?php
namespace Plugins\System\PSC\XmlCalc\Api;
use PSC\Shop\ContactBundle\Repository\ContactRepository;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Tests\RefreshDatabaseTrait;
class PreviewTest extends WebTestCase
{
use RefreshDatabaseTrait;
public function testSimplePreview(): void
{
$client = static::createClient();
$client->jsonRequest(
'POST',
'/api/plugin/system/psc/xmlcalc/product/pd',
[
'shop' => '771a1176-d531-48ed-93b8-eec1fd4b917f',
'json' => json_decode(
'[{"uuid":"2db72c84-67a9-4fcf-99da-9307255572af","name":"2f9152d2-2ce4-42ea-8f04-69484e0e8577","options":[{"id":"auflage","name":"Auflage","default":"100","dependencys":[{"relation":"auflage","formula":"","borders":[{"calcValue":"","calcValue1":"","calcValue2":"","calcValue3":"","calcValue4":"","calcValue5":"","calcValue6":"","calcValue7":"","calcValue8":"","calcValue9":"","calcValue10":"","flatRate":"","formula":"$Vauflage$V*0.12","price":0,"value":"1-","dependencys":[]}]}],"placeHolder":"Placeholder","required":true,"minValue":1,"maxValue":200,"minCalc":"","maxCalc":"","type":2}]}]',
),
'values' => ['auflage' => 100],
],
[],
);
$this->assertResponseIsSuccessful();
$data = json_decode($client->getResponse()->getContent(), true);
self::assertSame(1200, $data['netto']);
}
}

View File

@ -7,6 +7,7 @@ namespace Tests;
use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\DocumentManager;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Hautelook\AliceBundle\PhpUnit\BaseDatabaseTrait; use Hautelook\AliceBundle\PhpUnit\BaseDatabaseTrait;
use Plugin\Custom\PSC\FormBuilder\Document\Layout;
use PSC\Shop\ContactBundle\Model\AccountType; use PSC\Shop\ContactBundle\Model\AccountType;
use PSC\Shop\EntityBundle\Document\Contact; use PSC\Shop\EntityBundle\Document\Contact;
use PSC\Shop\EntityBundle\Document\Country; use PSC\Shop\EntityBundle\Document\Country;
@ -53,11 +54,35 @@ trait RefreshDatabaseTrait
$doc->getSchemaManager()->dropDocumentCollection(Order::class); $doc->getSchemaManager()->dropDocumentCollection(Order::class);
$doc->getSchemaManager()->dropDocumentCollection(Position::class); $doc->getSchemaManager()->dropDocumentCollection(Position::class);
$doc->getSchemaManager()->dropDocumentCollection(Instance::class); $doc->getSchemaManager()->dropDocumentCollection(Instance::class);
$doc->getSchemaManager()->dropDocumentCollection(Plugin::class);
$doc->getSchemaManager()->dropDocumentCollection(Shop::class); $doc->getSchemaManager()->dropDocumentCollection(Shop::class);
$doc->getSchemaManager()->dropDocumentCollection(Help::class); $doc->getSchemaManager()->dropDocumentCollection(Help::class);
$doc->getSchemaManager()->dropDocumentCollection(Country::class); $doc->getSchemaManager()->dropDocumentCollection(Country::class);
$doc->getSchemaManager()->dropDocumentCollection(Contact::class); $doc->getSchemaManager()->dropDocumentCollection(Contact::class);
$bulk = new \MongoDB\Driver\BulkWrite();
$bulk->insert([
'name' => 'XML Kalkulations Produkt',
'installed' => true,
'shouldBeDeInstalled' => false,
'shouldBeInstalled' => false,
'path' => 'System/PSC/XmlCalc',
'namespace' => '\\Plugin\\System\\PSC\\XmlCalc\\Plugin',
'pluginId' => '19ff3fd21de9dbd7452fd0a67c928758',
]);
$bulk->insert([
'name' => 'FormBuilder',
'installed' => true,
'shouldBeDeInstalled' => false,
'shouldBeInstalled' => false,
'path' => 'Custom/PSC/FormBuilder',
'namespace' => '\\Plugin\\Custom\\PSC\\FormBuilder\\Plugin',
'pluginId' => '19ff3fd21de9dbd7452fd0a67c928759',
]);
$doc->getClient()->getManager()->executeBulkWrite('psc_test.Plugin', $bulk);
$doc->getSchemaManager()->dropDocumentCollection(Layout::class);
if (!$doc->getRepository(Plugin::class)->findOneBy(['pluginId' => '19ff3fd21de9dbd7452fd0a67c928758'])) { if (!$doc->getRepository(Plugin::class)->findOneBy(['pluginId' => '19ff3fd21de9dbd7452fd0a67c928758'])) {
$plugin = new Plugin(); $plugin = new Plugin();
$plugin->setInstalled(true); $plugin->setInstalled(true);
@ -70,6 +95,18 @@ trait RefreshDatabaseTrait
$doc->clear(); $doc->clear();
} }
if (!$doc->getRepository(Plugin::class)->findOneBy(['pluginId' => '19ff3fd21de9dbd7452fd0a67c928759'])) {
$plugin = new Plugin();
$plugin->setInstalled(true);
$plugin->setPluginId('19ff3fd21de9dbd7452fd0a67c928759');
$plugin->setName('FormBuilder');
$plugin->setNamespace('\Plugin\Custom\PSC\FormBuilder\Plugin');
$plugin->setPath('Custom/PSC/FormBuilder');
$doc->persist($plugin);
$doc->flush();
$doc->clear();
}
$instance = new Instance(); $instance = new Instance();
$instance->setAppId('1'); $instance->setAppId('1');
$instance->setInvoiceNumberStart(1); $instance->setInvoiceNumberStart(1);

View File

@ -0,0 +1,50 @@
<?php
namespace Plugin\Custom\PSC\FormBuilder\Api\Layout;
use Doctrine\ODM\MongoDB\DocumentManager;
use Nelmio\ApiDocBundle\Attribute\Model;
use Nelmio\ApiDocBundle\Attribute\Security;
use OpenApi\Attributes\JsonContent;
use OpenApi\Attributes\RequestBody;
use OpenApi\Attributes\Response;
use OpenApi\Attributes\Tag;
use Plugin\Custom\PSC\FormBuilder\Document\Layout as PSCLayout;
use Plugin\Custom\PSC\FormBuilder\Dto\Layout\Input;
use Plugin\Custom\PSC\FormBuilder\Model\Layout;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class Add extends AbstractController
{
public function __construct(
private readonly DocumentManager $dm,
) {}
#[Response(response: 200, description: 'add layout', ref: Layout::class)]
#[Route(path: '/layouts/add', methods: ['POST'])]
#[Tag('FormBuilder')]
#[RequestBody(content: new Model(type: Input::class))]
#[ParamConverter('data', class: Input::class, converter: 'psc_rest.request_body')]
#[IsGranted('ROLE_ADMIN')]
#[Security(name: 'Security')]
public function add(Input $data)
{
$layout = new PSCLayout();
$layout->setTitle($data->title);
$layout->setData($data->data);
$layout->setShop($data->shop);
$this->dm->persist($layout);
$this->dm->flush();
$lModel = new Layout();
$lModel->setUuid($layout->getId());
$lModel->setTitle($layout->getTitle());
$lModel->setData($layout->getData());
$lModel->setShop($layout->getShop());
return $this->json($lModel);
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace Plugin\Custom\PSC\FormBuilder\Api\Layout;
use Doctrine\ODM\MongoDB\DocumentManager;
use MongoDB\BSON\ObjectId;
use Nelmio\ApiDocBundle\Attribute\Model;
use Nelmio\ApiDocBundle\Attribute\Security;
use OpenApi\Attributes\JsonContent;
use OpenApi\Attributes\Response;
use OpenApi\Attributes\Tag;
use Plugin\Custom\PSC\FormBuilder\Document\Layout;
use Plugin\Custom\PSC\FormBuilder\Dto\Layout\All as PluginAll;
use Plugin\Custom\PSC\FormBuilder\Model\Layout as PluginLayout;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class All extends AbstractController
{
public function __construct(
private readonly DocumentManager $dm,
) {}
#[Response(
response: 200,
description: 'get all layouts',
content: new JsonContent(ref: new Model(type: PluginAll::class)),
)]
#[Route(path: '/layouts/all/{uuid}', methods: ['GET'])]
#[Tag('FormBuilder')]
#[IsGranted('ROLE_ADMIN')]
#[Security(name: 'Bearer')]
public function all(string $uuid)
{
$layouts = $this->dm
->getRepository(Layout::class)
->createQueryBuilder('layout')
->field('shop')
->equals($uuid)
->sort('title', 'ASC')
->getQuery()
->execute();
$output = new PluginAll();
foreach ($layouts as $layout) {
$l = new PluginLayout();
$l->setTitle($layout->getTitle());
$l->setUuid($layout->getId());
$l->setData($layout->getData());
$l->setShop($layout->getShop());
$l->setJson(json_encode($layout->getData()));
$output->data[] = $l;
}
return $this->json($output);
}
}

View File

@ -0,0 +1,90 @@
<?php
namespace Plugin\Custom\PSC\FormBuilder\Document;
use Doctrine\ODM\MongoDB\Mapping\Annotations\Document;
use Doctrine\ODM\MongoDB\Mapping\Annotations\Field;
use Doctrine\ODM\MongoDB\Mapping\Annotations\Id;
#[Document(collection: 'formbuilder_layouts')]
class Layout
{
#[Id]
protected string $id;
#[Field(type: 'date')]
protected \DateTime $created;
#[Field(type: 'date')]
protected \DateTime $updated;
#[Field(type: 'string')]
protected string $title;
#[Field(type: 'string')]
protected string $shop;
#[Field(type: 'hash')]
protected array $data = [];
public function __construct()
{
$this->created = new \DateTime();
$this->updated = new \DateTime();
}
public function getTitle(): string
{
return $this->title;
}
public function setTitle(string $title): void
{
$this->title = $title;
}
public function getId(): string
{
return $this->id;
}
public function setId(string $id): void
{
$this->id = $id;
}
public function getCreated(): \DateTime
{
return $this->created;
}
public function setCreated(\DateTime $created): void
{
$this->created = $created;
}
public function getUpdated(): \DateTime
{
return $this->updated;
}
public function setUpdated(\DateTime $updated): void
{
$this->updated = $updated;
}
public function getData(): array
{
return $this->data;
}
public function setData(array $data): void
{
$this->data = $data;
}
public function getShop(): string
{
return $this->shop;
}
public function setShop(string $shop): void
{
$this->shop = $shop;
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace Plugin\Custom\PSC\FormBuilder\Dto\Layout;
use Nelmio\ApiDocBundle\Attribute\Model;
use OpenApi\Attributes\Items;
use OpenApi\Attributes\Property;
use Plugin\Custom\PSC\FormBuilder\Model\Layout;
use Symfony\Component\Serializer\Attribute\MaxDepth;
final class All
{
#[Property(type: 'array', items: new Items(ref: new Model(type: Layout::class)))]
#[MaxDepth(4)]
public array $data;
}

View File

@ -0,0 +1,21 @@
<?php
namespace Plugin\Custom\PSC\FormBuilder\Dto\Layout;
use OpenApi\Attributes\Items;
use OpenApi\Attributes\Property;
final class Input
{
#[Property(type: 'string')]
public string $title;
#[Property(type: 'string')]
public string $shop;
#[Property(type: 'array', items: new Items(
type: 'array',
items: new Items(),
))]
public array $data;
}

View File

@ -11,7 +11,7 @@ onMounted(() => {
const uuid = params.get('uuid') const uuid = params.get('uuid')
const shopUuid = params.get('shop') const shopUuid = params.get('shop')
const mode = params.get('mode') const mode = params.get('mode')
globalStore.setProductUuid(shopUuid) globalStore.setShopUuid(shopUuid)
globalStore.setMode(mode) globalStore.setMode(mode)
if (uuid) { if (uuid) {
globalStore.setProductUuid(uuid) globalStore.setProductUuid(uuid)
@ -21,8 +21,22 @@ onMounted(() => {
globalStore.loadFormulaAnalyserDataFromApi(uuid) globalStore.loadFormulaAnalyserDataFromApi(uuid)
} }
let debounceTimer: number;
const debounce = (func: Function, delay: number) => {
return function(this: any, ...args: any[]) {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => func.apply(this, args), delay);
};
};
const debouncedLoadPreview = debounce((json: object[]) => {
globalStore.loadPreview(json);
}, 500);
itemStore.$subscribe((mutation, state) => { itemStore.$subscribe((mutation, state) => {
globalStore.saveDesign(itemStore.loadJSON()); const json = itemStore.loadJSON();
globalStore.saveDesign(json);
debouncedLoadPreview(json);
}); });
}) })
</script> </script>

View File

@ -17,6 +17,9 @@ import XmlView from './app/XmlView.vue'
import ParameterView from './app/ParameterView.vue' import ParameterView from './app/ParameterView.vue'
import PaperDBView from './app/PaperDBView.vue' import PaperDBView from './app/PaperDBView.vue'
import FormelView from './app/FormelView.vue' import FormelView from './app/FormelView.vue'
import SaveLayoutDialog from './app/dialogs/SaveLayoutDialog.vue';
import LoadLayoutDialog from './app/dialogs/LoadLayoutDialog.vue';
import Preview from './app/preview/Preview.vue';
const globalStore = useGlobalStore() const globalStore = useGlobalStore()
</script> </script>
@ -62,7 +65,8 @@ const globalStore = useGlobalStore()
</div> </div>
<TabsContent value="designer" class="h-full overflow-y-auto"> <TabsContent value="designer" class="h-full overflow-y-auto">
<div class="flex h-full p-6"> <div class="flex h-full p-6">
<Main /> <Main v-if="!globalStore.showPreview" />
<Preview v-else />
</div> </div>
</TabsContent> </TabsContent>
<TabsContent value="preview" class="h-full overflow-y-auto"> <TabsContent value="preview" class="h-full overflow-y-auto">
@ -86,6 +90,8 @@ const globalStore = useGlobalStore()
<ElementProperties /> <ElementProperties />
<SpecialElementProperties /> <SpecialElementProperties />
<ElementDependency /> <ElementDependency />
<SaveLayoutDialog />
<LoadLayoutDialog />
</div> </div>
</div> </div>
</template> </template>

View File

@ -7,12 +7,17 @@ const globalStore = useGlobalStore()
function manualSave() { function manualSave() {
globalStore.manualSave() globalStore.manualSave()
} }
function openSaveLayoutDialog() {
globalStore.setShowSaveLayoutDialog(true)
}
</script> </script>
<template> <template>
<div class="w-full p-2 flex"> <div class="w-full p-2 flex gap-2">
<Button @click="manualSave" :disabled="globalStore.saving"> <Button @click="manualSave" :disabled="globalStore.saving">
{{ globalStore.saving ? $t('saving') : $t('save') }} {{ globalStore.saving ? $t('saving') : $t('save') }}
</Button> </Button>
<Button @click="openSaveLayoutDialog" variant="outline">{{ $t('save_layout') }}</Button>
</div> </div>
</template> </template>

View File

@ -0,0 +1,68 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { Button } from '../../ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle
} from '../../ui/dialog';
import { useGlobalStore } from '../../../stores/Global';
import { useItemStore } from '../../../stores/Items';
import { fetchLayouts } from '../../../lib/api';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const globalStore = useGlobalStore();
const itemStore = useItemStore();
const layouts = ref<any[]>([]);
const loadLayouts = async () => {
try {
const response: any = await fetchLayouts(globalStore.getShopUuid);
layouts.value = response.data;
} catch (error) {
console.error('Failed to fetch layouts', error);
}
};
const handleLoad = (layoutContent: string) => {
if (confirm(t('load_layout_confirm'))) {
itemStore.parseJSON(layoutContent);
globalStore.setShowLoadLayoutDialog(false);
}
};
const onOpenChange = (open: boolean) => {
if (!open) {
globalStore.setShowLoadLayoutDialog(false);
}
}
globalStore.$subscribe((mutation, state) => {
if(state.showLoadLayoutDialog) {
loadLayouts()
}
})
</script>
<template>
<Dialog :open="globalStore.showLoadLayoutDialog" @update:open="onOpenChange">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ $t('load_layout_title') }}</DialogTitle>
<DialogDescription>
{{ $t('load_layout_description') }}
</DialogDescription>
</DialogHeader>
<div class="grid gap-4 py-4">
<div v-for="layout in layouts" :key="layout.uuid" class="flex items-center justify-between">
<span>{{ layout.title }}</span>
<Button @click="handleLoad(layout.json)">{{ $t('load') }}</Button>
</div>
</div>
</DialogContent>
</Dialog>
</template>

View File

@ -0,0 +1,70 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Button } from '../../ui/button';
import { Input } from '../../ui/input';
import { Label } from '../../ui/label';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from '../../ui/dialog';
import { useGlobalStore } from '../../../stores/Global';
import { useItemStore } from '../../../stores/Items';
import { saveLayout } from '../../../lib/api';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const globalStore = useGlobalStore();
const itemStore = useItemStore();
const layoutName = ref('');
const handleSave = async () => {
if (!layoutName.value) {
alert(t('enter_layout_name_alert'));
return;
}
try {
const layoutContent = itemStore.loadJSON();
await saveLayout(layoutName.value, globalStore.getShopUuid, layoutContent);
globalStore.setShowSaveLayoutDialog(false);
layoutName.value = '';
} catch (error) {
console.error('Failed to save layout', error);
alert(t('save_layout_failed_alert'));
}
};
const onOpenChange = (open: boolean) => {
if (!open) {
globalStore.setShowSaveLayoutDialog(false);
}
}
</script>
<template>
<Dialog :open="globalStore.showSaveLayoutDialog" @update:open="onOpenChange">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ $t('save_layout_title') }}</DialogTitle>
<DialogDescription>
{{ $t('save_layout_description') }}
</DialogDescription>
</DialogHeader>
<div class="grid gap-4 py-4">
<div class="grid grid-cols-4 items-center gap-4">
<Label for="name" class="text-right">
{{ $t('name') }}
</Label>
<Input id="name" v-model="layoutName" class="col-span-3" />
</div>
</div>
<DialogFooter>
<Button @click="handleSave">{{ $t('save_layout') }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>

View File

@ -3,6 +3,7 @@ import { useGlobalStore } from '../../../stores/Global'
import { Image, Rows3, TableCellsMerge, SquareParking, SquareDot, SquareMenu, SquarePen, SquareChevronDown, SquareDashed, type LucideProps } from 'lucide-vue-next'; import { Image, Rows3, TableCellsMerge, SquareParking, SquareDot, SquareMenu, SquarePen, SquareChevronDown, SquareDashed, type LucideProps } from 'lucide-vue-next';
import { Switch } from '../../../components/ui/switch' import { Switch } from '../../../components/ui/switch'
import { Label } from '../../../components/ui/label' import { Label } from '../../../components/ui/label'
import { Button } from '../../../components/ui/button'
import { ref, watch, h } from 'vue' import { ref, watch, h } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
@ -20,6 +21,10 @@ function startDrag(event: DragEvent, item: string) {
globalStore.setDragMode("insert") globalStore.setDragMode("insert")
} }
function openLoadLayoutDialog() {
globalStore.setShowLoadLayoutDialog(true)
}
watch(previewMode, (newPreviewMode) => { watch(previewMode, (newPreviewMode) => {
if(newPreviewMode === false) { if(newPreviewMode === false) {
globalStore.setShowPreview(false) globalStore.setShowPreview(false)
@ -76,6 +81,10 @@ const renderIcon = (icon: any) => {
</div> </div>
</div> </div>
<div>
<Button @click="openLoadLayoutDialog" class="w-full">{{ $t('load_layout') }}</Button>
</div>
<div v-for="group in libraryItems" :key="group.category"> <div v-for="group in libraryItems" :key="group.category">
<h3 class="font-bold my-2">{{ $t(group.category) }}</h3> <h3 class="font-bold my-2">{{ $t(group.category) }}</h3>
<div v-for="element in group.elements" :key="element.id" <div v-for="element in group.elements" :key="element.id"

View File

@ -0,0 +1,42 @@
<script setup lang="ts">
import { computed, watch } from 'vue'
import { useGlobalStore } from '../../../stores/Global'
import { Codemirror } from 'vue-codemirror'
import { json } from '@codemirror/lang-json'
import RenderElements from './RenderElements.vue'
import PriceDisplay from './PriceDisplay.vue';
const globalStore = useGlobalStore()
const previewData = computed(() => globalStore.getPreviewData)
const error = computed(() => globalStore.previewError)
const isLoading = computed(() => globalStore.isPreviewLoading)
const items = computed(() => previewData.value?.elements || [])
</script>
<template>
<div class="w-full p-6 min-h-screen">
<div v-if="error" class="mb-4 bg-red-100 border-l-4 border-red-500 text-red-700 p-4" role="alert">
<p class="font-bold">Preview Error</p>
<p>{{ error }}</p>
</div>
<div v-if="isLoading" class="text-center py-10">
<p>Loading Preview...</p>
</div>
<div v-if="!isLoading && previewData">
<div class="overflow-auto h-full">
<div class="flex flex-row">
<div class="w-3/5 flex flex-col gap-2">
<RenderElements :items="items"/>
</div>
<div class="w-2/5 pl-6">
<PriceDisplay :tax="previewData.tax" :brutto="previewData.brutto" :netto="previewData.netto" />
</div>
</div>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,39 @@
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps<{
netto: any;
tax: any;
brutto: any;
}>();
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(value);
};
const netPrice = computed(() => formatCurrency(props.netto/100 || 0));
const tax = computed(() => formatCurrency(props.tax/100 || 0));
const grossPrice = computed(() => formatCurrency(props.brutto/100 || 0));
</script>
<template>
<div class="p-6 bg-gray-50 rounded-lg shadow-md">
<h3 class="text-lg font-semibold text-gray-800 mb-4">Preisübersicht</h3>
<div class="space-y-3">
<div class="flex justify-between items-center text-gray-600">
<span>Nettopreis</span>
<span class="font-medium text-gray-900">{{ netPrice }}</span>
</div>
<div class="flex justify-between items-center text-gray-600">
<span>+ MwSt. (19%)</span>
<span class="font-medium text-gray-900">{{ tax }}</span>
</div>
<hr class="my-3 border-t border-gray-200">
<div class="flex justify-between items-center text-xl font-bold">
<span class="text-gray-900">Gesamt</span>
<span class="text-primary">{{ grossPrice }}</span>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,38 @@
<script lang="ts" setup>
import { computed } from 'vue';
import TextElement from './elements/TextElement.vue'
import InputElement from './elements/InputElement.vue'
import HiddenElement from './elements/HiddenElement.vue'
import HeadlineElement from './elements/HeadlineElement.vue'
import SelectElement from './elements/SelectElement.vue'
const props = defineProps<{
items: any
}>()
</script>
<template>
<div class="d-flex flex flex-col relative" v-for="item in items" v-if="items.length > 0" :key="item.uuid">
<HeadlineElement
v-if="item.valid && item.htmlType === 'headline'"
:item="item"
/>
<HiddenElement
v-if="item.valid && item.htmlType === 'hidden'"
:item="item"
/>
<InputElement
v-if="item.valid && item.htmlType === 'input'"
:item="item"
/>
<TextElement
v-if="item.valid && item.htmlType === 'text'"
:item="item"
/>
<SelectElement
v-if="item.valid && item.htmlType === 'select'"
:item="item"
/>
</div>
</template>

View File

@ -0,0 +1,20 @@
<script lang="ts" setup>
import { computed } from 'vue';
const props = defineProps<{
item: any
}>()
</script>
<template>
<div class="flex gap-2 flex-row">
<h1 v-if="item.variant == '1'" class="text-4xl">{{item.defaultValue}}</h1>
<h6 v-else-if="item.variant == '6'" class="text-base">{{item.defaultValue}}</h6>
<h5 v-else-if="item.variant == '5'" class="text-lg">{{item.defaultValue}}</h5>
<h4 v-else-if="item.variant == '4'" class="text-xl">{{item.defaultValue}}</h4>
<h3 v-else-if="item.variant == '3'" class="text-2xl">{{item.defaultValue}}</h3>
<h2 v-else-if="item.variant == '2'" class="text-3xl">{{item.defaultValue}}</h2>
<h1 v-else class="text-4xl">{{item.defaultValue}}</h1>
</div>
</template>

View File

@ -0,0 +1,16 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { Input } from '../../../../components/ui/input'
const props = defineProps<{
item: any
}>()
</script>
<template>
<div class="flex gap-2 flex-row">
<Input type="hidden" name="item.name" id="item.id" value="item.value"/>
</div>
</template>

View File

@ -0,0 +1,19 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { Input } from '../../../../components/ui/input'
import { useGlobalStore } from '../../../../stores/Global'
const props = defineProps<{
item: any
}>()
const globalStore = useGlobalStore()
</script>
<template>
<div class="flex gap-2 flex-row items-center">
<label class="w-60 flex-inital">{{item.name}}</label>
<Input v-model:placeholder="item.placeHolder" v-model="item.value" v-model:name="item.name" v-model:id="item.id" v-model:required="item.required"/>
</div>
</template>

View File

@ -0,0 +1,36 @@
<script lang="ts" setup>
import { computed } from 'vue'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from '../../../../components/ui/select'
const props = defineProps<{
item: any
}>()
</script>
<template>
<div class="flex gap-2 flex-row items-center">
<label class="w-60 flex-inital">{{item.name}}</label>
<div class="w-full">
<Select v-model="item.rawValue">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem v-for="option in item.options" :key="option.uuid" :value="option.id">
{{option.name}}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</div>
</template>

View File

@ -0,0 +1,14 @@
<script lang="ts" setup>
import { computed } from 'vue';
const props = defineProps<{
item: any
}>()
</script>
<template>
<div class="flex gap-2 flex-row">
<p style="white-space: pre-line;">{{item.defaultValue}}</p>
</div>
</template>

View File

@ -34,18 +34,6 @@ const itemStore = useItemStore()
const globalStore = useGlobalStore() const globalStore = useGlobalStore()
const dragUuid = ref("") const dragUuid = ref("")
let isPreview = ref(false)
isPreview.value = globalStore.showPreview
globalStore.$subscribe((mutation, state) => {
if(state.showPreview) {
isPreview.value = true
}else{
isPreview.value = false
}
})
const startDrag = (event: DragEvent, uuid: string) => { const startDrag = (event: DragEvent, uuid: string) => {
event.dataTransfer!.dropEffect = 'move' event.dataTransfer!.dropEffect = 'move'
event.dataTransfer!.effectAllowed = 'move' event.dataTransfer!.effectAllowed = 'move'
@ -108,14 +96,14 @@ const editElementDependency = (item: BaseElement) => {
<div class="overflow-auto h-full"> <div class="overflow-auto h-full">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="d-flex flex flex-col relative items-center" v-for="item in items" v-if="items.length > 0" :key="item.uuid"> <div class="d-flex flex flex-col relative items-center" v-for="item in items" v-if="items.length > 0" :key="item.uuid">
<div class="w-full" v-if="item.type !== 1 || (item.type === 1 && !isPreview) "> <div class="w-full" v-if="item.type !== 1 || (item.type === 1) ">
<div class="h-8 group w-full" @dragleave.self="dragLeave($event, item.uuid)" @dragenter.self="dragEnter($event, item.uuid)" @drop="stopDrag($event, item.uuid)" v-if="!isPreview"> <div class="h-8 group w-full" @dragleave.self="dragLeave($event, item.uuid)" @dragenter.self="dragEnter($event, item.uuid)" @drop="stopDrag($event, item.uuid)">
<div class="inline-flex items-center justify-center w-full pointer-events-none"> <div class="inline-flex items-center justify-center w-full pointer-events-none">
<hr class="w-64 h-px my-2 bg-gray-200 border-0 dark:bg-gray-700 transition duration-200 pointer-events-none" :class="{ 'bg-orange-500': dragUuid == item.uuid }" > <hr class="w-64 h-px my-2 bg-gray-200 border-0 dark:bg-gray-700 transition duration-200 pointer-events-none" :class="{ 'bg-orange-500': dragUuid == item.uuid }" >
<span class="absolute px-3 font-medium text-gray-900 -translate-x-1/2 bg-white left-1/2 dark:text-white dark:bg-gray-900 pointer-events-none"><CirclePlus :class="{ 'text-orange-500': dragUuid == item.uuid }" class="transition duration-200 pointer-events-none" /></span> <span class="absolute px-3 font-medium text-gray-900 -translate-x-1/2 bg-white left-1/2 dark:text-white dark:bg-gray-900 pointer-events-none"><CirclePlus :class="{ 'text-orange-500': dragUuid == item.uuid }" class="transition duration-200 pointer-events-none" /></span>
</div> </div>
</div> </div>
<div :class="{ 'border-white' : !item.hasDependencys() || isPreview, 'border-blue-500': item.hasDependencys() && !isPreview }" @dragstart="startDrag($event, item.uuid)" draggable="true" class="element w-full flex flex-row border-l-2 hover:border-orange-500 pl-2 transition duration-500 min-h-5" v-bind:class="{ ' bg-slate-50': item.isFocused === true }"> <div :class="{ 'border-white' : !item.hasDependencys() , 'border-blue-500': item.hasDependencys() }" @dragstart="startDrag($event, item.uuid)" draggable="true" class="element w-full flex flex-row border-l-2 hover:border-orange-500 pl-2 transition duration-500 min-h-5" v-bind:class="{ ' bg-slate-50': item.isFocused === true }">
<div class="grow content-center items-center"> <div class="grow content-center items-center">
<InputElementForm <InputElementForm
v-if="item.type === 2" v-if="item.type === 2"
@ -154,7 +142,7 @@ const editElementDependency = (item: BaseElement) => {
v-model="item as MediaElement" v-model="item as MediaElement"
/> />
</div> </div>
<div class="buttons absolute rounded-sm invisible right-0 bg-slate-100/70 flex flex-row gap-2" v-if="!isPreview"> <div class="buttons absolute rounded-sm invisible right-0 bg-slate-100/70 flex flex-row gap-2">
<div v-on:click="editElementDependency(item)" :title="$t('dependencies')" class="m-2 cursor-pointer"> <div v-on:click="editElementDependency(item)" :title="$t('dependencies')" class="m-2 cursor-pointer">
<Option /> <Option />
</div> </div>

View File

@ -50,5 +50,15 @@
"sync": "Synchronisieren", "sync": "Synchronisieren",
"cms_elements": "CMS-Elemente", "cms_elements": "CMS-Elemente",
"form_elements": "Formular-Elemente", "form_elements": "Formular-Elemente",
"structure_elements": "Struktur-Elemente" "structure_elements": "Struktur-Elemente",
"save_layout": "Layout speichern",
"load_layout": "Vorlage laden",
"save_layout_title": "Layout speichern",
"save_layout_description": "Gib einen Namen für deine neue Layout-Vorlage ein.",
"load_layout_title": "Vorlage laden",
"load_layout_description": "Wähle eine Layout-Vorlage aus, um sie in den Designer zu laden.",
"load": "Laden",
"enter_layout_name_alert": "Bitte gib einen Namen für das Layout ein.",
"save_layout_failed_alert": "Layout konnte nicht gespeichert werden.",
"load_layout_confirm": "Möchtest du dieses Layout wirklich laden? Dein aktuelles Design wird überschrieben."
} }

View File

@ -50,5 +50,15 @@
"sync": "Sync", "sync": "Sync",
"cms_elements": "CMS Elements", "cms_elements": "CMS Elements",
"form_elements": "Form Elements", "form_elements": "Form Elements",
"structure_elements": "Structure Elements" "structure_elements": "Structure Elements",
"save_layout": "Save Layout",
"load_layout": "Load Layout",
"save_layout_title": "Save Layout",
"save_layout_description": "Enter a name for your new layout template.",
"load_layout_title": "Load Layout",
"load_layout_description": "Select a layout template to load it into the designer.",
"load": "Load",
"enter_layout_name_alert": "Please enter a name for the layout.",
"save_layout_failed_alert": "Failed to save layout.",
"load_layout_confirm": "Are you sure you want to load this layout? This will overwrite your current design."
} }

View File

@ -48,9 +48,9 @@ export const loadPriceFromApi = async (uuid: string) => {
} }
}; };
export const saveDesignToApi = async (uuid: string, json: object[]) => { export const saveDesignToApi = async (uuid: string, shopUuid: string, json: object[]) => {
try { try {
const response = await api.post('api/plugin/system/psc/xmlcalc/product/design', { json: { product: uuid, jsonProduct: json } }); const response = await api.post('api/plugin/system/psc/xmlcalc/product/design', { json: { product: uuid, shop: shopUuid, jsonProduct: json } });
return await response.json(); return await response.json();
} catch (error) { } catch (error) {
console.error('Error saving design to API:', error); console.error('Error saving design to API:', error);
@ -144,4 +144,34 @@ export const fetchMediaByFolder = async (folderId: string, page: number = 1) =>
} }
}; };
export const saveLayout = async (name: string, shop: string, data: object[]) => {
try {
const response = await api.post('api/plugin/custom/psc/formbuilder/layouts/add', { json: { title: name, data: data, shop: shop } });
return await response.json();
} catch (error) {
console.error('Error saving layout:', error);
throw error;
}
};
export const fetchLayouts = async (shop: string) => {
try {
const response = await api.get('api/plugin/custom/psc/formbuilder/layouts/all/' + shop);
return await response.json();
} catch (error) {
console.error('Error fetching layouts:', error);
throw error;
}
};
export const fetchPreview = async (shopUuid: string, json: object[]) => {
try {
const response = await api.post('api/plugin/system/psc/xmlcalc/product/pd', { json: { shop: shopUuid, json: json } });
return await response.json();
} catch (error) {
console.error('Error fetching preview:', error);
throw error;
}
};
export default api; export default api;

View File

@ -1,7 +1,7 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import BaseElement from '../model/BaseElement' import BaseElement from '../model/BaseElement'
import Mode from '../model/Mode' import Mode from '../model/Mode'
import { saveProductToApi, saveFomulasAndParameterToApi, loadJsonFromApi, loadPriceFromApi, savePaperContainerToApi, saveDesignToApi, saveXmlToApi } from '../lib/api' import { saveProductToApi, saveFomulasAndParameterToApi, loadJsonFromApi, loadPriceFromApi, savePaperContainerToApi, saveDesignToApi, saveXmlToApi, fetchPreview } from '../lib/api'
import { useItemStore } from './Items' import { useItemStore } from './Items'
export const useGlobalStore = defineStore('global', { export const useGlobalStore = defineStore('global', {
@ -15,6 +15,8 @@ export const useGlobalStore = defineStore('global', {
showDependency: false, showDependency: false,
showOptions: false, showOptions: false,
showPreview: false, showPreview: false,
showSaveLayoutDialog: false,
showLoadLayoutDialog: false,
sourceDragUuid: "", sourceDragUuid: "",
dragMode: "", dragMode: "",
json: "", json: "",
@ -27,6 +29,9 @@ export const useGlobalStore = defineStore('global', {
saving: false, saving: false,
syncing: false, syncing: false,
currentTab: 'designer' as string | number, currentTab: 'designer' as string | number,
previewData: {} as object,
isPreviewLoading: false,
previewError: '',
}), }),
getters: { getters: {
getActiveItem: (state) => state.activeItem as BaseElement, getActiveItem: (state) => state.activeItem as BaseElement,
@ -35,9 +40,11 @@ export const useGlobalStore = defineStore('global', {
isShowOptions: (state) => state.showOptions, isShowOptions: (state) => state.showOptions,
isShowPreview: (state) => state.showPreview, isShowPreview: (state) => state.showPreview,
getSourceDragUuid: (state) => state.sourceDragUuid, getSourceDragUuid: (state) => state.sourceDragUuid,
getShopUuid: (state) => state.shopUuid,
getDragMode: (state) => state.dragMode, getDragMode: (state) => state.dragMode,
getFormulaData: (state) => state.formulaData, getFormulaData: (state) => state.formulaData,
getFormulaError: (state) => state.formulaError, getFormulaError: (state) => state.formulaError,
getPreviewData: (state) => state.previewData,
}, },
actions: { actions: {
setXml(value: string) { setXml(value: string) {
@ -82,6 +89,12 @@ export const useGlobalStore = defineStore('global', {
setDragMode(mode: string) { setDragMode(mode: string) {
this.dragMode = mode this.dragMode = mode
}, },
setShowSaveLayoutDialog(value: boolean) {
this.showSaveLayoutDialog = value
},
setShowLoadLayoutDialog(value: boolean) {
this.showLoadLayoutDialog = value
},
setShopUuid(value: string) { setShopUuid(value: string) {
this.shopUuid = value this.shopUuid = value
}, },
@ -122,7 +135,7 @@ export const useGlobalStore = defineStore('global', {
this.json = json this.json = json
}, },
saveDesign(json: object[]) { saveDesign(json: object[]) {
saveDesignToApi(this.productUuid, json).then((result: any) => { saveDesignToApi(this.productUuid, this.shopUuid, json).then((result: any) => {
this.setXML(result.xml) this.setXML(result.xml)
this.setJSON(result.json) this.setJSON(result.json)
this.formulaData = JSON.parse(result.jsonGraph); this.formulaData = JSON.parse(result.jsonGraph);
@ -162,6 +175,19 @@ export const useGlobalStore = defineStore('global', {
}, },
setCurrentTab(tab: string | number) { setCurrentTab(tab: string | number) {
this.currentTab = tab this.currentTab = tab
},
async loadPreview(json: object[]) {
this.isPreviewLoading = true;
this.previewError = '';
try {
const response: any = await fetchPreview(this.shopUuid, json);
this.previewData = response;
} catch (e: any) {
this.previewError = `Failed to load preview data: ${e.message}`;
console.error(e);
} finally {
this.isPreviewLoading = false;
}
} }
} }
}) })

View File

@ -33,7 +33,7 @@ export default defineConfig({
changeOrigin: true, changeOrigin: true,
configure: (proxy) => { configure: (proxy) => {
proxy.on('proxyReq', (proxyReq) => { proxy.on('proxyReq', (proxyReq) => {
proxyReq.setHeader('Authorization', 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE3NTQzMjQwMzAsImV4cCI6MTc1NDMyNzYzMCwicm9sZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfU0hPUF9PUEVSQVRPUiIsIlJPTEVfVVNFUiIsIlJPTEVfVVNFUiIsIlJPTEVfUFNDX0NvbGxlY3RfQ29udGFjdF9FZGl0IiwiUk9MRV9QU0NfQ29sbGVjdF9Db250YWN0X0FkZCIsIlJPTEVfUFNDX0NvbGxlY3RfQ29udGFjdF9EZWxldGUiLCJST0xFX1BTQ19Db2xsZWN0X0NvbnRhY3RfTG9jayIsIlJPTEVfUFNDX1IyX1NlbmRjbG91ZF9TaG93Il0sInVpZCI6MX0.JQV0pGK1C_9sL4-0zLlXBzwMV8tUwCLis9KDUC_5DVOZf8Ujb0Yqgmie9B9DISAGvjtWUSvuUzcbcsV4m2gkNN-6dar-Y6XCC54KbkVCAOhssMp3KsZ1pbCiZ_VdUt78WbAFMkvhToHjjdpD4KqhetQjqFlGF1jYXfJmFzRDHh0YnUfYZDsqgun423JeUXbRYB0sJ3FLQzCuyUDWFvdsQVwCGsgs0ffkro42qMbLXZtdRPaAPZTlEYbE5H-wck1iKvAeEgNuqGJEnk-oEy6UQ1mGUHz7NT5N7NmXhkca2byInCMXhDPn2tmQvte5AKUAte0GELt3FjF5rhk1Iu2rZw'); proxyReq.setHeader('Authorization', 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE3NTQ1OTUxMjUsImV4cCI6MTc1NDU5ODcyNSwicm9sZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfU0hPUF9PUEVSQVRPUiIsIlJPTEVfVVNFUiIsIlJPTEVfVVNFUiIsIlJPTEVfUFNDX0NvbGxlY3RfQ29udGFjdF9FZGl0IiwiUk9MRV9QU0NfQ29sbGVjdF9Db250YWN0X0FkZCIsIlJPTEVfUFNDX0NvbGxlY3RfQ29udGFjdF9EZWxldGUiLCJST0xFX1BTQ19Db2xsZWN0X0NvbnRhY3RfTG9jayIsIlJPTEVfUFNDX1IyX1NlbmRjbG91ZF9TaG93Il0sInVpZCI6MX0.C-_PO6C-uFj_6zOtJymfYRsMi0tn-Aq_-HAjpYIqh4rAvY5fTnf54kli1F1bqYogRTqWiTgzPh3EYkPuU7dC8UlCN1eMwXzLFzqtlI7IPPh2UhUvFv6EGix5XTWHnfO-I53vYhrd1qO5Kp--nqFyCCMSaXsRd9daJ7fkbPfGXrwIXPxUeFhhcbMYP4SsUHgS3ZGInM1J_txO62LJHBc91pAtzSliVpUhwJzHjHuiTeC8WTUhdRgVaQo2echLWPfPrMlWolh3cN6wk41wCNivpTIQ4h-a3WVEHvlRz61W9jehzWMiMoTZglmatn8cPsiFFb_nmUacYP5avazXAdpgjA');
}); });
}, },
}, },

View File

@ -0,0 +1,76 @@
<?php
namespace Plugin\Custom\PSC\FormBuilder\Model;
use OpenApi\Attributes as OA;
class Layout
{
#[OA\Property(type: 'string')]
private string $title = '';
#[OA\Property(type: 'string')]
private string $uuid = '';
#[OA\Property(type: 'string')]
private string $shop = '';
#[Property(type: 'array', items: new Items(
type: 'array',
items: new Items(),
))]
private array $data;
#[OA\Property(type: 'string')]
private string $json = '';
public function getTitle(): string
{
return $this->title;
}
public function setTitle(string $title): void
{
$this->title = $title;
}
public function getUuid(): string
{
return $this->uuid;
}
public function setUuid(string $uuid): void
{
$this->uuid = $uuid;
}
public function getData(): array
{
return $this->data;
}
public function setData(array $data): void
{
$this->data = $data;
}
public function getJson(): string
{
return $this->json;
}
public function setJson(string $json): void
{
$this->json = $json;
}
public function getShop(): string
{
return $this->shop;
}
public function setShop(string $shop): void
{
$this->shop = $shop;
}
}

View File

@ -0,0 +1,4 @@
api_platform:
mapping:
paths:
- './../../Model'

View File

@ -1,6 +1,9 @@
psc_backend_component_formbuilder: plugin_custom_psc_formbuilder:
resource: "@PluginCustomPSCFormBuilder/Controller/Backend" resource: "@PluginCustomPSCFormBuilder/Controller/Backend"
type: annotation type: annotation
prefix: /backend/component/formbuilder prefix: /backend/component/formbuilder
plugin_system_psc_formbuilder_api:
resource: "@PluginCustomPSCFormBuilder/Api"
type: annotation
prefix: /api/plugin/custom/psc/formbuilder

View File

@ -57,27 +57,28 @@ class Design extends AbstractController
->getRepository('PSC\Shop\EntityBundle\Entity\Product') ->getRepository('PSC\Shop\EntityBundle\Entity\Product')
->findOneBy(['uuid' => $data->product]); ->findOneBy(['uuid' => $data->product]);
$selectedShop = $this->shopService->getShopByUid($data->shop);
$paperContainer = new PaperContainer(); $paperContainer = new PaperContainer();
$paperContainer->parse(simplexml_load_string($product->getShop()->getInstall()->getPaperContainer())); $paperContainer->parse(simplexml_load_string($selectedShop->getInstall()->getPaperContainer()));
$engine = new Engine(); $engine = new Engine();
$engine->setPaperRepository($this->paperDB); $engine->setPaperRepository($this->paperDB);
$engine->setPaperContainer($paperContainer); $engine->setPaperContainer($paperContainer);
if ($product->getShop()->getInstall()->getCalcTemplates() && !$data->test) { if ($selectedShop->getInstall()->getCalcTemplates() && !$data->test) {
$engine->setTemplates('<root>' . $product->getShop()->getInstall()->getCalcTemplates() . '</root>'); $engine->setTemplates('<root>' . $selectedShop->getInstall()->getCalcTemplates() . '</root>');
} }
if ($product->getShop()->getInstall()->getCalcTemplatesTest() && $data->test) { if ($selectedShop->getInstall()->getCalcTemplatesTest() && $data->test) {
$engine->setTemplates('<root>' . $product->getShop()->getInstall()->getCalcTemplatesTest() . '</root>'); $engine->setTemplates('<root>' . $selectedShop->getInstall()->getCalcTemplatesTest() . '</root>');
} }
$engine->loadJson(json_encode($data->jsonProduct)); $engine->loadJson(json_encode($data->jsonProduct));
if (!$data->test) { if (!$data->test) {
$engine->setFormulas($product->getShop()->getFormel()); $engine->setFormulas($selectedShop->getFormel());
$engine->setParameters($product->getShop()->getParameter()); $engine->setParameters($selectedShop->getParameter());
} }
if ($data->test) { if ($data->test) {
$engine->setFormulas($product->getShop()->getTestFormel()); $engine->setFormulas($selectedShop->getTestFormel());
$engine->setParameters($product->getShop()->getTestParameter()); $engine->setParameters($selectedShop->getTestParameter());
} }
// $engine->setVariables($data->values);
$engine->setTax($product->getMwert()); $engine->setTax($product->getMwert());
if ($this->tokenStorage->getToken()) { if ($this->tokenStorage->getToken()) {
$contact = new Contact(); $contact = new Contact();
@ -90,12 +91,12 @@ class Design extends AbstractController
return $this->json([ return $this->json([
'json' => $engine->generateJson(), 'json' => $engine->generateJson(),
'parameter' => $engine->getParameters(), 'parameter' => $engine->getParameters(),
'paperContainer' => $product->getShop()->getInstall()->getPaperContainer(), 'paperContainer' => $selectedShop->getInstall()->getPaperContainer(),
'formulas' => $engine->getFormulas(), 'formulas' => $engine->getFormulas(),
'xml' => $engine->generateXML(true), 'xml' => $engine->generateXML(true),
'jsonGraph' => $engine->getCalcGraph()->generateJsonGraph(), 'jsonGraph' => $engine->getCalcGraph()->generateJsonGraph(),
'price' => $engine->getPrice() * 100, 'price' => $engine->getPrice() * 100,
'shopUuid' => $product->getShop()->getUid(), 'shopUuid' => $selectedShop->getUid(),
]); ]);
} }
} }

View File

@ -0,0 +1,206 @@
<?php
namespace Plugin\System\PSC\XmlCalc\Api\Product;
use Doctrine\ODM\MongoDB\DocumentManager;
use Doctrine\ORM\EntityManagerInterface;
use Nelmio\ApiDocBundle\Attribute\Model;
use OpenApi\Attributes\JsonContent;
use OpenApi\Attributes\RequestBody;
use OpenApi\Attributes\Response as OpenApiResponse;
use OpenApi\Attributes\Tag;
use Plugin\System\PSC\XmlCalc\Dto\Input\PDInput;
use Plugin\System\PSC\XmlCalc\Dto\Output\Display\Group as DisplayGroup;
use Plugin\System\PSC\XmlCalc\Dto\Output\Price\Element;
use Plugin\System\PSC\XmlCalc\Dto\Output\Price\Option;
use Plugin\System\PSC\XmlCalc\Dto\Output\Price\Validation\Input\Max as PluginMax;
use Plugin\System\PSC\XmlCalc\Dto\Output\Price\Validation\Input\Min;
use PSC\Library\Calc\Engine;
use PSC\Library\Calc\Error\Validation\Input\Max;
use PSC\Library\Calc\Error\Validation\Input\Min as PSCMin;
use PSC\Library\Calc\Option\Type\Base;
use PSC\Library\Calc\Option\Type\ColorDBSelect;
use PSC\Library\Calc\Option\Type\DeliverySelect;
use PSC\Library\Calc\Option\Type\Select\Opt;
use PSC\Library\Calc\PaperContainer;
use PSC\Shop\ContactBundle\Model\Contact;
use PSC\Shop\ContactBundle\Transformer\Model\Contact as ContactTransformer;
use PSC\System\SettingsBundle\Service\Help;
use PSC\System\SettingsBundle\Service\PaperDB;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class PreviewDesigner extends AbstractController
{
private \PSC\System\SettingsBundle\Service\Shop $shopService;
/**
* @var DocumentManager
*/
private DocumentManager $documentManager;
/**
* @var PaperDB
*/
private PaperDB $paperDB;
/**
* @var EntityManagerInterface
*/
private EntityManagerInterface $entityManager;
private TokenStorageInterface $tokenStorage;
private ContactTransformer $contactTransformer;
private Help $helpService;
public function __construct(
\PSC\System\SettingsBundle\Service\Shop $shopService,
DocumentManager $documentManager,
EntityManagerInterface $entityManager,
PaperDB $paperDB,
ContactTransformer $contactTransformer,
TokenStorageInterface $tokenStorage,
Help $helpService,
) {
$this->shopService = $shopService;
$this->documentManager = $documentManager;
$this->entityManager = $entityManager;
$this->tokenStorage = $tokenStorage;
$this->paperDB = $paperDB;
$this->contactTransformer = $contactTransformer;
$this->helpService = $helpService;
}
#[Route(path: '/product/pd', methods: ['POST'])]
#[OpenApiResponse(
response: 200,
description: 'generate preview for new designer',
content: new JsonContent(ref: new Model(type: PDOutput::class)),
)]
#[Tag(name: 'Plugin/System/psc/Xmlcalc/Price')]
#[RequestBody(ref: new Model(type: PDInput::class))]
#[ParamConverter('data', class: '\Plugin\System\PSC\XmlCalc\Dto\Input\PDInput', converter: 'psc_rest.request_body')]
public function preview(PDInput $data)
{
$output = new \Plugin\System\PSC\XmlCalc\Dto\Output\Product\PDOutput();
$shop = $this->shopService->getShopByUid($data->shop);
$paperContainer = new PaperContainer();
$paperContainer->parse(simplexml_load_string($shop->getInstall()->getPaperContainer()));
$engine = new Engine();
$engine->setPaperRepository($this->paperDB);
$engine->setPaperContainer($paperContainer);
$engine->setTemplates('<root>' . $shop->getInstall()->getCalcTemplates() . '</root>');
$engine->loadJson(json_encode($data->json));
$engine->setFormulas($shop->getFormel());
$engine->setParameters($shop->getParameter());
$engine->setVariables($data->values);
if ($this->tokenStorage->getToken()) {
$contact = new Contact();
$this->contactTransformer->fromDb($contact, $this->tokenStorage->getToken()->getUser());
$engine->setVariable('contact.accountType', $contact->getAccountType()->value);
$engine->setVariable('contact.account', $contact->getAccount()->getUid());
}
$engine->setTax(19);
$output->netto = $engine->getPrice() * 100;
$output->tax = $engine->getTaxPrice() * 100;
$output->brutto = $engine->getCompletePrice() * 100;
/**
* @var Base $option
*/
foreach ($engine->getArticle()->getOptions() as $option) {
$tmp = new Element();
$tmp->name = $option->getName();
$tmp->required = $option->isRequire();
if (is_array($option->getRawValue())) {
$tmp->rawValues = $option->getRawValue();
} else {
$tmp->rawValue = $option->getRawValue();
}
if ($option->getDefault()) {
$tmp->defaultValue = $option->getDefault();
}
$tmp->value = $option->getValue();
if ($help = $this->helpService->getHelp(null, $option->getId())) {
$tmp->help = $help->helpText;
$tmp->helpTitle = $help->helpTitle;
} else {
$tmp->help = $option->getHelp();
$tmp->helpLink = $option->getHelpLink();
}
$tmp->id = $option->getId();
$tmp->valid = $option->isValid();
$tmp->htmlType = $option->type;
$tmp->displayGroup = $option->getDisplayGroup();
if ($option->type == 'select' || $option->type == 'checkbox' || $option->type == 'radio') {
/**
* @var Opt $option
*/
if ($option instanceof ColorDBSelect) {
$tmp->colorSystem = $option->getColorSystem();
if (!isset($output->colorDb[$option->getColorSystem()])) {
$output->colorDb[$option->getColorSystem()] = [];
foreach ($option->getOptions() as $opt) {
$element = array_find((array) $option->getSelectedOptions(), function (Opt $o1) use ($opt) {
return $o1->getId() === $opt->getId();
});
$tmpOpt = new Option();
$tmpOpt->id = $opt->getId();
$tmpOpt->name = $opt->getLabel();
$tmpOpt->prefix = $opt->getPrefix();
$tmpOpt->suffix = $opt->getSuffix();
$tmpOpt->valid = $opt->isValid();
$tmpOpt->selected = $element ? true : false;
$output->colorDb[$option->getColorSystem()][] = $tmpOpt;
}
}
} else {
foreach ($option->getOptions() as $opt) {
$element = array_find((array) $option->getSelectedOptions(), function (Opt $o1) use ($opt) {
return $o1->getId() === $opt->getId();
});
if ($option instanceof DeliverySelect) {
$tmpOpt = new Option();
$tmpOpt->id = $opt->getId();
$tmpOpt->name = $opt->getLabel();
$tmpOpt->valid = $opt->isValid();
$tmpOpt->selected = $element ? true : false;
$tmpOpt->info = $opt->getInfo();
$tmpOpt->deliveryDate = $opt->getDeliveryDateAsString();
} else {
$tmpOpt = new Option();
$tmpOpt->id = $opt->getId();
$tmpOpt->name = $opt->getLabel();
$tmpOpt->valid = $opt->isValid();
$tmpOpt->selected = $element ? true : false;
}
$tmp->options[] = $tmpOpt;
}
}
}
if ($option->type == 'input') {
$tmp->minValue = $option->getMinValue();
$tmp->maxValue = $option->getMaxValue();
$tmp->placeHolder = $option->getPlaceHolder();
$tmp->pattern = $option->getPattern();
foreach ($option->getValidationErrors() as $error) {
if ($error instanceof PSCMin) {
$tmp->validationErrors[] = new Min($tmp->value, $option->getMinValue());
}
if ($error instanceof Max) {
$tmp->validationErrors[] = new PluginMax($tmp->value, $option->getMaxValue());
}
}
}
$output->elements[] = $tmp;
}
return $this->json($output);
}
}

View File

@ -2,36 +2,27 @@
namespace Plugin\System\PSC\XmlCalc\Dto\Input; namespace Plugin\System\PSC\XmlCalc\Dto\Input;
use Nelmio\ApiDocBundle\Annotation\Model; use OpenApi\Attributes as OA;
use OpenApi\Annotations as OA; use OpenApi\Attributes\Items;
use OpenApi\Attributes\Property;
final class DesignInput final class DesignInput
{ {
/** #[Property(type: 'string')]
* @var string
*
* @OA\Property(type="string")
*/
public string $product; public string $product;
/** #[Property(type: 'string')]
* @var array public string $shop;
*
* @OA\Property(type="array", @OA\Items(type="array", @OA\Items())) #[Property(type: 'array', items: new Items(
*/ type: 'array',
items: new Items(),
))]
public array $jsonProduct = []; public array $jsonProduct = [];
/** #[Property(type: 'boolean')]
* @var bool
*
* @OA\Property(type="boolean")
*/
public bool $test = false; public bool $test = false;
/** #[Property(type: 'array', items: new Items(type: 'string'))]
* @var array
*
* @OA\Property(type="array", @OA\Items(type="string"))
*/
public array $values = ['auflage' => 100]; public array $values = ['auflage' => 100];
} }

View File

@ -0,0 +1,21 @@
<?php
namespace Plugin\System\PSC\XmlCalc\Dto\Input;
use OpenApi\Attributes\Items;
use OpenApi\Attributes\Property;
final class PDInput
{
#[Property(type: 'string')]
public string $shop;
#[Property(type: 'array', items: new Items(
type: 'array',
items: new Items(),
))]
public array $json = [];
#[Property(type: 'array', items: new Items(type: 'string'))]
public array $values = ['auflage' => 100];
}

View File

@ -0,0 +1,32 @@
<?php
namespace Plugin\System\PSC\XmlCalc\Dto\Output\Product;
use Nelmio\ApiDocBundle\Attribute\Model;
use OpenApi\Attributes\Items;
use OpenApi\Attributes\Property;
use Plugin\System\PSC\XmlCalc\Dto\Output\Price\Element;
final class PDOutput
{
#[Property(type: 'boolean')]
public $success = true;
#[Property(type: 'int')]
public int $netto;
#[Property(type: 'int')]
public int $tax;
#[Property(type: 'int')]
public int $brutto;
#[Property(type: 'array', items: new Items(
type: 'array',
items: new Items(),
))]
public array $colorDb = [];
#[Property(type: 'array', items: new Items(ref: new Model(type: Element::class)))]
public array $elements = [];
}

File diff suppressed because it is too large Load Diff