wip: implement admin crud
This commit is contained in:
1 parent
306cd6bec4
commit
e902a5e298
78 files changed
+2555
-101
No files matched your search
@@ -0,0 +1,8 @@
|
||||
import htmx from 'htmx.org'
|
||||
htmx.config.includeIndicatorStyles = false
|
||||
htmx.config.historyCacheSize = 0
|
||||
htmx.config.allowScriptTags = false
|
||||
|
||||
import './bootstrap.js'
|
||||
|
||||
import './styles/admin.css'
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Controller } from '@hotwired/stimulus'
|
||||
import { useFetch } from '../mixins/use_fetch'
|
||||
import Autocomplete from '@trevoreyre/autocomplete-js'
|
||||
|
||||
export default class extends Controller {
|
||||
|
||||
static targets = [ 'input', 'field', 'resetButton', 'searchIcon' ]
|
||||
|
||||
static values = { url: String, choices: Array, selected: Boolean }
|
||||
|
||||
initialize() {
|
||||
useFetch(this)
|
||||
}
|
||||
|
||||
connect() {
|
||||
const searchHandler = input => {
|
||||
return new Promise(resolve => {
|
||||
// Check input minimum length
|
||||
if (input.length < 2) {
|
||||
return resolve([])
|
||||
}
|
||||
|
||||
// Search choices if provided
|
||||
if (this.choicesValue.length > 0) {
|
||||
return resolve(this.choicesValue.filter(choice => {
|
||||
return -1 !== choice.text.toLowerCase().search(input.toLowerCase())
|
||||
}))
|
||||
}
|
||||
|
||||
// Use API otherwise
|
||||
let data = { search: input }
|
||||
fetch(this.urlValue, { method: 'POST', body: JSON.stringify(data), credentials: 'include' })
|
||||
.then(this.checkStatus)
|
||||
.then(this.parseJSON)
|
||||
.then(data => {
|
||||
resolve(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const submitHandler = result => {
|
||||
this.fieldTarget.value = result.value
|
||||
this.selectedValue = true
|
||||
this.element.dispatchEvent(new CustomEvent('select', {
|
||||
detail: { result }
|
||||
}))
|
||||
}
|
||||
|
||||
this.autocomplete = new Autocomplete(this.element, {
|
||||
search: searchHandler,
|
||||
getResultValue: result => result.text,
|
||||
submitOnEnter: true,
|
||||
onSubmit: submitHandler,
|
||||
debounceTime: 250,
|
||||
})
|
||||
|
||||
if (this.fieldTarget.value) {
|
||||
this.selectedValue = true
|
||||
}
|
||||
|
||||
// prevent form submission when selecting entry in autocomplete list with enter key
|
||||
this.element.addEventListener('keypress', e => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.inputTarget.value = null
|
||||
this.fieldTarget.value = null
|
||||
this.selectedValue = false
|
||||
this.element.dispatchEvent(new CustomEvent('reset'))
|
||||
}
|
||||
|
||||
selectedValueChanged() {
|
||||
this.resetButtonTarget.classList.toggle('hidden', false === this.selectedValue)
|
||||
this.searchIconTarget.classList.toggle('hidden', this.selectedValue)
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.autocomplete.destroy()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Controller } from '@hotwired/stimulus'
|
||||
import flatpickr from 'flatpickr';
|
||||
import { German } from 'flatpickr/dist/l10n/de';
|
||||
|
||||
/* stimulusFetch: 'lazy' */
|
||||
export default class extends Controller {
|
||||
|
||||
static targets = [ 'field' ]
|
||||
static values = {
|
||||
minDate: String,
|
||||
maxDate: String,
|
||||
disableWeekends: Boolean,
|
||||
}
|
||||
|
||||
initialize() {
|
||||
flatpickr.localize(German)
|
||||
}
|
||||
|
||||
connect() {
|
||||
const minDate = this.minDateValue
|
||||
const maxDate = this.maxDateValue
|
||||
const options = {
|
||||
dateFormat: 'Y-m-d',
|
||||
altFormat: 'd.m.Y',
|
||||
minDate,
|
||||
maxDate,
|
||||
altInput: true,
|
||||
allowInput: true,
|
||||
onClose(dates, currentdatestring, instance) {
|
||||
instance.setDate(instance.altInput.value, true, instance.config.altFormat)
|
||||
this.element.dispatchEvent(new CustomEvent('datepicker:closed'))
|
||||
},
|
||||
onChange(dates, currentdatestring, instance) {
|
||||
this.element.dispatchEvent(new CustomEvent('datepicker:picked', {
|
||||
detail: {
|
||||
date: instance.altInput.value
|
||||
}
|
||||
}))
|
||||
},
|
||||
}
|
||||
|
||||
if (true === this.disableWeekendsValue) {
|
||||
options['disable'] = [
|
||||
function(date) {
|
||||
return (date.getDay() === 0 || date.getDay() === 6);
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
this.picker = flatpickr(this.fieldTarget, options)
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.picker.destroy()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Controller } from '@hotwired/stimulus'
|
||||
import { useClickOutside } from 'stimulus-use'
|
||||
|
||||
export default class extends Controller {
|
||||
|
||||
static targets = [ 'label', 'dropdown', 'choice' ]
|
||||
static values = { placeholder: String }
|
||||
|
||||
connect() {
|
||||
this.update()
|
||||
useClickOutside(this)
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.dropdownTarget.classList.toggle('hidden')
|
||||
}
|
||||
|
||||
update() {
|
||||
let labels = []
|
||||
|
||||
this.choiceTargets.forEach((choice) => {
|
||||
if (choice.checked) {
|
||||
labels.push(choice.dataset.label)
|
||||
}
|
||||
this.labelTarget.innerText = labels.join(', ')
|
||||
})
|
||||
|
||||
if (0 === labels.length) {
|
||||
this.labelTarget.innerText = this.placeholderValue
|
||||
}
|
||||
}
|
||||
|
||||
clickOutside(e) {
|
||||
this.dropdownTarget.classList.toggle('hidden', true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Controller } from '@hotwired/stimulus'
|
||||
import Sortable from 'sortablejs'
|
||||
import { useFetch } from '../mixins/use_fetch'
|
||||
|
||||
/* stimulusFetch: 'lazy' */
|
||||
export default class extends Controller {
|
||||
|
||||
static targets = [ 'item', 'handle', 'positionField', 'positionLabel' ]
|
||||
static values = { url: String }
|
||||
|
||||
connect() {
|
||||
useFetch(this)
|
||||
new Sortable(this.element, {
|
||||
item: '[data-sortable-target="item"]',
|
||||
handle: '[data-sortable-target="handle"]',
|
||||
sort: true,
|
||||
onEnd: () => {
|
||||
this.updateOrdering()
|
||||
if (this.urlValue) {
|
||||
this.saveOrdering()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
updateOrdering() {
|
||||
this.positionFieldTargets.forEach((field, index) => {
|
||||
field.value = index + 1
|
||||
})
|
||||
this.positionLabelTargets.forEach((label, index) => {
|
||||
label.innerText = index + 1
|
||||
})
|
||||
}
|
||||
|
||||
saveOrdering() {
|
||||
const body = new FormData()
|
||||
this.positionFieldTargets.forEach(field => {
|
||||
body.append(`sorting[${field.dataset.id}]`, field.value)
|
||||
})
|
||||
fetch(this.urlValue, this.getInitObject('post', body))
|
||||
}
|
||||
}
|
||||
@@ -181,6 +181,7 @@
|
||||
<symbol id="icon-no-symbol" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 0 0 5.636 5.636m12.728 12.728A9 9 0 0 1 5.636 5.636m12.728 12.728L5.636 5.636" />
|
||||
</symbol>
|
||||
<symbol id="icon-draggable" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M8.5 7C9.32843 7 10 6.32843 10 5.5C10 4.67157 9.32843 4 8.5 4C7.67157 4 7 4.67157 7 5.5C7 6.32843 7.67157 7 8.5 7ZM8.5 13.5C9.32843 13.5 10 12.8284 10 12C10 11.1716 9.32843 10.5 8.5 10.5C7.67157 10.5 7 11.1716 7 12C7 12.8284 7.67157 13.5 8.5 13.5ZM10 18.5C10 19.3284 9.32843 20 8.5 20C7.67157 20 7 19.3284 7 18.5C7 17.6716 7.67157 17 8.5 17C9.32843 17 10 17.6716 10 18.5ZM15.5 7C16.3284 7 17 6.32843 17 5.5C17 4.67157 16.3284 4 15.5 4C14.6716 4 14 4.67157 14 5.5C14 6.32843 14.6716 7 15.5 7ZM17 12C17 12.8284 16.3284 13.5 15.5 13.5C14.6716 13.5 14 12.8284 14 12C14 11.1716 14.6716 10.5 15.5 10.5C16.3284 10.5 17 11.1716 17 12ZM15.5 20C16.3284 20 17 19.3284 17 18.5C17 17.6716 16.3284 17 15.5 17C14.6716 17 14 17.6716 14 18.5C14 19.3284 14.6716 20 15.5 20Z"/></symbol>
|
||||
<symbol id="icon-bus" xmlns="http://www.w3.org/2000/svg" stroke-width="1.5" viewBox="0 0 24 24" stroke="currentColor" fill="none">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M6 17m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
||||
|
||||
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 38 KiB |
@@ -0,0 +1,29 @@
|
||||
export const useFetch = controller => {
|
||||
Object.assign(controller, {
|
||||
checkStatus(response) {
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
return response
|
||||
} else {
|
||||
const error = new Error(response.statusText)
|
||||
error.response = response
|
||||
throw error
|
||||
}
|
||||
},
|
||||
parseJSON(response) {
|
||||
return response.json()
|
||||
},
|
||||
parseHTML(response) {
|
||||
return response.text()
|
||||
},
|
||||
getInitObject(method = 'get', body = null) {
|
||||
return {
|
||||
method,
|
||||
body,
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
@import "components/button.css";
|
||||
@import "components/datatable.css";
|
||||
@import "components/forms.css";
|
||||
@import "components/pagination.css";
|
||||
@import "components/toast.css";
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
@import "components/autocomplete.css";
|
||||
@import "components/badge.css";
|
||||
@import "components/button.css";
|
||||
@import "components/datatable.css";
|
||||
@import "components/datepicker.css";
|
||||
@import "components/forms.css";
|
||||
@import "components/pagination.css";
|
||||
@import "components/toast.css";
|
||||
@import "components/tooltip.css";
|
||||
@import "components/typography.css";
|
||||
@@ -0,0 +1,7 @@
|
||||
@import "tailwindcss/base";
|
||||
@import "_base.css";
|
||||
|
||||
@import "tailwindcss/components";
|
||||
@import "_components_admin.css";
|
||||
|
||||
@import "tailwindcss/utilities";
|
||||
@@ -0,0 +1,33 @@
|
||||
.autocomplete {
|
||||
@apply relative w-full;
|
||||
}
|
||||
|
||||
.autocomplete[data-loading="true"]:after {
|
||||
@apply block absolute top-0 right-0;
|
||||
@apply absolute w-4 h-4 rounded-full right-0 mr-4;
|
||||
content: "";
|
||||
border: 3px solid rgba(0, 0, 0, 0.12);
|
||||
border-right: 3px solid rgba(0, 0, 0, 0.48);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
animation: rotate 1s infinite linear;
|
||||
}
|
||||
|
||||
.autocomplete-icon {
|
||||
@apply absolute top-1/2 right-0 transform -translate-y-1/2 mr-4;
|
||||
}
|
||||
.autocomplete[data-loading="true"] .autocomplete-icon {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
.autocomplete-result-list {
|
||||
@apply bg-white shadow-md max-h-64 overflow-y-auto;
|
||||
}
|
||||
|
||||
.autocomplete-result {
|
||||
@apply p-2;
|
||||
}
|
||||
.autocomplete-result:hover,
|
||||
.autocomplete-result[aria-selected="true"] {
|
||||
@apply bg-gray-200;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.badge {
|
||||
@apply inline-flex items-center rounded-md px-1.5 py-0.5 text-xs font-medium whitespace-nowrap;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
@import "flatpickr/dist/flatpickr.css";
|
||||
|
||||
.flatpickr-day.selected,
|
||||
.flatpickr-day.selected:hover,
|
||||
.flatpickr-day.selected:focus {
|
||||
background-color: theme('colors.secondary');
|
||||
border-color: theme('colors.secondary');
|
||||
}
|
||||
|
||||
.flatpickr-monthDropdown-months,
|
||||
.flatpickr-current-month .flatpickr-monthDropdown-months {
|
||||
@apply form-select pt-0 pb-0 focus:outline-none focus:shadow-none;
|
||||
}
|
||||
@@ -56,6 +56,7 @@
|
||||
"symfony/translation": "6.4.*",
|
||||
"symfony/twig-bundle": "6.4.*",
|
||||
"symfony/uid": "6.4.*",
|
||||
"symfony/ux-twig-component": "^2.36",
|
||||
"symfony/validator": "6.4.*",
|
||||
"symfony/web-link": "6.4.*",
|
||||
"symfony/webpack-encore-bundle": "^2.2",
|
||||
|
||||
Generated
+90
-1
@@ -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": "aa69dee5968ec6f7c3f152b8a8a6100d",
|
||||
"content-hash": "f04202a445d42ebff0dd0e8dce335163",
|
||||
"packages": [
|
||||
{
|
||||
"name": "carbonphp/carbon-doctrine-types",
|
||||
@@ -10080,6 +10080,95 @@
|
||||
],
|
||||
"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.42",
|
||||
|
||||
@@ -19,4 +19,5 @@ return [
|
||||
Flagception\Bundle\FlagceptionBundle\FlagceptionBundle::class => ['all' => true],
|
||||
Knp\Bundle\MenuBundle\KnpMenuBundle::class => ['all' => true],
|
||||
Knp\Bundle\PaginatorBundle\KnpPaginatorBundle::class => ['all' => true],
|
||||
Symfony\UX\TwigComponent\TwigComponentBundle::class => ['all' => true],
|
||||
];
|
||||
@@ -0,0 +1,5 @@
|
||||
twig_component:
|
||||
anonymous_template_directory: 'components/'
|
||||
defaults:
|
||||
# Namespace & directory for components
|
||||
App\Twig\Components\: 'components/'
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20260625154838 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE accommodation_price ADD price_per_night_override INT DEFAULT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE accommodation_price DROP price_per_night_override');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20260625173941 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE additional_service ADD selection_group VARCHAR(128) DEFAULT NULL, DROP `grouping`');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE additional_service ADD `grouping` VARCHAR(128) NOT NULL, DROP selection_group');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20260627121823 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE accommodation_price ADD accept_undersubscription TINYINT(1) NOT NULL');
|
||||
$this->addSql('DROP INDEX IDX_NEWSLETTER_CONSENT_EMAIL ON newsletter_consent');
|
||||
$this->addSql('ALTER TABLE newsletter_opt_in_request RENAME INDEX uniq_70505214b3bc57da TO UNIQ_EC0365DB3BC57DA');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE accommodation_price DROP accept_undersubscription');
|
||||
$this->addSql('CREATE INDEX IDX_NEWSLETTER_CONSENT_EMAIL ON newsletter_consent (email)');
|
||||
$this->addSql('ALTER TABLE newsletter_opt_in_request RENAME INDEX uniq_ec0365db3bc57da TO UNIQ_70505214B3BC57DA');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20260627130625 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE accommodation_price ADD price_additional_person_override INT DEFAULT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE accommodation_price DROP price_additional_person_override');
|
||||
}
|
||||
}
|
||||
Generated
+44
-1
@@ -6,6 +6,12 @@
|
||||
"": {
|
||||
"name": "myep",
|
||||
"license": "WTFPL",
|
||||
"dependencies": {
|
||||
"@trevoreyre/autocomplete-js": "^3.0.3",
|
||||
"flatpickr": "^4.6.13",
|
||||
"sortablejs": "^1.15.7",
|
||||
"stimulus-use": "^0.52.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.17.0",
|
||||
"@babel/preset-env": "^7.16.0",
|
||||
@@ -1645,7 +1651,6 @@
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@hotwired/stimulus/-/stimulus-3.2.2.tgz",
|
||||
"integrity": "sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@hotwired/stimulus-webpack-helpers": {
|
||||
@@ -2045,6 +2050,12 @@
|
||||
"tailwindcss": ">=3.0.0 || >=4.0.0 || insiders"
|
||||
}
|
||||
},
|
||||
"node_modules/@trevoreyre/autocomplete-js": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@trevoreyre/autocomplete-js/-/autocomplete-js-3.0.3.tgz",
|
||||
"integrity": "sha512-Z0KhA4EtMOixzYOFR+OiMc60IgNy9HkPnVH0kfxFkbP1I4my//sB/MRoPkqKmKgR+qHMujXhjdflMk5TpwVN2Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||
@@ -4382,6 +4393,12 @@
|
||||
"flat": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/flatpickr": {
|
||||
"version": "4.6.13",
|
||||
"resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.13.tgz",
|
||||
"integrity": "sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/forever-agent": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
|
||||
@@ -4670,6 +4687,16 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/hotkeys-js": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-4.0.4.tgz",
|
||||
"integrity": "sha512-hseNiqaskxSnujuGp8aRMLJfcjaFiTSS0I2GQhqru82N/sx6CGyUf6pvU5X1iycvw2EqmvILkFIb5OzYFXY+9A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://jaywcjlove.github.io/#/sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/htmlparser2": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
|
||||
@@ -7588,6 +7615,12 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/sortablejs": {
|
||||
"version": "1.15.7",
|
||||
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.7.tgz",
|
||||
"integrity": "sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/source-list-map": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
|
||||
@@ -7659,6 +7692,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stimulus-use": {
|
||||
"version": "0.52.3",
|
||||
"resolved": "https://registry.npmjs.org/stimulus-use/-/stimulus-use-0.52.3.tgz",
|
||||
"integrity": "sha512-stZ5dID6FUrGCR/ChWUa0FT5Z8iqkzT6lputOAb50eF+Ayg7RzJj4U/HoRlp2NV333QfvoRidru9HLbom4hZVw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@hotwired/stimulus": ">= 3",
|
||||
"hotkeys-js": ">= 3"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
|
||||
@@ -30,5 +30,11 @@
|
||||
"watch": "encore dev --watch",
|
||||
"build": "encore production --progress",
|
||||
"cy:open": "cypress open"
|
||||
},
|
||||
"dependencies": {
|
||||
"@trevoreyre/autocomplete-js": "^3.0.3",
|
||||
"flatpickr": "^4.6.13",
|
||||
"sortablejs": "^1.15.7",
|
||||
"stimulus-use": "^0.52.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Admin;
|
||||
|
||||
use App\Entity\Groups\Accommodation;
|
||||
use App\Form\Admin\Groups\AccommodationType;
|
||||
use App\Htmx\HxRedirectResponse;
|
||||
use App\Repository\Groups\AccommodationRepository;
|
||||
use App\Service\CmsDataProvider;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class AccommodationController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AccommodationRepository $accommodationRepository,
|
||||
private readonly CmsDataProvider $cmsDataProvider,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/admin/accommodation', name: 'app_admin_accommodation')]
|
||||
public function index(): Response
|
||||
{
|
||||
$accommodations = $this->accommodationRepository->findAll();
|
||||
|
||||
return $this->render('admin/accommodation/index.html.twig', [
|
||||
'accommodations' => $accommodations,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/accommodation/create', name: 'app_admin_accommodation_create')]
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
$accommodation = new Accommodation();
|
||||
|
||||
$form = $this->createForm(AccommodationType::class, $accommodation);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->entityManager->persist($accommodation);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->addFlash('success', 'Das Gruppenhaus wurde angelegt');
|
||||
|
||||
$this->logger->info('Created accommodation', [
|
||||
'name' => $accommodation->getName(),
|
||||
'code' => $accommodation->getCode(),
|
||||
]);
|
||||
|
||||
return $this->redirectToRoute('app_admin_accommodation');
|
||||
}
|
||||
|
||||
return $this->render('admin/accommodation/create.html.twig', [
|
||||
'form' => $form,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/accommodation/{id}/edit', name: 'app_admin_accommodation_edit')]
|
||||
public function edit(Accommodation $accommodation, Request $request): Response
|
||||
{
|
||||
// fetch additional data from cms if available
|
||||
$cmsData = [];
|
||||
|
||||
if (null !== $accommodation->getCode()) {
|
||||
$cmsData = $this->cmsDataProvider->getHotelDetails($accommodation->getCode());
|
||||
}
|
||||
|
||||
$form = $this->createForm(AccommodationType::class, $accommodation);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->entityManager->persist($accommodation);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->addFlash('success', 'Das Gruppenhaus wurde aktualisiert');
|
||||
|
||||
$this->logger->info('Updated accommodation', [
|
||||
'name' => $accommodation->getName(),
|
||||
'code' => $accommodation->getCode(),
|
||||
]);
|
||||
|
||||
return $this->redirectToRoute('app_admin_accommodation_edit', ['id' => $accommodation->getId()]);
|
||||
}
|
||||
|
||||
return $this->render('admin/accommodation/edit.html.twig', [
|
||||
'accommodation' => $accommodation,
|
||||
'form' => $form,
|
||||
'cmsData' => $cmsData,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/accommodation/{id}/delete', name: 'app_admin_accommodation_delete')]
|
||||
public function delete(Accommodation $accommodation, Request $request): Response
|
||||
{
|
||||
if (true === $request->isMethod(Request::METHOD_POST)) {
|
||||
$this->entityManager->remove($accommodation);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->addFlash('success', 'Das Gruppenhaus wurde gelöscht');
|
||||
|
||||
$this->logger->info('Deleted accommodation', [
|
||||
'name' => $accommodation->getName(),
|
||||
'code' => $accommodation->getCode(),
|
||||
]);
|
||||
|
||||
return new HxRedirectResponse($this->generateUrl('app_admin_accommodation'));
|
||||
}
|
||||
|
||||
return $this->render('admin/accommodation/modal_delete.html.twig', [
|
||||
'accommodation' => $accommodation,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Admin;
|
||||
|
||||
use App\Entity\Groups\Accommodation;
|
||||
use App\Entity\Groups\AccommodationPrice;
|
||||
use App\Form\Admin\Groups\AccommodationPriceType;
|
||||
use App\Htmx\HxRedirectResponse;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class AccommodationPriceController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/admin/accommodation-price/{id}/create', name: 'app_admin_accommodationprice_create')]
|
||||
public function create(Accommodation $accommodation, Request $request): Response
|
||||
{
|
||||
$accommodationPrice = new AccommodationPrice();
|
||||
|
||||
$form = $this->getForm($accommodationPrice, null);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$accommodation->addAccommodationPrice($accommodationPrice);
|
||||
|
||||
$this->entityManager->persist($accommodationPrice);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->addFlash('success', 'Der Preis wurde angelegt');
|
||||
|
||||
$this->logger->info('Created accommodation price', [
|
||||
'name' => $accommodation->getName(),
|
||||
'code' => $accommodation->getCode(),
|
||||
'date_range' => [
|
||||
'from' => $accommodationPrice->getDateFrom()->format('Y-m-d'),
|
||||
'to' => $accommodationPrice->getDateTo()->format('Y-m-d'),
|
||||
]
|
||||
]);
|
||||
|
||||
return $this->redirectToRoute('app_admin_accommodation_edit', [
|
||||
'id' => $accommodation->getId(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->render('admin/accommodation_price/create.html.twig', [
|
||||
'accommodation' => $accommodation,
|
||||
'form' => $form,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/accommodation-price/{id}/edit', name: 'app_admin_accommodationprice_edit', defaults: ['duplicate' => false])]
|
||||
#[Route('/admin/accommodation-price/{id}/duplicate', name: 'app_admin_accommodationprice_duplicate', defaults: ['duplicate' => true])]
|
||||
public function edit(AccommodationPrice $accommodationPrice, bool $duplicate, Request $request): Response
|
||||
{
|
||||
$accommodation = $accommodationPrice->getAccommodation();
|
||||
|
||||
if (true === $duplicate) {
|
||||
$accommodationPrice = clone $accommodationPrice;
|
||||
}
|
||||
|
||||
$form = $this->getForm($accommodationPrice, $accommodation);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
if (true === $duplicate) {
|
||||
$accommodation->addAccommodationPrice($accommodationPrice);
|
||||
}
|
||||
|
||||
$this->entityManager->persist($accommodationPrice);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->addFlash('success', 'Der Preis wurde aktualisiert');
|
||||
|
||||
$this->logger->info('Edited accommodation price', [
|
||||
'name' => $accommodation->getName(),
|
||||
'code' => $accommodation->getCode(),
|
||||
'date_range' => [
|
||||
'from' => $accommodationPrice->getDateFrom()->format('Y-m-d'),
|
||||
'to' => $accommodationPrice->getDateTo()->format('Y-m-d'),
|
||||
]
|
||||
]);
|
||||
|
||||
return $this->redirectToRoute('app_admin_accommodation_edit', [
|
||||
'id' => $accommodation->getId(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->render('admin/accommodation_price/edit.html.twig', [
|
||||
'accommodation' => $accommodation,
|
||||
'accommodationPrice' => $accommodationPrice,
|
||||
'form' => $form,
|
||||
'duplicate' => $duplicate,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/accommodation-price/{id}/delete', name: 'app_admin_accommodationprice_delete')]
|
||||
public function delete(AccommodationPrice $accommodationPrice, Request $request): Response
|
||||
{
|
||||
$accommodation = $accommodationPrice->getAccommodation();
|
||||
|
||||
if (true === $request->isMethod(Request::METHOD_POST)) {
|
||||
$this->entityManager->remove($accommodationPrice);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->addFlash('success', 'Der Preis wurde gelöscht');
|
||||
|
||||
$this->logger->info('Deleted accommodation price', [
|
||||
'name' => $accommodation->getName(),
|
||||
'code' => $accommodation->getCode(),
|
||||
'date_range' => [
|
||||
'from' => $accommodationPrice->getDateFrom()->format('Y-m-d'),
|
||||
'to' => $accommodationPrice->getDateTo()->format('Y-m-d'),
|
||||
]
|
||||
]);
|
||||
|
||||
$returnUrl = $this->generateUrl('app_admin_accommodation_edit', [
|
||||
'id' => $accommodation->getId(),
|
||||
]);
|
||||
|
||||
return new HxRedirectResponse($returnUrl);
|
||||
}
|
||||
|
||||
return $this->render('admin/accommodation_price/modal_delete.html.twig', [
|
||||
'accommodationPrice' => $accommodationPrice,
|
||||
'accommodation' => $accommodation,
|
||||
]);
|
||||
}
|
||||
|
||||
private function getForm(AccommodationPrice $accommodationPrice, ?Accommodation $accommodation): FormInterface
|
||||
{
|
||||
return $this->createForm(AccommodationPriceType::class, $accommodationPrice, ['currency' => $accommodation->getCurrency()]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Admin;
|
||||
|
||||
use App\Entity\Groups\Accommodation;
|
||||
use App\Entity\Groups\AdditionalService;
|
||||
use App\Form\Admin\Groups\AdditionalServiceType;
|
||||
use App\Htmx\HxRedirectResponse;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class AdditionalServiceController extends AbstractController
|
||||
{
|
||||
use SortingControllerTrait;
|
||||
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/admin/additional-service/{id}/create', name: 'app_admin_additionalservice_create')]
|
||||
public function create(Accommodation $accommodation, Request $request): Response
|
||||
{
|
||||
$additionalService = new AdditionalService();
|
||||
|
||||
$form = $this->getForm($additionalService, $accommodation);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$accommodation->addAdditionalService($additionalService);
|
||||
|
||||
$this->entityManager->persist($additionalService);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->addFlash('success', 'Die Zusatzleistung wurde angelegt');
|
||||
|
||||
$this->logger->info('Created additional service', [
|
||||
'name' => $accommodation->getName(),
|
||||
'code' => $accommodation->getCode(),
|
||||
'label' => $additionalService->getLabel(),
|
||||
]);
|
||||
|
||||
return $this->redirectToRoute('app_admin_accommodation_edit', [
|
||||
'id' => $accommodation->getId(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->render('admin/additional_service/create.html.twig', [
|
||||
'accommodation' => $accommodation,
|
||||
'form' => $form,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/additional-service/{id}/edit', name: 'app_admin_additionalservice_edit', defaults: ['duplicate' => false])]
|
||||
#[Route('/admin/additional-service/{id}/duplicate', name: 'app_admin_additionalservice_duplicate', defaults: ['duplicate' => true])]
|
||||
public function edit(AdditionalService $additionalService, bool $duplicate, Request $request): Response
|
||||
{
|
||||
$accommodation = $additionalService->getAccommodation();
|
||||
|
||||
if (true === $duplicate) {
|
||||
$additionalService = clone $additionalService;
|
||||
}
|
||||
|
||||
$form = $this->getForm($additionalService, $accommodation);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
if (true === $duplicate) {
|
||||
$accommodation->addAdditionalService($additionalService);
|
||||
}
|
||||
|
||||
$this->entityManager->persist($additionalService);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->addFlash('success', 'Die Zusatzleistung wurde aktualisiert');
|
||||
|
||||
$this->logger->info('Edited additional service', [
|
||||
'name' => $accommodation->getName(),
|
||||
'code' => $accommodation->getCode(),
|
||||
'label' => $additionalService->getLabel(),
|
||||
]);
|
||||
|
||||
return $this->redirectToRoute('app_admin_accommodation_edit', [
|
||||
'id' => $accommodation->getId(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->render('admin/additional_service/edit.html.twig', [
|
||||
'accommodation' => $accommodation,
|
||||
'additionalService' => $additionalService,
|
||||
'form' => $form,
|
||||
'duplicate' => $duplicate,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/additional-service/{id}/delete', name: 'app_admin_additionalservice_delete')]
|
||||
public function delete(AdditionalService $additionalService, Request $request): Response
|
||||
{
|
||||
$accommodation = $additionalService->getAccommodation();
|
||||
|
||||
if (true === $request->isMethod(Request::METHOD_POST)) {
|
||||
$this->entityManager->remove($additionalService);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->addFlash('success', 'Die Zusatzleistung wurde gelöscht');
|
||||
|
||||
$this->logger->info('Deleted additional service', [
|
||||
'name' => $accommodation->getName(),
|
||||
'code' => $accommodation->getCode(),
|
||||
'label' => $additionalService->getLabel(),
|
||||
]);
|
||||
|
||||
$returnUrl = $this->generateUrl('app_admin_accommodation_edit', [
|
||||
'id' => $accommodation->getId(),
|
||||
]);
|
||||
|
||||
return new HxRedirectResponse($returnUrl);
|
||||
}
|
||||
|
||||
return $this->render('admin/additional_service/modal_delete.html.twig', [
|
||||
'additionalService' => $additionalService,
|
||||
'accommodation' => $accommodation,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/additional-service/sort/{id}', name: 'app_admin_additionalservice_sort', methods: ['POST'])]
|
||||
public function sort(Accommodation $accommodation, Request $request): Response
|
||||
{
|
||||
$this->applySorting($accommodation->getAdditionalServices(), $request->request->all('sorting'));
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new Response('', Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
private function getForm(AdditionalService $additionalService, ?Accommodation $accommodation): FormInterface
|
||||
{
|
||||
return $this->createForm(AdditionalServiceType::class, $additionalService, ['currency' => $accommodation->getCurrency()]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Admin;
|
||||
|
||||
use App\Entity\Groups\Accommodation;
|
||||
use App\Entity\Groups\BoardService;
|
||||
use App\Form\Admin\Groups\BoardServiceType;
|
||||
use App\Htmx\HxRedirectResponse;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class BoardServiceController extends AbstractController
|
||||
{
|
||||
use SortingControllerTrait;
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/admin/board-service/{id}/create', name: 'app_admin_boardservice_create')]
|
||||
public function create(Accommodation $accommodation, Request $request): Response
|
||||
{
|
||||
$boardService = new BoardService();
|
||||
|
||||
$form = $this->getForm($boardService, $accommodation);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$accommodation->addBoardService($boardService);
|
||||
|
||||
$this->entityManager->persist($boardService);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->addFlash('success', 'Die Verpflegungsleistung wurde angelegt');
|
||||
|
||||
$this->logger->info('Created board service', [
|
||||
'name' => $accommodation->getName(),
|
||||
'code' => $accommodation->getCode(),
|
||||
'label' => $boardService->getLabel(),
|
||||
]);
|
||||
|
||||
return $this->redirectToRoute('app_admin_accommodation_edit', [
|
||||
'id' => $accommodation->getId(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->render('admin/board_service/create.html.twig', [
|
||||
'accommodation' => $accommodation,
|
||||
'form' => $form,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/board-service/{id}/edit', name: 'app_admin_boardservice_edit', defaults: ['duplicate' => false])]
|
||||
#[Route('/admin/board-service/{id}/duplicate', name: 'app_admin_boardservice_duplicate', defaults: ['duplicate' => true])]
|
||||
public function edit(BoardService $boardService, bool $duplicate, Request $request): Response
|
||||
{
|
||||
$accommodation = $boardService->getAccommodation();
|
||||
|
||||
if (true === $duplicate) {
|
||||
$boardService = clone $boardService;
|
||||
}
|
||||
|
||||
$form = $this->getForm($boardService, $accommodation);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
if (true === $duplicate) {
|
||||
$accommodation->addBoardService($boardService);
|
||||
}
|
||||
|
||||
$this->entityManager->persist($boardService);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->addFlash('success', 'Die Verpflegungsleistung wurde aktualisiert');
|
||||
|
||||
$this->logger->info('Edited board service', [
|
||||
'name' => $accommodation->getName(),
|
||||
'code' => $accommodation->getCode(),
|
||||
'label' => $boardService->getLabel(),
|
||||
]);
|
||||
|
||||
return $this->redirectToRoute('app_admin_accommodation_edit', [
|
||||
'id' => $accommodation->getId(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->render('admin/board_service/edit.html.twig', [
|
||||
'accommodation' => $accommodation,
|
||||
'boardService' => $boardService,
|
||||
'form' => $form,
|
||||
'duplicate' => $duplicate,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/board-service/{id}/delete', name: 'app_admin_boardservice_delete')]
|
||||
public function delete(BoardService $boardService, Request $request): Response
|
||||
{
|
||||
$accommodation = $boardService->getAccommodation();
|
||||
|
||||
if (true === $request->isMethod(Request::METHOD_POST)) {
|
||||
$this->entityManager->remove($boardService);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->addFlash('success', 'Die Verpflegungsleistung wurde gelöscht');
|
||||
|
||||
$this->logger->info('Deleted board service', [
|
||||
'name' => $accommodation->getName(),
|
||||
'code' => $accommodation->getCode(),
|
||||
'label' => $boardService->getLabel(),
|
||||
]);
|
||||
|
||||
$returnUrl = $this->generateUrl('app_admin_accommodation_edit', [
|
||||
'id' => $accommodation->getId(),
|
||||
]);
|
||||
|
||||
return new HxRedirectResponse($returnUrl);
|
||||
}
|
||||
|
||||
return $this->render('admin/board_service/modal_delete.html.twig', [
|
||||
'boardService' => $boardService,
|
||||
'accommodation' => $accommodation,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/board-service/sort/{id}', name: 'app_admin_boardservice_sort', methods: ['POST'])]
|
||||
public function sort(Accommodation $accommodation, Request $request): Response
|
||||
{
|
||||
$this->applySorting($accommodation->getBoardServices(), $request->request->all('sorting'));
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new Response('', Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
private function getForm(BoardService $boardService, ?Accommodation $accommodation): FormInterface
|
||||
{
|
||||
return $this->createForm(BoardServiceType::class, $boardService, ['currency' => $accommodation->getCurrency()]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Admin;
|
||||
|
||||
use App\Entity\Groups\SortableEntityInterface;
|
||||
|
||||
trait SortingControllerTrait
|
||||
{
|
||||
private function applySorting(iterable $collection, array $sortingMap): void
|
||||
{
|
||||
foreach ($collection as $entity) {
|
||||
/** @var SortableEntityInterface $entity */
|
||||
if (isset($sortingMap[$entity->getId()])) {
|
||||
$entity->setSorting((int) $sortingMap[$entity->getId()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ use App\Repository\Groups\AccommodationRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ORM\Entity(repositoryClass: AccommodationRepository::class)]
|
||||
class Accommodation implements BlameableEntityInterface, TimestampableEntityInterface
|
||||
@@ -23,36 +24,45 @@ class Accommodation implements BlameableEntityInterface, TimestampableEntityInte
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
#[Assert\NotBlank(message: 'required')]
|
||||
private ?string $name = null;
|
||||
|
||||
#[ORM\Column(length: 16, unique: true)]
|
||||
#[Assert\NotBlank(message: 'required')]
|
||||
private ?string $code = null;
|
||||
|
||||
#[ORM\Column]
|
||||
#[Assert\NotNull(message: 'required')]
|
||||
private ?int $minPaxSummer = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, AccommodationPrice>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: AccommodationPrice::class, mappedBy: 'accommodation')]
|
||||
#[ORM\OneToMany(targetEntity: AccommodationPrice::class, mappedBy: 'accommodation', cascade: ['all'])]
|
||||
#[ORM\OrderBy(['dateFrom' => 'ASC'])]
|
||||
private Collection $accommodationPrices;
|
||||
|
||||
/**
|
||||
* @var Collection<int, BoardService>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: BoardService::class, mappedBy: 'accommodation')]
|
||||
#[ORM\OneToMany(targetEntity: BoardService::class, mappedBy: 'accommodation', cascade: ['all'])]
|
||||
#[ORM\OrderBy(['sorting' => 'ASC'])]
|
||||
private Collection $boardServices;
|
||||
|
||||
/**
|
||||
* @var Collection<int, AdditionalService>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: AdditionalService::class, mappedBy: 'accommodation')]
|
||||
#[ORM\OneToMany(targetEntity: AdditionalService::class, mappedBy: 'accommodation', cascade: ['all'])]
|
||||
#[ORM\OrderBy(['sorting' => 'ASC'])]
|
||||
private Collection $additionalServices;
|
||||
|
||||
#[ORM\Column]
|
||||
#[Assert\NotNull(message: 'required')]
|
||||
private ?int $maxAdolescentAge = null;
|
||||
|
||||
#[ORM\Column(length: 3)]
|
||||
#[Assert\NotBlank(message: 'required')]
|
||||
#[Assert\Choice(choices: ['EUR', 'CHF'], message: 'invalid')]
|
||||
private ?string $currency = 'EUR';
|
||||
|
||||
public function __construct()
|
||||
|
||||
@@ -9,6 +9,8 @@ use App\Entity\TimestampableEntityInterface;
|
||||
use App\Repository\Groups\AccommodationPriceRepository;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
|
||||
#[ORM\Entity(repositoryClass: AccommodationPriceRepository::class)]
|
||||
class AccommodationPrice implements BlameableEntityInterface, TimestampableEntityInterface
|
||||
@@ -26,23 +28,44 @@ class AccommodationPrice implements BlameableEntityInterface, TimestampableEntit
|
||||
private ?Accommodation $accommodation = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATE_IMMUTABLE)]
|
||||
#[Assert\NotNull(message: 'required')]
|
||||
private ?\DateTimeImmutable $dateFrom = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATE_IMMUTABLE)]
|
||||
#[Assert\NotNull(message: 'required')]
|
||||
#[Assert\GreaterThan(propertyPath: 'dateFrom', message: 'Das Bis-Datum muss nach dem Von-Datum liegen')]
|
||||
private ?\DateTimeImmutable $dateTo = null;
|
||||
|
||||
#[ORM\Column]
|
||||
#[Assert\NotNull(message: 'required')]
|
||||
private ?int $includedPax = null;
|
||||
|
||||
#[ORM\Column]
|
||||
#[Assert\NotNull(message: 'required')]
|
||||
private ?int $pricePerNight = null;
|
||||
|
||||
#[ORM\Column]
|
||||
#[Assert\NotNull(message: 'required')]
|
||||
private ?int $priceAdditionalPerson = null;
|
||||
|
||||
#[ORM\Column]
|
||||
#[Assert\NotNull(message: 'required')]
|
||||
private ?int $minNights = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $pricePerNightOverride = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $priceAdditionalPersonOverride = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private bool $acceptUndersubscription = false;
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
$this->id = null;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
@@ -131,4 +154,71 @@ class AccommodationPrice implements BlameableEntityInterface, TimestampableEntit
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPricePerNightOverride(): ?int
|
||||
{
|
||||
return $this->pricePerNightOverride;
|
||||
}
|
||||
|
||||
public function setPricePerNightOverride(?int $pricePerNightOverride): self
|
||||
{
|
||||
$this->pricePerNightOverride = $pricePerNightOverride;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEffectivePricePerNight(): int
|
||||
{
|
||||
return $this->pricePerNightOverride ?? $this->pricePerNight;
|
||||
}
|
||||
|
||||
public function getPriceAdditionalPersonOverride(): ?int
|
||||
{
|
||||
return $this->priceAdditionalPersonOverride;
|
||||
}
|
||||
|
||||
public function setPriceAdditionalPersonOverride(?int $priceAdditionalPersonOverride): self
|
||||
{
|
||||
$this->priceAdditionalPersonOverride = $priceAdditionalPersonOverride;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEffectivePriceAdditionalPerson(): int
|
||||
{
|
||||
return $this->priceAdditionalPersonOverride ?? $this->priceAdditionalPerson;
|
||||
}
|
||||
|
||||
public function isAcceptUndersubscription(): bool
|
||||
{
|
||||
return $this->acceptUndersubscription;
|
||||
}
|
||||
|
||||
public function setAcceptUndersubscription(bool $acceptUndersubscription): self
|
||||
{
|
||||
$this->acceptUndersubscription = $acceptUndersubscription;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
#[Assert\Callback]
|
||||
public function assertDatesValid(ExecutionContextInterface $context): void
|
||||
{
|
||||
// we only check new entities
|
||||
if (null !== $this->getId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// come on, we need something to work with
|
||||
if (null === $this->getDateFrom() || null === $this->getDateTo()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->getDateFrom() < new \DateTimeImmutable()) {
|
||||
$context
|
||||
->buildViolation('Das Von-Datum darf nicht in der Vergangenheit liegen')
|
||||
->atPath('dateFrom')
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: AdditionalServiceRepository::class)]
|
||||
class AdditionalService implements BlameableEntityInterface, TimestampableEntityInterface
|
||||
class AdditionalService implements BlameableEntityInterface, TimestampableEntityInterface, SortableEntityInterface
|
||||
{
|
||||
use BlameableEntity;
|
||||
use TimestampableEntity;
|
||||
@@ -41,12 +41,17 @@ class AdditionalService implements BlameableEntityInterface, TimestampableEntity
|
||||
#[ORM\Column(type: Types::DATE_IMMUTABLE)]
|
||||
private ?\DateTimeImmutable $dateTo = null;
|
||||
|
||||
#[ORM\Column(length: 128)]
|
||||
private ?string $grouping = null;
|
||||
#[ORM\Column(length: 128, nullable: true)]
|
||||
private ?string $selectionGroup = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?int $sorting = 0;
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
$this->id = null;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
@@ -124,14 +129,14 @@ class AdditionalService implements BlameableEntityInterface, TimestampableEntity
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getGrouping(): ?string
|
||||
public function getSelectionGroup(): ?string
|
||||
{
|
||||
return $this->grouping;
|
||||
return $this->selectionGroup;
|
||||
}
|
||||
|
||||
public function setGrouping(?string $grouping): self
|
||||
public function setSelectionGroup(?string $selectionGroup): self
|
||||
{
|
||||
$this->grouping = $grouping;
|
||||
$this->selectionGroup = $selectionGroup;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: BoardServiceRepository::class)]
|
||||
class BoardService implements BlameableEntityInterface, TimestampableEntityInterface
|
||||
class BoardService implements BlameableEntityInterface, TimestampableEntityInterface, SortableEntityInterface
|
||||
{
|
||||
use BlameableEntity;
|
||||
use TimestampableEntity;
|
||||
@@ -40,6 +40,11 @@ class BoardService implements BlameableEntityInterface, TimestampableEntityInter
|
||||
#[ORM\Column]
|
||||
private ?int $sorting = 0;
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
$this->id = null;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity\Groups;
|
||||
|
||||
interface SortableEntityInterface
|
||||
{
|
||||
public function getId(): ?int;
|
||||
|
||||
public function setSorting(?int $sorting): self;
|
||||
}
|
||||
@@ -8,4 +8,9 @@ enum AdditionalServiceType: string
|
||||
case PerPerson = 'per_person';
|
||||
case PerNight = 'per_night';
|
||||
case PerPersonPerNight = 'per_person_per_night';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return 'enum.additional_service.'.$this->value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Admin\Groups;
|
||||
|
||||
use App\Entity\Groups\AccommodationPrice;
|
||||
use App\Form\DatepickerType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class AccommodationPriceType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('dateFrom', DatepickerType::class, [
|
||||
'label' => 'Datum von',
|
||||
'html5' => true,
|
||||
'widget' => 'single_text',
|
||||
])
|
||||
->add('dateTo', DatepickerType::class, [
|
||||
'label' => 'Datum bis',
|
||||
'html5' => true,
|
||||
'widget' => 'single_text',
|
||||
])
|
||||
->add('includedPax', IntegerType::class, [
|
||||
'label' => 'Inklusiv-Personen',
|
||||
])
|
||||
->add('minNights', IntegerType::class, [
|
||||
'label' => 'Mindestbelegung (Nächte)',
|
||||
])
|
||||
->add('pricePerNight', MoneyType::class, [
|
||||
'label' => 'Preis pro Nacht',
|
||||
'currency' => $options['currency'],
|
||||
'divisor' => 100,
|
||||
])
|
||||
->add('pricePerNightOverride', MoneyType::class, [
|
||||
'label' => 'Streich-Preis pro Nacht',
|
||||
'required' => false,
|
||||
'currency' => $options['currency'],
|
||||
'divisor' => 100,
|
||||
])
|
||||
->add('priceAdditionalPerson', MoneyType::class, [
|
||||
'label' => 'Preis weitere Person',
|
||||
'currency' => $options['currency'],
|
||||
'divisor' => 100,
|
||||
])
|
||||
->add('priceAdditionalPersonOverride', MoneyType::class, [
|
||||
'label' => 'Streich-Preis weitere Person',
|
||||
'required' => false,
|
||||
'currency' => $options['currency'],
|
||||
'divisor' => 100,
|
||||
])
|
||||
->add('acceptUndersubscription', CheckboxType::class, [
|
||||
'label' => 'Unterbelegung akzeptieren',
|
||||
'required' => false,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => AccommodationPrice::class,
|
||||
'currency' => 'EUR',
|
||||
]);
|
||||
$resolver->setAllowedValues('currency', ['EUR', 'CHF']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Admin\Groups;
|
||||
|
||||
use App\Entity\Groups\Accommodation;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class AccommodationType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('name', TextType::class, [
|
||||
'label' => 'Name',
|
||||
])
|
||||
->add('code', TextType::class, [
|
||||
'label' => 'Code',
|
||||
])
|
||||
->add('minPaxSummer', IntegerType::class, [
|
||||
'label' => 'Mindestbelegung Sommer',
|
||||
])
|
||||
->add('maxAdolescentAge', IntegerType::class, [
|
||||
'label' => 'Altersgrenze Jugendliche (6-X Jahre)',
|
||||
])
|
||||
->add('currency', ChoiceType::class, [
|
||||
'label' => 'Währung',
|
||||
'choices' => [
|
||||
'EUR' => 'EUR',
|
||||
'CHF' => 'CHF',
|
||||
],
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Accommodation::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Admin\Groups;
|
||||
|
||||
use App\Entity\Groups\AdditionalService;
|
||||
use App\Enum\Groups\AdditionalServiceType as AdditionalServiceTypeEnum;
|
||||
use App\Form\DatepickerType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EnumType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class AdditionalServiceType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('label', TextType::class, [
|
||||
'label' => 'Bezeichnung',
|
||||
])
|
||||
->add('type', EnumType::class, [
|
||||
'label' => 'Preistyp',
|
||||
'class' => AdditionalServiceTypeEnum::class,
|
||||
'choice_label' => fn($item) => $item->label(),
|
||||
])
|
||||
->add('price', MoneyType::class, [
|
||||
'label' => 'Preis',
|
||||
'currency' => $options['currency'],
|
||||
'divisor' => 100,
|
||||
])
|
||||
->add('selectionGroup', TextType::class, [
|
||||
'label' => 'Auswahlgruppe',
|
||||
'required' => false,
|
||||
])
|
||||
->add('dateFrom', DatepickerType::class, [
|
||||
'label' => 'Datum von',
|
||||
'html5' => true,
|
||||
'widget' => 'single_text',
|
||||
])
|
||||
->add('dateTo', DatepickerType::class, [
|
||||
'label' => 'Datum bis',
|
||||
'html5' => true,
|
||||
'widget' => 'single_text',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => AdditionalService::class,
|
||||
'currency' => 'EUR',
|
||||
]);
|
||||
$resolver->setAllowedValues('currency', ['EUR', 'CHF']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Admin\Groups;
|
||||
|
||||
use App\Entity\Groups\BoardService;
|
||||
use App\Form\DatepickerType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class BoardServiceType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('label', TextType::class, [
|
||||
'label' => 'Bezeichnung',
|
||||
])
|
||||
->add('price', MoneyType::class, [
|
||||
'label' => 'Preis pro Person/Nacht',
|
||||
'currency' => $options['currency'],
|
||||
'divisor' => 100,
|
||||
])
|
||||
->add('dateFrom', DatepickerType::class, [
|
||||
'label' => 'Datum von',
|
||||
'html5' => true,
|
||||
'widget' => 'single_text',
|
||||
])
|
||||
->add('dateTo', DatepickerType::class, [
|
||||
'label' => 'Datum bis',
|
||||
'html5' => true,
|
||||
'widget' => 'single_text',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => BoardService::class,
|
||||
'currency' => 'EUR',
|
||||
]);
|
||||
$resolver->setAllowedValues('currency', ['EUR', 'CHF']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class DatepickerType extends AbstractType
|
||||
{
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'html5' => false,
|
||||
'widget' => 'single_text',
|
||||
'input' => 'datetime_immutable',
|
||||
'min_date' => null,
|
||||
'max_date' => null,
|
||||
'disable_weekends' => false,
|
||||
]);
|
||||
$resolver->setAllowedTypes('min_date', ['null', \DateTimeImmutable::class]);
|
||||
$resolver->setAllowedTypes('max_date', ['null', \DateTimeImmutable::class]);
|
||||
$resolver->setAllowedTypes('disable_weekends', 'bool');
|
||||
}
|
||||
|
||||
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
$view->vars['min_date'] = $options['min_date'];
|
||||
$view->vars['max_date'] = $options['max_date'];
|
||||
$view->vars['disable_weekends'] = $options['disable_weekends'];
|
||||
}
|
||||
|
||||
public function getParent(): string
|
||||
{
|
||||
return DateType::class;
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ class AdminMenuBuilder extends AbstractMenuBuilder
|
||||
],
|
||||
'extras' => [
|
||||
'icon' => 'edit',
|
||||
'routes' => [['pattern' => '/^app_admin_bookingeditdraft/']],
|
||||
],
|
||||
]);
|
||||
$menu->addChild('Benutzer', [
|
||||
@@ -40,6 +41,16 @@ class AdminMenuBuilder extends AbstractMenuBuilder
|
||||
'icon' => 'users',
|
||||
],
|
||||
]);
|
||||
$menu->addChild('Gruppenhäuser', [
|
||||
'route' => 'app_admin_accommodation',
|
||||
'linkAttributes' => [
|
||||
'title' => 'Gruppenhäuser',
|
||||
],
|
||||
'extras' => [
|
||||
'icon' => 'house',
|
||||
'routes' => [['pattern' => '/^app_admin_accommodation/']],
|
||||
],
|
||||
]);
|
||||
$menu->addChild('Logs', [
|
||||
'route' => 'app_admin_log',
|
||||
'linkAttributes' => [
|
||||
@@ -47,6 +58,7 @@ class AdminMenuBuilder extends AbstractMenuBuilder
|
||||
],
|
||||
'extras' => [
|
||||
'icon' => 'list',
|
||||
'routes' => [['pattern' => '/^app_admin_log/']],
|
||||
],
|
||||
]);
|
||||
|
||||
|
||||
@@ -2,13 +2,18 @@
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class CmsDataProvider
|
||||
{
|
||||
public function __construct(private readonly HttpClientInterface $httpClient, private readonly string $apiKey)
|
||||
{
|
||||
public function __construct(
|
||||
private readonly HttpClientInterface $httpClient,
|
||||
private readonly CacheInterface $cache,
|
||||
private readonly string $apiKey,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,33 +32,98 @@ class CmsDataProvider
|
||||
return $result['hotel']['images'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function getHotelImages(string $hotelCode): ?array
|
||||
{
|
||||
$result = $this->getHotelDetails($hotelCode);
|
||||
|
||||
if (true === isset($result['success']) && false === $result['success']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $result['images'] ?? null;
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function getHotelDetails(string $hotelCode): array
|
||||
{
|
||||
return $this->cache->get(
|
||||
sprintf('cms_hotel_%s', $hotelCode),
|
||||
function (ItemInterface $item) use ($hotelCode): array {
|
||||
$item->expiresAfter(3600);
|
||||
|
||||
try {
|
||||
$request = $this->httpClient->request('GET', 'api/hotel', [
|
||||
'query' => [
|
||||
'hotel' => $hotelCode,
|
||||
'key' => $this->apiKey,
|
||||
],
|
||||
]);
|
||||
} catch (ExceptionInterface $e) {
|
||||
$item->expiresAfter(0);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
$data = $request->toArray();
|
||||
} catch (ExceptionInterface $e) {
|
||||
$item->expiresAfter(0);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function getProductDetails(string $productCode, ?string $hotelCode = null): array
|
||||
{
|
||||
try {
|
||||
$request = $this->httpClient->request('GET', 'api/product', [
|
||||
'query' => [
|
||||
'product' => $productCode,
|
||||
'hotel' => $hotelCode,
|
||||
'key' => $this->apiKey,
|
||||
],
|
||||
]);
|
||||
} catch (ExceptionInterface $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
return $this->cache->get(
|
||||
sprintf('cms_product_%s_%s', $productCode, $hotelCode ?? ''),
|
||||
function (ItemInterface $item) use ($productCode, $hotelCode): array {
|
||||
$item->expiresAfter(3600);
|
||||
|
||||
try {
|
||||
$data = $request->toArray();
|
||||
} catch (ExceptionInterface $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
try {
|
||||
$request = $this->httpClient->request('GET', 'api/product', [
|
||||
'query' => [
|
||||
'product' => $productCode,
|
||||
'hotel' => $hotelCode,
|
||||
'key' => $this->apiKey,
|
||||
],
|
||||
]);
|
||||
} catch (ExceptionInterface $e) {
|
||||
$item->expiresAfter(0);
|
||||
|
||||
return $data;
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
$data = $request->toArray();
|
||||
} catch (ExceptionInterface $e) {
|
||||
$item->expiresAfter(0);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -358,6 +358,18 @@
|
||||
"config/packages/uid.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/ux-twig-component": {
|
||||
"version": "2.36",
|
||||
"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": {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{% if app.session.flashBag.peek('error')|length > 0 %}
|
||||
{% include '_partials/_toast.html.twig' with { level: 'warning', messages: app.flashes('error') } %}
|
||||
{% endif %}
|
||||
{% if app.session.flashBag.peek('success')|length > 0 %}
|
||||
{% include '_partials/_toast.html.twig' with { level: 'success', messages: app.flashes('success') } %}
|
||||
{% endif %}
|
||||
{% if app.session.flashBag.peek('info')|length > 0 %}
|
||||
{% include '_partials/_toast.html.twig' with { level: 'info', messages: app.flashes('info') } %}
|
||||
{% endif %}
|
||||
@@ -41,9 +41,10 @@
|
||||
{% import _self as knp_menu %}
|
||||
<a href="{{ item.uri }}"
|
||||
class="{{ html_classes('', {
|
||||
'group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold text-gray-700 hover:bg-gray-50': item.level == 1,
|
||||
'block rounded-md py-2 pr-2 pl-9 text-sm leading-6 text-gray-700 hover:bg-gray-50': item.level > 1,
|
||||
'bg-gray-50': matcher.isCurrent(item) or matcher.isAncestor(item)
|
||||
'group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold': item.level == 1,
|
||||
'block rounded-md py-2 pr-2 pl-9 text-sm leading-6': item.level > 1,
|
||||
'text-gray-700 hover:bg-gray-50': not (matcher.isCurrent(item) or matcher.isAncestor(item)),
|
||||
'bg-gray-100 text-gray-900': matcher.isCurrent(item) or matcher.isAncestor(item),
|
||||
}) }}"
|
||||
{{ knp_menu.attributes(item.linkAttributes) }}>
|
||||
{{ block('label') }}
|
||||
@@ -73,7 +74,7 @@
|
||||
|
||||
{% block label %}
|
||||
{% if item.extras.icon is defined %}
|
||||
{{ icon(item.extras.icon, 'h-5 w-5 shrink-0 text-gray-400') }}
|
||||
{{ icon(item.extras.icon, 'h-5 w-5 shrink-0 ' ~ ((matcher.isCurrent(item) or matcher.isAncestor(item)) ? 'text-gray-600' : 'text-gray-400')) }}
|
||||
<span class="flex-1">{{ item.label|raw }}</span>
|
||||
{% else %}
|
||||
{{ item.label|raw }}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{% apply spaceless %}
|
||||
{% for message in messages %}
|
||||
{% if message is iterable %}
|
||||
{% set message = message.id | trans(message.parameters | default({}), message.domain | default(null), message.locale | default(null)) | raw %}
|
||||
{% else %}
|
||||
{% set message = message | trans | raw %}
|
||||
{% endif %}
|
||||
<div {{ stimulus_controller('toast', { 'text': message, 'class': 'toastify--' ~ level}) }}{{ qa_attribute('toast', level)}}></div>
|
||||
{% endfor %}
|
||||
{% endapply %}
|
||||
@@ -0,0 +1,296 @@
|
||||
{% form_theme form 'forms_admin.html.twig' %}
|
||||
|
||||
<h2 class="text-xl font-bold">
|
||||
Allgemein
|
||||
</h2>
|
||||
|
||||
{{ form_start(form) }}
|
||||
<div class="grid lg:grid-cols-2 gap-y-4 lg:gap-x-8">
|
||||
{{ form_row(form.name) }}
|
||||
{{ form_row(form.code) }}
|
||||
{{ form_row(form.minPaxSummer) }}
|
||||
{{ form_row(form.maxAdolescentAge) }}
|
||||
{{ form_row(form.currency) }}
|
||||
</div>
|
||||
<div class="flex justify-between pt-8">
|
||||
<a href="{{ path('app_admin_accommodation') }}" class="button button--secondary button--small">
|
||||
zurück
|
||||
</a>
|
||||
<button type="submit" class="button button--primary button--small">
|
||||
speichern
|
||||
</button>
|
||||
</div>
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
|
||||
<hr class="my-8">
|
||||
|
||||
<h2 class="text-xl font-bold">
|
||||
Preise
|
||||
</h2>
|
||||
<div class="data-table-wrapper mb-4">
|
||||
<div class="data-table-wrapper__inner text-sm">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>
|
||||
Datums-Bereich
|
||||
</th>
|
||||
<th>
|
||||
Preis pro Nacht
|
||||
</th>
|
||||
<th>
|
||||
Preise weitere Person
|
||||
</th>
|
||||
<th>
|
||||
Mindestbelegung (Nächte)
|
||||
</th>
|
||||
<th>
|
||||
Unterbelegung
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for price in accommodation.accommodationPrices %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
{{ price.dateFrom | date('d.m.Y') }} - {{ price.dateTo | date('d.m.Y') }}
|
||||
</td>
|
||||
<td>
|
||||
{{ (price.effectivePricePerNight / 100) | format_currency(accommodation.currency) }}
|
||||
{% if price.pricePerNightOverride is not null %}
|
||||
<span class="line-through text-xs">{{ (price.pricePerNight / 100) | format_currency(accommodation.currency) }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ (price.effectivePriceAdditionalPerson / 100) | format_currency(accommodation.currency) }}
|
||||
{% if price.priceAdditionalPersonOverride is not null %}
|
||||
<span class="line-through text-xs">{{ (price.priceAdditionalPerson / 100) | format_currency(accommodation.currency) }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ price.minNights }}
|
||||
</td>
|
||||
<td>
|
||||
{% if price.acceptUndersubscription %}
|
||||
{{ icon('check', 'w-4 h-4') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<twig:dropdown>
|
||||
<twig:dropdown:link url="{{ path('app_admin_accommodationprice_edit', { 'id': price.id }) }}">
|
||||
bearbeiten
|
||||
</twig:dropdown:link>
|
||||
<twig:dropdown:link url="{{ path('app_admin_accommodationprice_duplicate', { 'id': price.id }) }}">
|
||||
duplizieren
|
||||
</twig:dropdown:link>
|
||||
<twig:dropdown:hxbutton url="{{ path('app_admin_accommodationprice_delete', { 'id': price.id }) }}" warning>
|
||||
löschen
|
||||
</twig:dropdown:hxbutton>
|
||||
</twig:dropdown>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
Keine Daten
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<a href="{{ path('app_admin_accommodationprice_create', { 'id': accommodation.id }) }}" class="button button--primary button--small">
|
||||
neu
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<hr class="my-8">
|
||||
|
||||
<h2 class="text-xl font-bold">
|
||||
Verpflegungsleistungen
|
||||
</h2>
|
||||
<div class="data-table-wrapper mb-4">
|
||||
<div class="data-table-wrapper__inner text-sm">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>
|
||||
Datums-Bereich
|
||||
</th>
|
||||
<th>
|
||||
Bezeichnung
|
||||
</th>
|
||||
<th>
|
||||
Preis pro Person/Nacht
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-controller="sortable" data-sortable-url-value="{{ path('app_admin_boardservice_sort', { 'id': accommodation.id }) }}">
|
||||
{% for service in accommodation.boardServices %}
|
||||
<tr data-sortable-target="item">
|
||||
<td class="cursor-grab w-6" data-sortable-target="handle">
|
||||
{{ icon('draggable', 'w-4 h-4') }}
|
||||
</td>
|
||||
<td>
|
||||
{{ service.dateFrom | date('d.m.Y') }} - {{ service.dateTo | date('d.m.Y') }}
|
||||
</td>
|
||||
<td>
|
||||
{{ service.label }}
|
||||
<input type="hidden" name="sorting[{{ service.id }}]" value="{{ loop.index }}" data-sortable-target="positionField" data-id="{{ service.id }}">
|
||||
</td>
|
||||
<td>
|
||||
{{ (service.price / 100) | format_currency(accommodation.currency) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<twig:dropdown>
|
||||
<twig:dropdown:link url="{{ path('app_admin_boardservice_edit', { 'id': service.id }) }}">
|
||||
bearbeiten
|
||||
</twig:dropdown:link>
|
||||
<twig:dropdown:link url="{{ path('app_admin_boardservice_duplicate', { 'id': service.id }) }}">
|
||||
duplizieren
|
||||
</twig:dropdown:link>
|
||||
<twig:dropdown:hxbutton url="{{ path('app_admin_boardservice_delete', { 'id': service.id }) }}" warning>
|
||||
löschen
|
||||
</twig:dropdown:hxbutton>
|
||||
</twig:dropdown>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
Keine Daten
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<a href="{{ path('app_admin_boardservice_create', { 'id': accommodation.id }) }}" class="button button--primary button--small">
|
||||
neu
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<hr class="my-8">
|
||||
|
||||
<h2 class="text-xl font-bold">
|
||||
Optionale Zusatzleistungen
|
||||
</h2>
|
||||
<div class="data-table-wrapper mb-4">
|
||||
<div class="data-table-wrapper__inner text-sm">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>
|
||||
Datums-Bereich
|
||||
</th>
|
||||
<th>
|
||||
Bezeichnung
|
||||
</th>
|
||||
<th>
|
||||
Preistyp
|
||||
</th>
|
||||
<th>
|
||||
Preis
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-controller="sortable" data-sortable-url-value="{{ path('app_admin_additionalservice_sort', { 'id': accommodation.id }) }}">
|
||||
{% for service in accommodation.additionalServices %}
|
||||
<tr data-sortable-target="item">
|
||||
<td class="cursor-grab w-6" data-sortable-target="handle">
|
||||
{{ icon('draggable', 'w-4 h-4') }}
|
||||
</td>
|
||||
<td>
|
||||
{{ service.dateFrom | date('d.m.Y') }} - {{ service.dateTo | date('d.m.Y') }}
|
||||
</td>
|
||||
<td>
|
||||
{{ service.label }}
|
||||
<input type="hidden" name="sorting[{{ service.id }}]" value="{{ loop.index }}" data-sortable-target="positionField" data-id="{{ service.id }}">
|
||||
</td>
|
||||
<td>
|
||||
{{ service.type.label | trans }}
|
||||
</td>
|
||||
<td>
|
||||
{{ (service.price / 100) | format_currency(accommodation.currency) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<twig:dropdown>
|
||||
<twig:dropdown:link url="{{ path('app_admin_additionalservice_edit', { 'id': service.id }) }}">
|
||||
bearbeiten
|
||||
</twig:dropdown:link>
|
||||
<twig:dropdown:link url="{{ path('app_admin_additionalservice_duplicate', { 'id': service.id }) }}">
|
||||
duplizieren
|
||||
</twig:dropdown:link>
|
||||
<twig:dropdown:hxbutton url="{{ path('app_admin_additionalservice_delete', { 'id': service.id }) }}" warning>
|
||||
löschen
|
||||
</twig:dropdown:hxbutton>
|
||||
</twig:dropdown>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="6">
|
||||
Keine Daten
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<a href="{{ path('app_admin_additionalservice_create', { 'id': accommodation.id }) }}" class="button button--primary button--small">
|
||||
neu
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<hr class="my-8">
|
||||
|
||||
{% if cmsData is defined %}
|
||||
<h2 class="text-xl font-bold">
|
||||
CMS-Daten
|
||||
</h2>
|
||||
<div class="grid lg:grid-cols-2 gap-y-4 lg:gap-x-8">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-bold">
|
||||
Adresse
|
||||
</h3>
|
||||
<strong>{{ cmsData.name }}</strong>
|
||||
<br>
|
||||
{{ cmsData.address | default('-') | nl2br }}
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-bold">
|
||||
Beschreibung
|
||||
</h3>
|
||||
<div class="[&_p]:pb-4">
|
||||
{{ cmsData.description | default('-') | raw }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-bold">
|
||||
Bilder
|
||||
</h3>
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
{% for image in cmsData.images.resized.s %}
|
||||
<img src="{{ image.url }}" alt="{{ image.alt }}" class="block w-full h-auto">
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{% extends 'layout_admin.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<twig:page:heading>Gruppenhaus neu</twig:page:heading>
|
||||
{% include 'admin/accommodation/_form.html.twig' %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,6 @@
|
||||
{% extends 'layout_admin.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<twig:page:heading>Gruppenhaus {{ accommodation.name }}</twig:page:heading>
|
||||
{% include 'admin/accommodation/_form.html.twig' %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,71 @@
|
||||
{% extends 'layout_admin.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<twig:page:heading>Gruppenhäuser</twig:page:heading>
|
||||
<div class="data-table-wrapper">
|
||||
<div class="data-table-wrapper__inner text-sm">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Name
|
||||
</th>
|
||||
<th>
|
||||
Code
|
||||
</th>
|
||||
<th>
|
||||
erstellt
|
||||
</th>
|
||||
<th>
|
||||
aktualisiert
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for accommodation in accommodations %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ accommodation.name }}
|
||||
</td>
|
||||
<td>
|
||||
{{ accommodation.code }}
|
||||
</td>
|
||||
<td>
|
||||
{{ accommodation.createdAt | date('d.m.Y, H:i') }}
|
||||
</td>
|
||||
<td>
|
||||
{% if accommodation.updatedAt is not null %}
|
||||
{{ accommodation.updatedAt | date('d.m.Y, H:i') }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<twig:dropdown>
|
||||
<twig:dropdown:link url="{{ path('app_admin_accommodation_edit', { 'id': accommodation.id, 'r': return_url() }) }}">
|
||||
bearbeiten
|
||||
</twig:dropdown:link>
|
||||
<twig:dropdown:hxbutton url="{{ path('app_admin_accommodation_delete', { 'id': accommodation.id, 'r': return_url() }) }}" warning>
|
||||
löschen
|
||||
</twig:dropdown:hxbutton>
|
||||
</twig:dropdown>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
Keine Daten...
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<a href="{{ path('app_admin_accommodation_create') }}" class="button button--primary button--small">
|
||||
neu
|
||||
</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,7 @@
|
||||
{% extends 'htmx_confirmation_modal.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
Möchtest du das Gruppenhaus <em>{{ accommodation.name }}</em> wirklich löschen?
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,24 @@
|
||||
{% form_theme form 'forms_admin.html.twig' %}
|
||||
|
||||
{{ form_start(form) }}
|
||||
<div class="grid lg:grid-cols-2 gap-y-4 lg:gap-x-8">
|
||||
{{ form_row(form.dateFrom) }}
|
||||
{{ form_row(form.dateTo) }}
|
||||
{{ form_row(form.pricePerNight) }}
|
||||
{{ form_row(form.pricePerNightOverride) }}
|
||||
{{ form_row(form.priceAdditionalPerson) }}
|
||||
{{ form_row(form.priceAdditionalPersonOverride) }}
|
||||
{{ form_row(form.includedPax) }}
|
||||
{{ form_row(form.minNights) }}
|
||||
{{ form_row(form.acceptUndersubscription) }}
|
||||
</div>
|
||||
<div class="flex justify-between pt-8">
|
||||
<a href="{{ path('app_admin_accommodation_edit', { 'id': accommodation.id }) }}" class="button button--secondary button--small">
|
||||
zurück
|
||||
</a>
|
||||
<button type="submit" class="button button--primary button--small">
|
||||
speichern
|
||||
</button>
|
||||
</div>
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
@@ -0,0 +1,6 @@
|
||||
{% extends 'layout_admin.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<twig:page:heading>Preis neu für Gruppenhaus {{ accommodation.name }}</twig:page:heading>
|
||||
{% include 'admin/accommodation_price/_form.html.twig' %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,6 @@
|
||||
{% extends 'layout_admin.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<twig:page:heading>Preis für Gruppenhaus {{ accommodation.name }}{% if duplicate %} duplizieren{% endif %}</twig:page:heading>
|
||||
{% include 'admin/accommodation_price/_form.html.twig' %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,7 @@
|
||||
{% extends 'htmx_confirmation_modal.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
Möchtest du den Gruppenhaus-Preis <em>{{ accommodation.name }}, {{ accommodationPrice.dateFrom | date('d.m.Y') }} - {{ accommodationPrice.dateTo | date('d.m.Y') }}</em> wirklich löschen?
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,21 @@
|
||||
{% form_theme form 'forms_admin.html.twig' %}
|
||||
|
||||
{{ form_start(form) }}
|
||||
<div class="grid lg:grid-cols-2 gap-y-4 lg:gap-x-8">
|
||||
{{ form_row(form.label) }}
|
||||
{{ form_row(form.type) }}
|
||||
{{ form_row(form.price) }}
|
||||
{{ form_row(form.selectionGroup) }}
|
||||
{{ form_row(form.dateFrom) }}
|
||||
{{ form_row(form.dateTo) }}
|
||||
</div>
|
||||
<div class="flex justify-between pt-8">
|
||||
<a href="{{ path('app_admin_accommodation_edit', { 'id': accommodation.id }) }}" class="button button--secondary button--small">
|
||||
zurück
|
||||
</a>
|
||||
<button type="submit" class="button button--primary button--small">
|
||||
speichern
|
||||
</button>
|
||||
</div>
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
@@ -0,0 +1,6 @@
|
||||
{% extends 'layout_admin.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<twig:page:heading>Zusatzleistung neu für Gruppenhaus {{ accommodation.name }}</twig:page:heading>
|
||||
{% include 'admin/additional_service/_form.html.twig' %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,6 @@
|
||||
{% extends 'layout_admin.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<twig:page:heading>Zusatzleistung für Gruppenhaus {{ accommodation.name }}{% if duplicate %} duplizieren{% endif %}</twig:page:heading>
|
||||
{% include 'admin/additional_service/_form.html.twig' %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,7 @@
|
||||
{% extends 'htmx_confirmation_modal.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
Möchtest du die Zusatzleistung <em>{{ additionalService.label }}, {{ accommodation.name }}</em> wirklich löschen?
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,19 @@
|
||||
{% form_theme form 'forms_admin.html.twig' %}
|
||||
|
||||
{{ form_start(form) }}
|
||||
<div class="grid lg:grid-cols-2 gap-y-4 lg:gap-x-8">
|
||||
{{ form_row(form.label) }}
|
||||
{{ form_row(form.price) }}
|
||||
{{ form_row(form.dateFrom) }}
|
||||
{{ form_row(form.dateTo) }}
|
||||
</div>
|
||||
<div class="flex justify-between pt-8">
|
||||
<a href="{{ path('app_admin_accommodation_edit', { 'id': accommodation.id }) }}" class="button button--secondary button--small">
|
||||
zurück
|
||||
</a>
|
||||
<button type="submit" class="button button--primary button--small">
|
||||
speichern
|
||||
</button>
|
||||
</div>
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
@@ -0,0 +1,6 @@
|
||||
{% extends 'layout_admin.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<twig:page:heading>Verpflegungsleistung neu für Gruppenhaus {{ accommodation.name }}</twig:page:heading>
|
||||
{% include 'admin/board_service/_form.html.twig' %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,6 @@
|
||||
{% extends 'layout_admin.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<twig:page:heading>Verpflegungsleistung für Gruppenhaus {{ accommodation.name }}{% if duplicate %} duplizieren{% endif %}</twig:page:heading>
|
||||
{% include 'admin/board_service/_form.html.twig' %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,7 @@
|
||||
{% extends 'htmx_confirmation_modal.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
Möchtest du die Verpflegungsleistung <em>{{ boardService.label }}, {{ accommodation.name }}</em> wirklich löschen?
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,9 +1,7 @@
|
||||
{% extends 'layout_admin.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="text-2xl font-bold pb-8">
|
||||
Buchungsentwürfe
|
||||
</h1>
|
||||
<twig:page:heading>Buchungsentwürfe</twig:page:heading>
|
||||
<div class="data-table-wrapper">
|
||||
<div class="data-table-wrapper__inner text-sm">
|
||||
<table class="data-table">
|
||||
@@ -44,32 +42,17 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% embed '_partials/_dropdown.html.twig' %}
|
||||
{% block content %}
|
||||
<a href="{{ path('app_admin_bookingeditdraft_show', { 'id': draft.id, 'r': return_url() }) }}"
|
||||
class="text-gray-700 hover:text-primary block px-4 py-2 text-sm"
|
||||
role="menuitem"
|
||||
tabindex="-1">
|
||||
Details
|
||||
</a>
|
||||
<a href="{{ path('app_admin_bookingeditdraft_export', { 'id': draft.id }) }}"
|
||||
class="text-gray-700 hover:text-primary block px-4 py-2 text-sm"
|
||||
role="menuitem"
|
||||
tabindex="-1">
|
||||
XLS-Export
|
||||
</a>
|
||||
<button type="button"
|
||||
class="text-red-500 hover:text-primary block px-4 py-2 text-sm w-full text-left"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
hx-get="{{ path('app_admin_bookingeditdraft_delete', { 'id': draft.id, 'r': return_url() }) }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
löschen
|
||||
</button>
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
|
||||
<twig:dropdown>
|
||||
<twig:dropdown:link url="{{ path('app_admin_bookingeditdraft_show', { 'id': draft.id, 'r': return_url() }) }}">
|
||||
Details
|
||||
</twig:dropdown:link>
|
||||
<twig:dropdown:link url="{{ path('app_admin_bookingeditdraft_export', { 'id': draft.id }) }}">
|
||||
XLS-Export
|
||||
</twig:dropdown:link>
|
||||
<twig:dropdown:hxbutton url="{{ path('app_admin_bookingeditdraft_delete', { 'id': draft.id, 'r': return_url() }) }}" warning>
|
||||
löschen
|
||||
</twig:dropdown:hxbutton>
|
||||
</twig:dropdown>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
{% extends 'layout_admin.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="text-2xl font-bold pb-8">
|
||||
Buchungsentwurf
|
||||
</h1>
|
||||
<twig:page:heading>Buchungsentwurf</twig:page:heading>
|
||||
<h2 class="text-xl font-bold pb-4">
|
||||
Vorgang {{ draft.bookingNumber }}
|
||||
</h2>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
{% extends 'layout_admin.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="text-2xl font-bold pb-8">
|
||||
Logs
|
||||
</h1>
|
||||
<twig:page:heading>Logs</twig:page:heading>
|
||||
<div class="data-table-wrapper">
|
||||
<div class="data-table-wrapper__inner text-sm">
|
||||
<table class="data-table">
|
||||
@@ -53,16 +51,11 @@
|
||||
</td>
|
||||
<td>
|
||||
{% if entry.channel == 'bpn' %}
|
||||
{% embed '_partials/_dropdown.html.twig' %}
|
||||
{% block content %}
|
||||
<a href="{{ path('app_admin_log_xmldumps', { 'id': entry.id }) }}"
|
||||
class="text-gray-700 hover:text-primary block px-4 py-2 text-sm"
|
||||
role="menuitem"
|
||||
tabindex="-1">
|
||||
<twig:dropdown>
|
||||
<twig:dropdown:link url="{{ path('app_admin_log_xmldumps', { 'id': entry.id }) }}">
|
||||
XML-Dumps
|
||||
</a>
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
</twig:dropdown:link>
|
||||
</twig:dropdown>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
{% extends 'layout_admin.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="text-2xl font-bold pb-8">
|
||||
XML-Dumps für Request ID {{ logEntry.requestId }}
|
||||
</h1>
|
||||
<twig:page:heading>XML-Dumps für Request ID {{ logEntry.requestId }}</twig:page:heading>
|
||||
{% if dumps is empty %}
|
||||
<div class="alert alert-warning">
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
@@ -37,9 +35,9 @@
|
||||
</td>
|
||||
<td>
|
||||
{% if dump.type == 'request' %}
|
||||
<span class="badge bg-primary">Request</span>
|
||||
<twig:badge>Request</twig:badge>
|
||||
{% else %}
|
||||
<span class="badge bg-success">Response</span>
|
||||
<twig:badge variant="success">Response</twig:badge>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
{% extends 'layout_admin.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="text-2xl font-bold pb-8">
|
||||
Benutzeraccounts
|
||||
</h1>
|
||||
<twig:page:heading>Benutzeraccounts</twig:page:heading>
|
||||
<div class="data-table-wrapper">
|
||||
<div class="data-table-wrapper__inner text-sm">
|
||||
<table class="data-table">
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
<link rel="icon" href="{{ asset('build/favicon/icon.svg') }}" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="{{ asset('build/favicon/apple-touch-icon.png') }}">
|
||||
{% block stylesheets %}
|
||||
{{ encore_entry_link_tags('app') }}
|
||||
{{ encore_entry_link_tags('admin') }}
|
||||
{% endblock %}
|
||||
{% block javascripts %}
|
||||
{{ encore_entry_script_tags('app') }}
|
||||
{{ encore_entry_script_tags('admin') }}
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body class="bg-white text-gray-700 font-sans antialiased {{ booking_theme() }}">
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{% set variant = variant ?? 'primary' %}
|
||||
{% set cva = html_cva('badge', {
|
||||
variant: {
|
||||
primary: 'bg-primary text-white',
|
||||
success: 'bg-green-500 text-white',
|
||||
warning: 'bg-red-500 text-white',
|
||||
}
|
||||
}, [], { variant: 'primary' }) %}
|
||||
<span {{ attributes.defaults({class: cva.apply({variant: variant})}).without('variant') }}>
|
||||
{{ block('content') }}
|
||||
</span>
|
||||
@@ -0,0 +1,20 @@
|
||||
{% set warning = warning ?? false %}
|
||||
{% set method = method ?? 'get' %}
|
||||
<button type="button"
|
||||
class="{{ html_classes('block px-4 py-2 text-sm w-full text-left', {
|
||||
'text-red-500 hover:text-primary': warning,
|
||||
'text-gray-700 hover:text-primary': not warning
|
||||
}) }}"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
{{ attributes
|
||||
.defaults({
|
||||
'hx-target': 'body',
|
||||
'hx-swap': 'beforeend',
|
||||
})
|
||||
.without('url', 'warning', 'method')
|
||||
}}
|
||||
hx-{{ method | lower }}="{{ url }}"
|
||||
>
|
||||
{{ block('content') }}
|
||||
</button>
|
||||
+10
-3
@@ -1,9 +1,16 @@
|
||||
<div class="relative inline-block text-left group">
|
||||
<button type="button" class="flex items-center text-gray-400 hover:text-gray-600 focus:outline-none focus:ring-0" aria-expanded="true" aria-haspopup="true">
|
||||
<div class="relative inline-block text-left group cursor-pointer">
|
||||
<button type="button"
|
||||
class="flex items-center text-gray-400 hover:text-gray-600 focus:outline-none focus:ring-0"
|
||||
aria-expanded="true"
|
||||
aria-haspopup="true">
|
||||
<span class="sr-only">Funktionen</span>
|
||||
{{ icon('dots', 'w-6 h-6') }}
|
||||
</button>
|
||||
<div class="hidden group-hover:block absolute right-0 top-0 z-10 w-56 origin-top-right pt-4" role="menu" aria-orientation="vertical" aria-labelledby="menu-button" tabindex="-1">
|
||||
<div class="hidden group-hover:block absolute right-0 top-0 z-10 w-56 origin-top-right pt-4"
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
aria-labelledby="menu-button"
|
||||
tabindex="-1">
|
||||
<div class="rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
<div class="py-1 menu menu--pulldown" role="none">
|
||||
{% block content %}{% endblock %}
|
||||
@@ -0,0 +1,10 @@
|
||||
{% set warning = warning ?? false %}
|
||||
<a href="{{ url }}"
|
||||
class="{{ html_classes('block px-4 py-2 text-sm w-full', {
|
||||
'text-red-500 hover:text-primary': warning,
|
||||
'text-gray-700 hover:text-primary': not warning
|
||||
}) }}"
|
||||
role="menuitem"
|
||||
tabindex="-1">
|
||||
{{ block('content') }}
|
||||
</a>
|
||||
@@ -0,0 +1 @@
|
||||
<h1 class="text-2xl font-bold pb-8">{{ block('content') }}</h1>
|
||||
@@ -0,0 +1,235 @@
|
||||
{% use 'form_div_layout.html.twig' %}
|
||||
|
||||
{%- block form_row -%}
|
||||
{%- set widget_attr = {} -%}
|
||||
{%- if help is not empty -%}
|
||||
{%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%}
|
||||
{%- endif -%}
|
||||
<div{% with {attr: row_attr} %}{{ block('attributes') }}{% endwith %}>
|
||||
{{- form_label(form) -}}
|
||||
{{- form_widget(form, widget_attr) -}}
|
||||
{{- form_errors(form) -}}
|
||||
{{- form_help(form) -}}
|
||||
</div>
|
||||
{%- endblock form_row -%}
|
||||
|
||||
{%- block form_label -%}
|
||||
{% set class = 'block font-bold pb-2' %}
|
||||
{% if errors|length %}
|
||||
{% set class = class ~ ' text-red-500' %}
|
||||
{% endif %}
|
||||
{% set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ class)|trim}) %}
|
||||
{{ parent() }}
|
||||
{%- endblock form_label -%}
|
||||
|
||||
{%- block form_errors -%}
|
||||
{%- if errors|length > 0 -%}
|
||||
<ul class="pb-2">
|
||||
{%- for error in errors -%}
|
||||
<li class="text-red-500">{{ error.message }}</li>
|
||||
{%- endfor -%}
|
||||
</ul>
|
||||
{%- endif -%}
|
||||
{%- endblock form_errors -%}
|
||||
|
||||
{%- block form_widget_simple -%}
|
||||
{%- set type = type|default('text') -%}
|
||||
{%- if type != 'hidden' -%}
|
||||
{%- set attr = attr|merge({'class': (attr.class|default('') ~ ' block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset text-sm')|trim }) -%}
|
||||
{%- if errors|length -%}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' ring-red-500 placeholder-red-500 focus:ring-red-500' }) -%}
|
||||
{% else %}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' placeholder:text-gray-400 focus:ring-primary' }) -%}
|
||||
{%- endif -%}
|
||||
{%- if disabled is defined and disabled == true -%}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' cursor-not-allowed' }) -%}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{{ parent() }}
|
||||
{%- if disabled is defined and disabled == true -%}
|
||||
<input type="hidden" name="{{ form.vars.full_name }}" value="{{ form.vars.value }}">
|
||||
{%- endif -%}
|
||||
{%- endblock form_widget_simple -%}
|
||||
|
||||
{%- block textarea_widget -%}
|
||||
{%- set attr = attr|merge({'class': (attr.class|default('') ~ ' block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset text-sm')|trim }) -%}
|
||||
{%- if errors|length -%}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' ring-red-500 placeholder-red-500 focus:ring-red-500' }) -%}
|
||||
{% else %}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' placeholder:text-gray-400 focus:ring-primary' }) -%}
|
||||
{%- endif -%}
|
||||
{%- if disabled is defined and disabled == true -%}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' cursor-not-allowed' }) -%}
|
||||
{%- endif -%}
|
||||
{{ parent() }}
|
||||
{%- if disabled is defined and disabled == true -%}
|
||||
<input type="hidden" name="{{ form.vars.full_name }}" value="{{ form.vars.value }}">
|
||||
{%- endif -%}
|
||||
{%- endblock textarea_widget -%}
|
||||
|
||||
{%- block checkbox_widget -%}
|
||||
{%- set attr = attr|merge({'class': (attr.class|default('') ~ ' h-4 w-4 rounded text-primary')|trim }) -%}
|
||||
{%- if errors|length -%}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' border-red-500 text-red-500 ring-red-500 focus:ring-red-500' }) -%}
|
||||
{% else %}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' border-gray-300 focus:ring-primary' }) -%}
|
||||
{%- endif -%}
|
||||
{%- if attr.disabled is defined and attr.disabled == true -%}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' cursor-not-allowed' }) -%}
|
||||
{%- endif -%}
|
||||
<input type="checkbox" {{ block('widget_attributes') }}{% if value is defined %} value="{{ value }}"{% endif %}{% if checked %} checked="checked"{% endif %} />
|
||||
{%- endblock checkbox_widget -%}
|
||||
|
||||
{% block checkbox_row %}
|
||||
<div class="relative flex items-start">
|
||||
<div class="flex h-6 items-center">
|
||||
{{ form_widget(form) }}
|
||||
</div>
|
||||
<div class="{{ html_classes('ml-3 leading-6', { 'text-red-500': errors|length }) }}">
|
||||
<label for="{{ form.vars.id }}">
|
||||
{{ form.vars.label }}
|
||||
</label>
|
||||
{{- form_errors(form) -}}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{%- block choice_widget_collapsed -%}
|
||||
{%- set attr = attr|merge({'class': (attr.class|default('') ~ ' block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset text-sm')|trim }) -%}
|
||||
{%- if errors|length -%}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' ring-red-500 placeholder-red-500 focus:ring-red-500' }) -%}
|
||||
{% else %}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' placeholder:text-gray-400 focus:ring-primary' }) -%}
|
||||
{%- endif -%}
|
||||
{%- if disabled is defined and disabled == true -%}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' cursor-not-allowed' }) -%}
|
||||
{%- endif -%}
|
||||
{{ parent() }}
|
||||
{%- if disabled is defined and disabled == true -%}
|
||||
<input type="hidden" name="{{ form.vars.full_name }}" value="{{ form.vars.value }}">
|
||||
{%- endif -%}
|
||||
{%- endblock choice_widget_collapsed -%}
|
||||
|
||||
{%- block choice_widget_expanded -%}
|
||||
{%- for child in form %}
|
||||
<div class="relative flex items-start py-2 first:pt-0">
|
||||
<div class="min-w-0 flex-1 text-sm leading-4">
|
||||
<label for="{{ child.vars.id }}" class="select-none font-medium text-gray-900">
|
||||
{{ child.vars.label }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="ml-3 flex h-4 items-center">
|
||||
{{- form_widget(child) -}}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor -%}
|
||||
{%- endblock choice_widget_expanded -%}
|
||||
|
||||
{%- block birthday_widget -%}
|
||||
<div class="grid grid-cols-3 gap-x-1">
|
||||
{{ form_widget(form.children['day']) }}
|
||||
{{ form_widget(form.children['month']) }}
|
||||
{{ form_widget(form.children['year']) }}
|
||||
</div>
|
||||
{%- endblock -%}
|
||||
|
||||
{% block abbreviated_date_widget %}
|
||||
{%- set class = 'flex items-center space-x-1 ' ~ attr.class|default('') -%}
|
||||
{%- do form.setRendered -%}
|
||||
{%- if errors|length -%}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' ring-red-500 placeholder-red-500 focus:ring-red-500' }) -%}
|
||||
{%- endif -%}
|
||||
<div class="{{ class }}">
|
||||
{{ form_widget(form.month, { 'attr': attr })}}
|
||||
<span>/</span>
|
||||
{{ form_widget(form.year, { 'attr': attr })}}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{%- block money_widget -%}
|
||||
{% set currency_class = 'absolute top-1/2 transform -translate-y-1/2 right-0 mr-2' %}
|
||||
{% if errors|length %}
|
||||
{% set currency_class = currency_class ~ ' text-red-500' %}
|
||||
{% else %}
|
||||
{% set currency_class = currency_class ~ ' text-gray-600' %}
|
||||
{% endif %}
|
||||
<div class="relative">
|
||||
{{ block('form_widget_simple') }}
|
||||
<span class="{{ currency_class }}">€</span>
|
||||
</div>
|
||||
{%- endblock money_widget -%}
|
||||
|
||||
{%- block datepicker_widget -%}
|
||||
{%- set minDate = form.vars.min_date ? form.vars.min_date | date('Y-m-d') : null -%}
|
||||
{%- set maxDate = form.vars.max_date ? form.vars.max_date | date('Y-m-d') : null -%}
|
||||
{%- set attr = attr|merge({'class': (attr.class|default('') ~ ' block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset text-sm')|trim }) -%}
|
||||
{%- if errors|length -%}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' ring-red-500 placeholder:red-500 focus:ring-red-500' }) -%}
|
||||
{% else %}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' placeholder:text-gray-900 focus:ring-primary' }) -%}
|
||||
{%- endif -%}
|
||||
{%- if disabled is defined and disabled == true -%}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' cursor-not-allowed' }) -%}
|
||||
{%- endif -%}
|
||||
<div {{ stimulus_controller('datepicker', { 'minDate': minDate, 'maxDate': maxDate, 'disableWeekends': form.vars.disable_weekends }) }}>
|
||||
<input type="text" name="{{ form.vars.full_name }}" value="{{ value }}" {{ block('widget_attributes') }} {{ stimulus_target('datepicker', 'field') }} />
|
||||
</div>
|
||||
{%- if disabled is defined and disabled == true -%}
|
||||
<input type="hidden" name="{{ form.vars.full_name }}" value="{{ value }}">
|
||||
{%- endif -%}
|
||||
{%- endblock datepicker_widget %}
|
||||
|
||||
{%- block autocomplete_widget -%}
|
||||
{%- set attr = attr|merge({'class': (attr.class|default('') ~ ' autocomplete-input block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset text-sm')|trim }) -%}
|
||||
{%- if errors|length -%}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' ring-red-500 placeholder-red-500 focus:ring-red-500' }) -%}
|
||||
{% else %}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' placeholder:text-gray-400 focus:ring-primary' }) -%}
|
||||
{%- endif -%}
|
||||
{%- if disabled is defined and disabled == true -%}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' cursor-not-allowed' }) -%}
|
||||
{%- endif -%}
|
||||
<div class="autocomplete"
|
||||
{{ stimulus_controller('autocomplete', {
|
||||
'action': form.vars.action ?? null,
|
||||
'endpointUrl': form.vars.endpoint_url ?? null,
|
||||
'choices': form.vars.choices ?? null
|
||||
}) }}
|
||||
>
|
||||
<input type="text"
|
||||
class="{{ attr.class }}"
|
||||
value="{{ form.vars.initial_label }}"
|
||||
{{ stimulus_target('autocomplete', 'input') }}>
|
||||
<ul class="autocomplete-result-list"></ul>
|
||||
<input type="hidden"
|
||||
name="{{ form.vars.full_name }}"
|
||||
{{ stimulus_target('autocomplete', 'field') }}
|
||||
value="{{ form.vars.value }}">
|
||||
<button type="button"
|
||||
class="autocomplete-icon hidden"
|
||||
{{ stimulus_action('autocomplete', 'reset') }}
|
||||
{{ stimulus_target('autocomplete', 'resetButton') }}>
|
||||
{{ icon('close', 'w-4 h-4 text-gray-700') }}
|
||||
</button>
|
||||
<div class="autocomplete-icon"
|
||||
{{ stimulus_target('autocomplete', 'searchIcon') }}>
|
||||
{{ icon('search', 'w-4 h-4 text-gray-700') }}
|
||||
</div>
|
||||
</div>
|
||||
{%- endblock autocomplete_widget -%}
|
||||
|
||||
{%- block multiselect_widget -%}
|
||||
<div class="relative" {{ stimulus_controller('multiselect', { 'placeholder': form.vars.empty_label }) }}>
|
||||
<div {{ stimulus_action('multiselect', 'toggle', 'click')}}
|
||||
role="button"
|
||||
class="relative rounded-md border border-gray-300 flex items-start space-x-2 px-3">
|
||||
<div {{ stimulus_target('multiselect', 'label') }} class="flex-1 min-h-8 py-1.5 text-gray-900 text-sm"></div>
|
||||
{{ icon('dots', 'w-4 h-4 mt-2') }}
|
||||
</div>
|
||||
<div {{ stimulus_target('multiselect', 'dropdown')}} class="absolute z-10 top-full -mt-1.5 left-0 inset-x-0 max-h-48 overflow-y-scroll px-3 py-2 bg-white shadow-lg rounded-md rounded-t-none border border-gray-300 border-t-0 hidden">
|
||||
{% for item in form.children %}
|
||||
{{ form_row(item, { 'attr': { 'data-action': 'multiselect#update', 'data-multiselect-target': 'choice', 'data-label': item.vars.label } }) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{%- endblock multiselect_widget -%}
|
||||
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="hidden lg:block">
|
||||
<div class="hidden lg:block sticky top-0 h-screen">
|
||||
{{ knp_menu_render(knp_menu_get('admin_main')) }}
|
||||
</div>
|
||||
<div class="lg:col-span-3">
|
||||
@@ -41,4 +41,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include '_partials/_flashes_admin.html.twig' %}
|
||||
{% endblock %}
|
||||
@@ -13,6 +13,13 @@ service:
|
||||
mixed:
|
||||
separator: ' und '
|
||||
|
||||
enum:
|
||||
additional_service:
|
||||
flat: pauschal
|
||||
per_person: pro Person
|
||||
per_night: pro Nacht
|
||||
per_person_per_night: pro Person und Nacht
|
||||
|
||||
paginator:
|
||||
previous: vorherige
|
||||
next: nächste
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
required: Bitte angeben
|
||||
invalid: Bitte einen gültigen Wert angeben
|
||||
@@ -8,6 +8,7 @@ Encore
|
||||
.setOutputPath('public/build/')
|
||||
.setPublicPath('/build')
|
||||
.addEntry('app', './assets/app.js')
|
||||
.addEntry('admin', './assets/admin.js')
|
||||
.splitEntryChunks()
|
||||
|
||||
// enables the Symfony UX Stimulus bridge (used in assets/bootstrap.js)
|
||||
|
||||
Reference in new issue
Block a user