This commit is contained in:
Thomas Peterson 2026-03-28 14:19:30 +01:00
parent 965f56e0d4
commit 6f65735675
42 changed files with 1169 additions and 91 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

View File

@ -27,6 +27,7 @@ return [
PSC\Backend\DashboardBundle\PSCBackendDashboardBundle::class => ['all' => true], PSC\Backend\DashboardBundle\PSCBackendDashboardBundle::class => ['all' => true],
PSC\Shop\SettingsBundle\PSCShopSettingsBundle::class => ['all' => true], PSC\Shop\SettingsBundle\PSCShopSettingsBundle::class => ['all' => true],
PSC\System\SettingsBundle\PSCSystemSettingsBundle::class => ['all' => true], PSC\System\SettingsBundle\PSCSystemSettingsBundle::class => ['all' => true],
PSC\System\ContentEngineBundle\PSCSystemContentEngineBundle::class => ['all' => true],
PSC\Backend\ToolsBundle\PSCBackendToolsBundle::class => ['all' => true], PSC\Backend\ToolsBundle\PSCBackendToolsBundle::class => ['all' => true],
PSC\Shop\OrderBundle\PSCShopOrderBundle::class => ['all' => true], PSC\Shop\OrderBundle\PSCShopOrderBundle::class => ['all' => true],
PSC\Shop\ContactBundle\PSCShopContactBundle::class => ['all' => true], PSC\Shop\ContactBundle\PSCShopContactBundle::class => ['all' => true],

View File

@ -49,6 +49,8 @@ return static function (RoutingConfigurator $routingConfigurator): void {
$routingConfigurator->import('@PSCSystemSettingsBundle/Resources/config/routing.yml'); $routingConfigurator->import('@PSCSystemSettingsBundle/Resources/config/routing.yml');
$routingConfigurator->import('@PSCSystemContentEngineBundle/Resources/config/routing.yml');
$routingConfigurator->import('@PSCSystemUpdateBundle/Resources/config/routing.yml'); $routingConfigurator->import('@PSCSystemUpdateBundle/Resources/config/routing.yml');
$routingConfigurator->import('@PSCShopPaymentBundle/Resources/config/routing.yml'); $routingConfigurator->import('@PSCShopPaymentBundle/Resources/config/routing.yml');

View File

@ -2603,9 +2603,11 @@
<br/> <br/>
<ul> <ul>
{% for option in position.calc.options %} {% for option in position.calc.options %}
<li><b>{{option.name }}</b>: {{ option.value }}</li> {% if option is not instanceof('\\PSC\\Library\\Calc\\Option\\Type\\Hidden') and option.valid %}
<li><b>{{option.name }}</b>: {{ option.value }}</li>
{% endif %}
{% endfor %} {% endfor %}
</ul> </ul>
</td> </td>
<td valign="top">{{ position.count }}</td> <td valign="top">{{ position.count }}</td>
<td valign="top" style="text-align: right">{{ position.obj.priceAllNetto|number_format(2, ',', '.') }}€</td> <td valign="top" style="text-align: right">{{ position.obj.priceAllNetto|number_format(2, ',', '.') }}€</td>

View File

@ -0,0 +1,49 @@
<?php
namespace PSC\System\ContentEngineBundle\Api;
use Nelmio\ApiDocBundle\Attribute\Model;
use OpenApi\Attributes\JsonContent;
use OpenApi\Attributes\RequestBody;
use OpenApi\Attributes\Response;
use OpenApi\Attributes\Tag;
use PSC\Library\Calc\Engine;
use PSC\System\ContentEngineBundle\Dto\Input\ConfigInput;
use PSC\System\ContentEngineBundle\Dto\Output\ContentOutput;
use PSC\System\ContentEngineBundle\Service\ContentResolver;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\Routing\Attribute\Route;
class Config extends AbstractController
{
public function __construct(
private ContentResolver $contentResolver,
) {}
#[Response(
response: 200,
description: 'Load content configuration',
content: new JsonContent(ref: new Model(type: ContentOutput::class)),
)]
#[RequestBody(content: new JsonContent(ref: new Model(type: ConfigInput::class)))]
#[Tag(name: 'ContentEngine')]
#[Route(path: '/config', methods: ['POST'])]
public function config(#[MapRequestPayload] ConfigInput $data)
{
$xml = $this->contentResolver->loadXml($data->module, $data->id);
if (empty(trim($xml))) {
$xml = '<?xml version="1.0" encoding="utf-8"?><kalkulation><artikel><name></name></artikel></kalkulation>';
}
$engine = new Engine();
$engine->loadString($xml);
$output = new ContentOutput();
$output->json = $engine->generateJson();
$output->xml = $engine->generateXML();
return $this->json($output);
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace PSC\System\ContentEngineBundle\Api;
use Nelmio\ApiDocBundle\Attribute\Model;
use OpenApi\Attributes\JsonContent;
use OpenApi\Attributes\RequestBody;
use OpenApi\Attributes\Response;
use OpenApi\Attributes\Tag;
use PSC\Library\Calc\Engine;
use PSC\System\ContentEngineBundle\Dto\Input\DesignInput;
use PSC\System\ContentEngineBundle\Dto\Output\ContentOutput;
use PSC\System\ContentEngineBundle\Service\ContentResolver;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\Routing\Attribute\Route;
class Design extends AbstractController
{
public function __construct(
private ContentResolver $contentResolver,
) {}
#[Response(
response: 200,
description: 'Process JSON design',
content: new JsonContent(ref: new Model(type: ContentOutput::class)),
)]
#[RequestBody(content: new JsonContent(ref: new Model(type: DesignInput::class)))]
#[Tag(name: 'ContentEngine')]
#[Route(path: '/design', methods: ['POST'])]
public function design(#[MapRequestPayload] DesignInput $data)
{
$engine = new Engine();
$engine->loadJson(json_encode($data->json));
$engine->setVariables($data->values);
$engine->calc();
$output = new ContentOutput();
$output->json = $engine->generateJson();
$output->xml = $engine->generateXML();
$output->jsonGraph = json_decode($engine->getCalcGraph()->generateJsonGraph(), true);
return $this->json($output);
}
}

View File

@ -0,0 +1,149 @@
<?php
namespace PSC\System\ContentEngineBundle\Api;
use Nelmio\ApiDocBundle\Attribute\Model;
use OpenApi\Attributes\JsonContent;
use OpenApi\Attributes\RequestBody;
use OpenApi\Attributes\Response;
use OpenApi\Attributes\Tag;
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\DeliverySelect;
use PSC\Library\Calc\Option\Type\Select\Opt;
use PSC\System\ContentEngineBundle\Dto\Input\PreviewInput;
use PSC\System\ContentEngineBundle\Dto\Output\Element\Element;
use PSC\System\ContentEngineBundle\Dto\Output\Element\Option;
use PSC\System\ContentEngineBundle\Dto\Output\PreviewOutput;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\Routing\Attribute\Route;
class Preview extends AbstractController
{
#[Response(
response: 200,
description: 'Preview content form elements',
content: new JsonContent(ref: new Model(type: PreviewOutput::class)),
)]
#[RequestBody(content: new JsonContent(ref: new Model(type: PreviewInput::class)))]
#[Tag(name: 'ContentEngine')]
#[Route(path: '/preview', methods: ['POST'])]
public function preview(#[MapRequestPayload] PreviewInput $data)
{
$engine = new Engine();
$engine->loadJson(json_encode($data->json));
$engine->setVariables($data->values);
$output = new PreviewOutput();
/** @var Base $option */
foreach ($engine->getArticle()->getOptions() as $option) {
$output->elements[] = $this->parseOption($option);
}
return $this->json($output);
}
private function parseOption(Base $option): Element
{
$element = new Element();
if ($option->getName()) {
$element->name = $option->getName();
}
$element->required = $option->isRequire();
if (is_array($option->getRawValue())) {
$element->rawValues = $option->getRawValue();
} else {
$element->rawValue = (string) $option->getRawValue();
}
if ($option->getDefault()) {
$element->defaultValue = $option->getDefault();
}
$element->value = $option->getValue();
$element->help = $option->getHelp();
$element->helpLink = $option->getHelpLink();
$element->id = $option->getId();
$element->valid = $option->isValid();
$element->htmlType = $option->type;
$element->displayGroup = $option->getDisplayGroup();
if ($option->type == 'select' || $option->type == 'checkbox' || $option->type == 'radio') {
foreach ($option->getOptions() as $opt) {
$elementSelected = 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 = $elementSelected ? 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 = $elementSelected ? true : false;
}
$element->options[] = $tmpOpt;
}
}
if ($option->type == 'input') {
$element->minValue = $option->getMinValue();
$element->maxValue = $option->getMaxValue();
$element->placeHolder = $option->getPlaceHolder();
$element->pattern = $option->getPattern();
foreach ($option->getValidationErrors() as $error) {
if ($error instanceof PSCMin) {
$element->validationErrors[] = [
'type' => 'min',
'value' => $element->value,
'min' => $option->getMinValue(),
];
}
if ($error instanceof Max) {
$element->validationErrors[] = [
'type' => 'max',
'value' => $element->value,
'max' => $option->getMaxValue(),
];
}
}
}
if ($option->type == 'row') {
foreach ($option->getColumns() as $column) {
$element->elements[] = $this->parseOption($column);
}
}
if ($option->type == 'column') {
foreach ($option->getOptions() as $opt) {
$element->elements[] = $this->parseOption($opt);
}
}
return $element;
}
private function parseOpt(Opt $opt, ?array $selectedOptions): Option
{
$selected = $selectedOptions ? array_find($selectedOptions, fn(Opt $o) => $o->getId() === $opt->getId()) : null;
$option = new Option();
$option->id = $opt->getId();
$option->name = $opt->getLabel();
$option->prefix = $opt->getPrefix() ?? '';
$option->suffix = $opt->getSuffix() ?? '';
$option->valid = $opt->isValid();
$option->selected = $selected !== null;
return $option;
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace PSC\System\ContentEngineBundle\Api;
use Nelmio\ApiDocBundle\Attribute\Model;
use OpenApi\Attributes\JsonContent;
use OpenApi\Attributes\RequestBody;
use OpenApi\Attributes\Response;
use OpenApi\Attributes\Tag;
use PSC\System\ContentEngineBundle\Dto\Input\XmlInput;
use PSC\System\ContentEngineBundle\Service\ContentResolver;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class Save extends AbstractController
{
public function __construct(
private ContentResolver $contentResolver,
) {}
#[Response(response: 200, description: 'Save XML to content entity')]
#[RequestBody(content: new JsonContent(ref: new Model(type: XmlInput::class)))]
#[Tag(name: 'ContentEngine')]
#[Route(path: '/save', methods: ['PUT'])]
#[IsGranted('ROLE_USER')]
public function save(#[MapRequestPayload] XmlInput $data)
{
$this->contentResolver->saveXml($data->module, $data->id, $data->xml);
return $this->json(['success' => true]);
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace PSC\System\ContentEngineBundle\Api;
use Nelmio\ApiDocBundle\Attribute\Model;
use OpenApi\Attributes\JsonContent;
use OpenApi\Attributes\RequestBody;
use OpenApi\Attributes\Response;
use OpenApi\Attributes\Tag;
use PSC\Library\Calc\Engine;
use PSC\System\ContentEngineBundle\Dto\Input\XmlInput;
use PSC\System\ContentEngineBundle\Dto\Output\ContentOutput;
use PSC\System\ContentEngineBundle\Service\ContentResolver;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\Routing\Attribute\Route;
class Xml extends AbstractController
{
public function __construct(
private ContentResolver $contentResolver,
) {}
#[Response(
response: 200,
description: 'Process XML input',
content: new JsonContent(ref: new Model(type: ContentOutput::class)),
)]
#[RequestBody(content: new JsonContent(ref: new Model(type: XmlInput::class)))]
#[Tag(name: 'ContentEngine')]
#[Route(path: '/xml', methods: ['POST'])]
public function xml(#[MapRequestPayload] XmlInput $data)
{
$engine = new Engine();
$engine->loadString($data->xml);
$engine->calc();
$output = new ContentOutput();
$output->json = json_decode($engine->generateJson(), true);
$output->xml = $engine->generateXML(true);
$output->jsonGraph = json_decode($engine->getCalcGraph()->generateJsonGraph(), true);
return $this->json($output);
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace PSC\System\ContentEngineBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
class Configuration implements ConfigurationInterface
{
/**
* {@inheritDoc}
*/
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('psc_system_content_engine_bundle');
return $treeBuilder;
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace PSC\System\ContentEngineBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
class PSCSystemContentEngineExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$this->processConfiguration($configuration, $configs);
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
$loader->load('services.yml');
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace PSC\System\ContentEngineBundle\Dto\Input;
use OpenApi\Attributes\Property;
final class ConfigInput
{
#[Property(type: 'integer', description: 'ID of the content entity (e.g. CMS page)')]
public int $id;
#[Property(type: 'string', description: 'Module type (e.g. cms, form)')]
public string $module = 'cms';
}

View File

@ -0,0 +1,21 @@
<?php
namespace PSC\System\ContentEngineBundle\Dto\Input;
use OpenApi\Attributes\Items;
use OpenApi\Attributes\Property;
final class DesignInput
{
#[Property(type: 'integer', description: 'ID of the content entity')]
public int $id;
#[Property(type: 'string', description: 'Module type (e.g. cms, form)')]
public string $module = 'cms';
#[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 = [];
}

View File

@ -0,0 +1,15 @@
<?php
namespace PSC\System\ContentEngineBundle\Dto\Input;
use OpenApi\Attributes\Items;
use OpenApi\Attributes\Property;
final class PreviewInput
{
#[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 = [];
}

View File

@ -0,0 +1,17 @@
<?php
namespace PSC\System\ContentEngineBundle\Dto\Input;
use OpenApi\Attributes\Property;
final class XmlInput
{
#[Property(type: 'integer', description: 'ID of the content entity')]
public int $id;
#[Property(type: 'string', description: 'Module type (e.g. cms, form)')]
public string $module = 'cms';
#[Property(type: 'string')]
public string $xml = '';
}

View File

@ -0,0 +1,21 @@
<?php
namespace PSC\System\ContentEngineBundle\Dto\Output;
use OpenApi\Attributes\Items;
use OpenApi\Attributes\Property;
final class ContentOutput
{
#[Property(type: 'boolean')]
public bool $success = true;
#[Property(type: 'string')]
public string $json = '';
#[Property(type: 'string')]
public string $xml = '';
#[Property(type: 'array', items: new Items(type: 'string'))]
public array $jsonGraph = [];
}

View File

@ -0,0 +1,75 @@
<?php
namespace PSC\System\ContentEngineBundle\Dto\Output\Element;
use OpenApi\Attributes\Items;
use OpenApi\Attributes\Property;
class Element
{
#[Property(type: 'string')]
public string $id;
#[Property(type: 'string')]
public string $name;
#[Property(type: 'string')]
public string $pattern = '';
#[Property(type: 'string')]
public string $placeHolder = '';
#[Property(type: 'string')]
public string $displayGroup = '';
#[Property(type: 'string')]
public string $defaultValue = '';
#[Property(type: 'string')]
public string $value;
#[Property(type: 'integer')]
public null|int $minValue = null;
#[Property(type: 'integer')]
public null|int $maxValue = null;
#[Property(type: 'string')]
public null|string $help = null;
#[Property(type: 'string')]
public null|string $helpTitle = null;
#[Property(type: 'string')]
public null|string $helpLink = null;
#[Property(type: 'boolean')]
public bool $valid = true;
#[Property(type: 'boolean')]
public bool $required = false;
#[Property(type: 'string')]
public string $rawValue = '';
#[Property(type: 'array', items: new Items(type: 'string'))]
public array $rawValues = [];
#[Property(type: 'string')]
public string $type = '';
/** @var Option[] */
#[Property(type: 'array', items: new Items(type: 'object'))]
public array $options = [];
/** @var Element[] */
#[Property(type: 'array', items: new Items(type: 'object'))]
public array $elements = [];
#[Property(type: 'string')]
public string $htmlType = 'input';
/** @var array */
#[Property(type: 'array', items: new Items(type: 'object'))]
public array $validationErrors = [];
}

View File

@ -0,0 +1,29 @@
<?php
namespace PSC\System\ContentEngineBundle\Dto\Output\Element;
use OpenApi\Attributes as OA;
final class Option
{
#[OA\Property(type: 'string')]
public string $id;
#[OA\Property(type: 'string')]
public string $name;
#[OA\Property(type: 'string')]
public string $prefix = '';
#[OA\Property(type: 'string')]
public string $suffix = '';
#[OA\Property(type: 'string')]
public string $info = '';
#[OA\Property(type: 'boolean')]
public bool $valid = true;
#[OA\Property(type: 'boolean')]
public bool $selected = false;
}

View File

@ -0,0 +1,17 @@
<?php
namespace PSC\System\ContentEngineBundle\Dto\Output;
use OpenApi\Attributes\Items;
use OpenApi\Attributes\Property;
use PSC\System\ContentEngineBundle\Dto\Output\Element\Element;
final class PreviewOutput
{
#[Property(type: 'boolean')]
public bool $success = true;
/** @var Element[] */
#[Property(type: 'array', items: new Items(type: 'object'))]
public array $elements = [];
}

View File

@ -0,0 +1,9 @@
<?php
namespace PSC\System\ContentEngineBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class PSCSystemContentEngineBundle extends Bundle
{
}

View File

@ -0,0 +1,4 @@
psc_system_content_engine_api:
resource: "@PSCSystemContentEngineBundle/Api"
type: attribute
prefix: /api/content-engine

View File

@ -0,0 +1,7 @@
services:
_defaults:
autowire: true
autoconfigure: true
PSC\System\ContentEngineBundle\:
resource: '../../*/*'

View File

@ -0,0 +1,50 @@
<?php
namespace PSC\System\ContentEngineBundle\Service;
use Doctrine\ORM\EntityManagerInterface;
use PSC\Shop\EntityBundle\Entity\Cms;
class ContentResolver
{
public function __construct(
private EntityManagerInterface $entityManager,
) {}
public function loadXml(string $module, int $id): string
{
$entity = $this->resolveEntity($module, $id);
return match ($module) {
'cms' => $entity->getText() ?? '',
default => throw new \InvalidArgumentException("Unknown module: $module"),
};
}
public function saveXml(string $module, int $id, string $xml): void
{
$entity = $this->resolveEntity($module, $id);
match ($module) {
'cms' => $entity->setText($xml),
default => throw new \InvalidArgumentException("Unknown module: $module"),
};
$this->entityManager->persist($entity);
$this->entityManager->flush();
}
private function resolveEntity(string $module, int $id): object
{
$entity = match ($module) {
'cms' => $this->entityManager->getRepository(Cms::class)->find($id),
default => throw new \InvalidArgumentException("Unknown module: $module"),
};
if (!$entity) {
throw new \RuntimeException("Entity not found for module '$module' with id '$id'");
}
return $entity;
}
}

View File

@ -35,13 +35,28 @@
</td> </td>
<td class="px-4 py-3 text-gray-900 dark:text-gray-100 whitespace-nowrap">{{ entry.datum }}</td> <td class="px-4 py-3 text-gray-900 dark:text-gray-100 whitespace-nowrap">{{ entry.datum }}</td>
<td class="px-4 py-3"> <td class="px-4 py-3">
<ul class="space-y-1"> <ul class="space-y-2">
{% for change in entry.changes %} {% for change in entry.changes %}
<li class="flex items-start gap-2 text-gray-700 dark:text-gray-300"> <li class="flex items-start gap-2 text-gray-700 dark:text-gray-300">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 mt-0.5 shrink-0 text-psc-500"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 mt-0.5 shrink-0 text-psc-500">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /> <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg> </svg>
{{ change }} {% if change is iterable %}
<div>
<span>{{ change.text }}</span>
{% if change.images is defined %}
<div class="mt-2 flex flex-wrap gap-2">
{% for image in change.images %}
<img src="{{ asset('images/changelog/' ~ image) }}" alt="{{ change.text }}" class="rounded-lg border border-gray-200 shadow-sm max-h-32 max-w-48 object-cover cursor-pointer hover:opacity-80 transition-opacity dark:border-gray-700" loading="lazy" onclick="document.getElementById('changelog-lightbox').querySelector('img').src=this.src;document.getElementById('changelog-lightbox').classList.remove('hidden')"/>
{% endfor %}
</div>
{% elseif change.image is defined %}
<img src="{{ asset('images/changelog/' ~ change.image) }}" alt="{{ change.text }}" class="mt-2 rounded-lg border border-gray-200 shadow-sm max-h-32 max-w-48 object-cover cursor-pointer hover:opacity-80 transition-opacity dark:border-gray-700" loading="lazy" onclick="document.getElementById('changelog-lightbox').querySelector('img').src=this.src;document.getElementById('changelog-lightbox').classList.remove('hidden')"/>
{% endif %}
</div>
{% else %}
{{ change }}
{% endif %}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
@ -53,4 +68,8 @@
</div> </div>
</div> </div>
</div> </div>
<div id="changelog-lightbox" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm cursor-pointer" onclick="this.classList.add('hidden')">
<img src="" alt="" class="max-w-[90vw] max-h-[90vh] rounded-lg shadow-2xl"/>
</div>
{% endblock %} {% endblock %}

View File

@ -14,6 +14,6 @@ class VersionTest extends WebTestCase
$this->assertResponseIsSuccessful(); $this->assertResponseIsSuccessful();
$data = json_decode($client->getResponse()->getContent(), true); $data = json_decode($client->getResponse()->getContent(), true);
$this->assertSame('2.3.3', $data['release']); $this->assertSame('2.3.5', $data['release']);
} }
} }

View File

@ -0,0 +1,59 @@
<?php
namespace Tests\PSC\System\ContentEngineBundle\Api;
class ConfigTest extends ContentEngineTestCase
{
public function testLoadConfigForCms(): void
{
$client = static::createClient();
$cmsId = $this->createCmsPage(
'<?xml version="1.0" encoding="utf-8"?><kalkulation><artikel><name>Test</name><option id="auflage" name="Auflage" type="Input" default="10"/></artikel></kalkulation>',
);
$client->jsonRequest('POST', '/api/content-engine/config', [
'id' => $cmsId,
'module' => 'cms',
]);
$this->assertResponseIsSuccessful();
$data = json_decode($client->getResponse()->getContent(), true);
self::assertArrayHasKey('json', $data);
self::assertArrayHasKey('xml', $data);
self::assertNotEmpty($data['json']);
self::assertNotEmpty($data['xml']);
}
public function testLoadConfigForCmsWithEmptyText(): void
{
$client = static::createClient();
$cmsId = $this->createCmsPage('');
$client->jsonRequest('POST', '/api/content-engine/config', [
'id' => $cmsId,
'module' => 'cms',
]);
$this->assertResponseIsSuccessful();
$data = json_decode($client->getResponse()->getContent(), true);
self::assertArrayHasKey('json', $data);
self::assertArrayHasKey('xml', $data);
self::assertNotEmpty($data['xml']);
}
public function testLoadConfigWithInvalidModule(): void
{
$client = static::createClient();
$client->jsonRequest('POST', '/api/content-engine/config', [
'id' => 1,
'module' => 'invalid',
]);
self::assertSame(500, $client->getResponse()->getStatusCode());
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace Tests\PSC\System\ContentEngineBundle\Api;
use Doctrine\DBAL\Connection;
use PSC\Shop\EntityBundle\Repository\ShopRepository;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Tests\RefreshDatabaseTrait;
abstract class ContentEngineTestCase extends WebTestCase
{
use RefreshDatabaseTrait;
protected function createCmsPage(string $text = ''): int
{
$shop = static::getContainer()->get(ShopRepository::class)->findOneBy(['title' => 'Printchampion']);
/** @var Connection $conn */
$conn = static::getContainer()->get(Connection::class);
$conn->insert('cms', [
'shop_id' => $shop->getUid(),
'title' => 'Test CMS Page',
'text1' => $text,
'url' => 'test-cms-' . uniqid(),
'sor' => 1,
'enable' => 1,
'pos' => 'content',
'menu' => 'main',
'notinmenu' => 0,
'modul' => 'default',
'parameter' => '',
'copy_market' => 0,
]);
return (int) $conn->lastInsertId();
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Tests\PSC\System\ContentEngineBundle\Api;
class DesignTest extends ContentEngineTestCase
{
public function testProcessDesign(): void
{
$client = static::createClient();
$cmsId = $this->createCmsPage();
$json = json_decode('[{"uuid":"df2df718-b28e-482d-bf0c-67d246f05d32","name":"Test CMS","options":[{"id":"auflage","name":"Auflage","default":"100","dependencys":[],"placeHolder":"","required":false,"type":2}]}]', true);
$client->jsonRequest('POST', '/api/content-engine/design', [
'id' => $cmsId,
'module' => 'cms',
'json' => $json,
]);
$this->assertResponseIsSuccessful();
$data = json_decode($client->getResponse()->getContent(), true);
self::assertArrayHasKey('json', $data);
self::assertArrayHasKey('xml', $data);
self::assertArrayHasKey('jsonGraph', $data);
self::assertNotEmpty($data['json']);
self::assertNotEmpty($data['xml']);
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace Tests\PSC\System\ContentEngineBundle\Api;
class PreviewTest extends ContentEngineTestCase
{
public function testPreviewWithInputElement(): void
{
$client = static::createClient();
$json = json_decode('[{"uuid":"2db72c84-67a9-4fcf-99da-9307255572af","name":"CMS Preview Test","options":[{"id":"auflage","name":"Auflage","default":"100","dependencys":[{"relation":"auflage","formula":"","borders":[{"calcValue":"","formula":"$Vauflage$V*0.12","price":0,"value":"1-","dependencys":[]}]}],"placeHolder":"Placeholder","required":true,"minValue":1,"maxValue":200,"type":2}]}]', true);
$client->jsonRequest('POST', '/api/content-engine/preview', [
'json' => $json,
'values' => ['auflage' => 100],
]);
$this->assertResponseIsSuccessful();
$data = json_decode($client->getResponse()->getContent(), true);
self::assertArrayHasKey('elements', $data);
self::assertNotEmpty($data['elements']);
// ContentEngine preview should NOT contain pricing data
self::assertArrayNotHasKey('netto', $data);
self::assertArrayNotHasKey('brutto', $data);
}
public function testPreviewWithRowLayout(): void
{
$client = static::createClient();
$json = json_decode('[{"uuid":"2b6de5b2-4dac-4258-8a07-83c14552b7b8","name":"Row Test","options":[{"id":"row1","type":7,"dependencys":[],"columns":[{"id":"col1","type":8,"dependencys":[],"options":[{"id":"headline","type":6,"dependencys":[],"default":"Headline","name":"","variant":"1"}]},{"id":"col2","type":8,"dependencys":[],"options":[{"id":"text","type":6,"dependencys":[],"default":"Text","name":"","variant":"1"}]}]}]}]', true);
$client->jsonRequest('POST', '/api/content-engine/preview', [
'json' => $json,
'values' => [],
]);
$this->assertResponseIsSuccessful();
$data = json_decode($client->getResponse()->getContent(), true);
self::assertArrayHasKey('elements', $data);
}
public function testPreviewWithSelectElement(): void
{
$client = static::createClient();
$json = json_decode('[{"uuid":"df2df718-b28e-482d-bf0c-67d246f05d32","name":"Select Test","options":[{"id":"farbe","name":"Farbe","default":"1","dependencys":[],"type":3,"options":[{"id":"1","name":"Rot","dependencys":[]},{"id":"2","name":"Blau","dependencys":[]}],"mode":"normal"}]}]', true);
$client->jsonRequest('POST', '/api/content-engine/preview', [
'json' => $json,
'values' => ['farbe' => '1'],
]);
$this->assertResponseIsSuccessful();
$data = json_decode($client->getResponse()->getContent(), true);
self::assertArrayHasKey('elements', $data);
self::assertNotEmpty($data['elements']);
$element = $data['elements'][0];
self::assertSame('farbe', $element['id']);
self::assertNotEmpty($element['options']);
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace Tests\PSC\System\ContentEngineBundle\Api;
use Doctrine\DBAL\Connection;
use PSC\Shop\ContactBundle\Repository\ContactRepository;
class SaveTest extends ContentEngineTestCase
{
public function testSaveXmlToCms(): void
{
$client = static::createClient();
$cmsId = $this->createCmsPage('');
$userRepository = static::getContainer()->get(ContactRepository::class);
$testUser = $userRepository->loadUserByUsername('admin@shop.de');
$client->loginUser($testUser, 'api');
$newXml = '<?xml version="1.0" encoding="utf-8"?>
<kalkulation>
<artikel>
<name>Updated CMS</name>
<option id="field1" name="Feld 1" type="Input" default="test"/>
</artikel>
</kalkulation>';
$client->jsonRequest('PUT', '/api/content-engine/save', [
'id' => $cmsId,
'module' => 'cms',
'xml' => $newXml,
]);
$this->assertResponseIsSuccessful();
$data = json_decode($client->getResponse()->getContent(), true);
self::assertTrue($data['success']);
// Verify the XML was actually persisted
/** @var Connection $conn */
$conn = static::getContainer()->get(Connection::class);
$text = $conn->fetchOne('SELECT text1 FROM cms WHERE id = ?', [$cmsId]);
self::assertStringContainsString('Updated CMS', $text);
}
public function testSaveRequiresAuthentication(): void
{
$client = static::createClient();
$cmsId = $this->createCmsPage('');
$client->jsonRequest('PUT', '/api/content-engine/save', [
'id' => $cmsId,
'module' => 'cms',
'xml' => '<kalkulation/>',
]);
self::assertSame(401, $client->getResponse()->getStatusCode());
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace Tests\PSC\System\ContentEngineBundle\Api;
class XmlTest extends ContentEngineTestCase
{
public function testProcessXml(): void
{
$client = static::createClient();
$xml = '<?xml version="1.0" encoding="utf-8"?>
<kalkulation>
<artikel>
<name>CMS Test</name>
<option id="auflage" name="Auflage" type="Input" default="10"/>
<option id="calc" type="Hidden">
<auflage>
<grenze formel="$Vauflage$V*0.5">1-</grenze>
</auflage>
</option>
</artikel>
</kalkulation>';
$cmsId = $this->createCmsPage($xml);
$client->jsonRequest('POST', '/api/content-engine/xml', [
'id' => $cmsId,
'module' => 'cms',
'xml' => $xml,
]);
$this->assertResponseIsSuccessful();
$data = json_decode($client->getResponse()->getContent(), true);
self::assertArrayHasKey('json', $data);
self::assertArrayHasKey('xml', $data);
self::assertArrayHasKey('jsonGraph', $data);
self::assertNotEmpty($data['json']);
self::assertNotEmpty($data['xml']);
}
}

View File

@ -447,6 +447,7 @@ CREATE TABLE `article` (
`upload_steplayouter2` int(1) DEFAULT 0, `upload_steplayouter2` int(1) DEFAULT 0,
`upload_steplayouter2_status` int(8) DEFAULT NULL, `upload_steplayouter2_status` int(8) DEFAULT NULL,
`sub_title` varchar(100) DEFAULT NULL, `sub_title` varchar(100) DEFAULT NULL,
`confirmExternal` tinyint(1) DEFAULT 0,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `install_id_idx` (`install_id`), KEY `install_id_idx` (`install_id`),
KEY `shop_id_idx` (`shop_id`), KEY `shop_id_idx` (`shop_id`),
@ -1659,6 +1660,8 @@ CREATE TABLE `orderspos` (
`shipping_price_mwert` float DEFAULT NULL, `shipping_price_mwert` float DEFAULT NULL,
`ref` varchar(255) DEFAULT NULL, `ref` varchar(255) DEFAULT NULL,
`kst` varchar(255) DEFAULT NULL, `kst` varchar(255) DEFAULT NULL,
`external_approval_status` int(8) DEFAULT NULL,
`external_approval_message` text DEFAULT NULL,
PRIMARY KEY (`id`,`orders_id`), PRIMARY KEY (`id`,`orders_id`),
KEY `install_id_idx` (`install_id`), KEY `install_id_idx` (`install_id`),
KEY `shop_id_idx` (`shop_id`), KEY `shop_id_idx` (`shop_id`),

View File

@ -14,15 +14,18 @@ onMounted(() => {
globalStore.setShopUuid(shopUuid!) globalStore.setShopUuid(shopUuid!)
if(mode) { if(mode) {
let x: number = parseInt(mode!) const modeMap: Record<string, Mode> = { 'cms': Mode.CMS, 'news': Mode.News, 'product': Mode.Product }
globalStore.setMode(x as Mode) const parsed = modeMap[mode.toLowerCase()] ?? parseInt(mode)
if (parsed) globalStore.setMode(parsed as Mode)
} }
if (uuid) { if (uuid) {
globalStore.setProductUuid(uuid) globalStore.setProductUuid(uuid)
globalStore.loadConfigFromProductApi(uuid).then(data => { globalStore.loadConfigFromProductApi(uuid).then(data => {
itemStore.parseJSON(data) itemStore.parseJSON(data)
}) })
globalStore.loadFormulaAnalyserDataFromApi(uuid) if (globalStore.isProductMode) {
globalStore.loadFormulaAnalyserDataFromApi(uuid)
}
} }
itemStore.$subscribe((mutation, state) => { itemStore.$subscribe((mutation, state) => {

View File

@ -118,13 +118,14 @@ function toggleAi() {
<div class="flex-1 flex justify-center"> <div class="flex-1 flex justify-center">
<TabsList> <TabsList>
<TabsTrigger value="designer">{{ $t('designer') }}</TabsTrigger> <TabsTrigger value="designer">{{ $t('designer') }}</TabsTrigger>
<TabsTrigger value="preview">{{ $t('preview') }}</TabsTrigger> <TabsTrigger v-if="globalStore.isProductMode" value="preview">{{ $t('preview') }}</TabsTrigger>
<TabsTrigger value="xml">{{ $t('xml_view') }}</TabsTrigger> <TabsTrigger value="xml">{{ $t('xml_view') }}</TabsTrigger>
<TabsTrigger value="paperdb">{{ $t('paperdb_view') }}</TabsTrigger> <TabsTrigger v-if="globalStore.isProductMode" value="paperdb">{{ $t('paperdb_view') }}</TabsTrigger>
</TabsList> </TabsList>
</div> </div>
<div class="flex gap-1"> <div class="flex gap-1">
<Button <Button
v-if="globalStore.isProductMode"
@click="toggleFormel" @click="toggleFormel"
:variant="globalStore.showFormel ? 'default' : 'outline'" :variant="globalStore.showFormel ? 'default' : 'outline'"
size="sm" size="sm"
@ -133,6 +134,7 @@ function toggleAi() {
{{ $t('formel_view') }} {{ $t('formel_view') }}
</Button> </Button>
<Button <Button
v-if="globalStore.isProductMode"
@click="toggleParameter" @click="toggleParameter"
:variant="globalStore.showParameter ? 'default' : 'outline'" :variant="globalStore.showParameter ? 'default' : 'outline'"
size="sm" size="sm"
@ -157,13 +159,13 @@ function toggleAi() {
<Preview v-else /> <Preview v-else />
</div> </div>
</TabsContent> </TabsContent>
<TabsContent value="preview" class="flex-1 overflow-y-auto min-h-0"> <TabsContent v-if="globalStore.isProductMode" value="preview" class="flex-1 overflow-y-auto min-h-0">
<FormulaVisualizer /> <FormulaVisualizer />
</TabsContent> </TabsContent>
<TabsContent value="xml" class="flex-1 overflow-y-auto min-h-0"> <TabsContent value="xml" class="flex-1 overflow-y-auto min-h-0">
<XmlView /> <XmlView />
</TabsContent> </TabsContent>
<TabsContent value="paperdb" class="flex-1 overflow-y-auto min-h-0"> <TabsContent v-if="globalStore.isProductMode" value="paperdb" class="flex-1 overflow-y-auto min-h-0">
<PaperDBView /> <PaperDBView />
</TabsContent> </TabsContent>
</Tabs> </Tabs>
@ -187,6 +189,7 @@ function toggleAi() {
<div class="flex items-center justify-between px-3 py-1.5 border-b border-gray-200 bg-slate-50 shrink-0"> <div class="flex items-center justify-between px-3 py-1.5 border-b border-gray-200 bg-slate-50 shrink-0">
<span class="text-xs font-medium text-gray-500 uppercase tracking-wide">Code</span> <span class="text-xs font-medium text-gray-500 uppercase tracking-wide">Code</span>
<Button <Button
v-if="globalStore.isProductMode"
@click="globalStore.syncFormulasAndParameter()" @click="globalStore.syncFormulasAndParameter()"
:disabled="globalStore.syncing" :disabled="globalStore.syncing"
size="sm" size="sm"

View File

@ -47,10 +47,10 @@ onMounted(() => {
<div v-if="!isLoading && previewData"> <div v-if="!isLoading && previewData">
<div class="overflow-auto h-full"> <div class="overflow-auto h-full">
<div class="flex flex-row"> <div class="flex flex-row">
<div class="w-3/5 flex flex-col gap-2"> <div :class="globalStore.isProductMode ? 'w-3/5' : 'w-full'" class="flex flex-col gap-2">
<RenderElements :items="items" @update:value="handleUpdate"/> <RenderElements :items="items" @update:value="handleUpdate"/>
</div> </div>
<div class="w-2/5 pl-6"> <div v-if="globalStore.isProductMode" class="w-2/5 pl-6">
<PriceDisplay :price-data="price" /> <PriceDisplay :price-data="price" />
</div> </div>
</div> </div>

View File

@ -190,6 +190,58 @@ export const sendAiMessage = async (
// returns { reply: string, xml?: string, formulas?: string, parameter?: string } // returns { reply: string, xml?: string, formulas?: string, parameter?: string }
}; };
// ContentEngine API (for CMS/content mode — no pricing, formulas, paper)
export const loadContentConfig = async (id: number, module: string) => {
try {
const response = await api.post('api/content-engine/config', { json: { id, module } });
return await response.json();
} catch (error) {
console.error('Error loading content config:', error);
throw error;
}
};
export const saveContentDesign = async (id: number, module: string, json: object[]) => {
try {
const response = await api.post('api/content-engine/design', { json: { id, module, json } });
return await response.json();
} catch (error) {
console.error('Error saving content design:', error);
throw error;
}
};
export const saveContentXml = async (id: number, module: string, xml: string) => {
try {
const response = await api.post('api/content-engine/xml', { json: { id, module, xml } });
return await response.json();
} catch (error) {
console.error('Error saving content XML:', error);
throw error;
}
};
export const saveContentToApi = async (id: number, module: string, xml: string) => {
try {
const response = await api.put('api/content-engine/save', { json: { id, module, xml } });
return await response.json();
} catch (error) {
console.error('Error saving content:', error);
throw error;
}
};
export const fetchContentPreview = async (json: object[], values?: Record<string, any>) => {
try {
const response = await api.post('api/content-engine/preview', { json: { json, values } });
return await response.json();
} catch (error) {
console.error('Error fetching content preview:', error);
throw error;
}
};
export const fetchMediaUrl = async (uuid: string) => { export const fetchMediaUrl = async (uuid: string) => {
try { try {
const response = await api.get(`api/media/${uuid}`); const response = await api.get(`api/media/${uuid}`);

View File

@ -2,7 +2,7 @@ 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 type { PreviewResponse } from '../model/preview/types'; import type { PreviewResponse } from '../model/preview/types';
import { saveProductToApi, saveFomulasAndParameterToApi, loadJsonFromApi, loadPriceFromApi, savePaperContainerToApi, saveDesignToApi, saveXmlToApi, fetchPreview } from '../lib/api' import { saveProductToApi, saveFomulasAndParameterToApi, loadJsonFromApi, loadPriceFromApi, savePaperContainerToApi, saveDesignToApi, saveXmlToApi, fetchPreview, loadContentConfig, saveContentDesign, saveContentXml, saveContentToApi, fetchContentPreview } from '../lib/api'
import { useItemStore } from './Items' import { useItemStore } from './Items'
export const useGlobalStore = defineStore('global', { export const useGlobalStore = defineStore('global', {
@ -50,6 +50,10 @@ export const useGlobalStore = defineStore('global', {
getFormulaData: (state) => state.formulaData, getFormulaData: (state) => state.formulaData,
getFormulaError: (state) => state.formulaError, getFormulaError: (state) => state.formulaError,
getPreviewData: (state) => state.previewData as PreviewResponse | null, getPreviewData: (state) => state.previewData as PreviewResponse | null,
isProductMode: (state) => state.mode === Mode.Product,
isContentMode: (state) => state.mode === Mode.CMS || state.mode === Mode.News,
contentModule: (state) => state.mode === Mode.CMS ? 'cms' : state.mode === Mode.News ? 'news' : '',
contentId: (state) => parseInt(state.productUuid) || 0,
}, },
actions: { actions: {
setXml(value: string) { setXml(value: string) {
@ -120,10 +124,13 @@ export const useGlobalStore = defineStore('global', {
this.formulas = snapshot.formulas this.formulas = snapshot.formulas
this.parameter = snapshot.parameter this.parameter = snapshot.parameter
const itemStore = useItemStore() const itemStore = useItemStore()
return saveXmlToApi(this.productUuid, snapshot.xml).then((result: any) => { const xmlPromise = this.isContentMode
? saveContentXml(this.contentId, this.contentModule, snapshot.xml)
: saveXmlToApi(this.productUuid, snapshot.xml)
return xmlPromise.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) if (result.jsonGraph) this.formulaData = JSON.parse(result.jsonGraph)
itemStore.parseJSON(result.json) itemStore.parseJSON(result.json)
}) })
}, },
@ -133,10 +140,12 @@ export const useGlobalStore = defineStore('global', {
const itemStore = useItemStore() const itemStore = useItemStore()
this.syncing = true this.syncing = true
try { try {
const syncResult: any = await saveXmlToApi(this.productUuid, result.xml) const syncResult: any = this.isContentMode
? await saveContentXml(this.contentId, this.contentModule, result.xml)
: await saveXmlToApi(this.productUuid, result.xml)
this.setXML(syncResult.xml) this.setXML(syncResult.xml)
this.setJSON(syncResult.json) this.setJSON(syncResult.json)
this.formulaData = JSON.parse(syncResult.jsonGraph) if (syncResult.jsonGraph) this.formulaData = JSON.parse(syncResult.jsonGraph)
itemStore.parseJSON(syncResult.json) itemStore.parseJSON(syncResult.json)
} finally { } finally {
this.syncing = false this.syncing = false
@ -156,6 +165,12 @@ export const useGlobalStore = defineStore('global', {
this.shopUuid = value this.shopUuid = value
}, },
async loadConfigFromProductApi(uuid: string) { async loadConfigFromProductApi(uuid: string) {
if (this.isContentMode) {
const data: any = await loadContentConfig(this.contentId, this.contentModule)
this.json = data.json
this.xml = data.xml
return data.json
}
const data: any = await loadJsonFromApi(uuid) const data: any = await loadJsonFromApi(uuid)
this.json = data.json this.json = data.json
this.xml = data.xml this.xml = data.xml
@ -192,25 +207,34 @@ export const useGlobalStore = defineStore('global', {
this.json = json this.json = json
}, },
saveDesign(json: object[]) { saveDesign(json: object[]) {
saveDesignToApi(this.productUuid, this.shopUuid, json).then((result: any) => { const designPromise = this.isContentMode
? saveContentDesign(this.contentId, this.contentModule, json)
: saveDesignToApi(this.productUuid, this.shopUuid, json)
designPromise.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); if (result.jsonGraph) this.formulaData = JSON.parse(result.jsonGraph);
}) })
}, },
manualSave() { manualSave() {
this.saving = true this.saving = true
saveProductToApi(this.productUuid, this.xml).then((result: any) => { const savePromise = this.isContentMode
? saveContentToApi(this.contentId, this.contentModule, this.xml)
: saveProductToApi(this.productUuid, this.xml)
savePromise.then((result: any) => {
this.saving = false this.saving = false
}) })
}, },
manualSync() { manualSync() {
this.syncing = true this.syncing = true
if (this.currentTab == 'xml') { if (this.currentTab == 'xml') {
saveXmlToApi(this.productUuid, this.xml).then((result: any) => { const xmlPromise = this.isContentMode
? saveContentXml(this.contentId, this.contentModule, this.xml)
: saveXmlToApi(this.productUuid, this.xml)
xmlPromise.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); if (result.jsonGraph) this.formulaData = JSON.parse(result.jsonGraph);
this.syncing = false this.syncing = false
const itemStore = useItemStore() const itemStore = useItemStore()
itemStore.parseJSON(result.json) itemStore.parseJSON(result.json)
@ -236,7 +260,9 @@ export const useGlobalStore = defineStore('global', {
async loadPreview(json: object[], values?: Record<string, any>) { async loadPreview(json: object[], values?: Record<string, any>) {
this.previewError = ''; this.previewError = '';
try { try {
const response: any = await fetchPreview(this.shopUuid, json, values); const response: any = this.isContentMode
? await fetchContentPreview(json, values)
: await fetchPreview(this.shopUuid, json, values);
this.previewData = response; this.previewData = response;
} catch (e: any) { } catch (e: any) {
this.previewError = `Failed to load preview data: ${e.message}`; this.previewError = `Failed to load preview data: ${e.message}`;

View File

@ -1,11 +1,12 @@
<?php <?php
namespace Plugin\Custom\PSC\FormBuilder; namespace Plugin\Custom\PSC\FormBuilder;
use PSC\System\PluginBundle\Plugin\Base; use PSC\System\PluginBundle\Plugin\Base;
class Plugin extends Base implements \PSC\System\PluginBundle\Interfaces\Plugin { class Plugin extends Base implements \PSC\System\PluginBundle\Interfaces\Plugin
{
protected $name = 'FormBuilder'; protected $name = 'ContentBuilder';
public function getType() public function getType()
{ {
@ -14,7 +15,7 @@ class Plugin extends Base implements \PSC\System\PluginBundle\Interfaces\Plugin
public function getDescription() public function getDescription()
{ {
return 'Formulare Kalkulation und CMS Builder'; return 'Content, Formulare, Kalkulation-Builder';
} }
public function getVersion() public function getVersion()

File diff suppressed because one or more lines are too long

View File

@ -1254,6 +1254,14 @@ html {
height: 100vh; height: 100vh;
} }
.max-h-32{
max-height: 8rem;
}
.max-h-\[90vh\]{
max-height: 90vh;
}
.min-h-\[2\.25rem\]{ .min-h-\[2\.25rem\]{
min-height: 2.25rem; min-height: 2.25rem;
} }
@ -1407,6 +1415,10 @@ html {
max-width: 75%; max-width: 75%;
} }
.max-w-\[90vw\]{
max-width: 90vw;
}
.max-w-md{ .max-w-md{
max-width: 28rem; max-width: 28rem;
} }
@ -1953,6 +1965,10 @@ html {
border-color: rgb(250 204 21 / var(--tw-border-opacity)); border-color: rgb(250 204 21 / var(--tw-border-opacity));
} }
.bg-black\/70{
background-color: rgb(0 0 0 / 0.7);
}
.bg-blue-100{ .bg-blue-100{
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(219 234 254 / var(--tw-bg-opacity)); background-color: rgb(219 234 254 / var(--tw-bg-opacity));
@ -2675,6 +2691,12 @@ html {
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
} }
.backdrop-blur-sm{
--tw-backdrop-blur: blur(4px);
-webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
}
.backdrop-blur-xl{ .backdrop-blur-xl{
--tw-backdrop-blur: blur(24px); --tw-backdrop-blur: blur(24px);
-webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
@ -4130,6 +4152,10 @@ html {
text-decoration-line: underline; text-decoration-line: underline;
} }
.hover\:opacity-80:hover{
opacity: 0.8;
}
.hover\:shadow-lg:hover{ .hover\:shadow-lg:hover{
--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);

View File

@ -1,11 +1,16 @@
info: info:
datum: 20.03.2026 datum: 26.03.2026
release: 2.3.5 release: 2.3.5
changelog: changelog:
- version: 2.3.5 - version: 2.3.5
datum: 20.03.2026 datum: 26.03.2026
changes: changes:
- text: "CMS ContentBuilder Plugin kann aktiviert. (Custom Plugin und das Storefront Template muss die Ausgabe unterstützen)"
images:
- "screen1.png"
- "screen2.png"
- "CMS Wysiwyg respektieren von h,ul tags"
- "Löschen von Kunden wenn kein Afträge da." - "Löschen von Kunden wenn kein Afträge da."
- version: 2.3.4 - version: 2.3.4
datum: 19.03.2026 datum: 19.03.2026