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 @@
+
+
+ Funktionen
+ {{ icon('dots', 'w-6 h-6') }}
+
+
+
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 %}
{% for key, item in value %}
@@ -17,6 +18,5 @@
{{ value }}
{% endif %}
{% endmacro %}
-
- {% import _self as self %}
- {{ self.render_value(field.value) }}
+
+{{ self.render_value(value) }}
diff --git a/templates/_partials/_menu.html.twig b/templates/_partials/_menu.html.twig
new file mode 100644
index 0000000..47c2e74
--- /dev/null
+++ b/templates/_partials/_menu.html.twig
@@ -0,0 +1,82 @@
+{% extends 'knp_menu.html.twig' %}
+
+{% block list %}
+ {% if item.hasChildren and options.depth is not same as(0) and item.displayChildren %}
+ {% if item.level == 0 %}
+
+
+
+ {{ block('children') }}
+
+
+
+ {% else %}
+
+ {{ block('children') }}
+
+ {% endif %}
+ {% endif %}
+{% endblock %}
+
+{% block item %}
+ {% if item.displayed %}
+
+ {%- if item.hasChildren and item.displayChildren %}
+
+ {%- endif %}
+ {%- if item.uri is not empty %}
+ {{ block('linkElement') }}
+ {%- else %}
+ {{ block('spanElement') }}
+ {%- endif %}
+ {{ block('list') }}
+ {%- if item.hasChildren and item.displayChildren %}
+
+ {%- endif %}
+
+ {% endif %}
+{% endblock %}
+
+{% block linkElement %}
+ {% import _self as knp_menu %}
+
+ {{ block('label') }}
+
+{% endblock %}
+
+{% block spanElement %}
+ {% import _self as knp_menu %}
+ {% if item.hasChildren %}
+
+ {{ block('label') }}
+
+ {% elseif item.extras.divider is defined %}
+
+ {% else %}
+
+ {{ block('label') }}
+
+ {% endif %}
+{% endblock %}
+
+{% block label %}
+ {% if item.extras.icon is defined %}
+ {{ icon(item.extras.icon, 'h-5 w-5 shrink-0 text-gray-400') }}
+ {{ item.label|raw }}
+ {% else %}
+ {{ item.label|raw }}
+ {% endif %}
+{% endblock %}
+
diff --git a/templates/_partials/_paginator_sliding.html.twig b/templates/_partials/_paginator_sliding.html.twig
new file mode 100644
index 0000000..008383f
--- /dev/null
+++ b/templates/_partials/_paginator_sliding.html.twig
@@ -0,0 +1,69 @@
+{% if pageCount > 0 %}
+
+
+
+
+ {{ 'paginator.info'|trans({ '{from}': firstItemNumber, '{to}': lastItemNumber, '{total}': totalCount })|raw }}
+
+
+ 10 pro Seite
+ 20 pro Seite
+ 50 pro Seite
+
+
+
+
+ {% if pageCount > 1 %}
+ {% if first is defined and current != first %}
+
+
+
+
+
+ {% endif %}
+ {% if previous is defined %}
+
+
+
+
+
+ {% endif %}
+ {% if current > 3 %}
+
+ ...
+
+ {% endif %}
+ {% for page in pagesInRange %}
+
+ {{ page }}
+
+ {% endfor %}
+ {% if current < pageCount - 3 %}
+
+ ...
+
+ {% endif %}
+ {% if next is defined %}
+
+
+
+
+
+ {% endif %}
+ {% if last is defined and current != last %}
+
+
+
+
+
+ {% endif %}
+ {% endif %}
+
+
+
+
+{% endif %}
\ No newline at end of file
diff --git a/templates/_partials/_paginator_sliding_boxes.html.twig b/templates/_partials/_paginator_sliding_boxes.html.twig
new file mode 100644
index 0000000..1b9e1cb
--- /dev/null
+++ b/templates/_partials/_paginator_sliding_boxes.html.twig
@@ -0,0 +1,44 @@
+{% if pageCount > 0 %}
+
+
+
+
{{ 'paginator.info'|trans({ '{from}': firstItemNumber, '{to}': lastItemNumber, '{total}': totalCount })|raw }}
+
+
+
+ {% if pageCount > 1 %}
+ {% if previous is defined %}
+
+
+
+
+
+ {% endif %}
+ {% if current > 3 %}
+
+ ...
+
+ {% endif %}
+ {% for page in pagesInRange %}
+
+ {{ page }}
+
+ {% endfor %}
+ {% if current < pageCount - 3 %}
+
+ ...
+
+ {% endif %}
+ {% if next is defined %}
+
+
+
+
+
+ {% endif %}
+ {% endif %}
+
+
+
+
+{% endif %}
\ No newline at end of file
diff --git a/templates/_partials/_paginator_sortable_link.html.twig b/templates/_partials/_paginator_sortable_link.html.twig
new file mode 100644
index 0000000..a349dbf
--- /dev/null
+++ b/templates/_partials/_paginator_sortable_link.html.twig
@@ -0,0 +1,15 @@
+
+ {{ title|raw }}
+ {%- if sorted -%}
+ {%- if direction == 'desc' -%}
+
+
+
+ {%- else -%}
+
+
+
+ {%- endif -%}
+ {%- endif -%}
+
+
diff --git a/templates/account/index.html.twig b/templates/account/index.html.twig
index 133b78f..ad7e0dc 100644
--- a/templates/account/index.html.twig
+++ b/templates/account/index.html.twig
@@ -23,7 +23,7 @@
{% if is_granted('ROLE_ADMIN') %}
-
+
Admin
diff --git a/templates/admin/booking_edit_draft/index.html.twig b/templates/admin/booking_edit_draft/index.html.twig
new file mode 100644
index 0000000..b294ba5
--- /dev/null
+++ b/templates/admin/booking_edit_draft/index.html.twig
@@ -0,0 +1,87 @@
+{% extends 'layout_admin.html.twig' %}
+
+{% block content %}
+
+ Buchungsentwürfe
+
+
+
+
+
+
+
+ {{ knp_pagination_sortable(pagination, 'erstellt am', 'booking_edit_draft.createdAt') }}
+
+
+ {{ knp_pagination_sortable(pagination, 'Vorgang', 'booking_edit_draft.bookingNumber') }}
+
+
+ {{ knp_pagination_sortable(pagination, 'Kundenaccount', 'user.email') }}
+
+
+ {{ knp_pagination_sortable(pagination, 'aktualisiert am', 'booking_edit_draft.updatedAt') }}
+
+
+
+
+
+ {% for draft in pagination %}
+
+
+ {{ draft.createdAt | date('d.m.Y, H:i') }}
+
+
+ {{ draft.bookingNumber | default('-') }}
+
+
+ {{ draft.user.email | default('-') }}
+
+
+ {% if draft.updatedAt is not null and draft.updatedAt != draft.createdAt %}
+ {{ draft.updatedAt | date('d.m.Y, H:i') }}
+ {% else %}
+ -
+ {% endif %}
+
+
+ {% embed '_partials/_dropdown.html.twig' %}
+ {% block content %}
+
+ Details
+
+
+ XLS-Export
+
+
+ löschen
+
+ {% endblock %}
+ {% endembed %}
+
+
+
+ {% else %}
+
+
+ Keine Daten...
+
+
+ {% endfor %}
+
+
+ {{ knp_pagination_render(pagination) }}
+
+
+{% endblock %}
diff --git a/templates/admin/booking_edit_draft/modal_delete.html.twig b/templates/admin/booking_edit_draft/modal_delete.html.twig
new file mode 100644
index 0000000..9d5ad4e
--- /dev/null
+++ b/templates/admin/booking_edit_draft/modal_delete.html.twig
@@ -0,0 +1,7 @@
+{% extends 'htmx_confirmation_modal.html.twig' %}
+
+{% block content %}
+
+ Möchtest du den Buchungsentwurf zum Vorgang {{ draft.bookingNumber }} wirklich löschen?
+
+{% endblock %}
diff --git a/templates/admin/booking_edit_draft/show.html.twig b/templates/admin/booking_edit_draft/show.html.twig
new file mode 100644
index 0000000..7181cfd
--- /dev/null
+++ b/templates/admin/booking_edit_draft/show.html.twig
@@ -0,0 +1,16 @@
+{% extends 'layout_admin.html.twig' %}
+
+{% block content %}
+
+ Buchungsentwurf
+
+
+ Vorgang {{ draft.bookingNumber }}
+
+ {% include('_partials/_json.html.twig') with { 'value': draft.formData } %}
+
+{% endblock %}
diff --git a/templates/admin/dashboard.html.twig b/templates/admin/dashboard.html.twig
new file mode 100644
index 0000000..7cc1ac2
--- /dev/null
+++ b/templates/admin/dashboard.html.twig
@@ -0,0 +1,10 @@
+{% extends 'layout_admin.html.twig' %}
+
+{% block content %}
+
+ MyE&P Admin
+
+
+ Noch nicht wirklich was zu sehen hier...
+
+{% endblock %}
diff --git a/templates/admin/dashboard/index.html.twig b/templates/admin/dashboard/index.html.twig
deleted file mode 100644
index baf39ab..0000000
--- a/templates/admin/dashboard/index.html.twig
+++ /dev/null
@@ -1,5 +0,0 @@
-{% extends '@EasyAdmin/layout.html.twig' %}
-
-{% block main %}
-
-{% endblock main %}
diff --git a/templates/admin/log/index.html.twig b/templates/admin/log/index.html.twig
new file mode 100644
index 0000000..ad6a91b
--- /dev/null
+++ b/templates/admin/log/index.html.twig
@@ -0,0 +1,81 @@
+{% extends 'layout_admin.html.twig' %}
+
+{% block content %}
+
+ Logs
+
+
+
+
+
+
+
+ {{ knp_pagination_sortable(pagination, 'Datum', 'log.timestamp') }}
+
+
+ Fehlercode
+
+
+ Benutzer
+
+
+ Aktion
+
+
+ Request ID
+
+
+ URI
+
+
+
+
+
+ {% for entry in pagination %}
+
+
+ {{ entry.createdAt | date('d.m.Y H:i') }}
+
+
+ {{ entry.errorCode | default('-') }}
+
+
+ {{ entry.userName }}
+
+
+ {{ entry.message }}
+
+
+ {{ entry.requestId | default('-') }}
+
+
+ {{ entry.uri | default('-') | u.truncate(48, '...') }}
+
+
+ {% if entry.channel == 'bpn' %}
+ {% embed '_partials/_dropdown.html.twig' %}
+ {% block content %}
+
+ XML-Dumps
+
+ {% endblock %}
+ {% endembed %}
+ {% endif %}
+
+
+ {% else %}
+
+
+ Keine Daten...
+
+
+ {% endfor %}
+
+
+ {{ knp_pagination_render(pagination) }}
+
+
+{% endblock %}
diff --git a/templates/admin/log/xml_dumps.html.twig b/templates/admin/log/xml_dumps.html.twig
new file mode 100644
index 0000000..ff13a3e
--- /dev/null
+++ b/templates/admin/log/xml_dumps.html.twig
@@ -0,0 +1,61 @@
+{% extends 'layout_admin.html.twig' %}
+
+{% block content %}
+
+ XML-Dumps für Request ID {{ logEntry.requestId }}
+
+ {% if dumps is empty %}
+
+
+ Keine XML-Dumps gefunden. Die Dateien wurden vermutlich bereits bereinigt.
+
+ {% else %}
+
+
+
+
+
+
+ Dateiname
+
+
+ Typ
+
+
+ Dateigröße
+
+
+ Aktion
+
+
+
+
+ {% for dump in dumps %}
+
+
+ {{ dump.filename }}
+
+
+ {% if dump.type == 'request' %}
+ Request
+ {% else %}
+ Response
+ {% endif %}
+
+
+ {{ (dump.size / 1024)|number_format(1) }} KB
+
+
+
+ {{ icon('download', 'w-4 h-4')}} Download
+
+
+
+ {% endfor %}
+
+
+
+
+ {% endif %}
+{% endblock %}
diff --git a/templates/admin/user/index.html.twig b/templates/admin/user/index.html.twig
new file mode 100644
index 0000000..d5b1d41
--- /dev/null
+++ b/templates/admin/user/index.html.twig
@@ -0,0 +1,54 @@
+{% extends 'layout_admin.html.twig' %}
+
+{% block content %}
+
+ Benutzeraccounts
+
+
+
+
+
+
+
+ {{ knp_pagination_sortable(pagination, 'E-Mail', 'user.email') }}
+
+
+ {{ knp_pagination_sortable(pagination, 'BusPro Adress Id', 'user.addressId') }}
+
+
+ {{ knp_pagination_sortable(pagination, 'BusPro Personen Id', 'user.personId') }}
+
+
+ {{ knp_pagination_sortable(pagination, 'Letzter Login', 'user.lastLoginAt') }}
+
+
+
+
+ {% for user in pagination %}
+
+
+ {{ user.email }}
+
+
+ {{ user.addressId | default('-') }}
+
+
+ {{ user.personId | default('-') }}
+
+
+ {{ user.lastLoginAt | date('d.m.Y, H:i') }}
+
+
+ {% else %}
+
+
+ Keine Daten...
+
+
+ {% endfor %}
+
+
+ {{ knp_pagination_render(pagination) }}
+
+
+{% endblock %}
diff --git a/templates/admin/xml_dump/list.html.twig b/templates/admin/xml_dump/list.html.twig
deleted file mode 100644
index 1393a00..0000000
--- a/templates/admin/xml_dump/list.html.twig
+++ /dev/null
@@ -1,62 +0,0 @@
-{% extends '@EasyAdmin/layout.html.twig' %}
-
-{% block content_title %}
- XML Dumps
-{% endblock %}
-
-{% block main %}
-
-
-
-
- {% if dumps is empty %}
-
-
- Keine XML-Dumps gefunden. Die Dateien wurden vermutlich bereits bereinigt.
-
- {% else %}
-
-
-
- Dateiname
- Typ
- Dateigröße
- Aktion
-
-
-
- {% for dump in dumps %}
-
- {{ dump.filename }}
-
- {% if dump.type == 'request' %}
- Request
- {% else %}
- Response
- {% endif %}
-
- {{ (dump.size / 1024)|number_format(1) }} KB
-
-
- Download
-
-
-
- {% endfor %}
-
-
- {% endif %}
-
-
-
-
-{% endblock %}
diff --git a/templates/base_admin.html.twig b/templates/base_admin.html.twig
new file mode 100644
index 0000000..a22f927
--- /dev/null
+++ b/templates/base_admin.html.twig
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ {% block title %}MyE&P{% endblock %}
+
+
+
+ {% block stylesheets %}
+ {{ encore_entry_link_tags('app') }}
+ {% endblock %}
+ {% block javascripts %}
+ {{ encore_entry_script_tags('app') }}
+ {% endblock %}
+
+
+ {% block body %}{% endblock %}
+ {% block data_layer %}{% endblock %}
+
+
diff --git a/templates/htmx_confirmation_modal.html.twig b/templates/htmx_confirmation_modal.html.twig
new file mode 100644
index 0000000..7a3cd93
--- /dev/null
+++ b/templates/htmx_confirmation_modal.html.twig
@@ -0,0 +1,25 @@
+
+
+
+
+
+ {{ icon('alert', 'w-8 h-8') }}
+
+ {% block title %}Bist du sicher?{% endblock %}
+
+
+
+ {{ icon('close', 'w-8 h-8') }}
+
+
+ {% block content %}{% endblock %}
+
+
+ {% block button_confirm %}Ja{% endblock %}
+
+
+ {% block button_cancel %}Abbrechen{% endblock %}
+
+
+
+
diff --git a/templates/layout_admin.html.twig b/templates/layout_admin.html.twig
new file mode 100644
index 0000000..1840214
--- /dev/null
+++ b/templates/layout_admin.html.twig
@@ -0,0 +1,44 @@
+{% extends 'base_admin.html.twig' %}
+
+{% block body %}
+
+
+
+
+
+ {% if app.user %}
+
+
+ {{ icon('menu', 'w-8 h-8 text-gray-700') }}
+
+
+ {% endif %}
+
+
+ {{ knp_menu_render(knp_menu_get('admin_main')) }}
+
+
+ {% block content %}{% endblock %}
+
+
+
+
+
+
+
+ {{ icon('close', 'w-8 h-8') }}
+
+
+
+ {{ knp_menu_render(knp_menu_get('admin_main')) }}
+
+
+
+{% endblock %}
diff --git a/translations/messages.de.yaml b/translations/messages.de.yaml
index 3c0e4ba..8e50e5f 100644
--- a/translations/messages.de.yaml
+++ b/translations/messages.de.yaml
@@ -12,3 +12,8 @@ service:
max: 'Jahrgang bis %yearTo%'
mixed:
separator: ' und '
+
+paginator:
+ previous: vorherige
+ next: nächste
+ info: 'Einträge {from} bis {to} von {total}'