From 2fc7ffe458b8feb5f09c1b050ac5860ae56f439f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Thu, 18 Jun 2026 15:39:59 +0200 Subject: [PATCH] feat: remove easy admin bundle, adopt admin theme --- assets/controllers/mobilenav_controller.js | 16 + assets/controllers/sidebar_menu_controller.js | 30 + assets/images/icons.svg | 244 ++++++++ assets/styles/_components.css | 9 +- assets/styles/components/button.css | 5 + assets/styles/components/datatable.css | 36 ++ composer.json | 4 +- composer.lock | 568 ++++++++++++------ config/bundles.php | 4 +- config/packages/knp_menu.yaml | 3 + config/packages/knp_paginator.yaml | 11 + config/packages/monolog.yaml | 18 +- config/packages/twig_component.yaml | 5 - config/routes/easyadmin.yaml | 3 - config/services.yaml | 8 + src/Admin/Field/JsonDataField.php | 19 - .../Admin/BookingEditDraftController.php | 107 ++++ .../Admin/BookingEditDraftCrudController.php | 101 ---- src/Controller/Admin/DashboardController.php | 44 +- src/Controller/Admin/LogController.php | 90 +++ .../Admin/LogEntryCrudController.php | 98 --- src/Controller/Admin/UserController.php | 44 ++ src/Controller/Admin/UserCrudController.php | 45 -- src/Controller/Admin/XmlDumpController.php | 80 --- src/Controller/Traits/ReturnUrlTrait.php | 71 +++ src/Entity/User.php | 3 +- src/Menu/AbstractMenuBuilder.php | 133 ++++ src/Menu/AdminMenuBuilder.php | 54 ++ src/Repository/UserRepository.php | 20 + src/Twig/AppExtension.php | 2 + src/Twig/AppRuntime.php | 20 + symfony.lock | 30 +- templates/_partials/_dropdown.html.twig | 13 + .../_json.html.twig} | 8 +- templates/_partials/_menu.html.twig | 82 +++ .../_partials/_paginator_sliding.html.twig | 69 +++ .../_paginator_sliding_boxes.html.twig | 44 ++ .../_paginator_sortable_link.html.twig | 15 + templates/account/index.html.twig | 2 +- .../admin/booking_edit_draft/index.html.twig | 87 +++ .../booking_edit_draft/modal_delete.html.twig | 7 + .../admin/booking_edit_draft/show.html.twig | 16 + templates/admin/dashboard.html.twig | 10 + templates/admin/dashboard/index.html.twig | 5 - templates/admin/log/index.html.twig | 81 +++ templates/admin/log/xml_dumps.html.twig | 61 ++ templates/admin/user/index.html.twig | 54 ++ templates/admin/xml_dump/list.html.twig | 62 -- templates/base_admin.html.twig | 22 + templates/htmx_confirmation_modal.html.twig | 25 + templates/layout_admin.html.twig | 44 ++ translations/messages.de.yaml | 5 + 52 files changed, 1947 insertions(+), 690 deletions(-) create mode 100644 assets/controllers/mobilenav_controller.js create mode 100644 assets/controllers/sidebar_menu_controller.js create mode 100644 assets/images/icons.svg create mode 100644 assets/styles/components/datatable.css create mode 100644 config/packages/knp_menu.yaml create mode 100644 config/packages/knp_paginator.yaml delete mode 100644 config/packages/twig_component.yaml delete mode 100644 config/routes/easyadmin.yaml delete mode 100644 src/Admin/Field/JsonDataField.php create mode 100644 src/Controller/Admin/BookingEditDraftController.php delete mode 100644 src/Controller/Admin/BookingEditDraftCrudController.php create mode 100644 src/Controller/Admin/LogController.php delete mode 100644 src/Controller/Admin/LogEntryCrudController.php create mode 100644 src/Controller/Admin/UserController.php delete mode 100644 src/Controller/Admin/UserCrudController.php delete mode 100644 src/Controller/Admin/XmlDumpController.php create mode 100644 src/Controller/Traits/ReturnUrlTrait.php create mode 100644 src/Menu/AbstractMenuBuilder.php create mode 100644 src/Menu/AdminMenuBuilder.php create mode 100644 src/Repository/UserRepository.php create mode 100644 templates/_partials/_dropdown.html.twig rename templates/{admin/field/json.html.twig => _partials/_json.html.twig} (63%) create mode 100644 templates/_partials/_menu.html.twig create mode 100644 templates/_partials/_paginator_sliding.html.twig create mode 100644 templates/_partials/_paginator_sliding_boxes.html.twig create mode 100644 templates/_partials/_paginator_sortable_link.html.twig create mode 100644 templates/admin/booking_edit_draft/index.html.twig create mode 100644 templates/admin/booking_edit_draft/modal_delete.html.twig create mode 100644 templates/admin/booking_edit_draft/show.html.twig create mode 100644 templates/admin/dashboard.html.twig delete mode 100644 templates/admin/dashboard/index.html.twig create mode 100644 templates/admin/log/index.html.twig create mode 100644 templates/admin/log/xml_dumps.html.twig create mode 100644 templates/admin/user/index.html.twig delete mode 100644 templates/admin/xml_dump/list.html.twig create mode 100644 templates/base_admin.html.twig create mode 100644 templates/htmx_confirmation_modal.html.twig create mode 100644 templates/layout_admin.html.twig diff --git a/assets/controllers/mobilenav_controller.js b/assets/controllers/mobilenav_controller.js new file mode 100644 index 0000000..5100bfd --- /dev/null +++ b/assets/controllers/mobilenav_controller.js @@ -0,0 +1,16 @@ +import { Controller } from '@hotwired/stimulus' + +export default class extends Controller { + static outlets = [ 'mobilenav' ] + static classes = [ 'closed' ] + + trigger({ params: { mode } }) { + this.mobilenavOutlet.toggle(mode) + } + + toggle(mode) { + // Add animation classes only now to avoid fouc + this.element.classList.add('transition-transform', 'duration-300') + this.element.classList.toggle(this.closedClass, 'close' === mode) + } +} \ No newline at end of file diff --git a/assets/controllers/sidebar_menu_controller.js b/assets/controllers/sidebar_menu_controller.js new file mode 100644 index 0000000..bbc6087 --- /dev/null +++ b/assets/controllers/sidebar_menu_controller.js @@ -0,0 +1,30 @@ +import { Controller } from '@hotwired/stimulus' + +export default class extends Controller { + + static classes = ['closed', 'iconClosed', 'iconExpanded'] + static targets = ['submenu', 'button', 'icon'] + static values = { expanded: Boolean } + + toggleSubmenu() { + this.expandedValue = !this.expandedValue + } + + expandedValueChanged(expanded) { + this.submenuTarget.classList.toggle(this.closedClass, false === expanded) + if (false === this.hasButtonTarget) { + return + } + this.buttonTarget.setAttribute('aria-expanded', expanded) + if (false === this.hasIconTarget) { + return + } + if (true === expanded) { + this.iconTarget.classList.add(...this.iconExpandedClasses) + this.iconTarget.classList.remove(...this.iconClosedClasses) + } else { + this.iconTarget.classList.add(...this.iconClosedClasses) + this.iconTarget.classList.remove(...this.iconExpandedClasses) + } + } +} \ No newline at end of file diff --git a/assets/images/icons.svg b/assets/images/icons.svg new file mode 100644 index 0000000..9e62d41 --- /dev/null +++ b/assets/images/icons.svg @@ -0,0 +1,244 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/styles/_components.css b/assets/styles/_components.css index 44a3cf9..062c292 100644 --- a/assets/styles/_components.css +++ b/assets/styles/_components.css @@ -1,6 +1,7 @@ -@import "components/typography.css"; -@import "components/forms.css"; @import "components/button.css"; -@import "components/tooltip.css"; -@import "components/toast.css"; +@import "components/datatable.css"; +@import "components/forms.css"; @import "components/pagination.css"; +@import "components/toast.css"; +@import "components/tooltip.css"; +@import "components/typography.css"; diff --git a/assets/styles/components/button.css b/assets/styles/components/button.css index 7ad73c5..2b31cb6 100644 --- a/assets/styles/components/button.css +++ b/assets/styles/components/button.css @@ -29,6 +29,11 @@ @apply hover:bg-gray-200/50; } +.button--warning { + @apply bg-none bg-red-500 text-white; + @apply hover:bg-red-500/50; +} + .sbw { .button--primary { @apply bg-none bg-sbw-secondary text-white; diff --git a/assets/styles/components/datatable.css b/assets/styles/components/datatable.css new file mode 100644 index 0000000..8cf6528 --- /dev/null +++ b/assets/styles/components/datatable.css @@ -0,0 +1,36 @@ +.data-table-wrapper { + @apply overflow-x-auto lg:overflow-x-visible mb-8; +} + +.data-table-wrapper__inner { + @apply align-middle inline-block min-w-full overflow-hidden lg:overflow-visible; +} + +table { + @apply min-w-full; +} + +tbody { + @apply bg-white; +} + +tr { + @apply border border-gray-300; +} + +th { + @apply px-4 py-2 bg-gray-200 text-left leading-4 font-bold text-primary align-top; +} + +td { + @apply px-4 py-2 leading-5 align-top; + +} + +table.horizontal td { + @apply py-4; +} + +.data-table tbody tr { + @apply hover:bg-gray-100; +} diff --git a/composer.json b/composer.json index 4bd9855..14d8f40 100644 --- a/composer.json +++ b/composer.json @@ -14,9 +14,10 @@ "doctrine/doctrine-bundle": "^2.13", "doctrine/doctrine-migrations-bundle": "^3.3", "doctrine/orm": "^3.3", - "easycorp/easyadmin-bundle": "^4.27", "fakerphp/faker": "^1.24", "flagception/flagception-bundle": "^6.1", + "knplabs/knp-menu-bundle": "^3.7", + "knplabs/knp-paginator-bundle": "^6.10", "league/flysystem-bundle": "^3.4", "league/flysystem-sftp-v3": "^3.29", "league/oauth2-server-bundle": "^1.0", @@ -62,6 +63,7 @@ "twig/extra-bundle": "^2.12|^3.0", "twig/html-extra": "^3.15", "twig/intl-extra": "*", + "twig/string-extra": "^3.24", "twig/twig": "^2.12|^3.0", "zenstruck/schedule-bundle": "^1.8" }, diff --git a/composer.lock b/composer.lock index 46456ae..c1b420e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2fd76853483361829d0bdb4d8d68c7ef", + "content-hash": "aa69dee5968ec6f7c3f152b8a8a6100d", "packages": [ { "name": "carbonphp/carbon-doctrine-types", @@ -1405,111 +1405,6 @@ ], "time": "2025-10-31T18:51:33+00:00" }, - { - "name": "easycorp/easyadmin-bundle", - "version": "v4.29.12", - "source": { - "type": "git", - "url": "https://github.com/EasyCorp/EasyAdminBundle.git", - "reference": "c6e8111f8215d5ba5ae7c7ec23462d1a56ff41b2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/EasyCorp/EasyAdminBundle/zipball/c6e8111f8215d5ba5ae7c7ec23462d1a56ff41b2", - "reference": "c6e8111f8215d5ba5ae7c7ec23462d1a56ff41b2", - "shasum": "" - }, - "require": { - "doctrine/doctrine-bundle": "^2.5|^3.0", - "doctrine/orm": "^2.12|^3.0", - "php": ">=8.1", - "symfony/asset": "^5.4|^6.0|^7.0|^8.0", - "symfony/cache": "^5.4|^6.0|^7.0|^8.0", - "symfony/config": "^5.4|^6.0|^7.0|^8.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0|^8.0", - "symfony/deprecation-contracts": "^3.0", - "symfony/doctrine-bridge": "^5.4|^6.0|^7.0|^8.0", - "symfony/event-dispatcher": "^5.4|^6.0|^7.0|^8.0", - "symfony/filesystem": "^5.4|^6.0|^7.0|^8.0", - "symfony/form": "^5.4|^6.0|^7.0|^8.0", - "symfony/framework-bundle": "^5.4|^6.0|^7.0|^8.0", - "symfony/http-foundation": "^5.4|^6.0|^7.0|^8.0", - "symfony/http-kernel": "^5.4|^6.0|^7.0|^8.0", - "symfony/intl": "^5.4|^6.0|^7.0|^8.0", - "symfony/property-access": "^5.4|^6.0|^7.0|^8.0", - "symfony/security-bundle": "^5.4|^6.0|^7.0|^8.0", - "symfony/string": "^5.4|^6.0|^7.0|^8.0", - "symfony/translation": "^5.4|^6.0|^7.0|^8.0", - "symfony/twig-bridge": "^5.4.48|^6.4.16|^7.1.9|^8.0", - "symfony/twig-bundle": "^5.4|^6.0|^7.0|^8.0", - "symfony/uid": "^5.4|^6.0|^7.0|^8.0", - "symfony/ux-twig-component": "^2.21", - "symfony/validator": "^5.4|^6.0|^7.0|^8.0", - "twig/extra-bundle": "^3.17", - "twig/html-extra": "^3.17", - "twig/twig": "^3.20" - }, - "conflict": { - "symfony/error-handler": "<5.4.35" - }, - "require-dev": { - "doctrine/doctrine-fixtures-bundle": "^3.4|3.5.x-dev|^4.0", - "phpstan/extension-installer": "^1.4", - "phpstan/phpstan": "^2.0", - "phpstan/phpstan-phpunit": "^2.0", - "phpstan/phpstan-strict-rules": "^2.0", - "phpstan/phpstan-symfony": "^2.0", - "psr/log": "^1.0|^2.0|^3.0", - "symfony/browser-kit": "^5.4|^6.0|^7.0|^8.0", - "symfony/css-selector": "^5.4|^6.0|^7.0|^8.0", - "symfony/debug-bundle": "^5.4|^6.0|^7.0|^8.0", - "symfony/dom-crawler": "^5.4|^6.0|^7.0|^8.0", - "symfony/expression-language": "^5.4|^6.0|^7.0|^8.0", - "symfony/phpunit-bridge": "^6.1|^7.0|^8.0", - "symfony/process": "^5.4|^6.0|^7.0|^8.0", - "symfony/web-link": "^5.4|^6.0|^7.0|^8.0", - "vincentlanglet/twig-cs-fixer": "^3.10" - }, - "type": "symfony-bundle", - "extra": { - "branch-alias": { - "dev-master": "4.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "EasyCorp\\Bundle\\EasyAdminBundle\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Project Contributors", - "homepage": "https://github.com/EasyCorp/EasyAdminBundle/graphs/contributors" - } - ], - "description": "Admin generator for Symfony applications", - "homepage": "https://github.com/EasyCorp/EasyAdminBundle", - "keywords": [ - "admin", - "backend", - "generator" - ], - "support": { - "issues": "https://github.com/EasyCorp/EasyAdminBundle/issues", - "source": "https://github.com/EasyCorp/EasyAdminBundle/tree/v4.29.12" - }, - "funding": [ - { - "url": "https://github.com/javiereguiluz", - "type": "github" - } - ], - "time": "2026-06-03T19:31:07+00:00" - }, { "name": "egulias/email-validator", "version": "4.0.4", @@ -1773,6 +1668,311 @@ }, "time": "2026-06-01T10:02:14+00:00" }, + { + "name": "knplabs/knp-components", + "version": "v5.2.0", + "source": { + "type": "git", + "url": "https://github.com/KnpLabs/knp-components.git", + "reference": "eabf39263fff305c0024820c3736e5b03e7edf50" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/KnpLabs/knp-components/zipball/eabf39263fff305c0024820c3736e5b03e7edf50", + "reference": "eabf39263fff305c0024820c3736e5b03e7edf50", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/event-dispatcher-contracts": "^3.0" + }, + "conflict": { + "doctrine/dbal": "<3.8" + }, + "require-dev": { + "doctrine/dbal": "^3.8 || ^4.0", + "doctrine/mongodb-odm": "^2.5.5", + "doctrine/orm": "^2.13 || ^3.0", + "doctrine/phpcr-odm": "^1.8 || ^2.0", + "ext-pdo_sqlite": "*", + "jackalope/jackalope-doctrine-dbal": "^1.12 || ^2.0", + "phpunit/phpunit": "^10.5 || ^11.3", + "propel/propel1": "^1.7", + "ruflin/elastica": "^7.0", + "solarium/solarium": "^6.0", + "symfony/http-foundation": "^5.4.38 || ^6.4.4 || ^7.0", + "symfony/http-kernel": "^5.4.38 || ^6.4.4 || ^7.0", + "symfony/property-access": "^5.4.38 || ^6.4.4 || ^7.0" + }, + "suggest": { + "doctrine/common": "to allow usage pagination with Doctrine ArrayCollection", + "doctrine/mongodb-odm": "to allow usage pagination with Doctrine ODM MongoDB", + "doctrine/orm": "to allow usage pagination with Doctrine ORM", + "doctrine/phpcr-odm": "to allow usage pagination with Doctrine ODM PHPCR", + "propel/propel1": "to allow usage pagination with Propel ORM", + "ruflin/elastica": "to allow usage pagination with ElasticSearch Client", + "solarium/solarium": "to allow usage pagination with Solarium Client", + "symfony/http-foundation": "to retrieve arguments from Request", + "symfony/property-access": "to allow sorting arrays" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "Knp\\Component\\": "src/Knp/Component" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KnpLabs Team", + "homepage": "https://knplabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://github.com/KnpLabs/knp-components/contributors" + } + ], + "description": "Knplabs component library", + "homepage": "https://github.com/KnpLabs/knp-components", + "keywords": [ + "components", + "knp", + "knplabs", + "pager", + "paginator" + ], + "support": { + "issues": "https://github.com/KnpLabs/knp-components/issues", + "source": "https://github.com/KnpLabs/knp-components/tree/v5.2.0" + }, + "time": "2025-03-20T07:35:37+00:00" + }, + { + "name": "knplabs/knp-menu", + "version": "v3.8.0", + "source": { + "type": "git", + "url": "https://github.com/KnpLabs/KnpMenu.git", + "reference": "79d325909a1d428a93f1a0f55e90177830e283bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/KnpLabs/KnpMenu/zipball/79d325909a1d428a93f1a0f55e90177830e283bb", + "reference": "79d325909a1d428a93f1a0f55e90177830e283bb", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "symfony/http-foundation": "<5.4", + "twig/twig": "<2.16" + }, + "require-dev": { + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^9.6", + "psr/container": "^1.0 || ^2.0", + "symfony/http-foundation": "^5.4 || ^6.0 || ^7.0", + "symfony/phpunit-bridge": "^7.0", + "symfony/routing": "^5.4 || ^6.0 || ^7.0", + "twig/twig": "^2.16 || ^3.0" + }, + "suggest": { + "twig/twig": "for the TwigRenderer and the integration with your templates" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Knp\\Menu\\": "src/Knp/Menu" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KnpLabs", + "homepage": "https://knplabs.com" + }, + { + "name": "Christophe Coevoet", + "email": "stof@notk.org" + }, + { + "name": "The Community", + "homepage": "https://github.com/KnpLabs/KnpMenu/contributors" + } + ], + "description": "An object oriented menu library", + "homepage": "https://knplabs.com", + "keywords": [ + "menu", + "tree" + ], + "support": { + "issues": "https://github.com/KnpLabs/KnpMenu/issues", + "source": "https://github.com/KnpLabs/KnpMenu/tree/v3.8.0" + }, + "time": "2025-06-13T15:03:33+00:00" + }, + { + "name": "knplabs/knp-menu-bundle", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/KnpLabs/KnpMenuBundle.git", + "reference": "aa22e57f8f41c34ad5e382aae4d0c12998c0eb5a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/KnpLabs/KnpMenuBundle/zipball/aa22e57f8f41c34ad5e382aae4d0c12998c0eb5a", + "reference": "aa22e57f8f41c34ad5e382aae4d0c12998c0eb5a", + "shasum": "" + }, + "require": { + "knplabs/knp-menu": "^3.8", + "php": "^8.1", + "symfony/config": "^6.4 | ^7.0 | ^8.0", + "symfony/dependency-injection": "^6.4 | ^7.0 | ^8.0", + "symfony/deprecation-contracts": "^2.5 | ^3.3", + "symfony/http-kernel": "^6.4 | ^7.0 | ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5 | ^11.5 | ^12.4", + "symfony/expression-language": "^6.4 | ^7.0 | ^8.0", + "symfony/phpunit-bridge": "^7.0 | ^8.0", + "symfony/templating": "^6.4 | ^7.0 | ^8.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Knp\\Bundle\\MenuBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Knplabs", + "homepage": "http://knplabs.com" + }, + { + "name": "Christophe Coevoet", + "email": "stof@notk.org" + }, + { + "name": "Symfony Community", + "homepage": "https://github.com/KnpLabs/KnpMenuBundle/contributors" + } + ], + "description": "This bundle provides an integration of the KnpMenu library", + "keywords": [ + "menu" + ], + "support": { + "issues": "https://github.com/KnpLabs/KnpMenuBundle/issues", + "source": "https://github.com/KnpLabs/KnpMenuBundle/tree/v3.7.0" + }, + "time": "2025-11-30T08:30:04+00:00" + }, + { + "name": "knplabs/knp-paginator-bundle", + "version": "v6.10.0", + "source": { + "type": "git", + "url": "https://github.com/KnpLabs/KnpPaginatorBundle.git", + "reference": "8d41f8ed47d880f8fa569389ffa4fecfbc5b8d41" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/KnpLabs/KnpPaginatorBundle/zipball/8d41f8ed47d880f8fa569389ffa4fecfbc5b8d41", + "reference": "8d41f8ed47d880f8fa569389ffa4fecfbc5b8d41", + "shasum": "" + }, + "require": { + "knplabs/knp-components": "^4.4 || ^5.0", + "php": "^8.1", + "symfony/config": "^6.4 || ^7.0 || ^8.0", + "symfony/dependency-injection": "^6.4 || ^7.0 || ^8.0", + "symfony/event-dispatcher": "^6.4 || ^7.0 || ^8.0", + "symfony/http-foundation": "^6.4 || ^7.0 || ^8.0", + "symfony/http-kernel": "^6.4 || ^7.0 || ^8.0", + "symfony/routing": "^6.4 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.12", + "phpunit/phpunit": "^10.5 || ^11.5 || ^12.2", + "symfony/templating": "^6.4 || ^7.0 || ^8.0", + "symfony/translation": "^6.4 || ^7.0 || ^8.0", + "twig/twig": "^3.0" + }, + "suggest": { + "symfony/translation": "To use the templates", + "twig/twig": "To use the templates" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, + "autoload": { + "psr-4": { + "Knp\\Bundle\\PaginatorBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KnpLabs Team", + "homepage": "https://knplabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://github.com/KnpLabs/KnpPaginatorBundle/contributors" + } + ], + "description": "Paginator bundle for Symfony to automate pagination and simplify sorting and other features", + "homepage": "https://github.com/KnpLabs/KnpPaginatorBundle", + "keywords": [ + "bundle", + "knp", + "knplabs", + "pager", + "pagination", + "paginator", + "symfony" + ], + "support": { + "issues": "https://github.com/KnpLabs/KnpPaginatorBundle/issues", + "source": "https://github.com/KnpLabs/KnpPaginatorBundle/tree/v6.10.0" + }, + "time": "2025-11-29T09:14:09+00:00" + }, { "name": "lcobucci/clock", "version": "3.5.0", @@ -9944,95 +10144,6 @@ ], "time": "2025-12-23T15:07:59+00:00" }, - { - "name": "symfony/ux-twig-component", - "version": "v2.36.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/ux-twig-component.git", - "reference": "d64b068d8339e905cd48974bdd6e9ba54dc8f247" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/ux-twig-component/zipball/d64b068d8339e905cd48974bdd6e9ba54dc8f247", - "reference": "d64b068d8339e905cd48974bdd6e9ba54dc8f247", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "symfony/dependency-injection": "^5.4|^6.0|^7.0|^8.0", - "symfony/deprecation-contracts": "^2.2|^3.0", - "symfony/event-dispatcher": "^5.4|^6.0|^7.0|^8.0", - "symfony/property-access": "^5.4|^6.0|^7.0|^8.0", - "twig/twig": "^3.10.3" - }, - "conflict": { - "symfony/config": "<5.4.0" - }, - "require-dev": { - "symfony/console": "^5.4|^6.0|^7.0|^8.0", - "symfony/css-selector": "^5.4|^6.0|^7.0|^8.0", - "symfony/dom-crawler": "^5.4|^6.0|^7.0|^8.0", - "symfony/framework-bundle": "^5.4|^6.0|^7.0|^8.0", - "symfony/phpunit-bridge": "^6.0|^7.0|^8.0", - "symfony/stimulus-bundle": "^2.9.1|^3.0", - "symfony/twig-bundle": "^5.4|^6.0|^7.0|^8.0", - "symfony/webpack-encore-bundle": "^1.15|^2.3.0", - "twig/extra-bundle": "^3.10.3", - "twig/html-extra": "^3.10.3" - }, - "type": "symfony-bundle", - "extra": { - "thanks": { - "url": "https://github.com/symfony/ux", - "name": "symfony/ux" - } - }, - "autoload": { - "psr-4": { - "Symfony\\UX\\TwigComponent\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Twig components for Symfony", - "homepage": "https://symfony.com", - "keywords": [ - "components", - "symfony-ux", - "twig" - ], - "support": { - "source": "https://github.com/symfony/ux-twig-component/tree/v2.36.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-04-03T05:13:59+00:00" - }, { "name": "symfony/validator", "version": "v6.4.37", @@ -10748,6 +10859,73 @@ ], "time": "2026-05-19T20:44:48+00:00" }, + { + "name": "twig/string-extra", + "version": "v3.24.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/string-extra.git", + "reference": "6ec8f2e8ca9b2193221a02cb599dc92c36384368" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/string-extra/zipball/6ec8f2e8ca9b2193221a02cb599dc92c36384368", + "reference": "6ec8f2e8ca9b2193221a02cb599dc92c36384368", + "shasum": "" + }, + "require": { + "php": ">=8.1.0", + "symfony/string": "^5.4|^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^1.1|^2|^3", + "twig/twig": "^3.13|^4.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Twig\\Extra\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + } + ], + "description": "A Twig extension for Symfony String", + "homepage": "https://twig.symfony.com", + "keywords": [ + "html", + "string", + "twig", + "unicode" + ], + "support": { + "source": "https://github.com/twigphp/string-extra/tree/v3.24.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2025-12-02T14:45:16+00:00" + }, { "name": "twig/twig", "version": "v3.27.1", diff --git a/config/bundles.php b/config/bundles.php index 1bf116d..de9164d 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -16,7 +16,7 @@ return [ League\FlysystemBundle\FlysystemBundle::class => ['all' => true], League\Bundle\OAuth2ServerBundle\LeagueOAuth2ServerBundle::class => ['all' => true], Zenstruck\ScheduleBundle\ZenstruckScheduleBundle::class => ['all' => true], - Symfony\UX\TwigComponent\TwigComponentBundle::class => ['all' => true], - EasyCorp\Bundle\EasyAdminBundle\EasyAdminBundle::class => ['all' => true], Flagception\Bundle\FlagceptionBundle\FlagceptionBundle::class => ['all' => true], + Knp\Bundle\MenuBundle\KnpMenuBundle::class => ['all' => true], + Knp\Bundle\PaginatorBundle\KnpPaginatorBundle::class => ['all' => true], ]; diff --git a/config/packages/knp_menu.yaml b/config/packages/knp_menu.yaml new file mode 100644 index 0000000..6ecd9e5 --- /dev/null +++ b/config/packages/knp_menu.yaml @@ -0,0 +1,3 @@ +knp_menu: + twig: + template: _partials/_menu.html.twig diff --git a/config/packages/knp_paginator.yaml b/config/packages/knp_paginator.yaml new file mode 100644 index 0000000..5b9979f --- /dev/null +++ b/config/packages/knp_paginator.yaml @@ -0,0 +1,11 @@ +knp_paginator: + default_options: + page_name: page + sort_field_name: sort + sort_direction_name: direction + distinct: true + filter_field_name: filterField + filter_value_name: filterValue + template: + pagination: '_partials/_paginator_sliding.html.twig' + sortable: '_partials/_paginator_sortable_link.html.twig' diff --git a/config/packages/monolog.yaml b/config/packages/monolog.yaml index d8c2746..71ea5d9 100644 --- a/config/packages/monolog.yaml +++ b/config/packages/monolog.yaml @@ -4,6 +4,7 @@ monolog: - bpn - core - auth + - admin when@dev: monolog: @@ -27,11 +28,16 @@ when@dev: path: "%kernel.logs_dir%/%kernel.environment%.core.log" level: debug channels: ["core"] + admin: + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.admin.log" + level: debug + channels: ["admin"] main: type: stream path: "%kernel.logs_dir%/%kernel.environment%.log" level: debug - channels: ["!event", "!bpn", "!core", "!auth"] + channels: ["!event", "!bpn", "!core", "!auth", "!admin"] # uncomment to get logging in your browser # you may have to allow bigger header sizes in your Web server configuration #firephp: @@ -84,13 +90,19 @@ when@prod: &prod_config path: "%kernel.logs_dir%/%kernel.environment%.core.log" level: info channels: ["core"] + admin: + type: rotating_file + max_files: 7 + path: "%kernel.logs_dir%/%kernel.environment%.admin.log" + level: info + channels: ["admin"] main: type: fingers_crossed action_level: error handler: nested excluded_http_codes: [404, 405] buffer_size: 50 # How many messages should be saved? Prevent memory leaks - channels: ["!bpn", "!core", "!auth"] + channels: ["!bpn", "!core", "!auth", "!admin"] nested: type: rotating_file path: "%kernel.logs_dir%/%kernel.environment%.framework.log" @@ -106,4 +118,4 @@ when@prod: &prod_config path: php://stderr formatter: monolog.formatter.json -when@staging: *prod_config \ No newline at end of file +when@staging: *prod_config diff --git a/config/packages/twig_component.yaml b/config/packages/twig_component.yaml deleted file mode 100644 index fd17ac6..0000000 --- a/config/packages/twig_component.yaml +++ /dev/null @@ -1,5 +0,0 @@ -twig_component: - anonymous_template_directory: 'components/' - defaults: - # Namespace & directory for components - App\Twig\Components\: 'components/' diff --git a/config/routes/easyadmin.yaml b/config/routes/easyadmin.yaml deleted file mode 100644 index 083ca15..0000000 --- a/config/routes/easyadmin.yaml +++ /dev/null @@ -1,3 +0,0 @@ -easyadmin: - resource: . - type: easyadmin.routes diff --git a/config/services.yaml b/config/services.yaml index 9066e3b..28876ee 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -108,6 +108,14 @@ services: arguments: $path: '%path_to_keys%' + App\Menu\AdminMenuBuilder: + arguments: + $factory: '@knp_menu.factory' + tags: + - name: knp_menu.menu_builder + method: createMainMenu + alias: admin_main + App\Service\BpnXmlSyncManager: arguments: $xmlSource: '@xml_source.storage' diff --git a/src/Admin/Field/JsonDataField.php b/src/Admin/Field/JsonDataField.php deleted file mode 100644 index 1df8e10..0000000 --- a/src/Admin/Field/JsonDataField.php +++ /dev/null @@ -1,19 +0,0 @@ -setProperty($propertyName) - ->setLabel($label) - ->setTemplatePath('admin/field/json.html.twig'); - } -} diff --git a/src/Controller/Admin/BookingEditDraftController.php b/src/Controller/Admin/BookingEditDraftController.php new file mode 100644 index 0000000..b9e6349 --- /dev/null +++ b/src/Controller/Admin/BookingEditDraftController.php @@ -0,0 +1,107 @@ +bookingEditDraftRepository + ->createQueryBuilder('booking_edit_draft') + ->leftJoin('booking_edit_draft.user', 'user') + ; + + $pagination = $this->paginator->paginate( + $qb, + $request->query->getInt('page', 1), + $request->query->getInt('limit', 20), + [ + 'defaultSortFieldName' => 'booking_edit_draft.createdAt', + 'defaultSortDirection' => 'DESC', + ] + ); + + return $this->render('admin/booking_edit_draft/index.html.twig', [ + 'pagination' => $pagination, + ]); + } + + #[Route('/admin/booking-edit-draft/{id}/show', name: 'app_admin_bookingeditdraft_show')] + public function show(BookingEditDraft $draft, Request $request): Response + { + $returnUrl = $this->getReturnUrl($request, 'app_admin_bookingeditdraft'); + + return $this->render('admin/booking_edit_draft/show.html.twig', [ + 'draft' => $draft, + 'returnUrl' => $returnUrl, + ]); + } + + #[Route('/admin/booking-edit-draft/{id}/delete', name: 'app_admin_bookingeditdraft_delete')] + public function delete(BookingEditDraft $draft, Request $request): Response + { + if (true === $request->isMethod(Request::METHOD_POST)) { + $this->entityManager->remove($draft); + $this->entityManager->flush(); + + $this->adminLogger->info('Delete booking edit draft', [ + 'booking_number' => $draft->getBookingNumber(), + ]); + $this->addFlash('success', 'Der Buchungsentwurf wurde gelöscht'); + + $returnUrl = $this->getReturnUrl($request, 'app_admin_bookingeditdraft'); + + return new HxRedirectResponse($returnUrl); + } + + return $this->render('admin/booking_edit_draft/modal_delete.html.twig', [ + 'draft' => $draft, + ]); + } + + #[Route('/admin/booking-edit-draft/{id}/export', name: 'app_admin_bookingeditdraft_export')] + public function export(BookingEditDraft $draft): Response + { + if (false === $draft->hasExportData()) { + $this->addFlash('warning', 'Der Export ist fehlgeschlagen'); + + return $this->redirectToRoute('app_admin_bookingeditdraft'); + } + + try { + return $this->bookingExporter->createExportResponse($draft); + } catch (\RuntimeException $e) { + $this->addFlash('warning', 'Der Export ist fehlgeschlagen: '.$e->getMessage()); + + return $this->redirectToRoute('app_admin_bookingeditdraft'); + } + } +} diff --git a/src/Controller/Admin/BookingEditDraftCrudController.php b/src/Controller/Admin/BookingEditDraftCrudController.php deleted file mode 100644 index 8c922fa..0000000 --- a/src/Controller/Admin/BookingEditDraftCrudController.php +++ /dev/null @@ -1,101 +0,0 @@ - */ -class BookingEditDraftCrudController extends AbstractCrudController -{ - public function __construct( - private readonly BookingExporter $exportService, - private readonly AdminUrlGenerator $adminUrlGenerator, - ) { - } - - public static function getEntityFqcn(): string - { - return BookingEditDraft::class; - } - - public function configureActions(Actions $actions): Actions - { - $exportAction = Action::new('exportExcel', 'Excel Export', 'fa fa-file-excel') - ->linkToRoute( - 'admin_booking_draft_export', - static fn (BookingEditDraft $entity): array => ['id' => $entity->getId()] - ) - ->displayIf(static fn (BookingEditDraft $entity): bool => $entity->hasExportData()); - - return $actions - ->add(Crud::PAGE_INDEX, Action::DETAIL) - ->add(Crud::PAGE_INDEX, $exportAction) - ->add(Crud::PAGE_DETAIL, $exportAction) - ->remove(Crud::PAGE_INDEX, Action::NEW) - ->remove(Crud::PAGE_INDEX, Action::EDIT) - ->remove(Crud::PAGE_DETAIL, Action::EDIT) - ; - } - - #[Route('/admin/booking-draft/{id}/export', name: 'admin_booking_draft_export', requirements: ['id' => '\d+'])] - public function export(BookingEditDraft $draft): Response - { - if (false === $draft->hasExportData()) { - $this->addFlash('danger', 'Export nicht möglich: Reisedaten fehlen'); - - $url = $this->adminUrlGenerator - ->setController(self::class) - ->setAction(Action::INDEX) - ->generateUrl(); - - return $this->redirect($url); - } - - try { - return $this->exportService->createExportResponse($draft); - } catch (\RuntimeException $e) { - $this->addFlash('danger', 'Export fehlgeschlagen: '.$e->getMessage()); - - $url = $this->adminUrlGenerator - ->setController(self::class) - ->setAction(Action::INDEX) - ->generateUrl(); - - return $this->redirect($url); - } - } - - public function configureCrud(Crud $crud): Crud - { - return $crud - ->setEntityLabelInSingular('Buchungsentwurf') - ->setEntityLabelInPlural('Buchungsentwürfe') - ->setDefaultSort(['createdAt' => 'DESC']); - } - - public function configureFields(string $pageName): iterable - { - return [ - IntegerField::new('bookingNumber', 'Vorgang'), - AssociationField::new('user', 'Kundenaccount')->formatValue(fn (User $user) => $user->getEmail()), - DateTimeField::new('createdAt', 'erstellt am'), - DateTimeField::new('updatedAt', 'aktualisiert am'), - JsonDataField::new('formData', 'Daten')->onlyOnDetail(), - ]; - } -} diff --git a/src/Controller/Admin/DashboardController.php b/src/Controller/Admin/DashboardController.php index 6e3319f..513cc44 100644 --- a/src/Controller/Admin/DashboardController.php +++ b/src/Controller/Admin/DashboardController.php @@ -1,48 +1,18 @@ render('admin/dashboard/index.html.twig'); - } - - public function configureDashboard(): Dashboard - { - return Dashboard::new() - ->setTitle('MyE&P') - ->setLocales(['de']) - ->setFaviconPath('build/favicon/icon.svg') - ; - } - - public function configureUserMenu(UserInterface $user): UserMenu - { - return parent::configureUserMenu($user) - ->addMenuItems([ - MenuItem::linkToRoute('MyE&P', 'fa fa-user', 'app_account'), - ]); - } - - public function configureMenuItems(): iterable - { - yield MenuItem::linkToDashboard('Dashboard', 'fa fa-home'); - yield MenuItem::linkToCrud('Log', 'fa fa-list', LogEntry::class); - yield MenuItem::linkToCrud('Buchungsentwürfe', 'fa fa-pen-to-square', BookingEditDraft::class); - yield MenuItem::linkToCrud('Benutzeraccounts', 'fa fa-users', User::class); + return $this->render('admin/dashboard.html.twig'); } } diff --git a/src/Controller/Admin/LogController.php b/src/Controller/Admin/LogController.php new file mode 100644 index 0000000..95bfe4d --- /dev/null +++ b/src/Controller/Admin/LogController.php @@ -0,0 +1,90 @@ +logEntryRepository + ->createQueryBuilder('log_entry') + ; + + $pagination = $this->paginator->paginate( + $qb, + $request->query->getInt('page', 1), + $request->query->getInt('limit', 50), + [ + 'defaultSortFieldName' => 'log_entry.createdAt', + 'defaultSortDirection' => 'DESC', + ] + ); + + return $this->render('admin/log/index.html.twig', [ + 'pagination' => $pagination, + ]); + } + + #[Route('/admin/log/{id}/xml-dumps', name: 'app_admin_log_xmldumps')] + public function xmlDumps(LogEntry $logEntry): Response + { + $dumps = []; + try { + $dumps = $this->xmlDumpReader->findDumpsForRequestId($logEntry->getRequestId()); + } catch (FilesystemException) { + } + + return $this->render('admin/log/xml_dumps.html.twig', [ + 'logEntry' => $logEntry, + 'dumps' => $dumps, + ]); + } + + #[Route('/admin/log/download/{filename}', name: 'app_admin_log_xmldump_download', requirements: ['filename' => '.+'])] + public function downloadXmlDump(string $filename): Response + { + try { + if (false === $this->xmlDumpReader->fileExists($filename)) { + throw $this->createNotFoundException('Dump file not found. It may have been cleaned up.'); + } + + $content = $this->xmlDumpReader->getContent($filename); + } catch (FilesystemException $e) { + throw $this->createNotFoundException('Failed to read dump file: '.$e->getMessage()); + } + + $response = new Response($content); + $response->headers->set('Content-Type', 'application/xml'); + + $disposition = $response->headers->makeDisposition( + ResponseHeaderBag::DISPOSITION_ATTACHMENT, + basename($filename) + ); + $response->headers->set('Content-Disposition', $disposition); + + return $response; + } +} diff --git a/src/Controller/Admin/LogEntryCrudController.php b/src/Controller/Admin/LogEntryCrudController.php deleted file mode 100644 index 277aa19..0000000 --- a/src/Controller/Admin/LogEntryCrudController.php +++ /dev/null @@ -1,98 +0,0 @@ - */ -class LogEntryCrudController extends AbstractCrudController -{ - public function __construct( - private readonly LogEntryRepository $logEntryRepository, - ) { - } - - public static function getEntityFqcn(): string - { - return LogEntry::class; - } - - public function configureActions(Actions $actions): Actions - { - $viewDumps = Action::new('viewDumps', 'XML Dumps', 'fa fa-file-code') - ->linkToCrudAction('xmlDumps') - ->displayIf(static fn (LogEntry $entity): bool => 'bpn' === $entity->getChannel()); - - return $actions - ->add(Crud::PAGE_INDEX, Action::DETAIL) - ->add(Crud::PAGE_INDEX, $viewDumps) - ->add(Crud::PAGE_DETAIL, $viewDumps) - ->remove(Crud::PAGE_INDEX, Action::NEW) - ->remove(Crud::PAGE_INDEX, Action::EDIT) - ->remove(Crud::PAGE_INDEX, Action::DELETE) - ->remove(Crud::PAGE_DETAIL, Action::EDIT) - ->remove(Crud::PAGE_DETAIL, Action::DELETE) - ; - } - - public function configureCrud(Crud $crud): Crud - { - return $crud - ->setEntityLabelInSingular('Logeintrag') - ->setEntityLabelInPlural('Logeinträge') - ->setDefaultSort(['createdAt' => 'DESC']) - ->setSearchFields(['errorCode', 'message', 'channel', 'extra']); - } - - public function configureFields(string $pageName): iterable - { - yield DateTimeField::new('createdAt', 'Zeitstempel'); - yield TextField::new('errorCode', 'Fehlercode') - ->formatValue(static fn (?string $value): string => $value ?? '-'); - yield TextField::new('username', 'Benutzer'); - yield TextField::new('message', 'Aktion'); - yield TextField::new('requestId', 'Request ID'); - yield TextField::new('uri', 'URI'); - yield JsonDataField::new('context', 'Kontext')->onlyOnDetail(); - yield JsonDataField::new('extra', 'Extra')->onlyOnDetail(); - } - - public function xmlDumps(Request $request, XmlDumpReader $xmlDumpService): Response - { - $entityId = $request->query->getInt('entityId'); - if ($entityId <= 0) { - throw $this->createNotFoundException('Log entry not found.'); - } - - $entity = $this->logEntryRepository->find($entityId); - if (!$entity instanceof LogEntry) { - throw $this->createNotFoundException('Log entry not found.'); - } - - $dumps = []; - try { - $dumps = $xmlDumpService->findDumpsForRequestId($entity->getRequestId()); - } catch (FilesystemException) { - } - - return $this->render('admin/xml_dump/list.html.twig', [ - 'logEntry' => $entity, - 'requestId' => $entity->getRequestId(), - 'dumps' => $dumps, - ]); - } -} diff --git a/src/Controller/Admin/UserController.php b/src/Controller/Admin/UserController.php new file mode 100644 index 0000000..385a387 --- /dev/null +++ b/src/Controller/Admin/UserController.php @@ -0,0 +1,44 @@ +userRepository + ->createQueryBuilder('user') + ; + + $pagination = $this->paginator->paginate( + $qb, + $request->query->getInt('page', 1), + $request->query->getInt('limit', 50), + [ + 'defaultSortFieldName' => 'user.lastLoginAt', + 'defaultSortDirection' => 'DESC', + ] + ); + + return $this->render('admin/user/index.html.twig', [ + 'pagination' => $pagination, + ]); + } +} diff --git a/src/Controller/Admin/UserCrudController.php b/src/Controller/Admin/UserCrudController.php deleted file mode 100644 index d23754e..0000000 --- a/src/Controller/Admin/UserCrudController.php +++ /dev/null @@ -1,45 +0,0 @@ - */ -class UserCrudController extends AbstractCrudController -{ - public static function getEntityFqcn(): string - { - return User::class; - } - - public function configureActions(Actions $actions): Actions - { - return $actions - ->remove(Crud::PAGE_INDEX, Action::NEW) - ->remove(Crud::PAGE_INDEX, Action::EDIT) - ->remove(Crud::PAGE_INDEX, Action::DELETE) - ; - } - - public function configureCrud(Crud $crud): Crud - { - return $crud - ->setEntityLabelInSingular('Benutzeraccount') - ->setEntityLabelInPlural('Benutzeraccounts') - ->setDefaultSort(['lastLoginAt' => 'DESC']); - } - - public function configureFields(string $pageName): iterable - { - return [ - TextField::new('email', 'E-Mail'), - DateTimeField::new('lastLoginAt', 'letzter Login'), - ]; - } -} diff --git a/src/Controller/Admin/XmlDumpController.php b/src/Controller/Admin/XmlDumpController.php deleted file mode 100644 index 1f61030..0000000 --- a/src/Controller/Admin/XmlDumpController.php +++ /dev/null @@ -1,80 +0,0 @@ -logEntryRepository->find($id); - - if (null === $logEntry) { - throw new NotFoundHttpException('Log entry not found.'); - } - - if ('bpn' !== $logEntry->getChannel()) { - throw new NotFoundHttpException('XML dumps are only available for BPN channel entries.'); - } - - $requestId = $logEntry->getRequestId(); - $dumps = []; - - try { - $dumps = $this->xmlDumpService->findDumpsForRequestId($requestId); - } catch (FilesystemException) { - // Dumps directory may not exist or be inaccessible - } - - return $this->render('admin/xml_dump/list.html.twig', [ - 'logEntry' => $logEntry, - 'requestId' => $requestId, - 'dumps' => $dumps, - ]); - } - - #[Route('/download/{filename}', name: 'admin_xml_dump_download', methods: ['GET'], requirements: ['filename' => '.+'])] - public function download(string $filename): Response - { - try { - if (false === $this->xmlDumpService->fileExists($filename)) { - throw new NotFoundHttpException('Dump file not found. It may have been cleaned up.'); - } - - $content = $this->xmlDumpService->getContent($filename); - } catch (FilesystemException $e) { - throw new NotFoundHttpException('Failed to read dump file: '.$e->getMessage()); - } - - $response = new Response($content); - $response->headers->set('Content-Type', 'application/xml'); - - $disposition = $response->headers->makeDisposition( - ResponseHeaderBag::DISPOSITION_ATTACHMENT, - basename($filename) - ); - $response->headers->set('Content-Disposition', $disposition); - - return $response; - } -} diff --git a/src/Controller/Traits/ReturnUrlTrait.php b/src/Controller/Traits/ReturnUrlTrait.php new file mode 100644 index 0000000..85efaca --- /dev/null +++ b/src/Controller/Traits/ReturnUrlTrait.php @@ -0,0 +1,71 @@ +generateUrl($defaultRoute, $parameters); + + return rawurldecode($request->query->get('r', $defaultUrl)); + } + + /** + * Remove specified query parameters from a URL. + * + * @param string $url The URL to process + * @param string[] $parametersKeys Array of query parameter keys to remove + * + * @return string The URL without the specified query parameters + */ + public function removeQueryParameters(string $url, array $parametersKeys): string + { + if ([] === $parametersKeys) { + return $url; + } + + $urlParts = parse_url($url); + + if (false === isset($urlParts['query'])) { + return $url; + } + + parse_str($urlParts['query'], $queryParams); + + foreach ($parametersKeys as $key) { + unset($queryParams[$key]); + } + + $urlParts['query'] = http_build_query($queryParams); + + if ('' === $urlParts['query']) { + unset($urlParts['query']); + } + + // Rebuild URL + $result = ''; + if (isset($urlParts['scheme'])) { + $result .= $urlParts['scheme'].'://'; + } + if (isset($urlParts['host'])) { + $result .= $urlParts['host']; + } + if (isset($urlParts['port'])) { + $result .= ':'.$urlParts['port']; + } + if (isset($urlParts['path'])) { + $result .= $urlParts['path']; + } + if (isset($urlParts['query'])) { + $result .= '?'.$urlParts['query']; + } + if (isset($urlParts['fragment'])) { + $result .= '#'.$urlParts['fragment']; + } + + return $result; + } +} diff --git a/src/Entity/User.php b/src/Entity/User.php index 42b58ef..425452d 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -2,11 +2,12 @@ namespace App\Entity; +use App\Repository\UserRepository; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; -#[ORM\Entity] +#[ORM\Entity(repositoryClass: UserRepository::class)] class User implements UserInterface, PasswordAuthenticatedUserInterface { #[ORM\Id] diff --git a/src/Menu/AbstractMenuBuilder.php b/src/Menu/AbstractMenuBuilder.php new file mode 100644 index 0000000..49a9d90 --- /dev/null +++ b/src/Menu/AbstractMenuBuilder.php @@ -0,0 +1,133 @@ +factory->createItem('root'); + } + + protected function getDefaultRouteParameters(string $parameter = 'id', string $default = '0'): array + { + $request = $this->requestStack->getMainRequest(); + + return [ + $parameter => $request->get($parameter, $default), + 'r' => $request->get('r'), + ]; + } + + protected function addAdminItem(ItemInterface $menu): void + { + if ($this->security->isGranted('ROLE_ADMIN')) { + $this->addDivider($menu); + $menu->addChild('zum Adminbereich', [ + 'route' => 'app_admin_index', + 'linkAttributes' => [ + 'title' => 'zum Adminbereich', + ], + 'extras' => [ + 'icon' => 'user', + ], + ]); + } + } + + protected function addManagerItem(ItemInterface $menu): void + { + if ($this->security->isGranted('ROLE_MANAGER')) { + $this->addDivider($menu); + $menu->addChild('zum Managementbereich', [ + 'route' => 'app_manager_index', + 'linkAttributes' => [ + 'title' => 'zum Managementbereich', + ], + 'extras' => [ + 'icon' => 'user', + ], + ]); + } + } + + protected function addTeamerItem(ItemInterface $menu): void + { + if ($this->security->isGranted('ROLE_TEAMER')) { + $this->addDivider($menu); + $menu->addChild('zum Teambereich', [ + 'route' => 'app_teamer_index', + 'linkAttributes' => [ + 'title' => 'zum Teambereich', + ], + 'extras' => [ + 'icon' => 'user', + ], + ]); + } + } + + protected function addHouseManagerItem(ItemInterface $menu): void + { + if ($this->security->isGranted('ROLE_HOUSE_MANAGER')) { + $this->addDivider($menu); + $menu->addChild('zum Hausleitungsbereich', [ + 'route' => 'app_house_manager_index', + 'linkAttributes' => [ + 'title' => 'zum Hausleitungsbereich', + ], + 'extras' => [ + 'icon' => 'user', + ], + ]); + } + } + + protected function addLogoutItem(ItemInterface $menu): void + { + $this->addDivider($menu); + + $menu->addChild('MyE&P', [ + 'route' => 'app_account', + 'linkAttributes' => [ + 'title' => 'zu MyE&P', + ], + 'extras' => [ + 'icon' => 'logout', + ], + ]); + $menu->addChild('Logout', [ + 'route' => 'app_logout', + 'linkAttributes' => [ + 'title' => 'Logout', + ], + 'extras' => [ + 'icon' => 'logout', + ], + ]); + } + + protected function addDivider(ItemInterface $menu): void + { + $menu->addChild(Uuid::v4(), [ + 'extras' => [ + 'divider' => true, + ], + ]); + } +} diff --git a/src/Menu/AdminMenuBuilder.php b/src/Menu/AdminMenuBuilder.php new file mode 100644 index 0000000..74b29b6 --- /dev/null +++ b/src/Menu/AdminMenuBuilder.php @@ -0,0 +1,54 @@ +createRootElement(); + + $menu->addChild('Dashboard', [ + 'route' => 'app_admin_dashboard', + 'linkAttributes' => [ + 'title' => 'Dashboard', + ], + 'extras' => [ + 'icon' => 'chart', + ], + ]); + $menu->addChild('Buchungsentwürfe', [ + 'route' => 'app_admin_bookingeditdraft', + 'linkAttributes' => [ + 'title' => 'Buchungsentwürfe', + ], + 'extras' => [ + 'icon' => 'edit', + ], + ]); + $menu->addChild('Benutzer', [ + 'route' => 'app_admin_user', + 'linkAttributes' => [ + 'title' => 'Benutzer', + ], + 'extras' => [ + 'icon' => 'users', + ], + ]); + $menu->addChild('Logs', [ + 'route' => 'app_admin_log', + 'linkAttributes' => [ + 'title' => 'Logs', + ], + 'extras' => [ + 'icon' => 'list', + ], + ]); + + $this->addLogoutItem($menu); + + return $menu; + } +} diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php new file mode 100644 index 0000000..e85e4bf --- /dev/null +++ b/src/Repository/UserRepository.php @@ -0,0 +1,20 @@ + + */ +class UserRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, User::class); + } +} diff --git a/src/Twig/AppExtension.php b/src/Twig/AppExtension.php index aeaed62..4b31a37 100644 --- a/src/Twig/AppExtension.php +++ b/src/Twig/AppExtension.php @@ -33,6 +33,7 @@ class AppExtension extends AbstractExtension public function getFunctions(): array { return [ + new TwigFunction('icon', [AppRuntime::class, 'renderIcon'], ['needs_environment' => true, 'is_safe' => ['html']]), new TwigFunction('is_participant_eligible', [AppRuntime::class, 'isParticipantEligible']), new TwigFunction('qa_attribute', [AppRuntime::class, 'renderQaAttribute'], ['is_safe' => ['html']]), new TwigFunction('is_static_text', [AppRuntime::class, 'isStaticText']), @@ -42,6 +43,7 @@ class AppExtension extends AbstractExtension new TwigFunction('booking_theme', [AppRuntime::class, 'getBookingTheme']), new TwigFunction('gtm_id', [AppRuntime::class, 'getGtmId']), new TwigFunction('cmp_url', [AppRuntime::class, 'getCmpUrl']), + new TwigFunction('return_url', [AppRuntime::class, 'getEncodedReturnUrl']), ]; } } diff --git a/src/Twig/AppRuntime.php b/src/Twig/AppRuntime.php index b0ded76..afa7b3b 100644 --- a/src/Twig/AppRuntime.php +++ b/src/Twig/AppRuntime.php @@ -14,6 +14,7 @@ use App\Model\DomainConfig; use App\Service\ParticipantEligibilityChecker; use Symfony\Component\Form\FormView; use Symfony\Component\HttpFoundation\RequestStack; +use Twig\Environment; use Twig\Extension\RuntimeExtensionInterface; use Twig\Extra\Intl\IntlExtension; @@ -148,6 +149,14 @@ class AppRuntime implements RuntimeExtensionInterface return sprintf(' data-qa-%s="%s"', strtolower($label), $value); } + public function renderIcon(Environment $environment, string $icon, string $classes = 'w-5 h-5'): string + { + return $environment->render('_partials/_icon.html.twig', [ + 'icon' => $icon, + 'class' => $classes, + ]); + } + /** * Checks if a form field should be rendered as static text. * @@ -268,6 +277,17 @@ class AppRuntime implements RuntimeExtensionInterface return $this->getDomainConfig()->cmpUrl; } + public function getEncodedReturnUrl(): string + { + $masterRequest = $this->requestStack->getMainRequest(); + + if (null === $masterRequest) { + return ''; + } + + return rawurlencode($masterRequest->getRequestUri()); + } + private function getDomainConfig(): DomainConfig { $request = $this->requestStack->getCurrentRequest(); diff --git a/symfony.lock b/symfony.lock index 0359e65..381463e 100644 --- a/symfony.lock +++ b/symfony.lock @@ -35,18 +35,6 @@ "migrations/.gitignore" ] }, - "easycorp/easyadmin-bundle": { - "version": "4.27", - "recipe": { - "repo": "github.com/symfony/recipes", - "branch": "main", - "version": "4.14", - "ref": "13b3e524d1038a6861ad6c7fc64e31b644ba0c54" - }, - "files": [ - "config/routes/easyadmin.yaml" - ] - }, "flagception/flagception-bundle": { "version": "6.1", "recipe": { @@ -71,6 +59,12 @@ ".php-cs-fixer.dist.php" ] }, + "knplabs/knp-menu-bundle": { + "version": "v3.7.0" + }, + "knplabs/knp-paginator-bundle": { + "version": "v6.10.0" + }, "league/flysystem-bundle": { "version": "3.4", "recipe": { @@ -364,18 +358,6 @@ "config/packages/uid.yaml" ] }, - "symfony/ux-twig-component": { - "version": "2.32", - "recipe": { - "repo": "github.com/symfony/recipes", - "branch": "main", - "version": "2.13", - "ref": "f367ae2a1faf01c503de2171f1ec22567febeead" - }, - "files": [ - "config/packages/twig_component.yaml" - ] - }, "symfony/validator": { "version": "6.4", "recipe": { diff --git a/templates/_partials/_dropdown.html.twig b/templates/_partials/_dropdown.html.twig new file mode 100644 index 0000000..d4abf09 --- /dev/null +++ b/templates/_partials/_dropdown.html.twig @@ -0,0 +1,13 @@ +
+ + +
diff --git a/templates/admin/field/json.html.twig b/templates/_partials/_json.html.twig similarity index 63% rename from templates/admin/field/json.html.twig rename to templates/_partials/_json.html.twig index 78e3ef8..88be17f 100644 --- a/templates/admin/field/json.html.twig +++ b/templates/_partials/_json.html.twig @@ -1,5 +1,6 @@ +{% import _self as self %} + {% macro render_value(value) %} - {% import _self as self %} {% if value is iterable %}