This commit is contained in:
Thomas Peterson 2025-07-14 19:04:30 +02:00
parent ab2971e56d
commit dd2af7f810
51 changed files with 4347 additions and 2623 deletions

View File

@ -95,7 +95,9 @@ RUN docker-php-ext-install -j$(nproc) ldap
#RUN docker-php-ext-configure imap --with-kerberos --with-imap-ssl && \ #RUN docker-php-ext-configure imap --with-kerberos --with-imap-ssl && \
# docker-php-ext-install -j$(nproc) imap # docker-php-ext-install -j$(nproc) imap
RUN pecl install imap RUN pecl install imap \
&& docker-php-ext-enable imap
# COPY ./.docker/images/php/base/pdf/php_pdflib.so /pdflib.so # COPY ./.docker/images/php/base/pdf/php_pdflib.so /pdflib.so

View File

@ -18,7 +18,7 @@ NO_COLOR:=\033[0m
# Tool CLI config # Tool CLI config
PHPUNIT_CMD=php -dxdebug.mode=off vendor/bin/phpunit PHPUNIT_CMD=php -dxdebug.mode=off vendor/bin/phpunit
PHPUNIT_CMD_XDEBUG=php -dxdebug.client_host=172.30.171.37 vendor/bin/phpunit PHPUNIT_CMD_XDEBUG=php -dxdebug.client_host=172.30.171.37 vendor/bin/phpunit
PHPUNIT_ARGS= PHPUNIT_ARGS=--display-deprecations
PHPUNIT_FILES= PHPUNIT_FILES=
PHPSTAN_CMD=php -d xdebug.mode=off -d memory_limit=-1 vendor/bin/phpstan analyse PHPSTAN_CMD=php -d xdebug.mode=off -d memory_limit=-1 vendor/bin/phpstan analyse
PHPSTAN_ARGS=--level=9 PHPSTAN_ARGS=--level=9

View File

@ -12,8 +12,6 @@
/*!40103 SET TIME_ZONE='+00:00' */; /*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
-- --
-- Table structure for table `account` -- Table structure for table `account`

View File

@ -3,6 +3,7 @@ APP_ENV=dev
APP_SECRET=347829efiubvf347fbisdc27f APP_SECRET=347829efiubvf347fbisdc27f
###< symfony/framework-bundle ### ###< symfony/framework-bundle ###
MAILER_DSN=smtp://smtp4dev:25
###> doctrine/doctrine-bundle ### ###> doctrine/doctrine-bundle ###
DATABASE_URL=mysql://psc:psc@mysql:3306/psc DATABASE_URL=mysql://psc:psc@mysql:3306/psc
###< doctrine/doctrine-bundle ### ###< doctrine/doctrine-bundle ###

View File

@ -13,7 +13,7 @@
} }
}, },
"require": { "require": {
"php": "8.2.*", "php": "8.4.*",
"ext-ctype": "*", "ext-ctype": "*",
"ext-iconv": "*", "ext-iconv": "*",
"ext-mongodb": "^2", "ext-mongodb": "^2",
@ -24,15 +24,17 @@
"chillerlan/php-qrcode": "v5.0.x-dev", "chillerlan/php-qrcode": "v5.0.x-dev",
"cocur/slugify": "v3.1", "cocur/slugify": "v3.1",
"composer/package-versions-deprecated": "^1.8", "composer/package-versions-deprecated": "^1.8",
"ddeboer/imap": "1.18.*", "ddeboer/imap": "1.21.*",
"doctrine/annotations": "^2", "doctrine/annotations": "^2",
"doctrine/cache": "^2", "doctrine/cache": "^2",
"doctrine/doctrine-bundle": "^2", "doctrine/doctrine-bundle": "^2",
"doctrine/mongodb-odm-bundle": "^5", "doctrine/mongodb-odm-bundle": "^5",
"doctrine/orm": "^2.7", "doctrine/orm": "^2.7",
"mistic100/randomcolor": "^1.1",
"spatie/array-to-xml": "^3.4",
"gabrielbull/ups-api": "dev-master", "gabrielbull/ups-api": "dev-master",
"gregwar/captcha-bundle": "^2.2", "gregwar/captcha-bundle": "^2.2",
"guzzlehttp/guzzle": "^6", "guzzlehttp/guzzle": "^7",
"horstoeko/zugferd": "^1.0", "horstoeko/zugferd": "^1.0",
"incenteev/composer-parameter-handler": "^2.0", "incenteev/composer-parameter-handler": "^2.0",
"jms/serializer-bundle": "5.*", "jms/serializer-bundle": "5.*",
@ -48,12 +50,10 @@
"nelmio/api-doc-bundle": "v4.11.1", "nelmio/api-doc-bundle": "v4.11.1",
"nelmio/cors-bundle": "^2.2", "nelmio/cors-bundle": "^2.2",
"nicolab/php-ftp-client": "^1.4", "nicolab/php-ftp-client": "^1.4",
"ocramius/package-versions": "2.6.*",
"oneup/uploader-bundle": "^3", "oneup/uploader-bundle": "^3",
"oyejorge/less.php": "~1.5", "oyejorge/less.php": "~1.5",
"paypal/paypal-checkout-sdk": "dev-master", "paypal/paypal-checkout-sdk": "dev-master",
"paypal/rest-api-sdk-php": "dev-master", "paypal/rest-api-sdk-php": "dev-master",
"php-http/guzzle6-adapter": "^1.1",
"phpoffice/phpspreadsheet": "^1.28", "phpoffice/phpspreadsheet": "^1.28",
"phpseclib/phpseclib": "~3.0", "phpseclib/phpseclib": "~3.0",
"picqer/sendcloud-php-client": "v2.8.1", "picqer/sendcloud-php-client": "v2.8.1",
@ -103,10 +103,10 @@
"symfonycasts/sass-bundle": "^0.8.2", "symfonycasts/sass-bundle": "^0.8.2",
"symfonycasts/tailwind-bundle": "^0.7.0", "symfonycasts/tailwind-bundle": "^0.7.0",
"tp/paydirekt-php": "^4.0", "tp/paydirekt-php": "^4.0",
"twig/extra-bundle": "^2.12|^3.0", "twig/extra-bundle": "^3",
"twig/intl-extra": "^3.8", "twig/intl-extra": "^3",
"twig/string-extra": "^3.6", "twig/string-extra": "^3",
"twig/twig": "^3.0", "twig/twig": "^3",
"zfb/zfb-vm": "dev-master" "zfb/zfb-vm": "dev-master"
}, },
"require-dev": { "require-dev": {
@ -116,14 +116,14 @@
"mockery/mockery": "^1.5", "mockery/mockery": "^1.5",
"php-parallel-lint/php-parallel-lint": "dev-develop", "php-parallel-lint/php-parallel-lint": "dev-develop",
"phpstan/phpstan": "^1", "phpstan/phpstan": "^1",
"phpunit/phpunit": "9.5.x", "phpunit/phpunit": "^10",
"rector/rector": "0.19.2", "rector/rector": "0.19.2",
"squizlabs/php_codesniffer": "*", "squizlabs/php_codesniffer": "*",
"symfony/browser-kit": "*", "symfony/browser-kit": "*",
"symfony/css-selector": "*", "symfony/css-selector": "*",
"symfony/debug-bundle": "*", "symfony/debug-bundle": "*",
"symfony/panther": "^2.1", "symfony/panther": "^2.1",
"symfony/phpunit-bridge": "6.0.x-dev", "symfony/phpunit-bridge": "^7",
"symfony/stopwatch": "*", "symfony/stopwatch": "*",
"symfony/web-profiler-bundle": "*", "symfony/web-profiler-bundle": "*",
"symplify/config-transformer": "^11.1", "symplify/config-transformer": "^11.1",
@ -137,7 +137,7 @@
"sort-packages": true, "sort-packages": true,
"optimize-autoloader": true, "optimize-autoloader": true,
"platform": { "platform": {
"php": "8.2.28" "php": "8.4"
}, },
"allow-plugins": { "allow-plugins": {
"symfony/flex": true, "symfony/flex": true,

2089
src/new/composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -5,5 +5,8 @@ declare(strict_types=1);
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void { return static function (ContainerConfigurator $containerConfigurator): void {
$containerConfigurator->extension('framework', ['test' => true, 'session' => ['storage_factory_id' => 'session.storage.factory.mock_file']]); $containerConfigurator->extension('framework', [
'test' => true,
'session' => ['storage_factory_id' => 'session.storage.factory.mock_file'],
]);
}; };

View File

@ -15,14 +15,15 @@ use PSC\System\SettingsBundle\Service\Shop as AliasedShop;
class Shop class Shop
{ {
public function __construct(private readonly AliasedShop $shopService) public function __construct(private readonly AliasedShop $shopService)
{} {
}
public function fromEntity(\PSC\Component\ApiBundle\Model\Shop $shop, \PSC\Shop\EntityBundle\Entity\Shop $shopEntity): void public function fromEntity(\PSC\Component\ApiBundle\Model\Shop $shop, \PSC\Shop\EntityBundle\Entity\Shop $shopEntity): void
{ {
$shop->uuid = $shopEntity->getUuid(); $shop->uuid = $shopEntity->getUuid();
$shop->id = $shopEntity->getUID(); $shop->id = $shopEntity->getUID();
$shop->name = $shopEntity->getTitle(); $shop->name = $shopEntity->getTitle();
$shop->deleted = (bool)$shopEntity->isDeleted(); $shop->disabled = (bool)$shopEntity->isDeleted();
$shop->private = (bool)$shopEntity->isPrivate(); $shop->private = (bool)$shopEntity->isPrivate();
$shop->basketField1 = (string)$shopEntity->getBasketfield1(); $shop->basketField1 = (string)$shopEntity->getBasketfield1();
$shop->basketField2 = (string)$shopEntity->getBasketfield2(); $shop->basketField2 = (string)$shopEntity->getBasketfield2();

View File

@ -53,7 +53,8 @@ class FileHandler extends AbstractMediaHandler
/** /**
* Constructor * Constructor
* @param int $priority *
* @param int $priority
* @param MimeTypeGuesserFactoryInterface $mimeTypeGuesserFactory * @param MimeTypeGuesserFactoryInterface $mimeTypeGuesserFactory
*/ */
public function __construct($priority, MimeTypeGuesserFactoryInterface $mimeTypeGuesserFactory) public function __construct($priority, MimeTypeGuesserFactoryInterface $mimeTypeGuesserFactory)
@ -121,10 +122,9 @@ class FileHandler extends AbstractMediaHandler
*/ */
public function canHandle($object) public function canHandle($object)
{ {
if ( if ($object instanceof File
$object instanceof File || || ($object instanceof Media
($object instanceof Media && && $object->getLocation() == 'local')
(is_file($object->getContent()) || $object->getLocation() == 'local'))
) { ) {
return true; return true;
} }
@ -197,15 +197,15 @@ class FileHandler extends AbstractMediaHandler
if ($exif['Orientation'] == 3 or $exif['Orientation'] == 6 or $exif['Orientation'] == 8) { if ($exif['Orientation'] == 3 or $exif['Orientation'] == 6 or $exif['Orientation'] == 8) {
$imageResource = imagecreatefromjpeg($filename); $imageResource = imagecreatefromjpeg($filename);
switch ($exif['Orientation']) { switch ($exif['Orientation']) {
case 3: case 3:
$image = imagerotate($imageResource, 180, 0); $image = imagerotate($imageResource, 180, 0);
break; break;
case 6: case 6:
$image = imagerotate($imageResource, -90, 0); $image = imagerotate($imageResource, -90, 0);
break; break;
case 8: case 8:
$image = imagerotate($imageResource, 90, 0); $image = imagerotate($imageResource, 90, 0);
break; break;
} }
imagejpeg($image, $filename); imagejpeg($image, $filename);
imagedestroy($imageResource); imagedestroy($imageResource);
@ -255,7 +255,7 @@ class FileHandler extends AbstractMediaHandler
/** /**
* *
* *
* @param Media $media * @param Media $media
* @return string * @return string
*/ */
private function getFilePath(Media $media) private function getFilePath(Media $media)
@ -286,7 +286,9 @@ class FileHandler extends AbstractMediaHandler
public function createNew($data) public function createNew($data)
{ {
if ($data instanceof File) { if ($data instanceof File) {
/** @var $data File */ /**
* @var $data File
*/
$media = new Media(); $media = new Media();
if (method_exists($data, 'getClientOriginalName')) { if (method_exists($data, 'getClientOriginalName')) {

View File

@ -64,9 +64,9 @@ class Order extends Base
$order->setStatus($orderEntity->getStatus()); $order->setStatus($orderEntity->getStatus());
$order->setBasketField1((string)$orderEntity->getBasketfield1()); $order->setBasketField1((string)$orderEntity->getBasketfield1());
$order->setBasketField2((string)$orderEntity->getBasketfield2()); $order->setBasketField2((string)$orderEntity->getBasketfield2());
$order->setNet($orderEntity->getNetto() * 100); $order->setNet((int)($orderEntity->getNetto() * 100));
$order->setVat($orderEntity->getSteuer() * 100); $order->setVat((int)($orderEntity->getSteuer() * 100));
$order->setGross($orderEntity->getBrutto() * 100); $order->setGross((int)($orderEntity->getBrutto() * 100));
$order->setExternalOrderNumber((string)$orderEntity->getPackage()); $order->setExternalOrderNumber((string)$orderEntity->getPackage());
$order->setPaymentRef((string)$orderDoc->getPaymentRef()); $order->setPaymentRef((string)$orderDoc->getPaymentRef());
$order->setPaymentGateway((string)$orderDoc->getPaymentGateway()); $order->setPaymentGateway((string)$orderDoc->getPaymentGateway());
@ -134,9 +134,9 @@ class Order extends Base
$vat += $discounts->getPrice()->getVat(); $vat += $discounts->getPrice()->getVat();
$gross += $discounts->getPrice()->getGross(); $gross += $discounts->getPrice()->getGross();
} }
$order->setNetWithDiscount($order->getNet() - $net); $order->setNetWithDiscount((int)($order->getNet() - $net));
$order->setVatWithDiscount($order->getVat() - $vat); $order->setVatWithDiscount((int)($order->getVat() - $vat));
$order->setGrossWithDiscount($order->getGross() - $gross); $order->setGrossWithDiscount((int)($order->getGross() - $gross));
} }
} }

View File

@ -118,12 +118,12 @@ class Position extends Base
$position->setUuid($pos->getUuid()); $position->setUuid($pos->getUuid());
$position->setUid($pos->getId()); $position->setUid($pos->getId());
$position->getPrice()->setCount($pos->getCount()); $position->getPrice()->setCount($pos->getCount());
$position->getPrice()->setNet($pos->getPriceOneNetto() * 100); $position->getPrice()->setNet((int)($pos->getPriceOneNetto() * 100));
$position->getPrice()->setAllNet($pos->getPriceAllNetto() * 100); $position->getPrice()->setAllNet((int)($pos->getPriceAllNetto() * 100));
$position->getPrice()->setVat($pos->getPriceOneSteuer() * 100); $position->getPrice()->setVat((int)($pos->getPriceOneSteuer() * 100));
$position->getPrice()->setAllVat($pos->getPriceAllSteuer() * 100); $position->getPrice()->setAllVat((int)($pos->getPriceAllSteuer() * 100));
$position->getPrice()->setGross($pos->getPriceOneBrutto() * 100); $position->getPrice()->setGross((int)($pos->getPriceOneBrutto() * 100));
$position->getPrice()->setAllGross($pos->getPriceAllBrutto() * 100); $position->getPrice()->setAllGross((int)($pos->getPriceAllBrutto() * 100));
$position->setReOrder($positionDoc->isReOrder()); $position->setReOrder($positionDoc->isReOrder());
$position->setReOrderOrder($positionDoc->getReOrderOrder()); $position->setReOrderOrder($positionDoc->getReOrderOrder());
$position->setReOrderPos($positionDoc->getReOrderPos()); $position->setReOrderPos($positionDoc->getReOrderPos());

View File

@ -45,17 +45,17 @@ class Calc
$gross += $discount->getPrice()->getGross(); $gross += $discount->getPrice()->getGross();
} }
} }
$order->setNetWithDiscount($order->getNet() - $net); $order->setNetWithDiscount((int)($order->getNet() - $net));
$order->setVatWithDiscount($order->getVat() - $vat); $order->setVatWithDiscount((int)($order->getVat() - $vat));
$order->setGrossWithDiscount($order->getGross() - $gross); $order->setGrossWithDiscount((int)($order->getGross() - $gross));
} }
private function calcMwert(Price $price, ?Tax $tax): void private function calcMwert(Price $price, ?Tax $tax): void
{ {
if ($tax != null) { if ($tax != null) {
$price->setNet($price->getGross() / ($tax->getCalculatedAmount() / 10000 + 1)); $price->setNet((int)($price->getGross() / ($tax->getCalculatedAmount() / 10000 + 1)));
$price->setVat($price->getGross() - ($price->getGross() / ($tax->getCalculatedAmount() / 10000 + 1))); $price->setVat((int)($price->getGross() - ($price->getGross() / ($tax->getCalculatedAmount() / 10000 + 1))));
} }
} }
} }

View File

@ -340,21 +340,12 @@
"phar-io/version": { "phar-io/version": {
"version": "3.1.0" "version": "3.1.0"
}, },
"php-http/guzzle6-adapter": {
"version": "v1.1.1"
},
"php-http/httplug": {
"version": "v1.1.0"
},
"php-http/message": { "php-http/message": {
"version": "1.8.0" "version": "1.8.0"
}, },
"php-http/message-factory": { "php-http/message-factory": {
"version": "v1.0.2" "version": "v1.0.2"
}, },
"php-http/promise": {
"version": "v1.0.0"
},
"phpdocumentor/reflection-common": { "phpdocumentor/reflection-common": {
"version": "2.0.0" "version": "2.0.0"
}, },
@ -468,12 +459,6 @@
"sebastian/cli-parser": { "sebastian/cli-parser": {
"version": "1.0.1" "version": "1.0.1"
}, },
"sebastian/code-unit": {
"version": "1.0.8"
},
"sebastian/code-unit-reverse-lookup": {
"version": "2.0.3"
},
"sebastian/comparator": { "sebastian/comparator": {
"version": "4.0.6" "version": "4.0.6"
}, },
@ -504,9 +489,6 @@
"sebastian/recursion-context": { "sebastian/recursion-context": {
"version": "4.0.4" "version": "4.0.4"
}, },
"sebastian/resource-operations": {
"version": "3.0-dev"
},
"sebastian/type": { "sebastian/type": {
"version": "2.3.x-dev" "version": "2.3.x-dev"
}, },

View File

@ -10,7 +10,7 @@ class AddTest extends WebTestCase
{ {
use RefreshDatabaseTrait; use RefreshDatabaseTrait;
public function testAddLagacyBasketWithAccountCalc(): void public function tesAddLagacyBasketWithAccountCalc(): void
{ {
$client = static::createClient(); $client = static::createClient();
$userRepository = static::getContainer()->get(ContactRepository::class); $userRepository = static::getContainer()->get(ContactRepository::class);

View File

@ -24,7 +24,9 @@ class ImportCalcTest extends KernelTestCase
$container = static::getContainer(); $container = static::getContainer();
/** @var Calc $calcService */ /**
* @var Calc $calcService
*/
$calcService = $container->get(Calc::class); $calcService = $container->get(Calc::class);
$shop = new Shop(); $shop = new Shop();
@ -39,7 +41,7 @@ class ImportCalcTest extends KernelTestCase
$payment = new Payment(); $payment = new Payment();
$payment->setTaxClass(19); $payment->setTaxClass(19);
$payment->setPrice(25.5); $payment->setPrice(25);
$order->setShipping($shipping); $order->setShipping($shipping);
$order->setPayment($payment); $order->setPayment($payment);

View File

@ -1,22 +0,0 @@
<?php
declare(strict_types=1);
namespace App\tests\PSC\System\SettingsBundle\Twig\BarcodeExtension;
;
use PSC\System\SettingsBundle\Twig\BarcodeExtension;
use Twig\Test\IntegrationTestCase;
class BarcodeExtensionTest extends IntegrationTestCase
{
protected function getFixturesDir(): string
{
return __DIR__;
}
public function getExtensions(): iterable
{
yield new BarcodeExtension();
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Plugins\System\PSC\XmlCalc\Api;
use PSC\Shop\ContactBundle\Repository\ContactRepository;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Tests\RefreshDatabaseTrait;
class GetJsonConfigTest extends WebTestCase
{
use RefreshDatabaseTrait;
public function testGetPriceWithoutUser(): void
{
$client = static::createClient();
$client->jsonRequest(
'POST',
'/api/plugin/system/psc/xmlcalc/product/config',
['product' => '01938686-0e4d-7da9-bae3-b2e1b1681f9f'],
[],
);
$this->assertResponseIsSuccessful();
self::assertJson($client->getResponse()->getContent());
}
}

View File

@ -1,20 +1,21 @@
<?php <?php
namespace Plugin\Custom\PSC\FormBuilder\Controller\Backend; namespace Plugin\Custom\PSC\FormBuilder\Controller\Backend;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
#[Route('/index')] #[Route('/product')]
class IndexController extends AbstractController class ProductController extends AbstractController
{ {
#[Template] #[Template]
#[Security("is_granted('ROLE_USER')")] #[Security("is_granted('ROLE_USER')")]
#[Route(path: '/create', name: 'psc_backend_invoice_index_create')] #[Route(path: '/edit', name: 'psc_backend_invoice_index_create')]
public function indexAction(JWTTokenManagerInterface $jwtManager) public function edit(JWTTokenManagerInterface $jwtManager)
{ {
return array('jwt' => $jwtManager->create($this->getUser())); return ['jwt' => $jwtManager->create($this->getUser())];
} }
} }

View File

@ -0,0 +1,38 @@
# Gemini Workspace
This file stores context for the Gemini agent.
## Project: FormBuilderTS
This is a Vue.js application built with Vite and TypeScript. It appears to be a form builder, allowing users to drag and drop elements to create forms.
### Key Technologies:
* **Framework:** Vue.js 3
* **Build Tool:** Vite
* **Language:** TypeScript
* **State Management:** Pinia
* **Styling:** Tailwind CSS (using shadcn-vue for UI components)
* **API Client:** ky
### Development Setup:
* Run `npm run dev` to start the development server.
* The development server uses a proxy to forward requests from `/apps` to `http://type-dev-tp.local`.
* A JWT token is injected into the `Authorization` header of proxied requests in `vite.config.ts` for personalized responses.
### Startup Behavior:
* On startup, the application checks for a `uuid` in the URL parameters.
* If a `uuid` is present, it makes a **POST** request to `api/plugin/system/psc/xmlcalc/product/config` with the `uuid` in the request body as `product` to load initial form data.
### UI Changes:
* The main content area in `Gui.vue` has been refactored to use `shadcn-vue` Tabs.
* The first tab is named "Designer" and contains the `Main` component.
* The second tab is named "Kalkulations Analyse" and contains the `FormulaVisualizer` component.
* The tab selection is centered.
* The `bg-slate-50` background color has been removed from the `Main` component's container.
* The `TabsContent` components in `Gui.vue` are scrollable.
### Kalkulations Analyse Tab:
* This tab features a `FormulaVisualizer` component that displays a dynamic, collapsible tree structure of formulas.
* The data for the visualizer is fetched from the `api/plugin/system/psc/xmlcalc/price` endpoint.
* The component automatically updates whenever the Pinia store (`Items.ts`) changes, with a 500ms debounce to prevent excessive API calls.
* The `FormulaVisualizer` is composed of a main component and a recursive `NodeRenderer` component to display the formula tree.

View File

@ -4,19 +4,22 @@
"": { "": {
"name": "my-vue-app", "name": "my-vue-app",
"dependencies": { "dependencies": {
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-xml": "^6.1.0",
"@tailwindcss/vite": "^4.1.10", "@tailwindcss/vite": "^4.1.10",
"@vueuse/core": "^13.4.0", "@vueuse/core": "^13.5.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"ky": "^1.8.1", "ky": "^1.8.1",
"lucide-vue-next": "^0.514.0", "lucide-vue-next": "^0.514.0",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"reka-ui": "^2.3.1", "reka-ui": "^2.3.2",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.10", "tailwindcss": "^4.1.10",
"tw-animate-css": "^1.3.4", "tw-animate-css": "^1.3.4",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-codemirror": "^6.1.1",
"vue-draggable-plus": "^0.6.0", "vue-draggable-plus": "^0.6.0",
}, },
"devDependencies": { "devDependencies": {
@ -93,6 +96,24 @@
"@babel/types": ["@babel/types@7.27.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q=="], "@babel/types": ["@babel/types@7.27.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q=="],
"@codemirror/autocomplete": ["@codemirror/autocomplete@6.18.6", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg=="],
"@codemirror/commands": ["@codemirror/commands@6.8.1", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw=="],
"@codemirror/lang-json": ["@codemirror/lang-json@6.0.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/json": "^1.0.0" } }, "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ=="],
"@codemirror/lang-xml": ["@codemirror/lang-xml@6.1.0", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.4.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/xml": "^1.0.0" } }, "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg=="],
"@codemirror/language": ["@codemirror/language@6.11.2", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", "@lezer/common": "^1.1.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, "sha512-p44TsNArL4IVXDTbapUmEkAlvWs2CFQbcfc0ymDsis1kH2wh0gcY96AS29c/vp2d0y2Tquk1EDSaawpzilUiAw=="],
"@codemirror/lint": ["@codemirror/lint@6.8.5", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.35.0", "crelt": "^1.0.5" } }, "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA=="],
"@codemirror/search": ["@codemirror/search@6.5.11", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "crelt": "^1.0.5" } }, "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA=="],
"@codemirror/state": ["@codemirror/state@6.5.2", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA=="],
"@codemirror/view": ["@codemirror/view@6.38.0", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-yvSchUwHOdupXkd7xJ0ob36jdsSR/I+/C+VbY0ffBiL5NiSTEBDfB1ZGWbbIlDd5xgdUkody+lukAdOxYrOBeg=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.5", "", { "os": "android", "cpu": "arm" }, "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.25.5", "", { "os": "android", "cpu": "arm" }, "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA=="],
@ -167,6 +188,18 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
"@lezer/common": ["@lezer/common@1.2.3", "", {}, "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA=="],
"@lezer/highlight": ["@lezer/highlight@1.2.1", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA=="],
"@lezer/json": ["@lezer/json@1.0.3", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ=="],
"@lezer/lr": ["@lezer/lr@1.4.2", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA=="],
"@lezer/xml": ["@lezer/xml@1.0.6", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww=="],
"@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="],
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
"@rollup/pluginutils": ["@rollup/pluginutils@5.1.4", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ=="], "@rollup/pluginutils": ["@rollup/pluginutils@5.1.4", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ=="],
@ -305,11 +338,11 @@
"@vue/tsconfig": ["@vue/tsconfig@0.7.0", "", { "peerDependencies": { "typescript": "5.x", "vue": "^3.4.0" }, "optionalPeers": ["typescript", "vue"] }, "sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg=="], "@vue/tsconfig": ["@vue/tsconfig@0.7.0", "", { "peerDependencies": { "typescript": "5.x", "vue": "^3.4.0" }, "optionalPeers": ["typescript", "vue"] }, "sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg=="],
"@vueuse/core": ["@vueuse/core@13.4.0", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "13.4.0", "@vueuse/shared": "13.4.0" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-OnK7zW3bTq/QclEk17+vDFN3tuAm8ONb9zQUIHrYQkkFesu3WeGUx/3YzpEp+ly53IfDAT9rsYXgGW6piNZC5w=="], "@vueuse/core": ["@vueuse/core@13.5.0", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "13.5.0", "@vueuse/shared": "13.5.0" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-wV7z0eUpifKmvmN78UBZX8T7lMW53Nrk6JP5+6hbzrB9+cJ3jr//hUlhl9TZO/03bUkMK6gGkQpqOPWoabr72g=="],
"@vueuse/metadata": ["@vueuse/metadata@13.4.0", "", {}, "sha512-CPDQ/IgOeWbqItg1c/pS+Ulum63MNbpJ4eecjFJqgD/JUCJ822zLfpw6M9HzSvL6wbzMieOtIAW/H8deQASKHg=="], "@vueuse/metadata": ["@vueuse/metadata@13.5.0", "", {}, "sha512-euhItU3b0SqXxSy8u1XHxUCdQ8M++bsRs+TYhOLDU/OykS7KvJnyIFfep0XM5WjIFry9uAPlVSjmVHiqeshmkw=="],
"@vueuse/shared": ["@vueuse/shared@13.4.0", "", { "peerDependencies": { "vue": "^3.5.0" } }, "sha512-+AxuKbw8R1gYy5T21V5yhadeNM7rJqb4cPaRI9DdGnnNl3uqXh+unvQ3uCaA2DjYLbNr1+l7ht/B4qEsRegX6A=="], "@vueuse/shared": ["@vueuse/shared@13.5.0", "", { "peerDependencies": { "vue": "^3.5.0" } }, "sha512-K7GrQIxJ/ANtucxIXbQlUHdB0TPA8c+q5i+zbrjxuhJCnJ9GtBg75sBSnvmLSxHKPg2Yo8w62PWksl9kwH0Q8g=="],
"alien-signals": ["alien-signals@1.0.13", "", {}, "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg=="], "alien-signals": ["alien-signals@1.0.13", "", {}, "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg=="],
@ -333,10 +366,14 @@
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"codemirror": ["codemirror@6.0.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/lint": "^6.0.0", "@codemirror/search": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" } }, "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"copy-anything": ["copy-anything@3.0.5", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w=="], "copy-anything": ["copy-anything@3.0.5", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w=="],
"crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
@ -497,7 +534,7 @@
"pretty-ms": ["pretty-ms@9.2.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg=="], "pretty-ms": ["pretty-ms@9.2.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg=="],
"reka-ui": ["reka-ui@2.3.1", "", { "dependencies": { "@floating-ui/dom": "^1.6.13", "@floating-ui/vue": "^1.1.6", "@internationalized/date": "^3.5.0", "@internationalized/number": "^3.5.0", "@tanstack/vue-virtual": "^3.12.0", "@vueuse/core": "^12.5.0", "@vueuse/shared": "^12.5.0", "aria-hidden": "^1.2.4", "defu": "^6.1.4", "ohash": "^2.0.11" }, "peerDependencies": { "vue": ">= 3.2.0" } }, "sha512-2SjGeybd7jvD8EQUkzjgg7GdOQdf4cTwdVMq/lDNTMqneUFNnryGO43dg8WaM/jaG9QpSCZBvstfBFWlDdb2Zg=="], "reka-ui": ["reka-ui@2.3.2", "", { "dependencies": { "@floating-ui/dom": "^1.6.13", "@floating-ui/vue": "^1.1.6", "@internationalized/date": "^3.5.0", "@internationalized/number": "^3.5.0", "@tanstack/vue-virtual": "^3.12.0", "@vueuse/core": "^12.5.0", "@vueuse/shared": "^12.5.0", "aria-hidden": "^1.2.4", "defu": "^6.1.4", "ohash": "^2.0.11" }, "peerDependencies": { "vue": ">= 3.2.0" } }, "sha512-lCysSCILH2uqShEnt93/qzlXnB7ySvK7scR0Q5C+a2iXwFVzHhvZQsMaSnbQYueoCihx6yyUZTYECepnmKrbRA=="],
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
@ -521,6 +558,8 @@
"strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], "strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="],
"style-mod": ["style-mod@4.1.2", "", {}, "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="],
"superjson": ["superjson@2.2.2", "", { "dependencies": { "copy-anything": "^3.0.2" } }, "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q=="], "superjson": ["superjson@2.2.2", "", { "dependencies": { "copy-anything": "^3.0.2" } }, "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q=="],
"tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="], "tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="],
@ -565,12 +604,16 @@
"vue": ["vue@3.5.16", "", { "dependencies": { "@vue/compiler-dom": "3.5.16", "@vue/compiler-sfc": "3.5.16", "@vue/runtime-dom": "3.5.16", "@vue/server-renderer": "3.5.16", "@vue/shared": "3.5.16" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-rjOV2ecxMd5SiAmof2xzh2WxntRcigkX/He4YFJ6WdRvVUrbt6DxC1Iujh10XLl8xCDRDtGKMeO3D+pRQ1PP9w=="], "vue": ["vue@3.5.16", "", { "dependencies": { "@vue/compiler-dom": "3.5.16", "@vue/compiler-sfc": "3.5.16", "@vue/runtime-dom": "3.5.16", "@vue/server-renderer": "3.5.16", "@vue/shared": "3.5.16" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-rjOV2ecxMd5SiAmof2xzh2WxntRcigkX/He4YFJ6WdRvVUrbt6DxC1Iujh10XLl8xCDRDtGKMeO3D+pRQ1PP9w=="],
"vue-codemirror": ["vue-codemirror@6.1.1", "", { "dependencies": { "@codemirror/commands": "6.x", "@codemirror/language": "6.x", "@codemirror/state": "6.x", "@codemirror/view": "6.x" }, "peerDependencies": { "codemirror": "6.x", "vue": "3.x" } }, "sha512-rTAYo44owd282yVxKtJtnOi7ERAcXTeviwoPXjIc6K/IQYUsoDkzPvw/JDFtSP6T7Cz/2g3EHaEyeyaQCKoDMg=="],
"vue-demi": ["vue-demi@0.14.10", "", { "peerDependencies": { "@vue/composition-api": "^1.0.0-rc.1", "vue": "^3.0.0-0 || ^2.6.0" }, "optionalPeers": ["@vue/composition-api"], "bin": { "vue-demi-fix": "bin/vue-demi-fix.js", "vue-demi-switch": "bin/vue-demi-switch.js" } }, "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg=="], "vue-demi": ["vue-demi@0.14.10", "", { "peerDependencies": { "@vue/composition-api": "^1.0.0-rc.1", "vue": "^3.0.0-0 || ^2.6.0" }, "optionalPeers": ["@vue/composition-api"], "bin": { "vue-demi-fix": "bin/vue-demi-fix.js", "vue-demi-switch": "bin/vue-demi-switch.js" } }, "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg=="],
"vue-draggable-plus": ["vue-draggable-plus@0.6.0", "", { "dependencies": { "@types/sortablejs": "^1.15.8" } }, "sha512-G5TSfHrt9tX9EjdG49InoFJbt2NYk0h3kgjgKxkFWr3ulIUays0oFObr5KZ8qzD4+QnhtALiRwIqY6qul4egqw=="], "vue-draggable-plus": ["vue-draggable-plus@0.6.0", "", { "dependencies": { "@types/sortablejs": "^1.15.8" } }, "sha512-G5TSfHrt9tX9EjdG49InoFJbt2NYk0h3kgjgKxkFWr3ulIUays0oFObr5KZ8qzD4+QnhtALiRwIqY6qul4egqw=="],
"vue-tsc": ["vue-tsc@2.2.10", "", { "dependencies": { "@volar/typescript": "~2.4.11", "@vue/language-core": "2.2.10" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "./bin/vue-tsc.js" } }, "sha512-jWZ1xSaNbabEV3whpIDMbjVSVawjAyW+x1n3JeGQo7S0uv2n9F/JMgWW90tGWNFRKya4YwKMZgCtr0vRAM7DeQ=="], "vue-tsc": ["vue-tsc@2.2.10", "", { "dependencies": { "@volar/typescript": "~2.4.11", "@vue/language-core": "2.2.10" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "./bin/vue-tsc.js" } }, "sha512-jWZ1xSaNbabEV3whpIDMbjVSVawjAyW+x1n3JeGQo7S0uv2n9F/JMgWW90tGWNFRKya4YwKMZgCtr0vRAM7DeQ=="],
"w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],

View File

@ -10,19 +10,22 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-xml": "^6.1.0",
"@tailwindcss/vite": "^4.1.10", "@tailwindcss/vite": "^4.1.10",
"@vueuse/core": "^13.4.0", "@vueuse/core": "^13.5.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"ky": "^1.8.1", "ky": "^1.8.1",
"lucide-vue-next": "^0.514.0", "lucide-vue-next": "^0.514.0",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"reka-ui": "^2.3.1", "reka-ui": "^2.3.2",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.10", "tailwindcss": "^4.1.10",
"tw-animate-css": "^1.3.4", "tw-animate-css": "^1.3.4",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-codemirror": "^6.1.1",
"vue-draggable-plus": "^0.6.0" "vue-draggable-plus": "^0.6.0"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,5 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import Gui from './components/Gui.vue' import Gui from './components/Gui.vue'
import { onMounted } from 'vue'
import { useElementStore } from './stores/Items'
onMounted(() => {
const store = useElementStore();
const params = new URLSearchParams(window.location.search);
const uuid = params.get('uuid');
if (uuid) {
store.loadFromApi(uuid);
}
})
</script> </script>
<template> <template>

View File

@ -4,16 +4,20 @@ import {
ResizablePanel, ResizablePanel,
ResizablePanelGroup, ResizablePanelGroup,
} from '../components/ui/resizable' } from '../components/ui/resizable'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../components/ui/tabs'
import { Library } from '../components/app/library' import { Library } from '../components/app/library'
import { Debug } from '../components/app/debug' import { Debug } from '../components/app/debug'
import { ElementProperties } from '../components/app/elementproperties' import { ElementProperties } from '../components/app/elementproperties'
import { ElementDependency } from '../components/app/elementdependency' import { ElementDependency } from '../components/app/elementdependency'
import { Main } from '../components/app/main' import { Main } from '../components/app/main'
import FormulaVisualizer from './app/FormulaVisualizer.vue'
import JsonView from './app/JsonView.vue'
import XmlView from './app/XmlView.vue'
</script> </script>
<template> <template>
<div class="w-scree h-screen"> <div class="w-screen h-screen">
<ResizablePanelGroup <ResizablePanelGroup
id="handle-demo-group-1" id="handle-demo-group-1"
direction="horizontal" direction="horizontal"
@ -27,9 +31,38 @@ import { Main } from '../components/app/main'
</ResizablePanel> </ResizablePanel>
<ResizableHandle id="" with-handle /> <ResizableHandle id="" with-handle />
<ResizablePanel id="" :default-size="85"> <ResizablePanel id="" :default-size="85">
<div class="flex h-full p-6 bg-slate-50"> <Tabs default-value="designer" class="w-full h-full">
<Main /> <div class="flex justify-center">
</div> <TabsList>
<TabsTrigger value="designer">
Designer
</TabsTrigger>
<TabsTrigger value="preview">
Kalkulations Analyse
</TabsTrigger>
<TabsTrigger value="xml">
XML Ansicht
</TabsTrigger>
<TabsTrigger value="json">
JSON Ansicht
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="designer" class="h-full overflow-y-auto">
<div class="flex h-full p-6">
<Main />
</div>
</TabsContent>
<TabsContent value="preview" class="h-full overflow-y-auto">
<FormulaVisualizer />
</TabsContent>
<TabsContent value="xml" class="h-full overflow-y-auto">
<XmlView />
</TabsContent>
<TabsContent value="json" class="h-full overflow-y-auto">
<JsonView />
</TabsContent>
</Tabs>
</ResizablePanel> </ResizablePanel>
</ResizablePanelGroup> </ResizablePanelGroup>
<ElementProperties /> <ElementProperties />

View File

@ -0,0 +1,229 @@
<script setup lang="ts">
import { ref, onMounted, provide } from 'vue';
import NodeRenderer from './NodeRenderer.vue';
import { loadPriceFromApi } from '../../lib/api';
import { useElementStore } from '../../stores/Items';
// Debounce function to limit the rate at which a function gets called.
function debounce<T extends (...args: any[]) => any>(func: T, delay: number): (...args: Parameters<T>) => void {
let timeoutId: ReturnType<typeof setTimeout> | null = null;
return (...args: Parameters<T>) => {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
func(...args);
}, delay);
};
}
interface Node {
name: string;
unParsed?: string;
parsed?: string;
result?: number;
parts: Node[];
}
const parsedData = ref<Node[] | null>(null);
const expandedNodes = ref(new Set<string>());
const error = ref('');
const isLoading = ref(false);
const store = useElementStore();
const loadFormulaData = async () => {
isLoading.value = true;
error.value = '';
parsedData.value = null;
const urlParams = new URLSearchParams(window.location.search);
const uuid = urlParams.get('uuid');
if (!uuid) {
error.value = 'Keine UUID in der URL gefunden.';
isLoading.value = false;
return;
}
try {
const response = await loadPriceFromApi(uuid);
if (response && response.debug && response.debug.graphJson) {
const graphData = JSON.parse(response.debug.graphJson);
parsedData.value = graphData;
} else {
throw new Error('Ungültiges oder leeres Antwortformat von der API.');
}
} catch (e: any) {
error.value = `Fehler beim Laden der Formeldaten: ${e.message}`;
console.error(e);
} finally {
isLoading.value = false;
}
};
const debouncedLoadFormulaData = debounce(loadFormulaData, 500);
onMounted(() => {
loadFormulaData();
store.$subscribe(() => {
debouncedLoadFormulaData();
});
});
const toggleNode = (nodeId: string) => {
const newExpanded = new Set(expandedNodes.value);
if (newExpanded.has(nodeId)) {
newExpanded.delete(nodeId);
} else {
newExpanded.add(nodeId);
}
expandedNodes.value = newExpanded;
};
const getNodeType = (name: string) => {
if (name.startsWith('$F') && name.endsWith('$F')) return 'formula';
if (name.startsWith('$P') && name.endsWith('$P')) return 'parameter';
if (name.startsWith('$V') && name.endsWith('$V')) return 'variable';
if (name.startsWith('$CV') && name.endsWith('$CV')) return 'calc-variable';
if (/^[0-9.]+$/.test(name)) return 'value';
if (name.startsWith('calc')) return 'main';
return 'function';
};
const getNodeColor = (type: string) => {
switch (type) {
case 'formula': return 'bg-purple-100 border-purple-300 text-purple-800';
case 'parameter': return 'bg-blue-100 border-blue-300 text-blue-800';
case 'variable': return 'bg-orange-100 border-orange-300 text-orange-800';
case 'calc-variable': return 'bg-teal-100 border-teal-300 text-teal-800';
case 'value': return 'bg-lime-100 border-lime-400 text-lime-800';
case 'main': return 'bg-red-100 border-red-300 text-red-800';
case 'function': return 'bg-yellow-100 border-yellow-300 text-yellow-800';
default: return 'bg-gray-100 border-gray-300 text-gray-800';
}
};
const getColoredFormulaParts = (formulaString: string) => {
const parts = [];
let currentIndex = 0;
const regex = /(\$F[^$]*\$F|\$P[^$]*\$P|\$CV[^$]*\$CV|\$V[^$]*\$V)/g;
let match;
while ((match = regex.exec(formulaString)) !== null) {
if (match.index > currentIndex) {
parts.push({
text: formulaString.substring(currentIndex, match.index),
colorClass: 'text-gray-800'
});
}
const matchedText = match[0];
let colorClass = '';
if (matchedText.startsWith('$F')) colorClass = 'text-purple-600 font-semibold';
else if (matchedText.startsWith('$P')) colorClass = 'text-blue-600 font-semibold';
else if (matchedText.startsWith('$CV')) colorClass = 'text-teal-600 font-semibold';
else if (matchedText.startsWith('$V')) colorClass = 'text-orange-600 font-semibold';
parts.push({ text: matchedText, colorClass });
currentIndex = match.index + matchedText.length;
}
if (currentIndex < formulaString.length) {
parts.push({
text: formulaString.substring(currentIndex),
colorClass: 'text-gray-800'
});
}
return parts;
};
const totalSum = () => {
if (!parsedData.value) return 0;
return parsedData.value.reduce((sum, root) => sum + (root.result || 0), 0);
};
provide('expandedNodes', expandedNodes);
provide('toggleNode', toggleNode);
provide('getNodeType', getNodeType);
provide('getNodeColor', getNodeColor);
provide('getColoredFormulaParts', getColoredFormulaParts);
</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">Fehler</p>
<p>{{ error }}</p>
</div>
<div v-if="isLoading" class="text-center py-10">
<p>Lade Formeldaten...</p>
</div>
<div v-if="!isLoading && parsedData" class="grid grid-cols-1 gap-6">
<!-- Baum-Ansicht -->
<div class="p-4 border m-1 p-4 rounded-xl w-full h-full shadow bg-white">
<h2 class="text-xl font-semibold mb-4 text-gray-700">Baum-Struktur</h2>
<div>
<NodeRenderer
v-for="(root, idx) in parsedData"
:key="idx"
:node="root"
:level="0"
parent-id="root"
:index="idx"
/>
</div>
</div>
<!-- Summenausgabe -->
<div class="p-4 border m-1 p-4 rounded-xl w-full h-full shadow bg-white border-l-4 border-green-500">
<h2 class="text-xl font-semibold mb-3 text-gray-700">Gesamtsumme</h2>
<div class="flex items-center justify-between bg-green-50 p-4 rounded-lg">
<div class="flex items-center space-x-3">
<span class="text-lg font-medium text-gray-800">
{{ parsedData.map(root => root.result || 0).join(' + ') }}
</span>
<span class="text-gray-500">=</span>
<span class="text-2xl font-bold text-green-600">
{{ totalSum() }}
</span>
</div>
<div class="text-sm text-gray-500">
({{ parsedData.length }} Formel{{ parsedData.length !== 1 ? 'n' : '' }})
</div>
</div>
</div>
<!-- Legende -->
<div class="p-4 border m-1 p-4 rounded-xl w-full h-full shadow bg-white">
<h2 class="text-xl font-semibold mb-4 text-gray-700">Legende</h2>
<div class="grid grid-cols-2 md:grid-cols-6 gap-4">
<div class="flex items-center">
<div class="w-4 h-4 bg-purple-100 border-2 border-purple-300 rounded mr-2"></div>
<span class="text-sm">Formel ($F...$F)</span>
</div>
<div class="flex items-center">
<div class="w-4 h-4 bg-blue-100 border-2 border-blue-300 rounded mr-2"></div>
<span class="text-sm">Parameter ($P...$P)</span>
</div>
<div class="flex items-center">
<div class="w-4 h-4 bg-orange-100 border-2 border-orange-300 rounded mr-2"></div>
<span class="text-sm">Variable ($V...$V)</span>
</div>
<div class="flex items-center">
<div class="w-4 h-4 bg-teal-100 border-2 border-teal-300 rounded mr-2"></div>
<span class="text-sm">Kalk-Variable ($CV...$CV)</span>
</div>
<div class="flex items-center">
<div class="w-4 h-4 bg-lime-100 border-2 border-lime-400 rounded mr-2"></div>
<span class="text-sm">Wert (Zahlen)</span>
</div>
<div class="flex items-center">
<div class="w-4 h-4 bg-red-100 border-2 border-red-300 rounded mr-2"></div>
<span class="text-sm">Hauptformel</span>
</div>
</div>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,39 @@
<template>
<div class="p-4">
<codemirror
v-model="jsonString"
:options="cmOptions"
:extensions="extensions"
disabled
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { Codemirror } from 'vue-codemirror';
import { json } from '@codemirror/lang-json';
import { useElementStore } from '../../stores/Items';
const store = useElementStore();
const jsonString = computed(() => {
try {
// The store.json is already a string, but we parse and re-stringify it
// to ensure it's well-formed and nicely formatted.
const parsed = JSON.parse(store.json);
return JSON.stringify(parsed, null, 2);
} catch (e) {
return "Invalid JSON in store";
}
});
const extensions = [json()];
const cmOptions = {
lineNumbers: true,
mode: 'application/json',
theme: 'default',
readOnly: true,
};
</script>

View File

@ -0,0 +1,93 @@
<script setup lang="ts">
import { inject, computed } from 'vue';
import { ChevronDown, ChevronRight } from 'lucide-vue-next';
import type { Ref } from 'vue';
// The component recursively calls itself, so we need to use its name in the template.
// In Vue 3 <script setup>, components are automatically registered with their filename,
// so we can just use <NodeRenderer ...> in the template for recursion.
interface Node {
name: string;
unParsed?: string;
parsed?: string;
result?: number;
parts: Node[];
}
const props = defineProps<{
node: Node;
level: number;
parentId: string;
index: number;
}>();
// Inject state and functions from parent
const expandedNodes = inject<Ref<Set<string>>>('expandedNodes');
const toggleNode = inject<(nodeId: string) => void>('toggleNode');
const getNodeType = inject<(name: string) => string>('getNodeType');
const getNodeColor = inject<(type: string) => string>('getNodeColor');
const getColoredFormulaParts = inject<(formulaString: string) => { text: string, colorClass: string }[]>('getColoredFormulaParts');
const nodeId = computed(() => `${props.parentId}-${props.index}`);
const hasChildren = computed(() => props.node.parts && props.node.parts.length > 0);
const isExpanded = computed(() => expandedNodes?.value.has(nodeId.value));
const nodeType = computed(() => getNodeType ? getNodeType(props.node.name) : '');
const colorClasses = computed(() => getNodeColor && nodeType.value ? getNodeColor(nodeType.value) : '');
const formulaString = computed(() => props.node.unParsed);
const handleToggle = () => {
if (hasChildren.value && toggleNode) {
toggleNode(nodeId.value);
}
}
</script>
<template>
<div class="mb-2">
<div
:class="['p-3 rounded-lg border-2 transition-all hover:shadow-md', colorClasses]"
:style="{ marginLeft: level * 20 + 'px' }"
>
<div
class="flex items-center cursor-pointer"
@click="handleToggle"
>
<span v-if="hasChildren" class="mr-2">
<ChevronDown v-if="isExpanded" :size="16" />
<ChevronRight v-else :size="16" />
</span>
<span class="font-medium">{{ node.name }}</span>
<span class="ml-2 text-xs bg-white px-2 py-1 rounded opacity-75">{{ nodeType }}</span>
<span v-if="node.result !== undefined" class="ml-2 text-xs bg-green-200 px-2 py-1 rounded font-mono">
= {{ node.result }}
</span>
</div>
<div v-if="formulaString" class="mt-2 ml-6 space-y-1">
<div class="p-2 bg-gray-50 rounded text-sm font-mono">
<span class="font-semibold text-gray-700">{{ node.name }} = </span>
<template v-if="getColoredFormulaParts">
<span v-for="(part, i) in getColoredFormulaParts(formulaString)" :key="i" :class="part.colorClass">{{ part.text }}</span>
</template>
</div>
<div v-if="node.parsed && node.parsed !== node.unParsed" class="p-2 bg-blue-50 rounded text-sm font-mono">
<span class="font-semibold text-blue-700">Aufgelöst: </span>
<span class="text-blue-800">{{ node.parsed }}</span>
</div>
</div>
</div>
<div v-if="hasChildren && isExpanded" class="mt-2">
<NodeRenderer
v-for="(child, idx) in node.parts"
:key="idx"
:node="child"
:level="level + 1"
:parent-id="nodeId"
:index="idx"
/>
</div>
</div>
</template>

View File

@ -0,0 +1,28 @@
<template>
<div class="p-4">
<codemirror
v-model="xmlString"
:options="cmOptions"
:extensions="extensions"
disabled
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Codemirror } from 'vue-codemirror';
import { xml } from '@codemirror/lang-xml';
const xmlString = ref('<root>\n <!-- XML Data Source Not Yet Implemented -->\n</root>');
const extensions = [xml()];
const cmOptions = {
lineNumbers: true,
mode: 'application/xml',
theme: 'default',
readOnly: true,
};
</script>

View File

@ -2,7 +2,7 @@
import MediaElement from '../../../model/MediaElement'; import MediaElement from '../../../model/MediaElement';
import { computed } from 'vue'; import { computed } from 'vue';
import { Input } from '../../../components/ui/input' import { Input } from '../../../components/ui/input'
import { useMedia } from '../../../composables/useMedia' //import { useMedia } from '../../../composables/useMedia'
const props = defineProps({ const props = defineProps({
modelValue: MediaElement modelValue: MediaElement
@ -15,7 +15,7 @@ const theModel = computed({
set: (value) => emit('update:modelValue', value), set: (value) => emit('update:modelValue', value),
}); });
const { media, loading, error } = useMedia() //const { media, loading, error } = useMedia()
</script> </script>

View File

@ -0,0 +1,23 @@
<script setup lang="ts">
import type { TabsRootEmits, TabsRootProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { TabsRoot, useForwardPropsEmits } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<TabsRootProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<TabsRootEmits>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<TabsRoot
data-slot="tabs"
v-bind="forwarded"
:class="cn('flex flex-col gap-2', props.class)"
>
<slot />
</TabsRoot>
</template>

View File

@ -0,0 +1,20 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { TabsContent, type TabsContentProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<TabsContentProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<TabsContent
data-slot="tabs-content"
:class="cn('flex-1 outline-none', props.class)"
v-bind="delegatedProps"
>
<slot />
</TabsContent>
</template>

View File

@ -0,0 +1,23 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { TabsList, type TabsListProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<TabsListProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<TabsList
data-slot="tabs-list"
v-bind="delegatedProps"
:class="cn(
'bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-b-lg p-[3px]',
props.class,
)"
>
<slot />
</TabsList>
</template>

View File

@ -0,0 +1,25 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { TabsTrigger, type TabsTriggerProps, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<TabsTriggerProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<TabsTrigger
data-slot="tabs-trigger"
v-bind="forwardedProps"
:class="cn(
`data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,
props.class,
)"
>
<slot />
</TabsTrigger>
</template>

View File

@ -0,0 +1,4 @@
export { default as Tabs } from './Tabs.vue'
export { default as TabsContent } from './TabsContent.vue'
export { default as TabsList } from './TabsList.vue'
export { default as TabsTrigger } from './TabsTrigger.vue'

View File

@ -19,4 +19,26 @@ const api = ky.create({
}, },
}); });
export const loadJsonFromApi = async (uuid: string) => {
try {
const response = await api.post('api/plugin/system/psc/xmlcalc/product/config', { json: { product: uuid } });
return await response.json();
} catch (error) {
console.error('Error loading JSON from API:', error);
throw error;
}
};
export const loadPriceFromApi = async (uuid: string) => {
try {
const response = await api.post('api/plugin/system/psc/xmlcalc/price', { json: { product: uuid } });
return await response.json();
} catch (error) {
console.error('Error loading price from API:', error);
throw error;
}
};
export default api; export default api;

View File

@ -9,7 +9,8 @@ export enum ElementType {
TextareaElement = 5, TextareaElement = 5,
HeadlineElement = 6, HeadlineElement = 6,
Column = 8, Column = 8,
Row = 7 Row = 7,
Media = 9,
} }
export default class BaseElement { export default class BaseElement {

View File

@ -1,143 +1,150 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import BaseElement from '../model/BaseElement' import BaseElement from '../model/BaseElement'
import Parser from '../lib/parser' import Parser from '../lib/parser'
import {v4 as uuidv4} from 'uuid' import {v4 as uuidv4} from 'uuid'
import { loadJsonFromApi } from '../lib/api'
export const useElementStore = defineStore('items', {
state: () => ({
uuid: uuidv4(), export const useElementStore = defineStore('items', {
items: [] as BaseElement[], state: () => ({
activeItem: {} as BaseElement | {}, uuid: uuidv4(),
showProperties: false, items: [] as BaseElement[],
showDependency: false, activeItem: {} as BaseElement | {},
showPreview: false, showProperties: false,
sourceDragUuid: "", showDependency: false,
dragMode: "", showPreview: false,
json: "", sourceDragUuid: "",
name: uuidv4(), dragMode: "",
}), json: "",
getters: { name: uuidv4(),
getCount: (state) => state.items.length, }),
getItems: (state) => state.items, getters: {
getActiveItem: (state) => state.activeItem as BaseElement, getCount: (state) => state.items.length,
isShowPropierties: (state) => state.showProperties, getItems: (state) => state.items,
isShowDependency: (state) => state.showDependency, getActiveItem: (state) => state.activeItem as BaseElement,
isShowPreview: (state) => state.showPreview, isShowPropierties: (state) => state.showProperties,
getSourceDragUuid: (state) => state.sourceDragUuid, isShowDependency: (state) => state.showDependency,
getDragMode: (state) => state.dragMode, isShowPreview: (state) => state.showPreview,
}, getSourceDragUuid: (state) => state.sourceDragUuid,
actions: { getDragMode: (state) => state.dragMode,
addElement(item: BaseElement) { },
this.items.push(item) actions: {
}, addElement(item: BaseElement) {
loadJSON() { this.items.push(item)
let options: object[] = this.items.reduce((result: object[], opt: BaseElement) => { },
result.push(opt.toJSON()) loadJSON() {
return result let options: object[] = this.items.reduce((result: object[], opt: BaseElement) => {
}, []) result.push(opt.toJSON())
return result
}, [])
this.json = JSON.stringify([{
uuid: this.uuid,
name: this.name, this.json = JSON.stringify([{
options: options uuid: this.uuid,
}]) name: this.name,
options: options
}, }])
parseJSON() {
this.items = [] },
let obj = JSON.parse(this.json) parseJSON() {
this.name = obj[0].name this.items = []
if(obj[0].uuid) { let obj = JSON.parse(this.json)
this.uuid = obj[0].uuid this.name = obj[0].name
} if(obj[0].uuid) {
obj[0].options.map((ob: any) => { this.uuid = obj[0].uuid
const item = Parser.getModelForType(ob.type) }
item.fromJSON(ob) obj[0].options.map((ob: any) => {
this.addElement(item) const item = Parser.getModelForType(ob.type)
}) item.fromJSON(ob)
}, this.addElement(item)
changeFocus(uuid: string) { })
this.items.forEach((element: BaseElement) => { },
element.changeFocus(uuid) changeFocus(uuid: string) {
}) this.items.forEach((element: BaseElement) => {
}, element.changeFocus(uuid)
clearSelection() { })
this.items.forEach((element: BaseElement) => { },
element.changeFocus("xx") clearSelection() {
}) this.items.forEach((element: BaseElement) => {
this.showProperties = false element.changeFocus("xx")
}, })
setShowDependency(value: boolean) { this.showProperties = false
this.showDependency = value },
}, setShowDependency(value: boolean) {
setShowProperties(value: boolean) { this.showDependency = value
this.showProperties = value },
}, setShowProperties(value: boolean) {
setShowPreview(value: boolean) { this.showProperties = value
this.showPreview = value },
}, setShowPreview(value: boolean) {
setActiveItem(item: BaseElement) { this.showPreview = value
this.activeItem = item },
}, setActiveItem(item: BaseElement) {
deleteItem(item: BaseElement): boolean { this.activeItem = item
return this.items.some((element: BaseElement,indexArray: number) => { },
if(element.uuid === item.uuid) { deleteItem(item: BaseElement): boolean {
item = this.items.splice(indexArray, 1)[0] return this.items.some((element: BaseElement,indexArray: number) => {
return true if(element.uuid === item.uuid) {
} item = this.items.splice(indexArray, 1)[0]
if(element.deleteItem(item)) { return true
return true }
} if(element.deleteItem(item)) {
}) return true
}, }
moveItemBefore(dragUuid: string, targetUuid: string): boolean { })
const item = this.cutItem(dragUuid) },
if(item) { moveItemBefore(dragUuid: string, targetUuid: string): boolean {
return this.insertItem(this.items, item, targetUuid) const item = this.cutItem(dragUuid)
} if(item) {
return false return this.insertItem(this.items, item, targetUuid)
}, }
addElementAfter(item: BaseElement, targetUuid: string) { return false
this.insertItem(this.items, item, targetUuid) },
}, addElementAfter(item: BaseElement, targetUuid: string) {
setSourceDragUuid(uuid: string) { this.insertItem(this.items, item, targetUuid)
this.sourceDragUuid = uuid },
}, setSourceDragUuid(uuid: string) {
cutItem(existingUuid: string) { this.sourceDragUuid = uuid
let item: BaseElement|null = null },
this.items.some((element: BaseElement,indexArray: number) => { cutItem(existingUuid: string) {
if(element.uuid === existingUuid) { let item: BaseElement|null = null
item = this.items.splice(indexArray, 1)[0] this.items.some((element: BaseElement,indexArray: number) => {
return true if(element.uuid === existingUuid) {
} item = this.items.splice(indexArray, 1)[0]
if(item === null) { return true
item = element.cutItem(existingUuid) }
if(item !== null) { if(item === null) {
return true item = element.cutItem(existingUuid)
} if(item !== null) {
} return true
}) }
return item }
}, })
insertItem(items: BaseElement[], item: BaseElement, targetUuid: string): boolean { return item
let inserted = false },
for (let i = 0; i < items.length; ++i) { insertItem(items: BaseElement[], item: BaseElement, targetUuid: string): boolean {
if(items[i].uuid === targetUuid) { let inserted = false
items.splice(i, 0, item) for (let i = 0; i < items.length; ++i) {
inserted = true if(items[i].uuid === targetUuid) {
break items.splice(i, 0, item)
} inserted = true
if(!inserted) { break
inserted = items[i].insertItem(item, targetUuid) }
} if(!inserted) {
} inserted = items[i].insertItem(item, targetUuid)
}
return inserted }
},
setDragMode(mode: string) { return inserted
this.dragMode = mode },
} setDragMode(mode: string) {
this.dragMode = mode
} },
}) async loadFromApi(id: string) {
const data = await loadJsonFromApi(id);
this.json = data;
this.parseJSON();
}
}
})

View File

@ -22,4 +22,17 @@ export default defineConfig({
'@': path.resolve(__dirname, './src'), '@': path.resolve(__dirname, './src'),
}, },
}, },
server: {
proxy: {
'/apps': {
target: 'http://type-dev-tp.local',
changeOrigin: true,
configure: (proxy, options) => {
proxy.on('proxyReq', (proxyReq, req, res) => {
proxyReq.setHeader('Authorization', 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE3NTI1MTE3MTgsImV4cCI6MTc1MjUxNTMxOCwicm9sZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfU0hPUF9PUEVSQVRPUiIsIlJPTEVfVVNFUiIsIlJPTEVfVVNFUiIsIlJPTEVfUFNDX0NvbGxlY3RfQ29udGFjdF9FZGl0IiwiUk9MRV9QU0NfQ29sbGVjdF9Db250YWN0X0FkZCIsIlJPTEVfUFNDX0NvbGxlY3RfQ29udGFjdF9EZWxldGUiLCJST0xFX1BTQ19Db2xsZWN0X0NvbnRhY3RfTG9jayIsIlJPTEVfUFNDX1IyX1NlbmRjbG91ZF9TaG93Il0sInVpZCI6MX0.MlNFnqbWlCbaKejkcJgnri2d4pg569vfk6TrOe32rJbyyyb0X3svfhzvCsyO9i1-XwR0frm5s2fHdeGEumCjtel9VzLLIvbmr43NhUVPA03EG17pAX4QnM-GaL9vzIeZQlrIcwSprFKz9qo6Toc1Wq0mFEjvTwHj5UR5JBIgnV7TtScIVl83XJljMUbX-NrUSoOeGn6W2SRtH_bDP47ZC-P4wtDcXcrJWM9ka1Vknn-1DQgitVLtOEsxzU7bkxPpfC_ENuRqDE8HmpPZsizF4Pt9jzfAXcPy0CviBJxvg1-tu57h164VSsnz1-K6duBMTB18afi987-dXtBc7nKJhQ');
});
},
},
},
},
}) })

File diff suppressed because one or more lines are too long

View File

@ -1,14 +1,4 @@
<?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 Plugin\System\PSC\XmlCalc\Api; namespace Plugin\System\PSC\XmlCalc\Api;
@ -16,12 +6,6 @@ use Doctrine\ODM\MongoDB\DocumentManager;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Nelmio\ApiDocBundle\Annotation\Model; use Nelmio\ApiDocBundle\Annotation\Model;
use OpenApi\Annotations as OA; use OpenApi\Annotations as OA;
use PSC\Library\Calc\Error\Validation\Input\Max;
use PSC\Library\Calc\Error\Validation\Input\Min as PSCMin;
use PSC\Library\Calc\Option\Type\ColorDBSelect;
use PSC\Shop\ContactBundle\Model\Contact;
use PSC\Shop\ContactBundle\Transformer\Model\Contact as ContactTransformer;
use PSC\System\SettingsBundle\Service\Help;
use Plugin\System\PSC\XmlCalc\Dto\Input\PriceInput; use Plugin\System\PSC\XmlCalc\Dto\Input\PriceInput;
use Plugin\System\PSC\XmlCalc\Dto\Output\Display\Group as DisplayGroup; use Plugin\System\PSC\XmlCalc\Dto\Output\Display\Group as DisplayGroup;
use Plugin\System\PSC\XmlCalc\Dto\Output\PreCalc\Group; use Plugin\System\PSC\XmlCalc\Dto\Output\PreCalc\Group;
@ -29,15 +13,23 @@ use Plugin\System\PSC\XmlCalc\Dto\Output\PreCalc\Value;
use Plugin\System\PSC\XmlCalc\Dto\Output\PreCalc\Variant; use Plugin\System\PSC\XmlCalc\Dto\Output\PreCalc\Variant;
use Plugin\System\PSC\XmlCalc\Dto\Output\Price\Element; 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\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\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\Base;
use PSC\Library\Calc\Option\Type\ColorDBSelect;
use PSC\Library\Calc\Option\Type\DeliverySelect; use PSC\Library\Calc\Option\Type\DeliverySelect;
use PSC\Library\Calc\Option\Type\Select\Opt; use PSC\Library\Calc\Option\Type\Select\Opt;
use PSC\Library\Calc\PaperContainer; use PSC\Library\Calc\PaperContainer;
use PSC\Shop\ContactBundle\Model\Contact;
use PSC\Shop\ContactBundle\Transformer\Model\Contact as ContactTransformer;
use PSC\Shop\EntityBundle\Entity\Product; use PSC\Shop\EntityBundle\Entity\Product;
use PSC\System\SettingsBundle\Service\Help;
use PSC\System\SettingsBundle\Service\PaperDB; use PSC\System\SettingsBundle\Service\PaperDB;
use Plugin\System\PSC\XmlCalc\Dto\Output\Price\Validation\Input\Max as PluginMax; use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Plugin\System\PSC\XmlCalc\Dto\Output\Price\Validation\Input\Min; use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
@ -45,8 +37,6 @@ use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
class GetPrice extends AbstractController class GetPrice extends AbstractController
{ {
@ -76,7 +66,7 @@ class GetPrice extends AbstractController
PaperDB $paperDB, PaperDB $paperDB,
ContactTransformer $contactTransformer, ContactTransformer $contactTransformer,
TokenStorageInterface $tokenStorage, TokenStorageInterface $tokenStorage,
Help $helpService Help $helpService,
) { ) {
$this->shopService = $shopService; $this->shopService = $shopService;
$this->documentManager = $documentManager; $this->documentManager = $documentManager;
@ -96,24 +86,28 @@ class GetPrice extends AbstractController
* @OA\JsonContent(ref=@Model(type=\Plugin\System\PSC\XmlCalc\Dto\Output\PriceOutput::class)) * @OA\JsonContent(ref=@Model(type=\Plugin\System\PSC\XmlCalc\Dto\Output\PriceOutput::class))
* ) * )
* @OA\RequestBody( * @OA\RequestBody(
* description="This is a request body", *
* @Model(type=\Plugin\System\PSC\XmlCalc\Dto\Input\PriceInput::class)) * @Model(type=\Plugin\System\PSC\XmlCalc\Dto\Input\PriceInput::class))
* ) * )
* @OA\Tag(name="Plugin/System/psc/Xmlcalc/Price") * @OA\Tag(name="Plugin/System/psc/Xmlcalc/Price")
*/ */
#[Route(path: '/price', methods: ['POST'])] #[Route(path: '/price', methods: ['POST'])]
#[ParamConverter('data', class: '\Plugin\System\PSC\XmlCalc\Dto\Input\PriceInput', converter: 'psc_rest.request_body')] #[ParamConverter(
'data',
class: '\Plugin\System\PSC\XmlCalc\Dto\Input\PriceInput',
converter: 'psc_rest.request_body',
)]
public function getprice(PriceInput $data) public function getprice(PriceInput $data)
{ {
$output = new \Plugin\System\PSC\XmlCalc\Dto\Output\PriceOutput(); $output = new \Plugin\System\PSC\XmlCalc\Dto\Output\PriceOutput();
$output->product = $data->product; $output->product = $data->product;
/** /**
* @var Product $product * @var Product $product
*/ */
$product = $this->entityManager $product = $this->entityManager
->getRepository('PSC\Shop\EntityBundle\Entity\Product')->findOneBy(['uuid' => $data->product]); ->getRepository('PSC\Shop\EntityBundle\Entity\Product')
->findOneBy(['uuid' => $data->product]);
$paperContainer = new PaperContainer(); $paperContainer = new PaperContainer();
$paperContainer->parse(simplexml_load_string($product->getShop()->getInstall()->getPaperContainer())); $paperContainer->parse(simplexml_load_string($product->getShop()->getInstall()->getPaperContainer()));
@ -121,10 +115,10 @@ class GetPrice extends AbstractController
$engine->setPaperRepository($this->paperDB); $engine->setPaperRepository($this->paperDB);
$engine->setPaperContainer($paperContainer); $engine->setPaperContainer($paperContainer);
if ($product->getShop()->getInstall()->getCalcTemplates() && !$data->test) { if ($product->getShop()->getInstall()->getCalcTemplates() && !$data->test) {
$engine->setTemplates('<root>'.$product->getShop()->getInstall()->getCalcTemplates().'</root>'); $engine->setTemplates('<root>' . $product->getShop()->getInstall()->getCalcTemplates() . '</root>');
} }
if ($product->getShop()->getInstall()->getCalcTemplatesTest() && $data->test) { if ($product->getShop()->getInstall()->getCalcTemplatesTest() && $data->test) {
$engine->setTemplates('<root>'.$product->getShop()->getInstall()->getCalcTemplatesTest().'</root>'); $engine->setTemplates('<root>' . $product->getShop()->getInstall()->getCalcTemplatesTest() . '</root>');
} }
$engine->loadString($product->getCalcXml()); $engine->loadString($product->getCalcXml());
if (!$data->test) { if (!$data->test) {
@ -143,7 +137,7 @@ class GetPrice extends AbstractController
$engine->setVariable('contact.accountType', $contact->getAccountType()->value); $engine->setVariable('contact.accountType', $contact->getAccountType()->value);
$engine->setVariable('contact.account', $contact->getAccount()->getUid()); $engine->setVariable('contact.account', $contact->getAccount()->getUid());
} }
if ($data->xmlProduct != "") { if ($data->xmlProduct != '') {
$engine->setActiveArticle($data->xmlProduct); $engine->setActiveArticle($data->xmlProduct);
} }
@ -166,8 +160,8 @@ class GetPrice extends AbstractController
} }
/** /**
* @var Base $option * @var Base $option
*/ */
foreach ($engine->getArticle()->getOptions() as $option) { foreach ($engine->getArticle()->getOptions() as $option) {
$tmp = new Element(); $tmp = new Element();
$tmp->name = $option->getName(); $tmp->name = $option->getName();
@ -182,10 +176,10 @@ class GetPrice extends AbstractController
} }
$tmp->value = $option->getValue(); $tmp->value = $option->getValue();
if ($help = $this->helpService->getHelp((string)$product->getUid(), $option->getId())) { if ($help = $this->helpService->getHelp((string) $product->getUid(), $option->getId())) {
$tmp->help = $help->helpText; $tmp->help = $help->helpText;
$tmp->helpTitle = $help->helpTitle; $tmp->helpTitle = $help->helpTitle;
}else{ } else {
$tmp->help = $option->getHelp(); $tmp->help = $option->getHelp();
$tmp->helpLink = $option->getHelpLink(); $tmp->helpLink = $option->getHelpLink();
} }
@ -194,16 +188,16 @@ class GetPrice extends AbstractController
$tmp->htmlType = $option->type; $tmp->htmlType = $option->type;
$tmp->displayGroup = $option->getDisplayGroup(); $tmp->displayGroup = $option->getDisplayGroup();
if ($option->type == 'select' || $option->type == 'checkbox' || $option->type == 'radio') { if ($option->type == 'select' || $option->type == 'checkbox' || $option->type == 'radio') {
/** /**
* @var Opt $option * @var Opt $option
*/ */
if ($option instanceof ColorDBSelect) { if ($option instanceof ColorDBSelect) {
$tmp->colorSystem = $option->getColorSystem(); $tmp->colorSystem = $option->getColorSystem();
if (!isset($output->colorDb[$option->getColorSystem()])) { if (!isset($output->colorDb[$option->getColorSystem()])) {
$output->colorDb[$option->getColorSystem()] = []; $output->colorDb[$option->getColorSystem()] = [];
foreach ($option->getOptions() as $opt) { foreach ($option->getOptions() as $opt) {
$element = array_find((array)$option->getSelectedOptions(), function(Opt $o1) use ($opt) { $element = array_find((array) $option->getSelectedOptions(), function (Opt $o1) use ($opt) {
return $o1->getId() === $opt->getId(); return $o1->getId() === $opt->getId();
}); });
$tmpOpt = new Option(); $tmpOpt = new Option();
@ -212,13 +206,13 @@ class GetPrice extends AbstractController
$tmpOpt->prefix = $opt->getPrefix(); $tmpOpt->prefix = $opt->getPrefix();
$tmpOpt->suffix = $opt->getSuffix(); $tmpOpt->suffix = $opt->getSuffix();
$tmpOpt->valid = $opt->isValid(); $tmpOpt->valid = $opt->isValid();
$tmpOpt->selected = $element? true: false; $tmpOpt->selected = $element ? true : false;
$output->colorDb[$option->getColorSystem()][] = $tmpOpt; $output->colorDb[$option->getColorSystem()][] = $tmpOpt;
} }
} }
} else { } else {
foreach ($option->getOptions() as $opt) { foreach ($option->getOptions() as $opt) {
$element = array_find((array)$option->getSelectedOptions(), function(Opt $o1) use ($opt) { $element = array_find((array) $option->getSelectedOptions(), function (Opt $o1) use ($opt) {
return $o1->getId() === $opt->getId(); return $o1->getId() === $opt->getId();
}); });
@ -227,7 +221,7 @@ class GetPrice extends AbstractController
$tmpOpt->id = $opt->getId(); $tmpOpt->id = $opt->getId();
$tmpOpt->name = $opt->getLabel(); $tmpOpt->name = $opt->getLabel();
$tmpOpt->valid = $opt->isValid(); $tmpOpt->valid = $opt->isValid();
$tmpOpt->selected = $element? true: false; $tmpOpt->selected = $element ? true : false;
$tmpOpt->info = $opt->getInfo(); $tmpOpt->info = $opt->getInfo();
$tmpOpt->deliveryDate = $opt->getDeliveryDateAsString(); $tmpOpt->deliveryDate = $opt->getDeliveryDateAsString();
} else { } else {
@ -235,7 +229,7 @@ class GetPrice extends AbstractController
$tmpOpt->id = $opt->getId(); $tmpOpt->id = $opt->getId();
$tmpOpt->name = $opt->getLabel(); $tmpOpt->name = $opt->getLabel();
$tmpOpt->valid = $opt->isValid(); $tmpOpt->valid = $opt->isValid();
$tmpOpt->selected = $element? true: false; $tmpOpt->selected = $element ? true : false;
} }
$tmp->options[] = $tmpOpt; $tmp->options[] = $tmpOpt;
} }
@ -247,13 +241,12 @@ class GetPrice extends AbstractController
$tmp->placeHolder = $option->getPlaceHolder(); $tmp->placeHolder = $option->getPlaceHolder();
$tmp->pattern = $option->getPattern(); $tmp->pattern = $option->getPattern();
foreach ($option->getValidationErrors() as $error) { foreach ($option->getValidationErrors() as $error) {
if ($error instanceof PSCMin) { if ($error instanceof PSCMin) {
$tmp->validationErrors[] = new Min($tmp->value, $option->getMinValue()); $tmp->validationErrors[] = new Min($tmp->value, $option->getMinValue());
} }
if ($error instanceof Max) { if ($error instanceof Max) {
$tmp->validationErrors[] = new PluginMax($tmp->value, $option->getMaxValue()); $tmp->validationErrors[] = new PluginMax($tmp->value, $option->getMaxValue());
} }
} }
} }
@ -282,11 +275,12 @@ class GetPrice extends AbstractController
$output->displayValues = $engine->getDisplayVariables(); $output->displayValues = $engine->getDisplayVariables();
$output->exportValues = $engine->getAjaxVariables(); $output->exportValues = $engine->getAjaxVariables();
if ($this->isGranted("ROLE_SHOP")) { if ($this->isGranted('ROLE_SHOP')) {
$output->debug['formels'] = $engine->getDebugCalcFormel(); $output->debug['formels'] = $engine->getDebugCalcFormel();
$output->debug['flatPrice'] = $engine->getDebugFlatPrice(); $output->debug['flatPrice'] = $engine->getDebugFlatPrice();
$output->debug['price'] = $engine->getDebugPrice(); $output->debug['price'] = $engine->getDebugPrice();
$output->debug['calcValues'] = $engine->getDebugCalcVariables(); $output->debug['calcValues'] = $engine->getDebugCalcVariables();
$output->debug['graphJson'] = $engine->getCalcGraph()->generateJsonGraph();
} }
return $this->json($output); return $this->json($output);
} }

View File

@ -0,0 +1,94 @@
<?php
namespace Plugin\System\PSC\XmlCalc\Api\Product;
use Doctrine\ODM\MongoDB\DocumentManager;
use Doctrine\ORM\EntityManagerInterface;
use Nelmio\ApiDocBundle\Annotation\Model;
use Nelmio\ApiDocBundle\Annotation\Security;
use OpenApi\Annotations as OA;
use Plugin\System\PSC\XmlCalc\Dto\Input\PriceInput;
use Plugin\System\PSC\XmlCalc\Model\Product as PluginProduct;
use PSC\Component\ApiBundle\Dto\Error\NotFound;
use PSC\Library\Calc\Engine;
use PSC\Library\Calc\PaperContainer;
use PSC\Shop\ContactBundle\Model\Contact;
use PSC\Shop\ContactBundle\Transformer\Model\Contact as ContactTransformer;
use PSC\Shop\EntityBundle\Entity\Product;
use PSC\System\SettingsBundle\Service\PaperDB;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class Config extends AbstractController
{
public function __construct(
private \PSC\System\SettingsBundle\Service\Shop $shopService,
private DocumentManager $documentManager,
private PaperDB $paperDB,
private ContactTransformer $contactTransformer,
private TokenStorageInterface $tokenStorage,
private EntityManagerInterface $entityManager,
) {}
/**
* @OA\Response(
* response=200,
* description="get config for product",
* @OA\JsonContent(ref=@Model(type=\Plugin\System\PSC\XmlCalc\Model\Product::class))
* )
* @OA\RequestBody(
*
* @Model(type=\Plugin\System\PSC\XmlCalc\Dto\Input\PriceInput::class))
* )
* @OA\Tag(name="Plugin/System/psc/Xmlcalc/Product")
* */
#[Route(path: '/product/config', methods: ['POST'])]
#[ParamConverter(
'data',
class: '\Plugin\System\PSC\XmlCalc\Dto\Input\PriceInput',
converter: 'psc_rest.request_body',
)]
public function config(PriceInput $data)
{
$product = $this->entityManager
->getRepository('PSC\Shop\EntityBundle\Entity\Product')
->findOneBy(['uuid' => $data->product]);
$paperContainer = new PaperContainer();
$paperContainer->parse(simplexml_load_string($product->getShop()->getInstall()->getPaperContainer()));
$engine = new Engine();
$engine->setPaperRepository($this->paperDB);
$engine->setPaperContainer($paperContainer);
if ($product->getShop()->getInstall()->getCalcTemplates() && !$data->test) {
$engine->setTemplates('<root>' . $product->getShop()->getInstall()->getCalcTemplates() . '</root>');
}
if ($product->getShop()->getInstall()->getCalcTemplatesTest() && $data->test) {
$engine->setTemplates('<root>' . $product->getShop()->getInstall()->getCalcTemplatesTest() . '</root>');
}
$engine->loadString($product->getCalcXml());
if (!$data->test) {
$engine->setFormulas($product->getShop()->getFormel());
$engine->setParameters($product->getShop()->getParameter());
}
if ($data->test) {
$engine->setFormulas($product->getShop()->getTestFormel());
$engine->setParameters($product->getShop()->getTestParameter());
}
$engine->setVariables($data->values);
$engine->setTax($product->getMwert());
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());
}
if ($data->xmlProduct != '') {
$engine->setActiveArticle($data->xmlProduct);
}
return $this->json($engine->generateJson());
}
}

View File

@ -5,8 +5,8 @@ namespace Plugin\System\PSC\XmlCalc\Dto\Input;
use Nelmio\ApiDocBundle\Annotation\Model; use Nelmio\ApiDocBundle\Annotation\Model;
use OpenApi\Annotations as OA; use OpenApi\Annotations as OA;
final class PriceInput { final class PriceInput
{
/** /**
* @var string * @var string
* *
@ -19,7 +19,7 @@ final class PriceInput {
* *
* @OA\Property(type="string") * @OA\Property(type="string")
*/ */
public string $xmlProduct = ""; public string $xmlProduct = '';
/** /**
* @var bool * @var bool
@ -34,5 +34,5 @@ final class PriceInput {
* @OA\Property(type="array", @OA\Items(type="string")) * @OA\Property(type="array", @OA\Items(type="string"))
*/ */
public array $values = ['auflage' => 100]; public array $values = ['auflage' => 100];
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace Plugin\System\PSC\XmlCalc\Dto\Output\Product;
use Nelmio\ApiDocBundle\Annotation\Model;
use OpenApi\Annotations as OA;
use Plugin\System\PSC\XmlCalc\Dto\Output\PreCalc\Group;
use Plugin\System\PSC\XmlCalc\Dto\Output\Price\Element;
final class ConfigOutput
{
}

View File

@ -69,6 +69,10 @@
<div class="card-header">{{'Preis'|trans}}</div> <div class="card-header">{{'Preis'|trans}}</div>
<div id="price" class="p-2"></div> <div id="price" class="p-2"></div>
</div> </div>
<div class="card mt-1">
<div class="card-header">{{'Json'|trans}}</div>
<div class="p-2 row"><textarea id="json"></textarea></div>
</div>
<div class="card mt-1"> <div class="card mt-1">
<div class="card-header">{{'Vorkalkulation'|trans}}</div> <div class="card-header">{{'Vorkalkulation'|trans}}</div>
<div id="preCalc" class="p-2 row"></div> <div id="preCalc" class="p-2 row"></div>
@ -585,12 +589,15 @@ function buildcollapseFooter(group) {
function buildDebug(debug) { function buildDebug(debug) {
$('#calcValues, #formels, #flatPrice, #price').html(''); $('#calcValues, #formels, #flatPrice, #price').html('');
if(debug && debug.calcValues) { if(debug && debug.calcValues) {
$.each(debug.calcValues, function (index, element) { $.each(debug.calcValues, function (index, element) {
$('#calcValues').append('<div class="row"><div class="col-4">' + index + '</div><div class="col-4">' + element[0] + '</div><div class="col-4">' + element[1] + '</div></div>') $('#calcValues').append('<div class="row"><div class="col-4">' + index + '</div><div class="col-4">' + element[0] + '</div><div class="col-4">' + element[1] + '</div></div>')
}); });
} }
if(debug && debug.graphJson) {
$('#json').val(debug.graphJson);
}
if(debug && debug.formels) { if(debug && debug.formels) {
$.each(debug.formels, function (index, element) { $.each(debug.formels, function (index, element) {
$('#formels').append('<div class="row"><div class="col-6">' + element[0] + '</div><div class="col-6">' + element[1] + '</div></div>') $('#formels').append('<div class="row"><div class="col-6">' + element[0] + '</div><div class="col-6">' + element[1] + '</div></div>')

View File

@ -1,4 +1,4 @@
<script> <script>
parent.postMessage({action: 'setBasketCount', data: 0}, '*'); parent.postMessage({action: 'setBasketCount', data: 0}, '*');
parent.postMessage({action: 'redirectBasket'}, '*'); parent.postMessage({action: 'redirectBasket'}, '*');
</script> </script>

View File

@ -1,9 +1,16 @@
<div class=" md:w-4/4 m-auto"> <script>
parent.postMessage({action: 'setBasketCount', data: 0}, '*');
</script><div class=" md:w-4/4 m-auto">
<h1 class="ml-1 mr-1 md:ml-0 md:mr-0 mt-4 text-xl"><?php echo $this->translate('Vielen Dank für Ihre Bestellung') ?></h1> <h1 class="ml-1 mr-1 md:ml-0 md:mr-0 mt-4 text-xl"><?php echo $this->translate('Vielen Dank für Ihre Bestellung') ?></h1>
<p class="ml-1 mr-1 md:ml-0 md:mr-0 mt-4 mb-4"><?php echo $this->translate('Sie erhalten in Kürze eine Bestätigungsmail Ihrer Bestellung.')?></p> <p class="ml-1 mr-1 md:ml-0 md:mr-0 mt-4 mb-4"><?php echo
$this->translate('Sie erhalten in Kürze eine Bestätigungsmail Ihrer Bestellung.')
?></p>
<div class="ml-1 mr-1 md:ml-0 md:mr-0 md:flex gap-4"> <div class="ml-1 mr-1 md:ml-0 md:mr-0 md:flex gap-4">
<a class="block text-center transition ease-in-out duration-300 delay-150 hover:bg-white hover:text-black border-gray-300 border bg-highlight text-black mt-2 mb-2 p-2 pl-5 pr-5 text-xs rounded-full disabled bg-white " href="/user/myorders"><?php echo $this->translate('Zu meinen Aufträgen')?></a> <a class="block text-center transition ease-in-out duration-300 delay-150 hover:bg-white hover:text-black border-gray-300 border bg-highlight text-black mt-2 mb-2 p-2 pl-5 pr-5 text-xs rounded-full disabled bg-white " href="/user/myorders"><?php echo
<a class="block text-center transition ease-in-out duration-300 delay-150 hover:bg-white hover:text-black border-gray-300 border bg-highlight text-black mt-2 mb-2 p-2 pl-5 pr-5 text-xs rounded-full disabled bg-white " href="/"><?php echo $this->translate('Zur Startseite')?></a> $this->translate('Zu meinen Aufträgen')
<a class="block text-center transition ease-in-out duration-300 delay-150 hover:bg-white hover:text-black border-gray-300 border bg-highlight text-black mt-2 mb-2 p-2 pl-5 pr-5 text-xs rounded-full disabled bg-white " href="/user?logout=1"><?php echo $this->translate('Abmelden')?></a> ?></a>
<a class="block text-center transition ease-in-out duration-300 delay-150 hover:bg-white hover:text-black border-gray-300 border bg-highlight text-black mt-2 mb-2 p-2 pl-5 pr-5 text-xs rounded-full disabled bg-white " href="/user?logout=1"><?php echo
$this->translate('Abmelden')
?></a>
</div> </div>
</div> </div>

View File

@ -1,3 +1,9 @@
<?php if($this->mode != null): ?>
<script>
parent.postMessage({action: 'redirectBasket'}, '*');
</script>
<?php else: ?>
<script> <script>
parent.postMessage({action: 'redirectLogin'}, '*'); parent.postMessage({action: 'redirectLogin'}, '*');
</script> </script>
<?php endif; ?>

View File

@ -4136,7 +4136,11 @@ class UserController extends TP_Controller_Action
$motivBasket->clearSession(); $motivBasket->clearSession();
if (file_exists($this->_templatePath . '/user/clogin.phtml')) { if (file_exists($this->_templatePath . '/user/clogin.phtml')) {
$this->_redirect('/user/clogin'); if ($this->_getParam('mode') == 'basket') {
$this->_redirect('/user/clogin?mode=basket');
}else{
$this->_redirect('/user/clogin');
}
return; return;
} }
@ -4281,7 +4285,7 @@ class UserController extends TP_Controller_Action
} }
public function cloginAction() public function cloginAction()
{ {
$this->view->mode = $this->_getParam('mode', null);
} }
public function clogoutAction() public function clogoutAction()
{ {

View File

@ -13,6 +13,10 @@ class Saxoprint {
init() { init() {
this.getProduct(); this.getProduct();
var self = this;
$(".printOffer").click(function(event) {
window.open('/apps/product/offer/' + productUUId);
});
} }
getProduct() { getProduct() {
@ -122,14 +126,14 @@ class Saxoprint {
item.label + item.label +
"</label>\n" + "</label>\n" +
' <div class="col-sm-8">\n' + ' <div class="col-sm-8">\n' +
' <select class="border border-gray-300 bg-gray-200 text-black p-2 w-full" disabled id="saxo_' + ' <select data-label="' + item.label + '" class="border border-gray-300 bg-gray-200 text-black p-2 w-full" disabled id="saxo_' +
item.id + item.id +
'" name="property[' + '" name="property[' +
item.id + item.id +
']">' + ']">' +
options + options +
"</select>" + "</select>" +
' <input type="hidden" name="property[' + ' <input data-label="' + item.label + '" type="hidden" name="property[' +
item.id + item.id +
']" id="disabled_input_' + ']" id="disabled_input_' +
item.id + item.id +
@ -149,7 +153,7 @@ class Saxoprint {
item.label + item.label +
"</label>\n" + "</label>\n" +
' <div class="col-sm-8">\n' + ' <div class="col-sm-8">\n' +
' <select class="border border-gray-300 bg-gray-200 text-black p-2 w-full" id="saxo_' + ' <select data-label="' + item.label + '" class="border border-gray-300 bg-gray-200 text-black p-2 w-full" id="saxo_' +
item.id + item.id +
'" name="property[' + '" name="property[' +
item.id + item.id +
@ -186,14 +190,14 @@ class Saxoprint {
"</label>\n" + "</label>\n" +
' <div class="col-sm-8">\n' + ' <div class="col-sm-8">\n' +
' <div class="input-group">\n' + ' <div class="input-group">\n' +
' <input class="border border-gray-300 bg-gray-200 text-black p-2 w-full" disabled id="saxo_' + ' <input data-label="' + item.label + '" class="border border-gray-300 bg-gray-200 text-black p-2 w-full" disabled id="saxo_' +
item.id + item.id +
'" name="custom[' + '" name="custom[' +
item.id + item.id +
']" value="' + ']" value="' +
item.defaultValue + item.defaultValue +
'" />' + '" />' +
' <input type="hidden" name="custom[' + ' <input data-label="' + item.label + '" type="hidden" name="custom[' +
item.id + item.id +
']" id="disabled_input_' + ']" id="disabled_input_' +
item.id + item.id +
@ -216,7 +220,7 @@ class Saxoprint {
"</label>\n" + "</label>\n" +
' <div class="col-sm-8">\n' + ' <div class="col-sm-8">\n' +
' <div class="flex">\n' + ' <div class="flex">\n' +
' <input class="flex-1 border border-gray-300 bg-gray-200 text-black p-2 w-full" id="saxo_' + ' <input data-label="' + item.label + '" class="flex-1 border border-gray-300 bg-gray-200 text-black p-2 w-full" id="saxo_' +
item.id + item.id +
'" name="custom[' + '" name="custom[' +
item.id + item.id +
@ -262,6 +266,7 @@ class Saxoprint {
} }
infos.push({ infos.push({
name: this.name, name: this.name,
label: $(this).data('label'),
value: $(this).find("option:selected").val(), value: $(this).find("option:selected").val(),
text: $(this).find("option:selected").text(), text: $(this).find("option:selected").text(),
}); });