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 && \
# 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

View File

@ -18,7 +18,7 @@ NO_COLOR:=\033[0m
# Tool CLI config
PHPUNIT_CMD=php -dxdebug.mode=off 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=
PHPSTAN_CMD=php -d xdebug.mode=off -d memory_limit=-1 vendor/bin/phpstan analyse
PHPSTAN_ARGS=--level=9

View File

@ -12,8 +12,6 @@
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_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`

View File

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

View File

@ -13,7 +13,7 @@
}
},
"require": {
"php": "8.2.*",
"php": "8.4.*",
"ext-ctype": "*",
"ext-iconv": "*",
"ext-mongodb": "^2",
@ -24,15 +24,17 @@
"chillerlan/php-qrcode": "v5.0.x-dev",
"cocur/slugify": "v3.1",
"composer/package-versions-deprecated": "^1.8",
"ddeboer/imap": "1.18.*",
"ddeboer/imap": "1.21.*",
"doctrine/annotations": "^2",
"doctrine/cache": "^2",
"doctrine/doctrine-bundle": "^2",
"doctrine/mongodb-odm-bundle": "^5",
"doctrine/orm": "^2.7",
"mistic100/randomcolor": "^1.1",
"spatie/array-to-xml": "^3.4",
"gabrielbull/ups-api": "dev-master",
"gregwar/captcha-bundle": "^2.2",
"guzzlehttp/guzzle": "^6",
"guzzlehttp/guzzle": "^7",
"horstoeko/zugferd": "^1.0",
"incenteev/composer-parameter-handler": "^2.0",
"jms/serializer-bundle": "5.*",
@ -48,12 +50,10 @@
"nelmio/api-doc-bundle": "v4.11.1",
"nelmio/cors-bundle": "^2.2",
"nicolab/php-ftp-client": "^1.4",
"ocramius/package-versions": "2.6.*",
"oneup/uploader-bundle": "^3",
"oyejorge/less.php": "~1.5",
"paypal/paypal-checkout-sdk": "dev-master",
"paypal/rest-api-sdk-php": "dev-master",
"php-http/guzzle6-adapter": "^1.1",
"phpoffice/phpspreadsheet": "^1.28",
"phpseclib/phpseclib": "~3.0",
"picqer/sendcloud-php-client": "v2.8.1",
@ -103,10 +103,10 @@
"symfonycasts/sass-bundle": "^0.8.2",
"symfonycasts/tailwind-bundle": "^0.7.0",
"tp/paydirekt-php": "^4.0",
"twig/extra-bundle": "^2.12|^3.0",
"twig/intl-extra": "^3.8",
"twig/string-extra": "^3.6",
"twig/twig": "^3.0",
"twig/extra-bundle": "^3",
"twig/intl-extra": "^3",
"twig/string-extra": "^3",
"twig/twig": "^3",
"zfb/zfb-vm": "dev-master"
},
"require-dev": {
@ -116,14 +116,14 @@
"mockery/mockery": "^1.5",
"php-parallel-lint/php-parallel-lint": "dev-develop",
"phpstan/phpstan": "^1",
"phpunit/phpunit": "9.5.x",
"phpunit/phpunit": "^10",
"rector/rector": "0.19.2",
"squizlabs/php_codesniffer": "*",
"symfony/browser-kit": "*",
"symfony/css-selector": "*",
"symfony/debug-bundle": "*",
"symfony/panther": "^2.1",
"symfony/phpunit-bridge": "6.0.x-dev",
"symfony/phpunit-bridge": "^7",
"symfony/stopwatch": "*",
"symfony/web-profiler-bundle": "*",
"symplify/config-transformer": "^11.1",
@ -137,7 +137,7 @@
"sort-packages": true,
"optimize-autoloader": true,
"platform": {
"php": "8.2.28"
"php": "8.4"
},
"allow-plugins": {
"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;
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
{
public function __construct(private readonly AliasedShop $shopService)
{}
{
}
public function fromEntity(\PSC\Component\ApiBundle\Model\Shop $shop, \PSC\Shop\EntityBundle\Entity\Shop $shopEntity): void
{
$shop->uuid = $shopEntity->getUuid();
$shop->id = $shopEntity->getUID();
$shop->name = $shopEntity->getTitle();
$shop->deleted = (bool)$shopEntity->isDeleted();
$shop->disabled = (bool)$shopEntity->isDeleted();
$shop->private = (bool)$shopEntity->isPrivate();
$shop->basketField1 = (string)$shopEntity->getBasketfield1();
$shop->basketField2 = (string)$shopEntity->getBasketfield2();

View File

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

View File

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

View File

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

View File

@ -45,17 +45,17 @@ class Calc
$gross += $discount->getPrice()->getGross();
}
}
$order->setNetWithDiscount($order->getNet() - $net);
$order->setVatWithDiscount($order->getVat() - $vat);
$order->setGrossWithDiscount($order->getGross() - $gross);
$order->setNetWithDiscount((int)($order->getNet() - $net));
$order->setVatWithDiscount((int)($order->getVat() - $vat));
$order->setGrossWithDiscount((int)($order->getGross() - $gross));
}
private function calcMwert(Price $price, ?Tax $tax): void
{
if ($tax != null) {
$price->setNet($price->getGross() / ($tax->getCalculatedAmount() / 10000 + 1));
$price->setVat($price->getGross() - ($price->getGross() / ($tax->getCalculatedAmount() / 10000 + 1)));
$price->setNet((int)($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": {
"version": "3.1.0"
},
"php-http/guzzle6-adapter": {
"version": "v1.1.1"
},
"php-http/httplug": {
"version": "v1.1.0"
},
"php-http/message": {
"version": "1.8.0"
},
"php-http/message-factory": {
"version": "v1.0.2"
},
"php-http/promise": {
"version": "v1.0.0"
},
"phpdocumentor/reflection-common": {
"version": "2.0.0"
},
@ -468,12 +459,6 @@
"sebastian/cli-parser": {
"version": "1.0.1"
},
"sebastian/code-unit": {
"version": "1.0.8"
},
"sebastian/code-unit-reverse-lookup": {
"version": "2.0.3"
},
"sebastian/comparator": {
"version": "4.0.6"
},
@ -504,9 +489,6 @@
"sebastian/recursion-context": {
"version": "4.0.4"
},
"sebastian/resource-operations": {
"version": "3.0-dev"
},
"sebastian/type": {
"version": "2.3.x-dev"
},

View File

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

View File

@ -24,7 +24,9 @@ class ImportCalcTest extends KernelTestCase
$container = static::getContainer();
/** @var Calc $calcService */
/**
* @var Calc $calcService
*/
$calcService = $container->get(Calc::class);
$shop = new Shop();
@ -39,7 +41,7 @@ class ImportCalcTest extends KernelTestCase
$payment = new Payment();
$payment->setTaxClass(19);
$payment->setPrice(25.5);
$payment->setPrice(25);
$order->setShipping($shipping);
$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
namespace Plugin\Custom\PSC\FormBuilder\Controller\Backend;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Symfony\Component\Routing\Annotation\Route;
#[Route('/index')]
class IndexController extends AbstractController
#[Route('/product')]
class ProductController extends AbstractController
{
#[Template]
#[Security("is_granted('ROLE_USER')")]
#[Route(path: '/create', name: 'psc_backend_invoice_index_create')]
public function indexAction(JWTTokenManagerInterface $jwtManager)
#[Route(path: '/edit', name: 'psc_backend_invoice_index_create')]
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",
"dependencies": {
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-xml": "^6.1.0",
"@tailwindcss/vite": "^4.1.10",
"@vueuse/core": "^13.4.0",
"@vueuse/core": "^13.5.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"ky": "^1.8.1",
"lucide-vue-next": "^0.514.0",
"pinia": "^3.0.3",
"reka-ui": "^2.3.1",
"reka-ui": "^2.3.2",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.10",
"tw-animate-css": "^1.3.4",
"uuid": "^11.1.0",
"vue": "^3.5.13",
"vue-codemirror": "^6.1.1",
"vue-draggable-plus": "^0.6.0",
},
"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=="],
"@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/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=="],
"@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=="],
"@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=="],
"@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=="],
@ -333,10 +366,14 @@
"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=="],
"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=="],
"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=="],
"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=="],
@ -521,6 +558,8 @@
"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=="],
"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-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-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=="],
"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=="],
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],

View File

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

View File

@ -1,5 +1,16 @@
<script setup lang="ts">
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>
<template>

View File

@ -4,16 +4,20 @@ import {
ResizablePanel,
ResizablePanelGroup,
} from '../components/ui/resizable'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../components/ui/tabs'
import { Library } from '../components/app/library'
import { Debug } from '../components/app/debug'
import { ElementProperties } from '../components/app/elementproperties'
import { ElementDependency } from '../components/app/elementdependency'
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>
<template>
<div class="w-scree h-screen">
<div class="w-screen h-screen">
<ResizablePanelGroup
id="handle-demo-group-1"
direction="horizontal"
@ -27,9 +31,38 @@ import { Main } from '../components/app/main'
</ResizablePanel>
<ResizableHandle id="" with-handle />
<ResizablePanel id="" :default-size="85">
<div class="flex h-full p-6 bg-slate-50">
<Main />
</div>
<Tabs default-value="designer" class="w-full h-full">
<div class="flex justify-center">
<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>
</ResizablePanelGroup>
<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 { computed } from 'vue';
import { Input } from '../../../components/ui/input'
import { useMedia } from '../../../composables/useMedia'
//import { useMedia } from '../../../composables/useMedia'
const props = defineProps({
modelValue: MediaElement
@ -15,7 +15,7 @@ const theModel = computed({
set: (value) => emit('update:modelValue', value),
});
const { media, loading, error } = useMedia()
//const { media, loading, error } = useMedia()
</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;

View File

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

View File

@ -2,6 +2,8 @@ import { defineStore } from 'pinia'
import BaseElement from '../model/BaseElement'
import Parser from '../lib/parser'
import {v4 as uuidv4} from 'uuid'
import { loadJsonFromApi } from '../lib/api'
export const useElementStore = defineStore('items', {
state: () => ({
@ -137,6 +139,11 @@ export const useElementStore = defineStore('items', {
},
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'),
},
},
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
/**
* 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;
@ -16,12 +6,6 @@ use Doctrine\ODM\MongoDB\DocumentManager;
use Doctrine\ORM\EntityManagerInterface;
use Nelmio\ApiDocBundle\Annotation\Model;
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\Output\Display\Group as DisplayGroup;
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\Price\Element;
use Plugin\System\PSC\XmlCalc\Dto\Output\Price\Option;
use Plugin\System\PSC\XmlCalc\Dto\Output\Price\Validation\Input\Max as PluginMax;
use Plugin\System\PSC\XmlCalc\Dto\Output\Price\Validation\Input\Min;
use PSC\Library\Calc\Engine;
use PSC\Library\Calc\Error\Validation\Input\Max;
use PSC\Library\Calc\Error\Validation\Input\Min as PSCMin;
use PSC\Library\Calc\Option\Type\Base;
use PSC\Library\Calc\Option\Type\ColorDBSelect;
use PSC\Library\Calc\Option\Type\DeliverySelect;
use PSC\Library\Calc\Option\Type\Select\Opt;
use PSC\Library\Calc\PaperContainer;
use PSC\Shop\ContactBundle\Model\Contact;
use PSC\Shop\ContactBundle\Transformer\Model\Contact as ContactTransformer;
use PSC\Shop\EntityBundle\Entity\Product;
use PSC\System\SettingsBundle\Service\Help;
use PSC\System\SettingsBundle\Service\PaperDB;
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 Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
@ -45,8 +37,6 @@ use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Yaml\Yaml;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
class GetPrice extends AbstractController
{
@ -76,7 +66,7 @@ class GetPrice extends AbstractController
PaperDB $paperDB,
ContactTransformer $contactTransformer,
TokenStorageInterface $tokenStorage,
Help $helpService
Help $helpService,
) {
$this->shopService = $shopService;
$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\RequestBody(
* description="This is a request body",
*
* @Model(type=\Plugin\System\PSC\XmlCalc\Dto\Input\PriceInput::class))
* )
* @OA\Tag(name="Plugin/System/psc/Xmlcalc/Price")
*/
#[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)
{
$output = new \Plugin\System\PSC\XmlCalc\Dto\Output\PriceOutput();
$output->product = $data->product;
/**
* @var Product $product
*/
* @var Product $product
*/
$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->parse(simplexml_load_string($product->getShop()->getInstall()->getPaperContainer()));
@ -121,10 +115,10 @@ class GetPrice extends AbstractController
$engine->setPaperRepository($this->paperDB);
$engine->setPaperContainer($paperContainer);
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) {
$engine->setTemplates('<root>'.$product->getShop()->getInstall()->getCalcTemplatesTest().'</root>');
$engine->setTemplates('<root>' . $product->getShop()->getInstall()->getCalcTemplatesTest() . '</root>');
}
$engine->loadString($product->getCalcXml());
if (!$data->test) {
@ -143,7 +137,7 @@ class GetPrice extends AbstractController
$engine->setVariable('contact.accountType', $contact->getAccountType()->value);
$engine->setVariable('contact.account', $contact->getAccount()->getUid());
}
if ($data->xmlProduct != "") {
if ($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) {
$tmp = new Element();
$tmp->name = $option->getName();
@ -182,10 +176,10 @@ class GetPrice extends AbstractController
}
$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->helpTitle = $help->helpTitle;
}else{
} else {
$tmp->help = $option->getHelp();
$tmp->helpLink = $option->getHelpLink();
}
@ -194,16 +188,16 @@ class GetPrice extends AbstractController
$tmp->htmlType = $option->type;
$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) {
$tmp->colorSystem = $option->getColorSystem();
if (!isset($output->colorDb[$option->getColorSystem()])) {
$output->colorDb[$option->getColorSystem()] = [];
foreach ($option->getOptions() as $opt) {
$element = array_find((array)$option->getSelectedOptions(), function(Opt $o1) use ($opt) {
$element = array_find((array) $option->getSelectedOptions(), function (Opt $o1) use ($opt) {
return $o1->getId() === $opt->getId();
});
$tmpOpt = new Option();
@ -212,13 +206,13 @@ class GetPrice extends AbstractController
$tmpOpt->prefix = $opt->getPrefix();
$tmpOpt->suffix = $opt->getSuffix();
$tmpOpt->valid = $opt->isValid();
$tmpOpt->selected = $element? true: false;
$tmpOpt->selected = $element ? true : false;
$output->colorDb[$option->getColorSystem()][] = $tmpOpt;
}
}
} else {
foreach ($option->getOptions() as $opt) {
$element = array_find((array)$option->getSelectedOptions(), function(Opt $o1) use ($opt) {
$element = array_find((array) $option->getSelectedOptions(), function (Opt $o1) use ($opt) {
return $o1->getId() === $opt->getId();
});
@ -227,7 +221,7 @@ class GetPrice extends AbstractController
$tmpOpt->id = $opt->getId();
$tmpOpt->name = $opt->getLabel();
$tmpOpt->valid = $opt->isValid();
$tmpOpt->selected = $element? true: false;
$tmpOpt->selected = $element ? true : false;
$tmpOpt->info = $opt->getInfo();
$tmpOpt->deliveryDate = $opt->getDeliveryDateAsString();
} else {
@ -235,7 +229,7 @@ class GetPrice extends AbstractController
$tmpOpt->id = $opt->getId();
$tmpOpt->name = $opt->getLabel();
$tmpOpt->valid = $opt->isValid();
$tmpOpt->selected = $element? true: false;
$tmpOpt->selected = $element ? true : false;
}
$tmp->options[] = $tmpOpt;
}
@ -247,13 +241,12 @@ class GetPrice extends AbstractController
$tmp->placeHolder = $option->getPlaceHolder();
$tmp->pattern = $option->getPattern();
foreach ($option->getValidationErrors() as $error) {
if ($error instanceof PSCMin) {
if ($error instanceof PSCMin) {
$tmp->validationErrors[] = new Min($tmp->value, $option->getMinValue());
}
if ($error instanceof Max) {
if ($error instanceof Max) {
$tmp->validationErrors[] = new PluginMax($tmp->value, $option->getMaxValue());
}
}
}
@ -282,11 +275,12 @@ class GetPrice extends AbstractController
$output->displayValues = $engine->getDisplayVariables();
$output->exportValues = $engine->getAjaxVariables();
if ($this->isGranted("ROLE_SHOP")) {
if ($this->isGranted('ROLE_SHOP')) {
$output->debug['formels'] = $engine->getDebugCalcFormel();
$output->debug['flatPrice'] = $engine->getDebugFlatPrice();
$output->debug['price'] = $engine->getDebugPrice();
$output->debug['calcValues'] = $engine->getDebugCalcVariables();
$output->debug['graphJson'] = $engine->getCalcGraph()->generateJsonGraph();
}
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 OpenApi\Annotations as OA;
final class PriceInput {
final class PriceInput
{
/**
* @var string
*
@ -19,7 +19,7 @@ final class PriceInput {
*
* @OA\Property(type="string")
*/
public string $xmlProduct = "";
public string $xmlProduct = '';
/**
* @var bool
@ -34,5 +34,5 @@ final class PriceInput {
* @OA\Property(type="array", @OA\Items(type="string"))
*/
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 id="price" class="p-2"></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-header">{{'Vorkalkulation'|trans}}</div>
<div id="preCalc" class="p-2 row"></div>
@ -585,12 +589,15 @@ function buildcollapseFooter(group) {
function buildDebug(debug) {
$('#calcValues, #formels, #flatPrice, #price').html('');
if(debug && debug.calcValues) {
$.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>')
});
}
if(debug && debug.graphJson) {
$('#json').val(debug.graphJson);
}
if(debug && debug.formels) {
$.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>')

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>
<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">
<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="/"><?php echo $this->translate('Zur Startseite')?></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>
<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?logout=1"><?php echo
$this->translate('Abmelden')
?></a>
</div>
</div>

View File

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

View File

@ -4136,7 +4136,11 @@ class UserController extends TP_Controller_Action
$motivBasket->clearSession();
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;
}
@ -4281,7 +4285,7 @@ class UserController extends TP_Controller_Action
}
public function cloginAction()
{
$this->view->mode = $this->_getParam('mode', null);
}
public function clogoutAction()
{

View File

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