diff --git a/CHANGELOG.md b/CHANGELOG.md index 376a05f..0cd4d5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ See [keep a changelog] for information about writing changes to this log. ## [Unreleased] +[PR-23](https://github.com/itk-dev/itqr/pull/23) + - Seperate visual representation of QR into own config + - Live preview of design and download [PR-22](https://github.com/itk-dev/itqr/pull/22) - Add temporary form login - Fix for "tenant null" error on URLs in embedded forms diff --git a/assets/app.js b/assets/app.js index d4f56b3..851be31 100644 --- a/assets/app.js +++ b/assets/app.js @@ -1,10 +1,12 @@ import './styles/app.css'; +const uploadBasePath = 'uploads/qr_codes/'; document.addEventListener('DOMContentLoaded', () => { const qrCodeContainer = document.getElementById('qrCodeContainer'); const tabsContainer = document.getElementById('qrCodeTabs'); // Navigation tabs const tabContentContainer = document.getElementById('qrCodeTabContent'); // Tab content const form = document.querySelector('.form-wrapper form'); + const formName = form.getAttribute('name'); const selectedQrCodes = document.getElementById('selectedQrCodes'); // Ensure all containers and elements exist @@ -20,7 +22,83 @@ document.addEventListener('DOMContentLoaded', () => { return; } + let isDesignUpdating = false; + let updatePromise = null; + const designSelect = document.querySelector('#batch_download_design'); + if (designSelect) { + designSelect.addEventListener('change', function () { + const selectedOption = this.options[this.selectedIndex]; + + if (!selectedOption.value) { + // Set default values if no design selected + const defaultFields = { + 'size': 400, + 'margin': 0, + 'backgroundColor': '#ffffff', + 'foregroundColor': '#000000', + 'labelText': '', + 'labelSize': 12, + 'labelTextColor': '#000000', + 'labelMarginTop': 0, + 'labelMarginBottom': 0, + 'errorCorrectionLevel': 'medium', + 'logo': '', + 'logoPath': '', + }; + + Object.entries(defaultFields).forEach(([field, value]) => { + const input = document.querySelector(`#batch_download_${field}`); + if (input) { + input.value = value; + } + }); + + return; + } + + isDesignUpdating = true; + // Get the design data using the API Platform endpoint + fetch(`/admin/qr_visual_configs/${selectedOption.value}`) + .then(response => response.json()) + .then(design => { + + // Update form fields with design values + const fields = { + 'size': design.size, + 'margin': design.margin, + 'backgroundColor': design.backgroundColor, + 'foregroundColor': design.foregroundColor, + 'labelText': design.labelText || '', + 'labelSize': design.labelSize, + 'labelTextColor': design.labelTextColor, + 'labelMarginTop': design.labelMarginTop, + 'labelMarginBottom': design.labelMarginBottom, + 'errorCorrectionLevel': design.errorCorrectionLevel, + 'logoPath': design.logo, + }; + + // Update each form field + Object.entries(fields).forEach(([field, value]) => { + const input = document.querySelector(`#batch_download_${field}`); + if (input) { + input.value = value; + } + }); + + isDesignUpdating = false; + }) + .catch(error => { + console.error('Error fetching design details:', error); + isDesignUpdating = false; + }); + }); + } + async function updateQRCode() { + if (updatePromise) { + return updatePromise; + } + // Clear tab and content containers tabsContainer.innerHTML = ''; tabContentContainer.innerHTML = ''; @@ -28,68 +106,77 @@ document.addEventListener('DOMContentLoaded', () => { // Prepare form data for POST request const formData = new FormData(form); formData.append('selectedQrCodes', selectedQrCodes.value); - - try { - // Fetch the QR codes from the endpoint - const response = await fetch(generateQrPath, { - method: 'POST', - body: formData, - }); - - if (response.ok) { - const data = await response.json(); - const qrCodes = data.qrCodes; // Array of qr titles and generated base64 images - - if (typeof qrCodes === 'object') { - // Loop through all images and create tabs dynamically - Object.entries(qrCodes).forEach(([title, imageSrc]) => { - // Sanitize the title to create valid IDs - const sanitizedTitle = title.replace(/[^a-zA-Z0-9-_]/g, '_'); - - // Create a unique tab ID - const tabId = `qrCodeTab-${sanitizedTitle}`; - const tabPaneId = `qrCodeContent-${sanitizedTitle}`; - - // Create tab navigation item - const tabItem = document.createElement('li'); - tabItem.className = 'nav-item'; - tabItem.role = 'presentation'; - tabItem.innerHTML = ` - - `; - tabsContainer.appendChild(tabItem); - - // Create tab content (image) - const tabContent = document.createElement('div'); - tabContent.className = `tab-pane fade ${tabsContainer.children.length === 1 ? 'show active' : ''} qr-code-tab-pane`; - tabContent.id = tabPaneId; - tabContent.role = 'tabpanel'; - tabContent.innerHTML = ` - QR Code titled ${title} - `; - tabContentContainer.appendChild(tabContent); - }); + formData.append('formName', formName); + + updatePromise = (async () => { + try { + formData.forEach((value, key) => { + console.log(key + ': ' + value); + }); + // Fetch the QR codes from the endpoint + const response = await fetch(generateQrPath, { + method: 'POST', + body: formData, + }); + + if (response.ok) { + const data = await response.json(); + const qrCodes = data.qrCodes; // Array of qr titles and generated base64 images + + if (typeof qrCodes === 'object') { + // Loop through all images and create tabs dynamically + Object.entries(qrCodes).forEach(([title, imageSrc]) => { + // Sanitize the title to create valid IDs + const sanitizedTitle = title.replace(/[^a-zA-Z0-9-_]/g, '_'); + + // Create a unique tab ID + const tabId = `qrCodeTab-${sanitizedTitle}`; + const tabPaneId = `qrCodeContent-${sanitizedTitle}`; + + // Create tab navigation item + const tabItem = document.createElement('li'); + tabItem.className = 'nav-item'; + tabItem.role = 'presentation'; + tabItem.innerHTML = ` + + `; + tabsContainer.appendChild(tabItem); + + // Create tab content (image) + const tabContent = document.createElement('div'); + tabContent.className = `tab-pane fade ${tabsContainer.children.length === 1 ? 'show active' : ''} qr-code-tab-pane`; + tabContent.id = tabPaneId; + tabContent.role = 'tabpanel'; + tabContent.innerHTML = ` + QR Code titled ${title} + `; + tabContentContainer.appendChild(tabContent); + }); + } else { + console.error('Invalid data format. Expected an array of QR code images.'); + } } else { - console.error('Invalid data format. Expected an array of QR code images.'); + console.error('Failed to fetch QR codes. Status:', response.status); } - } else { - console.error('Failed to fetch QR codes. Status:', response.status); + } catch (error) { + console.error('Error while fetching QR codes:', error); + } finally { + updatePromise = null; } - } catch (error) { - console.error('Error while fetching QR codes:', error); - } + })(); + + return updatePromise; } - // Timeout before updating qr code after typing let typingTimer; form.addEventListener('input', () => { @@ -99,6 +186,5 @@ document.addEventListener('DOMContentLoaded', () => { updateQRCode(); }, 500); }); - updateQRCode(); }); diff --git a/assets/styles/app.css b/assets/styles/app.css index ad7b868..63dd16f 100644 --- a/assets/styles/app.css +++ b/assets/styles/app.css @@ -26,11 +26,10 @@ transform: translateX(-50%); background-color: white; border: 1px solid #ccc; - border-radius: 5px; box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1); - padding: 5px; z-index: 1000; white-space: nowrap; + pointer-events: none; } .popover-content img { @@ -51,10 +50,18 @@ background-color: #f9f9f9; flex: 1; height: 475px; + position: sticky; + top: 70px; + z-index: 100; + #qrCodeContainer { + height: 100%; + } .nav-link { color: #000; } + + .nav-link.active { color: #000; font-weight: bold; @@ -66,7 +73,31 @@ } .qr-preview .qr-code-image { - max-width: 100%; + max-height: 400px; display: block; margin: 15px auto 0 auto; } + + +.qr-edit-visual-config-container, +.qr-new-visual-config-container { + display: flex; + flex-direction: row; + > div { + flex: 1; + } + + .form-fieldset-body { + > div { + > div { + width: 100%; + } + } + } +} + +form[name="batch_download"] .accordion-button { + box-shadow: none; + background-color: #fff; + border-bottom: 1px solid #e9e9e9; +} diff --git a/composer.json b/composer.json index b612ae3..3d3113f 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,7 @@ "ext-gd": "*", "ext-iconv": "*", "ext-zip": "*", - "api-platform/core": "^4.0.16", + "api-platform/core": "^4.0.22", "doctrine/dbal": "^3.9.4", "doctrine/doctrine-bundle": "^2.13.2", "doctrine/doctrine-migrations-bundle": "^3.4.1", @@ -24,6 +24,7 @@ "symfony/maker-bundle": "^1.62.1", "symfony/runtime": "~7.2.0", "symfony/twig-bundle": "~7.2.0", + "symfony/ux-twig-component": "^2.25.1", "symfony/webpack-encore-bundle": "^2.2", "symfony/yaml": "~7.2.0", "twig/twig": "^3.19.0" diff --git a/composer.lock b/composer.lock index 8a5e737..8e3fcbf 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ac8a6b572023102c4859b0e980b091e6", + "content-hash": "e21a05bf9e32eb61f21765164d09d854", "packages": [ { "name": "api-platform/core", - "version": "v4.0.16", + "version": "v4.1.12", "source": { "type": "git", "url": "https://github.com/api-platform/core.git", - "reference": "6cc70d3bc9695cb323a7117c90d992c8ca959ec1" + "reference": "abb1fac0c6201764589a8f22cb3e1ffd7e02aa16" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/core/zipball/6cc70d3bc9695cb323a7117c90d992c8ca959ec1", - "reference": "6cc70d3bc9695cb323a7117c90d992c8ca959ec1", + "url": "https://api.github.com/repos/api-platform/core/zipball/abb1fac0c6201764589a8f22cb3e1ffd7e02aa16", + "reference": "abb1fac0c6201764589a8f22cb3e1ffd7e02aa16", "shasum": "" }, "require": { @@ -29,10 +29,11 @@ "symfony/http-foundation": "^6.4 || ^7.0", "symfony/http-kernel": "^6.4 || ^7.0", "symfony/property-access": "^6.4 || ^7.0", - "symfony/property-info": "^6.4 || ^7.0", + "symfony/property-info": "^6.4 || ^7.1", "symfony/serializer": "^6.4 || ^7.0", "symfony/translation-contracts": "^3.3", - "symfony/web-link": "^6.4 || ^7.0", + "symfony/type-info": "^7.2", + "symfony/web-link": "^6.4 || ^7.1", "willdurand/negotiation": "^3.1" }, "conflict": { @@ -76,38 +77,38 @@ "doctrine/common": "^3.2.2", "doctrine/dbal": "^4.0", "doctrine/doctrine-bundle": "^2.11", - "doctrine/mongodb-odm": "^2.6", - "doctrine/mongodb-odm-bundle": "^4.0 || ^5.0", + "doctrine/mongodb-odm": "^2.10", + "doctrine/mongodb-odm-bundle": "^5.0", "doctrine/orm": "^2.17 || ^3.0", - "elasticsearch/elasticsearch": "^8.4", + "elasticsearch/elasticsearch": "^7.17 || ^8.4", "friends-of-behat/mink-browserkit-driver": "^1.3.1", "friends-of-behat/mink-extension": "^2.2", "friends-of-behat/symfony-extension": "^2.1", "guzzlehttp/guzzle": "^6.0 || ^7.0", - "illuminate/config": "^11.0", - "illuminate/contracts": "^11.0", - "illuminate/database": "^11.0", - "illuminate/http": "^11.0", - "illuminate/pagination": "^11.0", - "illuminate/routing": "^11.0", - "illuminate/support": "^11.0", + "illuminate/config": "^11.0 || ^12.0", + "illuminate/contracts": "^11.0 || ^12.0", + "illuminate/database": "^11.0 || ^12.0", + "illuminate/http": "^11.0 || ^12.0", + "illuminate/pagination": "^11.0 || ^12.0", + "illuminate/routing": "^11.0 || ^12.0", + "illuminate/support": "^11.0 || ^12.0", "jangregor/phpstan-prophecy": "^1.0", "justinrainbow/json-schema": "^5.2.11", - "laravel/framework": "^11.0", + "laravel/framework": "^11.0 || ^12.0", "orchestra/testbench": "^9.1", "phpspec/prophecy-phpunit": "^2.2", "phpstan/extension-installer": "^1.1", - "phpstan/phpdoc-parser": "^1.13|^2.0", + "phpstan/phpdoc-parser": "^1.29 || ^2.0", "phpstan/phpstan": "^1.10", "phpstan/phpstan-doctrine": "^1.0", "phpstan/phpstan-phpunit": "^1.0", "phpstan/phpstan-symfony": "^1.0", - "phpunit/phpunit": "^11.2", + "phpunit/phpunit": "11.5.x-dev", "psr/log": "^1.0 || ^2.0 || ^3.0", - "ramsey/uuid": "^4.0", + "ramsey/uuid": "^4.7", "ramsey/uuid-doctrine": "^2.0", "soyuka/contexts": "^3.3.10", - "soyuka/pmu": "^0.0.15", + "soyuka/pmu": "^0.2.0", "soyuka/stubs-mongodb": "^1.0", "symfony/asset": "^6.4 || ^7.0", "symfony/browser-kit": "^6.4 || ^7.0", @@ -216,9 +217,9 @@ ], "support": { "issues": "https://github.com/api-platform/core/issues", - "source": "https://github.com/api-platform/core/tree/v4.0.16" + "source": "https://github.com/api-platform/core/tree/v4.1.12" }, - "time": "2025-01-17T14:21:29+00:00" + "time": "2025-05-22T13:26:31+00:00" }, { "name": "bacon/bacon-qr-code", @@ -3518,16 +3519,16 @@ }, { "name": "symfony/dependency-injection", - "version": "v7.2.0", + "version": "v7.2.7", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "a475747af1a1c98272a5471abc35f3da81197c5d" + "reference": "8007d7aa52c5511e9e3f89d68577c2302567cf12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/a475747af1a1c98272a5471abc35f3da81197c5d", - "reference": "a475747af1a1c98272a5471abc35f3da81197c5d", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/8007d7aa52c5511e9e3f89d68577c2302567cf12", + "reference": "8007d7aa52c5511e9e3f89d68577c2302567cf12", "shasum": "" }, "require": { @@ -3535,7 +3536,7 @@ "psr/container": "^1.1|^2.0", "symfony/deprecation-contracts": "^2.5|^3", "symfony/service-contracts": "^3.5", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/var-exporter": "^6.4.20|^7.2.5" }, "conflict": { "ext-psr": "<1.1|>=2", @@ -3578,7 +3579,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v7.2.0" + "source": "https://github.com/symfony/dependency-injection/tree/v7.2.7" }, "funding": [ { @@ -3594,20 +3595,20 @@ "type": "tidelift" } ], - "time": "2024-11-25T15:45:00+00:00" + "time": "2025-05-19T13:28:18+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", "shasum": "" }, "require": { @@ -3620,7 +3621,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -3645,7 +3646,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" }, "funding": [ { @@ -3661,7 +3662,7 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/doctrine-bridge", @@ -3848,16 +3849,16 @@ }, { "name": "symfony/error-handler", - "version": "v7.2.1", + "version": "v7.2.7", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "6150b89186573046167796fa5f3f76601d5145f8" + "reference": "a4ba21e47e2e83ab466b808b42b29b77d9f7d867" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/6150b89186573046167796fa5f3f76601d5145f8", - "reference": "6150b89186573046167796fa5f3f76601d5145f8", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/a4ba21e47e2e83ab466b808b42b29b77d9f7d867", + "reference": "a4ba21e47e2e83ab466b808b42b29b77d9f7d867", "shasum": "" }, "require": { @@ -3903,7 +3904,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.2.1" + "source": "https://github.com/symfony/error-handler/tree/v7.2.7" }, "funding": [ { @@ -3919,7 +3920,7 @@ "type": "tidelift" } ], - "time": "2024-12-07T08:50:44+00:00" + "time": "2025-05-29T07:19:28+00:00" }, { "name": "symfony/event-dispatcher", @@ -4003,16 +4004,16 @@ }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f" + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", "shasum": "" }, "require": { @@ -4026,7 +4027,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -4059,7 +4060,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" }, "funding": [ { @@ -4075,7 +4076,7 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/filesystem", @@ -4524,16 +4525,16 @@ }, { "name": "symfony/http-foundation", - "version": "v7.2.2", + "version": "v7.2.7", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "62d1a43796ca3fea3f83a8470dfe63a4af3bc588" + "reference": "0c15d5e9fc6ae7b24bcd4cb1778bac4741eca7fe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/62d1a43796ca3fea3f83a8470dfe63a4af3bc588", - "reference": "62d1a43796ca3fea3f83a8470dfe63a4af3bc588", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/0c15d5e9fc6ae7b24bcd4cb1778bac4741eca7fe", + "reference": "0c15d5e9fc6ae7b24bcd4cb1778bac4741eca7fe", "shasum": "" }, "require": { @@ -4582,7 +4583,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.2.2" + "source": "https://github.com/symfony/http-foundation/tree/v7.2.7" }, "funding": [ { @@ -4598,20 +4599,20 @@ "type": "tidelift" } ], - "time": "2024-12-30T19:00:17+00:00" + "time": "2025-05-12T14:48:02+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.2.2", + "version": "v7.2.7", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "3c432966bd8c7ec7429663105f5a02d7e75b4306" + "reference": "4523deb9efb3c5033d84ea664a0da8341d3a4596" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/3c432966bd8c7ec7429663105f5a02d7e75b4306", - "reference": "3c432966bd8c7ec7429663105f5a02d7e75b4306", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/4523deb9efb3c5033d84ea664a0da8341d3a4596", + "reference": "4523deb9efb3c5033d84ea664a0da8341d3a4596", "shasum": "" }, "require": { @@ -4696,7 +4697,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.2.2" + "source": "https://github.com/symfony/http-kernel/tree/v7.2.7" }, "funding": [ { @@ -4712,7 +4713,7 @@ "type": "tidelift" } ], - "time": "2024-12-31T14:59:40+00:00" + "time": "2025-05-29T07:35:19+00:00" }, { "name": "symfony/intl", @@ -5117,7 +5118,7 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", @@ -5175,7 +5176,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" }, "funding": [ { @@ -5362,7 +5363,7 @@ }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -5423,7 +5424,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" }, "funding": [ { @@ -5443,19 +5444,20 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "shasum": "" }, "require": { + "ext-iconv": "*", "php": ">=7.2" }, "provide": { @@ -5503,7 +5505,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" }, "funding": [ { @@ -5519,11 +5521,11 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-12-23T08:48:59+00:00" }, { "name": "symfony/polyfill-php83", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", @@ -5579,7 +5581,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0" }, "funding": [ { @@ -5739,16 +5741,16 @@ }, { "name": "symfony/property-access", - "version": "v7.2.0", + "version": "v7.2.7", "source": { "type": "git", "url": "https://github.com/symfony/property-access.git", - "reference": "3ae42efba01e45aaedecf5c93c8d6a3ab3a82276" + "reference": "549915118a4c6534f008c09c7a1689b5da05c03a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-access/zipball/3ae42efba01e45aaedecf5c93c8d6a3ab3a82276", - "reference": "3ae42efba01e45aaedecf5c93c8d6a3ab3a82276", + "url": "https://api.github.com/repos/symfony/property-access/zipball/549915118a4c6534f008c09c7a1689b5da05c03a", + "reference": "549915118a4c6534f008c09c7a1689b5da05c03a", "shasum": "" }, "require": { @@ -5795,7 +5797,7 @@ "reflection" ], "support": { - "source": "https://github.com/symfony/property-access/tree/v7.2.0" + "source": "https://github.com/symfony/property-access/tree/v7.2.7" }, "funding": [ { @@ -5811,20 +5813,20 @@ "type": "tidelift" } ], - "time": "2024-09-26T12:28:35+00:00" + "time": "2025-05-07T15:39:53+00:00" }, { "name": "symfony/property-info", - "version": "v7.2.2", + "version": "v7.2.5", "source": { "type": "git", "url": "https://github.com/symfony/property-info.git", - "reference": "1dfeb0dac7a99f7b3be42db9ccc299c5a6483fcf" + "reference": "f00fd9685ecdbabe82ca25c7b739ce7bba99302c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-info/zipball/1dfeb0dac7a99f7b3be42db9ccc299c5a6483fcf", - "reference": "1dfeb0dac7a99f7b3be42db9ccc299c5a6483fcf", + "url": "https://api.github.com/repos/symfony/property-info/zipball/f00fd9685ecdbabe82ca25c7b739ce7bba99302c", + "reference": "f00fd9685ecdbabe82ca25c7b739ce7bba99302c", "shasum": "" }, "require": { @@ -5835,7 +5837,9 @@ "conflict": { "phpdocumentor/reflection-docblock": "<5.2", "phpdocumentor/type-resolver": "<1.5.1", - "symfony/dependency-injection": "<6.4" + "symfony/cache": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/serializer": "<6.4" }, "require-dev": { "phpdocumentor/reflection-docblock": "^5.2", @@ -5878,7 +5882,7 @@ "validator" ], "support": { - "source": "https://github.com/symfony/property-info/tree/v7.2.2" + "source": "https://github.com/symfony/property-info/tree/v7.2.5" }, "funding": [ { @@ -5894,7 +5898,7 @@ "type": "tidelift" } ], - "time": "2024-12-31T11:04:50+00:00" + "time": "2025-03-06T16:27:19+00:00" }, { "name": "symfony/routing", @@ -6409,16 +6413,16 @@ }, { "name": "symfony/serializer", - "version": "v7.2.3", + "version": "v7.2.7", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "320f30beb419ce4f96363ada5e225c41f1ef08ab" + "reference": "33734cd7b431cb426b784305eafc8bf507e8e19d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/320f30beb419ce4f96363ada5e225c41f1ef08ab", - "reference": "320f30beb419ce4f96363ada5e225c41f1ef08ab", + "url": "https://api.github.com/repos/symfony/serializer/zipball/33734cd7b431cb426b784305eafc8bf507e8e19d", + "reference": "33734cd7b431cb426b784305eafc8bf507e8e19d", "shasum": "" }, "require": { @@ -6487,7 +6491,7 @@ "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/serializer/tree/v7.2.3" + "source": "https://github.com/symfony/serializer/tree/v7.2.7" }, "funding": [ { @@ -6503,20 +6507,20 @@ "type": "tidelift" } ], - "time": "2025-01-29T07:13:55+00:00" + "time": "2025-05-12T14:48:02+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", "shasum": "" }, "require": { @@ -6534,7 +6538,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -6570,7 +6574,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" }, "funding": [ { @@ -6586,7 +6590,7 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2025-04-25T09:37:31+00:00" }, { "name": "symfony/stopwatch", @@ -6652,16 +6656,16 @@ }, { "name": "symfony/string", - "version": "v7.2.0", + "version": "v7.2.6", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82" + "reference": "a214fe7d62bd4df2a76447c67c6b26e1d5e74931" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/446e0d146f991dde3e73f45f2c97a9faad773c82", - "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82", + "url": "https://api.github.com/repos/symfony/string/zipball/a214fe7d62bd4df2a76447c67c6b26e1d5e74931", + "reference": "a214fe7d62bd4df2a76447c67c6b26e1d5e74931", "shasum": "" }, "require": { @@ -6719,7 +6723,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.2.0" + "source": "https://github.com/symfony/string/tree/v7.2.6" }, "funding": [ { @@ -6735,7 +6739,7 @@ "type": "tidelift" } ], - "time": "2024-11-13T13:31:26+00:00" + "time": "2025-04-20T20:18:16+00:00" }, { "name": "symfony/translation", @@ -6834,16 +6838,16 @@ }, { "name": "symfony/translation-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "4667ff3bd513750603a09c8dedbea942487fb07c" + "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/4667ff3bd513750603a09c8dedbea942487fb07c", - "reference": "4667ff3bd513750603a09c8dedbea942487fb07c", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d", "shasum": "" }, "require": { @@ -6856,7 +6860,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -6892,7 +6896,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.0" }, "funding": [ { @@ -6908,7 +6912,7 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-27T08:32:26+00:00" }, { "name": "symfony/twig-bridge", @@ -7106,16 +7110,16 @@ }, { "name": "symfony/type-info", - "version": "v7.2.2", + "version": "v7.2.5", "source": { "type": "git", "url": "https://github.com/symfony/type-info.git", - "reference": "3b5a17470fff0034f25fd4287cbdaa0010d2f749" + "reference": "c4824a6b658294c828e609d3d8dbb4e87f6a375d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/type-info/zipball/3b5a17470fff0034f25fd4287cbdaa0010d2f749", - "reference": "3b5a17470fff0034f25fd4287cbdaa0010d2f749", + "url": "https://api.github.com/repos/symfony/type-info/zipball/c4824a6b658294c828e609d3d8dbb4e87f6a375d", + "reference": "c4824a6b658294c828e609d3d8dbb4e87f6a375d", "shasum": "" }, "require": { @@ -7161,7 +7165,7 @@ "type" ], "support": { - "source": "https://github.com/symfony/type-info/tree/v7.2.2" + "source": "https://github.com/symfony/type-info/tree/v7.2.5" }, "funding": [ { @@ -7177,7 +7181,7 @@ "type": "tidelift" } ], - "time": "2024-12-20T13:38:37+00:00" + "time": "2025-03-24T09:03:36+00:00" }, { "name": "symfony/uid", @@ -7255,16 +7259,16 @@ }, { "name": "symfony/ux-twig-component", - "version": "v2.22.1", + "version": "v2.25.2", "source": { "type": "git", "url": "https://github.com/symfony/ux-twig-component.git", - "reference": "9b347f6ca2d9e18cee630787f0a6aa453982bf18" + "reference": "d20da25517fc09d147897d02819a046f0a0f6735" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/ux-twig-component/zipball/9b347f6ca2d9e18cee630787f0a6aa453982bf18", - "reference": "9b347f6ca2d9e18cee630787f0a6aa453982bf18", + "url": "https://api.github.com/repos/symfony/ux-twig-component/zipball/d20da25517fc09d147897d02819a046f0a0f6735", + "reference": "d20da25517fc09d147897d02819a046f0a0f6735", "shasum": "" }, "require": { @@ -7273,7 +7277,7 @@ "symfony/deprecation-contracts": "^2.2|^3.0", "symfony/event-dispatcher": "^5.4|^6.0|^7.0", "symfony/property-access": "^5.4|^6.0|^7.0", - "twig/twig": "^3.8" + "twig/twig": "^3.10.3" }, "conflict": { "symfony/config": "<5.4.0" @@ -7318,7 +7322,7 @@ "twig" ], "support": { - "source": "https://github.com/symfony/ux-twig-component/tree/v2.22.1" + "source": "https://github.com/symfony/ux-twig-component/tree/v2.25.2" }, "funding": [ { @@ -7334,7 +7338,7 @@ "type": "tidelift" } ], - "time": "2024-12-07T18:05:50+00:00" + "time": "2025-05-20T13:06:01+00:00" }, { "name": "symfony/validator", @@ -7435,16 +7439,16 @@ }, { "name": "symfony/var-dumper", - "version": "v7.2.0", + "version": "v7.2.6", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "c6a22929407dec8765d6e2b6ff85b800b245879c" + "reference": "9c46038cd4ed68952166cf7001b54eb539184ccb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/c6a22929407dec8765d6e2b6ff85b800b245879c", - "reference": "c6a22929407dec8765d6e2b6ff85b800b245879c", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/9c46038cd4ed68952166cf7001b54eb539184ccb", + "reference": "9c46038cd4ed68952166cf7001b54eb539184ccb", "shasum": "" }, "require": { @@ -7498,7 +7502,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.2.0" + "source": "https://github.com/symfony/var-dumper/tree/v7.2.6" }, "funding": [ { @@ -7514,20 +7518,20 @@ "type": "tidelift" } ], - "time": "2024-11-08T15:48:14+00:00" + "time": "2025-04-09T08:14:01+00:00" }, { "name": "symfony/var-exporter", - "version": "v7.2.0", + "version": "v7.2.7", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "1a6a89f95a46af0f142874c9d650a6358d13070d" + "reference": "785cff5a2f878bdbc5301965c1271e839aeb9a10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/1a6a89f95a46af0f142874c9d650a6358d13070d", - "reference": "1a6a89f95a46af0f142874c9d650a6358d13070d", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/785cff5a2f878bdbc5301965c1271e839aeb9a10", + "reference": "785cff5a2f878bdbc5301965c1271e839aeb9a10", "shasum": "" }, "require": { @@ -7574,7 +7578,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v7.2.0" + "source": "https://github.com/symfony/var-exporter/tree/v7.2.7" }, "funding": [ { @@ -7590,20 +7594,20 @@ "type": "tidelift" } ], - "time": "2024-10-18T07:58:17+00:00" + "time": "2025-05-15T09:03:48+00:00" }, { "name": "symfony/web-link", - "version": "v7.2.0", + "version": "v7.2.7", "source": { "type": "git", "url": "https://github.com/symfony/web-link.git", - "reference": "f537556a885e14a1d28f6c759d41e57e93d0a532" + "reference": "7697f74fce67555665339423ce453cc8216a98ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/web-link/zipball/f537556a885e14a1d28f6c759d41e57e93d0a532", - "reference": "f537556a885e14a1d28f6c759d41e57e93d0a532", + "url": "https://api.github.com/repos/symfony/web-link/zipball/7697f74fce67555665339423ce453cc8216a98ff", + "reference": "7697f74fce67555665339423ce453cc8216a98ff", "shasum": "" }, "require": { @@ -7657,7 +7661,7 @@ "push" ], "support": { - "source": "https://github.com/symfony/web-link/tree/v7.2.0" + "source": "https://github.com/symfony/web-link/tree/v7.3.0-RC1" }, "funding": [ { @@ -7673,7 +7677,7 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2025-05-19T13:28:18+00:00" }, { "name": "symfony/webpack-encore-bundle", diff --git a/config/services.yaml b/config/services.yaml index 1eb7d3f..16b55ca 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -6,25 +6,32 @@ parameters: services: - # default configuration for services in *this* file - _defaults: - autowire: true # Automatically injects dependencies in your services. - autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. - - - # makes classes in src/ available to be used as services - # this creates a service per class whose id is the fully-qualified class name - App\: - resource: '../src/' - exclude: - - '../src/DependencyInjection/' - - '../src/Entity/' - - '../src/Kernel.php' - - # add more service definitions when explicit configuration is needed - # please note that last definitions always *replace* previous ones - - # Automatically tag controllers to make them callable - App\Controller\: - resource: '../src/Controller/' - tags: [ 'controller.service_arguments' ] + # default configuration for services in *this* file + _defaults: + autowire: true # Automatically injects dependencies in your services. + autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. + + + # makes classes in src/ available to be used as services + # this creates a service per class whose id is the fully-qualified class name + App\: + resource: '../src/' + exclude: + - '../src/DependencyInjection/' + - '../src/Entity/' + - '../src/Kernel.php' + + # add more service definitions when explicit configuration is needed + # please note that last definitions always *replace* previous ones + + # Automatically tag controllers to make them callable + App\Controller\: + resource: '../src/Controller/' + tags: [ 'controller.service_arguments' ] + + App\Twig\QrCodeExtension: + tags: [ 'twig.extension' ] + + App\Service\QrCodePreviewService: + arguments: + $uploadPathConfig: '%env(APP_BASE_UPLOAD_PATH)%' diff --git a/migrations/Version20250602071253.php b/migrations/Version20250602071253.php new file mode 100644 index 0000000..f3408b5 --- /dev/null +++ b/migrations/Version20250602071253.php @@ -0,0 +1,31 @@ +addSql('CREATE TABLE qr_visual_config (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(50) NOT NULL, size INT NOT NULL, margin INT NOT NULL, background_color VARCHAR(10) NOT NULL, foreground_color VARCHAR(10) NOT NULL, label_text VARCHAR(20) DEFAULT NULL, label_size INT DEFAULT NULL, label_text_color VARCHAR(10) NOT NULL, label_margin_top INT NOT NULL, label_margin_bottom INT NOT NULL, logo VARCHAR(255) DEFAULT NULL, error_correction_level VARCHAR(255) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP TABLE qr_visual_config'); + } +} diff --git a/src/Controller/Admin/DashboardController.php b/src/Controller/Admin/DashboardController.php index 6dd005e..ec392d0 100644 --- a/src/Controller/Admin/DashboardController.php +++ b/src/Controller/Admin/DashboardController.php @@ -3,6 +3,7 @@ namespace App\Controller\Admin; use App\Entity\Tenant\Qr; +use App\Entity\Tenant\QrVisualConfig; use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard; use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem; use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController; @@ -34,7 +35,7 @@ public function configureDashboard(): Dashboard public function configureMenuItems(): iterable { - yield MenuItem::linkToDashboard('Dashboard', 'fa fa-home'); - yield MenuItem::linkToCrud(new TranslatableMessage('QR codes'), 'fa fa-qrcode', Qr::class); + yield MenuItem::linkToCrud(new TranslatableMessage('QR Codes'), 'fa fa-qrcode', Qr::class); + yield MenuItem::linkToCrud(new TranslatableMessage('QR Themes'), 'fa fa-palette', QrVisualConfig::class); } } diff --git a/src/Controller/Admin/QrCodePreviewController.php b/src/Controller/Admin/QrCodePreviewController.php index 826824b..2465fdf 100644 --- a/src/Controller/Admin/QrCodePreviewController.php +++ b/src/Controller/Admin/QrCodePreviewController.php @@ -2,15 +2,9 @@ namespace App\Controller\Admin; -use App\Helper\DownloadHelper; -use App\Repository\QrRepository; -use Endroid\QrCode\Builder\Builder; -use Endroid\QrCode\Encoding\Encoding; -use Endroid\QrCode\ErrorCorrectionLevel; +use App\Repository\QrVisualConfigRepository; +use App\Service\QrCodePreviewService; use Endroid\QrCode\Exception\ValidationException; -use Endroid\QrCode\Label\LabelAlignment; -use Endroid\QrCode\Label\Margin\Margin; -use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; @@ -18,8 +12,8 @@ readonly class QrCodePreviewController { public function __construct( - private DownloadHelper $downloadHelper, - private readonly QrRepository $qrRepository, + private QrVisualConfigRepository $qrVisualConfigRepository, + private QrCodePreviewService $qrCodePreviewService, ) { } @@ -27,91 +21,70 @@ public function __construct( * Handles the generation of QR codes for multiple selected entities. * Returns the QR codes as base64-encoded strings in an array. * + * Handles preview generation for both configuration of designs and batch download page. + * * @param Request $request the HTTP request containing parameters for the QR codes * * @return JsonResponse a JSON response containing the generated QR codes as a base64-encoded array * * @throws ValidationException + * @throws \Exception */ #[Route('/admin/generate-qr-codes', name: 'admin_generate_qr_codes', methods: ['POST'])] public function generateQrCode(Request $request): JsonResponse { - // Extract data from the request $data = $request->request->all(); + $formName = $data['formName']; + $files = $request->files->get($formName); - $downloadSettings = $data['batch_download'] ?? []; - $selectedQrCodes = $data['selectedQrCodes'] ?? []; - $selectedQrCodes = json_decode($selectedQrCodes, true); - - $logo = $request->files->get('batch_download')['logo'] ?? null; - - if (!$logo instanceof UploadedFile) { - $logo = null; - } + $downloadSettings = $data[$formName] ?? []; + $selectedQrCodes = $data['selectedQrCodes']; - // Validate selected QR codes - if (!is_array($selectedQrCodes) || empty($selectedQrCodes)) { - return new JsonResponse([ - 'error' => 'No QR codes selected.', - ], 400); + // Can be defined as such to prompt a qr example preview. + if ('examplePreview' !== $selectedQrCodes) { + $selectedQrCodes = json_decode($selectedQrCodes, true); + } else { + $selectedQrCodes = (array) $selectedQrCodes; } - // Get QR code settings or use defaults - $size = (int) min(400, $downloadSettings['size'] ?? 400); - $margin = (int) ($downloadSettings['margin'] ?? 0); - $backgroundColor = $downloadSettings['backgroundColor'] ?? '#ffffff'; - $backgroundColor = $this->downloadHelper->createColorFromHex($backgroundColor); - $foregroundColor = $downloadSettings['foregroundColor'] ?? '#000000'; - $foregroundColor = $this->downloadHelper->createColorFromHex($foregroundColor); - $labelText = $downloadSettings['labelText'] ?? ''; - $labelTextColor = $downloadSettings['labelTextColor'] ?? '#000000'; - $labelTextColor = $this->downloadHelper->createColorFromHex($labelTextColor); - $labelMargin = new Margin((int) $downloadSettings['labelMarginTop'] ?: 0, 0, (int) $downloadSettings['labelMarginBottom'] ?: 0, 0); - $errorCorrectionLevel = [ - 'low' => ErrorCorrectionLevel::Low, - 'medium' => ErrorCorrectionLevel::Medium, - 'quartile' => ErrorCorrectionLevel::Quartile, - 'high' => ErrorCorrectionLevel::High, - ][$downloadSettings['errorCorrectionLevel'] ?? 'medium'] ?? ErrorCorrectionLevel::Medium; + // Pass form data to service to generate qr code(s) + $generatedQrCodes = $this->qrCodePreviewService->generateQrCode($files, $selectedQrCodes, $downloadSettings); - // Initialize the array for storing base64-encoded QR codes - $data = []; - - // Loop through each selected QR code entity ID - foreach ($selectedQrCodes as $qrCodeId) { - // Replace this with logic to retrieve the URL (or string) for each QR code entity - $qrCodeUrl = $this->qrRepository->findOneBy(['id' => $qrCodeId])->getUrls()->first()->getUrl(); - $qrCodeTitle = $this->qrRepository->findOneBy(['id' => $qrCodeId])->getTitle(); - - if (!$qrCodeUrl) { - continue; - } + return new JsonResponse([ + 'qrCodes' => $generatedQrCodes, + ]); + } - // Generate the QR Code using Endroid QR Code Builder - $builder = new Builder(); - $result = $builder->build( - data: $qrCodeUrl, - encoding: new Encoding('UTF-8'), - errorCorrectionLevel: $errorCorrectionLevel, - size: $size, - margin: $margin, - foregroundColor: $foregroundColor, - backgroundColor: $backgroundColor, - labelText: $labelText, - labelAlignment: LabelAlignment::Center, - labelMargin: $labelMargin, - labelTextColor: $labelTextColor, - logoPath: $logo, - logoPunchoutBackground: false, - ); + /** + * Retrieves a QR Visual Config by its ID. + * + * @param int $id the identifier of the QR Visual Config to retrieve + * + * @return JsonResponse returns a JSON response containing the QR Visual Config details + * or an error message if the configuration is not found + */ + #[Route('/admin/qr_visual_configs/{id}', name: 'admin_qr_visual_config_get', methods: ['GET'])] + public function getQrVisualConfig(int $id): JsonResponse + { + $config = $this->qrVisualConfigRepository->findOneBy(['id' => $id]); - // Convert the QR code image to base64 and add to the array - $data[$qrCodeTitle] = 'data:image/png;base64,'.base64_encode($result->getString()); + if (!$config) { + return new JsonResponse(['error' => 'QR Visual Config not found'], 404); } - // Respond with the array of QR codes as base64-encoded PNGs return new JsonResponse([ - 'qrCodes' => $data, + 'id' => $config->getId(), + 'size' => $config->getSize(), + 'margin' => $config->getMargin(), + 'backgroundColor' => $config->getBackgroundColor(), + 'foregroundColor' => $config->getForegroundColor(), + 'labelText' => $config->getLabelText(), + 'labelSize' => $config->getLabelSize(), + 'labelTextColor' => $config->getLabelTextColor(), + 'labelMarginTop' => $config->getLabelMarginTop(), + 'labelMarginBottom' => $config->getLabelMarginBottom(), + 'errorCorrectionLevel' => $config->getErrorCorrectionLevel()->value, + 'logo' => $this->qrCodePreviewService->getLogoPath($config), ]); } } diff --git a/src/Controller/Admin/QrCrudController.php b/src/Controller/Admin/QrCrudController.php index 17c0b25..23adab6 100644 --- a/src/Controller/Admin/QrCrudController.php +++ b/src/Controller/Admin/QrCrudController.php @@ -7,6 +7,7 @@ use App\Helper\DownloadHelper; use EasyCorp\Bundle\EasyAdminBundle\Config\Action; use EasyCorp\Bundle\EasyAdminBundle\Config\Actions; +use EasyCorp\Bundle\EasyAdminBundle\Config\Assets; use EasyCorp\Bundle\EasyAdminBundle\Config\Crud; use EasyCorp\Bundle\EasyAdminBundle\Config\Filters; use EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext; @@ -108,10 +109,13 @@ public function configureActions(Actions $actions): Actions $batchDownloadAction = Action::new('download', new TranslatableMessage('Configure download')) ->linkToCrudAction('batchDownload') ->addCssClass('btn btn-success') - ->setIcon('fa fa-download'); + ->setIcon('fa fa-download') + ->displayAsButton(); + // Define single download action $singleDownloadAction = Action::new('quickDownload', new TranslatableMessage('Quick download')) - ->linkToCrudAction('quickDownload'); + ->linkToCrudAction('quickDownload') + ->setIcon('fa fa-download'); // Define batch url change action $setUrlAction = Action::new('setUrl', new TranslatableMessage('Set URL')) @@ -119,8 +123,9 @@ public function configureActions(Actions $actions): Actions ->addCssClass('btn btn-primary') ->setIcon('fa fa-link'); - // Set actions return $actions + ->update(Crud::PAGE_INDEX, Action::EDIT, fn (Action $action) => $action->setIcon('fa fa-pencil')->setLabel('Edit')) + ->update(Crud::PAGE_INDEX, Action::DELETE, fn (Action $action) => $action->setIcon('fa fa-trash')->setLabel('Delete')) ->addBatchAction($batchDownloadAction) ->addBatchAction($setUrlAction) ->add(Crud::PAGE_INDEX, $singleDownloadAction); @@ -159,4 +164,10 @@ public function batchDownload(BatchActionDto $batchActionDto): RedirectResponse { return $this->redirectToRoute('admin_batch_download', $batchActionDto->getEntityIds()); } + + public function configureAssets(Assets $assets): Assets + { + return $assets + ->addWebpackEncoreEntry('app'); + } } diff --git a/src/Controller/Admin/QrVisualConfigCrudController.php b/src/Controller/Admin/QrVisualConfigCrudController.php new file mode 100644 index 0000000..e98770c --- /dev/null +++ b/src/Controller/Admin/QrVisualConfigCrudController.php @@ -0,0 +1,128 @@ +setPageTitle('index', new TranslatableMessage('QR Themes')) + ->setPageTitle('new', new TranslatableMessage('Create Theme')) + ->setPageTitle('edit', new TranslatableMessage('Edit Theme')) + ->setEntityLabelInSingular(new TranslatableMessage('QR Theme')) + ->overrideTemplate('crud/edit', 'admin/qr_visual_config/edit.html.twig') + ->overrideTemplate('crud/new', 'admin/qr_visual_config/new.html.twig'); + } + + public function new(AdminContext $context) + { + return parent::new($context); + } + + public function configureActions(Actions $actions): Actions + { + return $actions + ->update(Crud::PAGE_INDEX, Action::EDIT, fn (Action $action) => $action->setIcon('fa fa-pencil')->setLabel('Edit')) + ->update(Crud::PAGE_INDEX, Action::DELETE, fn (Action $action) => $action->setIcon('fa fa-trash')->setLabel('Delete')); + } + + public function configureFields(string $pageName): iterable + { + if (Crud::PAGE_INDEX === $pageName) { + return [ + TextField::new('name')->setLabel(new TranslatableMessage('Name')), + IntegerField::new('size')->setLabel(new TranslatableMessage('Size (px)')), + Field::new('customUrlButton', new TranslatableMessage('Preview ')) + ->setTemplatePath('fields/link/linkExample.html.twig') + ->hideOnForm(), + ]; + } + if (Crud::PAGE_EDIT === $pageName || Crud::PAGE_NEW === $pageName) { + return [ + // Id should not be mapped, but we still need the id for the preview generation + HiddenField::new('id') + ->setFormTypeOption('mapped', false) + ->setFormTypeOption('data', $this->getContext()->getEntity()->getInstance()->getId()), + TextField::new('name') + ->setLabel(new TranslatableMessage('Name')) + ->setHelp(new TranslatableMessage('Name of the theme.')), + IntegerField::new('size') + ->setLabel(new TranslatableMessage('Size')) + ->setHelp(new TranslatableMessage('Size of the QR code in pixels.')), + IntegerField::new('margin') + ->setLabel(new TranslatableMessage('Margin')) + ->setHelp(new TranslatableMessage('Margin is the whitespace around the QR code in pixels.')), + Field::new('backgroundColor') + ->setFormType(ColorType::class) + ->setLabel(new TranslatableMessage('Background color')), + Field::new('foregroundColor') + ->setFormType(ColorType::class) + ->setLabel(new TranslatableMessage('Code color')), + TextField::new('labelText') + ->setLabel(new TranslatableMessage('Label')) + ->setHelp(new TranslatableMessage('Label is a text that is displayed below the QR code.')) + ->setRequired(false), + Field::new('labelSize') + ->setLabel(new TranslatableMessage('Text size')) + ->setHelp(new TranslatableMessage('Text size is the size of the label in pixels.')), + Field::new('labelTextColor') + ->setFormType(ColorType::class) + ->setLabel(new TranslatableMessage('Text color')), + Field::new('labelMarginTop') + ->setLabel(new TranslatableMessage('Text margin (top)')), + Field::new('labelMarginBottom') + ->setLabel(new TranslatableMessage('Text margin (bund)')), + ImageField::new('logo') + ->setBasePath('uploads/qr-logos') + ->setUploadedFileNamePattern('[ulid]-[slug].[extension]') + ->setUploadDir('public/uploads/qr-logos') + ->setFormTypeOptions([ + 'required' => false, + ]), + ChoiceField::new('errorCorrectionLevel') + ->setLabel(new TranslatableMessage('error_correction.label')) + ->setHelp(new TranslatableMessage('error_correction.help')) + ->setFormType(ChoiceType::class) + ->setFormTypeOptions([ + 'class' => ErrorCorrectionLevel::class, + 'choice_label' => function (ErrorCorrectionLevel $choice) { + return new TranslatableMessage('error_correction.'.$choice->name); + }, + 'choices' => ErrorCorrectionLevel::cases(), + ]), + ]; + } + + return []; + } + + public function configureAssets(Assets $assets): Assets + { + return $assets + ->addWebpackEncoreEntry('app'); + } +} diff --git a/src/DTO/DownloadSettingsDTO.php b/src/DTO/DownloadSettingsDTO.php new file mode 100644 index 0000000..b5096b2 --- /dev/null +++ b/src/DTO/DownloadSettingsDTO.php @@ -0,0 +1,27 @@ +id; + } + + public function getSize(): int + { + return $this->size; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): static + { + $this->name = $name; + + return $this; + } + + public function setSize(int $size): static + { + $this->size = $size; + + return $this; + } + + public function getMargin(): int + { + return $this->margin; + } + + public function setMargin(int $margin): static + { + $this->margin = $margin; + + return $this; + } + + public function getBackgroundColor(): string + { + return $this->backgroundColor; + } + + public function setBackgroundColor(string $backgroundColor): static + { + $this->backgroundColor = $backgroundColor; + + return $this; + } + + public function getForegroundColor(): string + { + return $this->foregroundColor; + } + + public function setForegroundColor(string $foregroundColor): static + { + $this->foregroundColor = $foregroundColor; + + return $this; + } + + public function getLabelText(): ?string + { + return $this->labelText; + } + + public function setLabelText(?string $labelText): static + { + $this->labelText = $labelText; + + return $this; + } + + public function getLabelSize(): ?int + { + return $this->labelSize; + } + + public function setLabelSize(?int $labelSize): static + { + $this->labelSize = $labelSize; + + return $this; + } + + public function getLabelTextColor(): string + { + return $this->labelTextColor; + } + + public function setLabelTextColor(string $labelTextColor): static + { + $this->labelTextColor = $labelTextColor; + + return $this; + } + + public function getLabelMarginTop(): int + { + return $this->labelMarginTop; + } + + public function setLabelMarginTop(int $labelMarginTop): static + { + $this->labelMarginTop = $labelMarginTop; + + return $this; + } + + public function getLabelMarginBottom(): int + { + return $this->labelMarginBottom; + } + + public function setLabelMarginBottom(int $labelMarginBottom): static + { + $this->labelMarginBottom = $labelMarginBottom; + + return $this; + } + + public function getLogo(): ?string + { + return $this->logo; + } + + public function setLogo(File|string|null $logo): static + { + if ($logo instanceof File) { + $this->logo = $logo->getFilename(); + } else { + $this->logo = $logo; + } + + return $this; + } + + public function getErrorCorrectionLevel(): ErrorCorrectionLevel + { + return $this->errorCorrectionLevel; + } + + public function setErrorCorrectionLevel(ErrorCorrectionLevel $errorCorrectionLevel): static + { + $this->errorCorrectionLevel = $errorCorrectionLevel; + + return $this; + } +} diff --git a/src/Form/Type/AdvancedSettingsType.php b/src/Form/Type/AdvancedSettingsType.php new file mode 100644 index 0000000..597c178 --- /dev/null +++ b/src/Form/Type/AdvancedSettingsType.php @@ -0,0 +1,75 @@ +add('size', TextType::class, [ + 'label' => new TranslatableMessage('Size (px)'), + 'data' => '400', + ]); + $builder->add('margin', TextType::class, [ + 'label' => new TranslatableMessage('Margin (px)'), + 'data' => '0', + ]); + $builder->add('backgroundColor', ColorType::class, [ + 'label' => new TranslatableMessage('Code background'), + 'data' => '#ffffff', + ]); + $builder->add('foregroundColor', ColorType::class, [ + 'label' => new TranslatableMessage('Code color'), + 'data' => '#000000', + ]); + + $builder->add('labelText', TextType::class, [ + 'label' => new TranslatableMessage('Text'), + 'required' => false, + ]); + $builder->add('labelSize', IntegerType::class, [ + 'label' => new TranslatableMessage('Text size'), + ]); + $builder->add('labelTextColor', ColorType::class, [ + 'label' => new TranslatableMessage('Text color'), + ]); + + $builder->add('labelMarginTop', IntegerType::class, [ + 'label' => new TranslatableMessage('Text margin (top)'), + 'data' => 15, + ]); + + $builder->add('labelMarginBottom', IntegerType::class, [ + 'label' => new TranslatableMessage('Text margin (bund)'), + 'data' => 15, + ]); + $builder->add('logo', FileType::class, [ + 'label' => new TranslatableMessage('Logo'), + 'required' => false, + ]); + $builder->add('logoPath', HiddenType::class, [ + 'label' => false, + 'required' => false, + ]); + $builder->add('errorCorrectionLevel', ChoiceType::class, [ + 'label' => new TranslatableMessage('Error correction level'), + 'choices' => [ + 'Low' => ErrorCorrectionLevel::Low->value, + 'Medium' => ErrorCorrectionLevel::Medium->value, + 'Quartile' => ErrorCorrectionLevel::Quartile->value, + 'High' => ErrorCorrectionLevel::High->value, + ], + ]); + } +} diff --git a/src/Form/Type/BatchDownloadType.php b/src/Form/Type/BatchDownloadType.php index 5dcd4bf..4548074 100644 --- a/src/Form/Type/BatchDownloadType.php +++ b/src/Form/Type/BatchDownloadType.php @@ -2,11 +2,14 @@ namespace App\Form\Type; +use App\Entity\Tenant\QrVisualConfig; use Endroid\QrCode\ErrorCorrectionLevel; +use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\ColorType; use Symfony\Component\Form\Extension\Core\Type\FileType; +use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\Extension\Core\Type\IntegerType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\TextType; @@ -14,61 +17,90 @@ use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Translation\TranslatableMessage; -/** - * @extends AbstractType - */ class BatchDownloadType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void { - $builder->add('size', TextType::class, [ - 'label' => new TranslatableMessage('Size (px)'), - 'data' => '400', - ]); - $builder->add('margin', TextType::class, [ - 'label' => new TranslatableMessage('Margin (px)'), - 'data' => '0', - ]); - $builder->add('backgroundColor', ColorType::class, [ - 'label' => new TranslatableMessage('Code background'), - 'data' => '#ffffff', - ]); - $builder->add('foregroundColor', ColorType::class, [ - 'label' => new TranslatableMessage('Code color'), - 'data' => '#000000', - ]); - - $builder->add('labelText', TextType::class, [ - 'label' => new TranslatableMessage('Text'), - 'required' => false, - ]); - $builder->add('labelTextColor', ColorType::class, [ - 'label' => new TranslatableMessage('Text color'), - ]); - - $builder->add('labelMarginTop', IntegerType::class, [ - 'label' => new TranslatableMessage('Text margin (top)'), - 'data' => 15, - ]); - - $builder->add('labelMarginBottom', IntegerType::class, [ - 'label' => new TranslatableMessage('Text margin (bund)'), - 'data' => 15, - ]); - $builder->add('logo', FileType::class, [ - 'label' => new TranslatableMessage('Logo'), - 'required' => false, - ]); - $builder->add('errorCorrectionLevel', ChoiceType::class, [ - 'label' => new TranslatableMessage('Error correction level'), - 'choices' => [ - 'Low' => ErrorCorrectionLevel::Low->value, - 'Medium' => ErrorCorrectionLevel::Medium->value, - 'Quartile' => ErrorCorrectionLevel::Quartile->value, - 'High' => ErrorCorrectionLevel::High->value, - ], - ]); - $builder->add('download', SubmitType::class); + $builder + ->add('design', EntityType::class, [ + 'class' => QrVisualConfig::class, + 'choice_label' => 'name', + 'placeholder' => new TranslatableMessage('qr.select_design'), + 'required' => false, + ]) + ->add('size', IntegerType::class, [ + 'label' => new TranslatableMessage('qr.size.label'), + 'data' => 400, + 'attr' => ['data-controller' => 'advanced-settings'], + 'help' => new TranslatableMessage('qr.size.help'), + ]) + ->add('margin', IntegerType::class, [ + 'label' => new TranslatableMessage('qr.margin.label'), + 'data' => '0', + 'attr' => ['data-controller' => 'advanced-settings'], + 'help' => new TranslatableMessage('qr.margin.help'), + ]) + ->add('backgroundColor', ColorType::class, [ + 'label' => new TranslatableMessage('qr.code_background'), + 'data' => '#ffffff', + 'attr' => ['data-controller' => 'advanced-settings'], + ]) + ->add('foregroundColor', ColorType::class, [ + 'label' => new TranslatableMessage('qr.code_color'), + 'data' => '#000000', + 'attr' => ['data-controller' => 'advanced-settings'], + ]) + ->add('labelText', TextType::class, [ + 'label' => new TranslatableMessage('text.label'), + 'required' => false, + 'attr' => ['data-controller' => 'advanced-settings'], + ]) + ->add('labelSize', IntegerType::class, [ + 'label' => new TranslatableMessage('text.size'), + 'data' => 15, + 'attr' => ['data-controller' => 'advanced-settings'], + ]) + ->add('labelTextColor', ColorType::class, [ + 'label' => new TranslatableMessage('text.color'), + 'attr' => ['data-controller' => 'advanced-settings'], + ]) + ->add('labelMarginTop', IntegerType::class, [ + 'label' => new TranslatableMessage('text.margin.top.label'), + 'data' => 15, + 'attr' => ['data-controller' => 'advanced-settings'], + 'help' => new TranslatableMessage('text.margin.top.help'), + ]) + ->add('labelMarginBottom', IntegerType::class, [ + 'label' => new TranslatableMessage('text.margin.bottom.label'), + 'data' => 15, + 'attr' => ['data-controller' => 'advanced-settings'], + 'help' => new TranslatableMessage('text.margin.bottom.help'), + ]) + ->add('logo', FileType::class, [ + 'label' => new TranslatableMessage('logo.label'), + 'required' => false, + 'attr' => ['data-controller' => 'advanced-settings'], + ]) + ->add('logoPath', HiddenType::class, [ + 'label' => false, + 'required' => false, + 'attr' => ['data-controller' => 'advanced-settings'], + ]) + ->add('errorCorrectionLevel', ChoiceType::class, [ + 'label' => new TranslatableMessage('error_correction.label'), + 'choices' => [ + 'error_correction.Low' => ErrorCorrectionLevel::Low->value, + 'error_correction.Medium' => ErrorCorrectionLevel::Medium->value, + 'error_correction.Quartile' => ErrorCorrectionLevel::Quartile->value, + 'error_correction.High' => ErrorCorrectionLevel::High->value, + ], + 'choice_translation_domain' => true, + 'attr' => ['data-controller' => 'advanced-settings'], + 'help' => new TranslatableMessage('error_correction.help'), + ]) + ->add('download', SubmitType::class, [ + 'label' => new TranslatableMessage('qr.download'), + ]); } public function configureOptions(OptionsResolver $resolver): void diff --git a/src/Helper/DownloadHelper.php b/src/Helper/DownloadHelper.php index 2ce467e..886bc12 100644 --- a/src/Helper/DownloadHelper.php +++ b/src/Helper/DownloadHelper.php @@ -8,6 +8,9 @@ use Endroid\QrCode\Encoding\Encoding; use Endroid\QrCode\ErrorCorrectionLevel; use Endroid\QrCode\Exception\ValidationException; +use Endroid\QrCode\Label\Font\Font; +use Endroid\QrCode\Label\Font\FontInterface; +use Endroid\QrCode\Label\Font\OpenSans; use Endroid\QrCode\Label\LabelAlignment; use Endroid\QrCode\Label\Margin\Margin; use Symfony\Component\HttpFoundation\File\UploadedFile; @@ -40,20 +43,16 @@ public function generateQrCodes(array $qrEntities, array $downloadSettings): Str 'backgroundColor' => $this->createColorFromHex($downloadSettings['backgroundColor'] ?? '#ffffff'), 'foregroundColor' => $this->createColorFromHex($downloadSettings['foregroundColor'] ?? '#000000'), 'labelText' => $downloadSettings['labelText'] ?? '', + 'labelFont' => $this->createFontInterface((int) ($downloadSettings['labelSize'] ?? 12)), 'labelTextColor' => $this->createColorFromHex($downloadSettings['labelTextColor'] ?? '#000000'), - 'labelMargin' => new Margin( - (int) ($downloadSettings['labelMarginTop'] ?? 0), - 0, - (int) ($downloadSettings['labelMarginBottom'] ?? 0), - 0 - ), + 'labelMargin' => $this->createLabelMargin((int) ($downloadSettings['labelMarginTop'] ?? 0), (int) ($downloadSettings['labelMarginBottom'] ?? 0)), 'errorCorrectionLevel' => [ 'low' => ErrorCorrectionLevel::Low, 'medium' => ErrorCorrectionLevel::Medium, 'quartile' => ErrorCorrectionLevel::Quartile, 'high' => ErrorCorrectionLevel::High, ][$downloadSettings['errorCorrectionLevel'] ?? 'medium'] ?? ErrorCorrectionLevel::Medium, - 'logo' => $this->processLogo($downloadSettings['logo'] ?? null), + 'logo' => $this->processLogo($downloadSettings['logo'] ?? null) ?? $downloadSettings['logoPath'] ?? null, ]; // Based on the number of entities, call the appropriate function @@ -162,10 +161,11 @@ private function buildQrCode( foregroundColor: $settings['foregroundColor'], backgroundColor: $settings['backgroundColor'], labelText: $settings['labelText'], + labelFont: $settings['labelFont'], labelAlignment: LabelAlignment::Center, labelMargin: $settings['labelMargin'], labelTextColor: $settings['labelTextColor'], - logoPath: $settings['logo'], + logoPath: $settings['logo'] ?? $settings['logoPath'], logoPunchoutBackground: false, ); @@ -175,7 +175,7 @@ private function buildQrCode( /** * Process the logo file for QR code generation. */ - private function processLogo(?UploadedFile $logo): ?string + public function processLogo(?UploadedFile $logo): ?string { if ($logo instanceof UploadedFile) { // Save the uploaded file and return its path @@ -203,4 +203,27 @@ public function createColorFromHex(string $hexColor): Color return new Color($r, $g, $b); } + + /** + * Create a margin object with specified top and bottom margins. + * + * @param int $top Top margin value + * @param int $bottom Bottom margin value + */ + public function createLabelMargin(int $top, int $bottom): Margin + { + return new Margin($top, 0, $bottom, 0); + } + + /** + * Create a font interface with the specified font size. + * + * @param int $size The font size + * + * @return FontInterface The created font interface + */ + public function createFontInterface(int $size): FontInterface + { + return new Font((new OpenSans())->getPath(), $size); + } } diff --git a/src/Repository/QrVisualConfigRepository.php b/src/Repository/QrVisualConfigRepository.php new file mode 100644 index 0000000..b8ad2c4 --- /dev/null +++ b/src/Repository/QrVisualConfigRepository.php @@ -0,0 +1,20 @@ + + * + * @method QrVisualConfig|null findOneBy(array $criteria, array $orderBy = null) + */ +class QrVisualConfigRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, QrVisualConfig::class); + } +} diff --git a/src/Service/QrCodePreviewService.php b/src/Service/QrCodePreviewService.php new file mode 100644 index 0000000..5a7afa4 --- /dev/null +++ b/src/Service/QrCodePreviewService.php @@ -0,0 +1,155 @@ +value => ErrorCorrectionLevel::Low, + ErrorCorrectionLevel::Medium->value => ErrorCorrectionLevel::Medium, + ErrorCorrectionLevel::Quartile->value => ErrorCorrectionLevel::Quartile, + ErrorCorrectionLevel::High->value => ErrorCorrectionLevel::High, + ]; + + public function __construct( + private readonly DownloadHelper $downloadHelper, + private readonly QrVisualConfigRepository $qrVisualConfigRepository, + private readonly QrRepository $qrRepository, + private string $uploadPathConfig, + ) { + } + + /** + * Generates QR code data using provided files, selected QR codes, and download settings. + * + * @param array $files array of files uploaded in the request + * @param array $selectedQrCodes array of QR codes selected for generation + * @param array $downloadSettings configuration data for QR generation + * + * @return array returns generated QR code data based on the provided settings + */ + public function generateQrCode(array $files, array $selectedQrCodes, array $downloadSettings): array + { + /* + Extract logo from the request (When a new image is uploaded either from batch download page or design config page. + ImageField places uploaded files in ['logo']['file'] + FileType places uploaded files in ['logo'] + */ + $logo = null; + + if ($files && isset($files['logo'])) { + if ($files['logo'] instanceof UploadedFile) { + $logo = $files['logo']; + } elseif (isset($files['logo']['file']) && $files['logo']['file'] instanceof UploadedFile) { + $logo = $files['logo']['file']; + } + } + + // If a design is edited, it contains an id from where we can grab the entity. + $entity = isset($downloadSettings['id']) && !$logo ? $this->qrVisualConfigRepository->findOneBy(['id' => $downloadSettings['id']]) : null; + + // Uploaded logo > logo from entity > logo from logoPath (only on batch download page) + if ($entity && $entity->getLogo()) { + $downloadSettings['logoPath'] = $this->getLogoPath($entity); + $logo = $downloadSettings['logoPath']; + } elseif (isset($downloadSettings['logoPath']) && !$logo instanceof UploadedFile) { + $logo = $downloadSettings['logoPath']; + } + + $backgroundColor = $downloadSettings['backgroundColor']; + $foregroundColor = $downloadSettings['foregroundColor']; + $labelSize = $downloadSettings['labelSize']; + $labelTextColor = $downloadSettings['labelTextColor']; + $labelMargin = [$downloadSettings['labelMarginTop'], 0, $downloadSettings['labelMarginBottom'], 0]; + $errorCorrectionLevel = self::ERROR_CORRECTION_LEVELS[$downloadSettings['errorCorrectionLevel']]; + + // Define DTO + $downloadSettingsDTO = new DownloadSettingsDTO( + $downloadSettings['size'], + $downloadSettings['margin'], + $this->downloadHelper->createColorFromHex($backgroundColor), + $this->downloadHelper->createColorFromHex($foregroundColor), + $downloadSettings['labelText'], + $this->downloadHelper->createFontInterface($labelSize), + $this->downloadHelper->createColorFromHex($labelTextColor), + new Margin(...$labelMargin), + $errorCorrectionLevel, + ); + + return $this->generateQrCodeData($selectedQrCodes, $downloadSettingsDTO, $logo); + } + + /** + * Generates QR Code data for a list of selected QR Code IDs, returning the QR codes + * as base64-encoded PNG images in a JSON response. + * + * @param array $selectedQrCodes list of QR code entity IDs to process + * @param DownloadSettingsDTO $downloadSettingsDTO data transfer object containing settings for QR code generation + * @param string|null $logo optional path to a logo image to include in the QR code + * + * @return array JSON response containing an array of QR codes as base64-encoded PNG images + * + * @throws \Exception if no QR codes are found or processed + */ + private function generateQrCodeData(array $selectedQrCodes, DownloadSettingsDTO $downloadSettingsDTO, ?string $logo = null): array + { + $data = []; + // Loop through each selected QR code entity ID + foreach ($selectedQrCodes as $qrCodeId) { + // Replace this with logic to retrieve the URL (or string) for each QR code entity + $qrCodeUrl = 'examplePreview' === $qrCodeId ? 'qr visual config example preview' : $this->qrRepository->findOneBy(['id' => $qrCodeId])->getUrls()->first()->getUrl(); + $qrCodeTitle = 'examplePreview' === $qrCodeId ? 'examplePreview' : $this->qrRepository->findOneBy(['id' => $qrCodeId])->getTitle(); + + if (!$qrCodeUrl) { + continue; + } + + // Generate the QR Code using Endroid QR Code Builder + $builder = new Builder(); + $result = $builder->build( + data: $qrCodeUrl, + encoding: new Encoding('UTF-8'), + errorCorrectionLevel: $downloadSettingsDTO->errorCorrectionLevel, + size: $downloadSettingsDTO->size, + margin: $downloadSettingsDTO->margin, + foregroundColor: $downloadSettingsDTO->foregroundColor, + backgroundColor: $downloadSettingsDTO->backgroundColor, + labelText: $downloadSettingsDTO->labelText, + labelFont: $downloadSettingsDTO->labelFont, + labelAlignment: LabelAlignment::Center, + labelMargin: $downloadSettingsDTO->labelMargin, + labelTextColor: $downloadSettingsDTO->labelTextColor, + logoPath: $logo, + logoPunchoutBackground: false, + ); + + // Convert the QR code image to base64 and add to the array + $data[$qrCodeTitle] = 'data:image/png;base64,'.base64_encode($result->getString()); + } + + return $data; + } + + public function getLogoPath(QrVisualConfig $qrVisualConfig): ?string + { + if (!$qrVisualConfig->getLogo()) { + return null; + } + + return $this->uploadPathConfig.$qrVisualConfig->getLogo(); + } +} diff --git a/src/Twig/QrCodeExtension.php b/src/Twig/QrCodeExtension.php new file mode 100644 index 0000000..24d1d59 --- /dev/null +++ b/src/Twig/QrCodeExtension.php @@ -0,0 +1,24 @@ +downloadHelper, 'createColorFromHex']), + new TwigFunction('create_label_margin', [$this->downloadHelper, 'createLabelMargin']), + new TwigFunction('create_font_interface', [$this->downloadHelper, 'createFontInterface']), + ]; + } +} diff --git a/symfony.lock b/symfony.lock index be9c91e..ca9d188 100644 --- a/symfony.lock +++ b/symfony.lock @@ -13,6 +13,15 @@ "src/ApiResource/.gitignore" ] }, + "doctrine/deprecations": { + "version": "1.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "87424683adc81d7dc305eefec1fced883084aab9" + } + }, "doctrine/doctrine-bundle": { "version": "2.13", "recipe": { diff --git a/templates/admin/qr_visual_config/edit.html.twig b/templates/admin/qr_visual_config/edit.html.twig new file mode 100644 index 0000000..417f925 --- /dev/null +++ b/templates/admin/qr_visual_config/edit.html.twig @@ -0,0 +1,21 @@ +{% extends '@!EasyAdmin/crud/edit.html.twig' %} + +{% block main %} +
+ +
+
+ {{ parent() }} +
+
+
+
+ + +
+
+
+
+
+{% endblock %} diff --git a/templates/admin/qr_visual_config/new.html.twig b/templates/admin/qr_visual_config/new.html.twig new file mode 100644 index 0000000..b6f75e8 --- /dev/null +++ b/templates/admin/qr_visual_config/new.html.twig @@ -0,0 +1,23 @@ +{% extends '@!EasyAdmin/crud/new.html.twig' %} + +{% block main %} +
+ +
+
+ {{ parent() }} +
+
+
+
+ + +
+
+
+
+
+ + +{% endblock %} diff --git a/templates/EasyAdminBundle/content.html.twig b/templates/bundles/EasyAdminBundle/content.html.twig similarity index 82% rename from templates/EasyAdminBundle/content.html.twig rename to templates/bundles/EasyAdminBundle/content.html.twig index 1ffd45b..1b176a0 100644 --- a/templates/EasyAdminBundle/content.html.twig +++ b/templates/bundles/EasyAdminBundle/content.html.twig @@ -1,4 +1,4 @@ -{% extends '@EasyAdmin/page/content.html.twig' %} +{% extends '@!EasyAdmin/page/content.html.twig' %} {% block head_javascript %} {{ parent() }} diff --git a/templates/fields/link/linkExample.html.twig b/templates/fields/link/linkExample.html.twig new file mode 100644 index 0000000..9c368ee --- /dev/null +++ b/templates/fields/link/linkExample.html.twig @@ -0,0 +1,38 @@ +{% if entity.instance.backgroundColor %} + {% set backgroundColor = create_color_from_hex(entity.instance.backgroundColor) %} +{% endif %} + +{% if entity.instance.foregroundColor %} + {% set foregroundColor = create_color_from_hex(entity.instance.foregroundColor) %} +{% endif %} + +{% if entity.instance.labelTextColor %} + {% set labelTextColor = create_color_from_hex(entity.instance.labelTextColor) %} +{% endif %} + +{% if entity.instance.labelMarginTop is defined or entity.instance.labelMarginBottom is defined %} + {% set labelMargin = create_label_margin(entity.instance.labelMarginTop|default(0), entity.instance.labelMarginBottom|default(0)) %} +{% endif %} + +{% if entity.instance.labelSize %} + {% set labelFont = create_font_interface(entity.instance.labelSize) %} +{% endif %} + + + View + + Generated Image + + diff --git a/templates/form/batchDownload.html.twig b/templates/form/batchDownload.html.twig index 00cf001..3066d7e 100644 --- a/templates/form/batchDownload.html.twig +++ b/templates/form/batchDownload.html.twig @@ -1,8 +1,7 @@ -{% extends 'EasyAdminBundle/content.html.twig' %} +{% extends '@EasyAdmin/content.html.twig' %} {% form_theme form '@EasyAdmin/crud/form_theme.html.twig' %} {% block main %} -
@@ -11,7 +10,40 @@ .setAction('index') }}"> - {{ form(form) }} + + {{ form_start(form) }} + + {# Design dropdown outside accordion #} + {{ form_row(form.design) }} + + {# Advanced Settings Accordion #} +
+
+

+ +

+
+
+ {# Render all remaining form fields except design and download #} + {% for field in form %} + {% if field.vars.name != 'design' and field.vars.name != 'download' %} + {{ form_row(field) }} + {% endif %} + {% endfor %} +
+
+
+
+ + {# Download button outside accordion #} + {{ form_row(form.download) }} + + {{ form_end(form) }}
diff --git a/translations/messages.da.yaml b/translations/messages.da.yaml index 6bc71ee..42c23a9 100644 --- a/translations/messages.da.yaml +++ b/translations/messages.da.yaml @@ -1,2 +1,37 @@ frontpage: - hello_world: 'Hej Verden!' + hello_world: 'Hej Verden!' + +qr: + size: + label: 'Størrelse' + help: 'QR-kodens størrelse defineret i pixels' + margin: + label: 'Margin' + help: 'QR-kodens margin defineret i pixels' + code_background: 'Baggrund' + code_color: 'QR-kode farve' + select_design: '-- Vælg et design --' + download: 'Download' + +text: + label: 'Tekst' + size: 'Tekststørrelse' + color: 'Tekstfarve' + margin: + top: + label: 'Øvre tekstmargin' + help: 'Margin over tekst defineret i pixels' + bottom: + label: 'Nedre tekstmargin' + help: 'Margin under tekst defineret i pixels' + +logo: + label: 'Logo' + +error_correction: + label: 'Fejlkorrektionsniveau' + Low: 'Lav' + Medium: 'Medium' + Quartile: 'Mellemhøjt' + High: 'Højt' + help: 'Højere niveau giver bedre fejlkorrektion men større QR-kode'