wip: continue implementation

This commit is contained in:
Björn Fromme
2024-12-02 17:38:56 +01:00
parent 84ab6446ea
commit 6f989d4c7a
24 changed files with 444 additions and 237 deletions
+1
View File
@@ -8,3 +8,4 @@ htmx.config.historyEnabled = false
htmx.config.historyCacheSize = 0 htmx.config.historyCacheSize = 0
htmx.config.allowScriptTags = false htmx.config.allowScriptTags = false
htmx.config.withCredentials = true htmx.config.withCredentials = true
htmx.config.selfRequestsOnly = false
@@ -1,5 +0,0 @@
import { Controller} from '@hotwired/stimulus'
export default class extends Controller {
static targets = ['field']
}
+29
View File
@@ -0,0 +1,29 @@
import { Controller} from '@hotwired/stimulus'
export default class extends Controller {
static targets = ['field']
static values = { availabilities: Object }
toggle(event) {
let field = event.target
let id = field.value
let availabilities = this.availabilitiesValue
if (field.checked) {
availabilities[id].available--
} else {
availabilities[id].available++
}
field.disabled = 0 >= availabilities[id].available
field.classList.toggle('cursor-not-allowed', field.disabled)
this.availabilitiesValue = availabilities
}
availabilitiesValueChanged(availabilities) {
this.fieldTargets.forEach((field) => {
let id = field.value
if (false === field.checked && 0 >= availabilities[id].available) {
field.disabled = true
}
})
}
}
+18
View File
@@ -0,0 +1,18 @@
import { Controller } from '@hotwired/stimulus'
import Toastify from 'toastify-js'
export default class extends Controller {
static values = { text: String, class: String }
connect() {
Toastify({
duration: 5000,
text: this.textValue,
gravity: 'top',
position: 'right',
className: this.classValue,
close: true,
}).showToast()
}
}
+36
View File
@@ -0,0 +1,36 @@
import { Controller } from '@hotwired/stimulus'
import tippy from 'tippy.js'
export default class extends Controller {
static targets = ['trigger', 'template']
static values = { content: String, placement: String }
connect() {
let options, element
let placement = this.placementValue ? this.placementValue : 'top'
if (this.hasTemplateTarget) {
options = {
content: this.templateTarget.innerHTML,
placement,
touch: true,
trigger: 'click',
allowHTML: true,
interactive: true,
}
} else {
options = {
content: this.contentValue,
placement,
touch: false,
allowHTML: true,
interactive: true,
}
}
if (this.hasTriggerTarget) {
element = this.triggerTarget
} else {
element = this.element
}
tippy(element, options)
}
}
+2 -2
View File
@@ -1,5 +1,5 @@
@import "components/forms.css"; @import "components/forms.css";
@import "components/button.css"; @import "components/button.css";
@import "components/menu.css"; @import "components/menu.css";
/*@import "components/toast.css";*/ @import "components/toast.css";
/*@import "components/tooltip.css";*/ @import "components/tooltip.css";
+26
View File
@@ -6,6 +6,10 @@
"": { "": {
"name": "myep-next", "name": "myep-next",
"license": "WTFPL", "license": "WTFPL",
"dependencies": {
"tippy.js": "^6.3.7",
"toastify-js": "^1.12.0"
},
"devDependencies": { "devDependencies": {
"@babel/core": "^7.17.0", "@babel/core": "^7.17.0",
"@babel/preset-env": "^7.16.0", "@babel/preset-env": "^7.16.0",
@@ -1807,6 +1811,15 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@sinclair/typebox": { "node_modules/@sinclair/typebox": {
"version": "0.27.8", "version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@@ -6485,6 +6498,14 @@
"node": ">=0.8" "node": ">=0.8"
} }
}, },
"node_modules/tippy.js": {
"version": "6.3.7",
"resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz",
"integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==",
"dependencies": {
"@popperjs/core": "^2.9.0"
}
},
"node_modules/tmp": { "node_modules/tmp": {
"version": "0.2.3", "version": "0.2.3",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
@@ -6506,6 +6527,11 @@
"node": ">=8.0" "node": ">=8.0"
} }
}, },
"node_modules/toastify-js": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/toastify-js/-/toastify-js-1.12.0.tgz",
"integrity": "sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ=="
},
"node_modules/ts-interface-checker": { "node_modules/ts-interface-checker": {
"version": "0.1.13", "version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+4
View File
@@ -27,5 +27,9 @@
"dev": "encore dev", "dev": "encore dev",
"watch": "encore dev --watch", "watch": "encore dev --watch",
"build": "encore production --progress" "build": "encore production --progress"
},
"dependencies": {
"tippy.js": "^6.3.7",
"toastify-js": "^1.12.0"
} }
} }
@@ -2,6 +2,7 @@
namespace App\BusProNet\ApiResponseParser; namespace App\BusProNet\ApiResponseParser;
use App\BusProNet\Model\CrmAction;
use App\BusProNet\Model\CrmAttributes; use App\BusProNet\Model\CrmAttributes;
use App\BusProNet\Model\CrmSelection; use App\BusProNet\Model\CrmSelection;
use App\BusProNet\Model\CrmSelectionGroup; use App\BusProNet\Model\CrmSelectionGroup;
@@ -18,11 +19,13 @@ class CrmAttributesResponseParser
public function parse(\SimpleXMLElement $xml): CrmAttributes public function parse(\SimpleXMLElement $xml): CrmAttributes
{ {
$groups = []; $groups = [];
$actions = [];
$isAdmin = $isManager = $isHouseManager = $isTeamer = false; $isAdmin = $isManager = $isHouseManager = $isTeamer = false;
$hotelCode = null; $hotelCode = null;
foreach ($xml->selektionsmerkmale->selektionsgruppe as $item) { foreach ($xml->selektionsmerkmale->selektionsgruppe as $item) {
$group = new CrmSelectionGroup(); $group = new CrmSelectionGroup();
$group->id = (int) $item->attributes()['id'];
$group->label = (string) $item->attributes()['bezeichnung']; $group->label = (string) $item->attributes()['bezeichnung'];
$attributes = []; $attributes = [];
@@ -32,6 +35,7 @@ class CrmAttributesResponseParser
$attribute = new CrmSelection(); $attribute = new CrmSelection();
$attribute->id = (int) $subItemAttributes['id']; $attribute->id = (int) $subItemAttributes['id'];
$attribute->label = $attributeLabel; $attribute->label = $attributeLabel;
$attribute->mutable = $this->stringToBool((string) $subItemAttributes['aenderbar']);
$attribute->selected = $this->stringToBool((string) $subItemAttributes['auswahl']); $attribute->selected = $this->stringToBool((string) $subItemAttributes['auswahl']);
if (1 === preg_match('/^Hausleitung ([A-Z0-9]+)$/', $attributeLabel, $matches) && true === $attribute->selected) { if (1 === preg_match('/^Hausleitung ([A-Z0-9]+)$/', $attributeLabel, $matches) && true === $attribute->selected) {
@@ -54,8 +58,21 @@ class CrmAttributesResponseParser
$groups[] = $group; $groups[] = $group;
} }
foreach ($xml->crmaktionen->crmaktion as $item) {
$attributes = $item->attributes();
$crmAction = new CrmAction();
$crmAction->id = (int) $attributes['id'];
$crmAction->code = (string) $attributes['code'];
$crmAction->label = (string) $attributes['bezeichnung'];
$crmAction->mutable = $this->stringToBool((string) $attributes['aenderbar']);
$crmAction->selected = $this->stringToBool((string) $attributes['auswahl']);
$actions[] = $crmAction;
}
$response = new CrmAttributes(); $response = new CrmAttributes();
$response->selectionGroups = $groups; $response->selectionGroups = $groups;
$response->crmActions = $actions;
$response->admin = $isAdmin; $response->admin = $isAdmin;
$response->manager = $isManager; $response->manager = $isManager;
$response->houseManager = $isHouseManager; $response->houseManager = $isHouseManager;
+9 -1
View File
@@ -2,10 +2,18 @@
namespace App\BusProNet\Model; namespace App\BusProNet\Model;
class Availability class Availability implements \JsonSerializable
{ {
public ?int $serviceId = null; public ?int $serviceId = null;
public ?string $status = null; public ?string $status = null;
public ?int $available = null; public ?int $available = null;
public ?float $price = null; public ?float $price = null;
public function jsonSerialize(): array
{
return [
'id' => $this->serviceId,
'available' => $this->available,
];
}
} }
+12
View File
@@ -0,0 +1,12 @@
<?php
namespace App\BusProNet\Model;
class CrmAction
{
public ?int $id = null;
public ?string $code = null;
public ?string $label = null;
public bool $mutable = false;
public bool $selected = false;
}
+1 -21
View File
@@ -5,30 +5,10 @@ namespace App\BusProNet\Model;
class CrmAttributes class CrmAttributes
{ {
public ?array $selectionGroups = null; public ?array $selectionGroups = null;
public ?array $crmActions = null;
public bool $admin = false; public bool $admin = false;
public bool $manager = false; public bool $manager = false;
public bool $houseManager = false; public bool $houseManager = false;
public bool $teamer = false; public bool $teamer = false;
public ?string $hotelCode = null; public ?string $hotelCode = null;
public function toArray(): array
{
$crmSelections = [];
foreach ($this->selectionGroups as $group) {
/** @var CrmSelectionGroup $group */
if (false === isset($crmSelections[$group->label])) {
$crmSelections[$group->label] = [];
}
foreach ($group->selections as $attribute) {
/** @var CrmSelection $attribute */
if (false === $attribute->selected) {
continue;
}
$crmSelections[$group->label][] = $attribute->label;
}
}
return $crmSelections;
}
} }
+1
View File
@@ -6,5 +6,6 @@ class CrmSelection
{ {
public ?int $id = null; public ?int $id = null;
public ?string $label = null; public ?string $label = null;
public bool $mutable = false;
public bool $selected = false; public bool $selected = false;
} }
+12
View File
@@ -19,4 +19,16 @@ class CrmSelectionGroup
public ?int $id = null; public ?int $id = null;
public ?string $label = null; public ?string $label = null;
public ?array $selections = null; public ?array $selections = null;
public function getMutableSelections(): array
{
return array_filter($this->selections, function (CrmSelection $selection) {
return $selection->mutable;
});
}
public function isVisible(): bool
{
return 0 < count($this->getMutableSelections());
}
} }
+4 -4
View File
@@ -32,14 +32,14 @@ class PersonalDataController extends AbstractController
->getPersonalData($bpnUser->getEmail(), $bpnUser->getPassword()) ->getPersonalData($bpnUser->getEmail(), $bpnUser->getPassword())
; ;
$form = $this->createForm(PersonalDataType::class, $personalData, [ $personalDataForm = $this->createForm(PersonalDataType::class, $personalData, [
'attr' => ['novalidate' => 'novalidate'], 'attr' => ['novalidate' => 'novalidate'],
'hx_post' => $this->generateUrl('app_personal_data'), 'hx_post' => $this->generateUrl('app_personal_data'),
'hx_target' => '#app', 'hx_target' => '#app',
]); ]);
$form->handleRequest($request); $personalDataForm->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($personalDataForm->isSubmitted() && $personalDataForm->isValid()) {
try { try {
$this->apiClient->updatePersonalData($bpnUser->getEmail(), $bpnUser->getPassword(), $personalData); $this->apiClient->updatePersonalData($bpnUser->getEmail(), $bpnUser->getPassword(), $personalData);
$this->addFlash('success', 'Deine persönlichen Daten wurden aktualisiert'); $this->addFlash('success', 'Deine persönlichen Daten wurden aktualisiert');
@@ -52,7 +52,7 @@ class PersonalDataController extends AbstractController
return $this->render('personal_data/index.html.twig', [ return $this->render('personal_data/index.html.twig', [
'personalData' => $personalData, 'personalData' => $personalData,
'form' => $form->createView(), 'personalDataForm' => $personalDataForm->createView(),
]); ]);
} }
+2 -2
View File
@@ -24,10 +24,10 @@ class ParticipantData
public ?string $gender = null; public ?string $gender = null;
public ?string $nationality = null; public ?string $nationality = null;
#[Assert\Range(notInRangeMessage: 'Bitte einen Wert im Bereich {{ min }}-{{ max }}cm angeben', min: 140, max: 210)] #[Assert\Range(notInRangeMessage: 'Bitte einen Wert im Bereich {{ min }}-{{ max }}cm angeben', min: 150, max: 210)]
public ?string $height = null; public ?string $height = null;
#[Assert\Range(notInRangeMessage: 'Bitte einen Wert im Bereich {{ min }}-{{ max }} angeben', min: 35, max: 49)] #[Assert\Range(notInRangeMessage: 'Bitte einen Wert im Bereich {{ min }}-{{ max }} angeben', min: 37, max: 49)]
public ?string $shoeSize = null; public ?string $shoeSize = null;
#[Assert\Range(notInRangeMessage: 'Bitte einen Wert im Bereich {{ min }}-{{ max }}kg angeben', min: 40, max: 130)] #[Assert\Range(notInRangeMessage: 'Bitte einen Wert im Bereich {{ min }}-{{ max }}kg angeben', min: 40, max: 130)]
+128 -133
View File
@@ -21,55 +21,53 @@ class ParticipantType extends AbstractType
{ {
public function buildForm(FormBuilderInterface $builder, array $options): void public function buildForm(FormBuilderInterface $builder, array $options): void
{ {
if (true === $options['personal_data_mutable']) { $builder
$builder ->add('firstName', TextType::class, [
->add('firstName', TextType::class, [ 'label' => 'Vorname',
'label' => 'Vorname', ])
]) ->add('lastName', TextType::class, [
->add('lastName', TextType::class, [ 'label' => 'Nachname',
'label' => 'Nachname', ])
]) ->add('dateOfBirth', BirthdayType::class, [
->add('dateOfBirth', BirthdayType::class, [ 'label' => 'Geburtsdatum',
'label' => 'Geburtsdatum', 'html5' => true,
'html5' => true, 'widget' => 'single_text',
'widget' => 'single_text', 'input' => 'datetime_immutable',
'input' => 'datetime_immutable', ])
]) ->add('gender', ChoiceType::class, [
->add('gender', ChoiceType::class, [ 'label' => 'Geschlecht',
'label' => 'Geschlecht', 'required' => false,
'required' => false, 'placeholder' => 'keine Angabe',
'placeholder' => 'keine Angabe', 'choices' => [
'choices' => [ 'männlich' => 'M',
'männlich' => 'M', 'weiblich' => 'W',
'weiblich' => 'W', 'divers' => 'D',
'divers' => 'D', ],
], ])
]) ->add('nationality', CountryType::class, [
->add('nationality', CountryType::class, [ 'label' => 'Nationalität',
'label' => 'Nationalität', 'property' => 'nationality',
'property' => 'nationality', 'preferred_choices' => ['D', 'A', 'CH'],
'preferred_choices' => ['D', 'A', 'CH'], ])
]) ->add('email', EmailType::class, [
->add('email', EmailType::class, [ 'label' => 'E-Mail',
'label' => 'E-Mail', ])
]) ->add('mobile', TextType::class, [
->add('mobile', TextType::class, [ 'label' => 'Telefon (mobil)',
'label' => 'Telefon (mobil)', ])
]) ->add('height', IntegerType::class, [
->add('height', IntegerType::class, [ 'label' => 'Körpergröße [cm]',
'label' => 'Körpergröße [cm]', 'required' => false,
'required' => false, ])
]) ->add('shoeSize', IntegerType::class, [
->add('shoeSize', IntegerType::class, [ 'label' => 'Schuhgröße',
'label' => 'Schuhgröße', 'required' => false,
'required' => false, ])
]) ->add('weight', IntegerType::class, [
->add('weight', IntegerType::class, [ 'label' => 'Gewicht [kg]',
'label' => 'Gewicht [kg]', 'required' => false,
'required' => false, ])
]) ;
;
}
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($options) { $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($options) {
/** @var ParticipantData $participantData */ /** @var ParticipantData $participantData */
@@ -99,8 +97,8 @@ class ParticipantType extends AbstractType
}, },
'choice_attr' => function (?Service $service) { 'choice_attr' => function (?Service $service) {
$attributes = [ $attributes = [
'data-available' => $service->available, 'data-booking-target' => 'field',
'data-availabilities-target' => 'field', 'data-action' => 'booking#toggle',
]; ];
// forcibly select mandatory services // forcibly select mandatory services
@@ -131,96 +129,93 @@ class ParticipantType extends AbstractType
]; ];
// Additional services // Additional services
if (true === $options['additional_services_mutable']) { if (0 < count($options['selectable_courses'])) {
if (0 < count($options['selectable_courses'])) { $form->add('courses', ChoiceType::class, [
$form->add('courses', ChoiceType::class, [ ...$commonChoiceFieldOptions,
...$commonChoiceFieldOptions, 'label' => 'Kurse',
'label' => 'Kurse', 'choices' => $options['selectable_courses'],
'choices' => $options['selectable_courses'], ]);
]); }
} if (0 < count($options['selectable_services'])) {
if (0 < count($options['selectable_services'])) { $form->add('additionalServices', ChoiceType::class, [
$form->add('additionalServices', ChoiceType::class, [ ...$commonChoiceFieldOptions,
...$commonChoiceFieldOptions, 'label' => 'Zusatzleistungen',
'label' => 'Zusatzleistungen', 'choices' => $options['selectable_services'],
'choices' => $options['selectable_services'], ]);
]); }
} if (0 < count($options['selectable_ski_passes'])) {
if (0 < count($options['selectable_ski_passes'])) { $form->add('skiPass', ChoiceType::class, [
$form->add('skiPass', ChoiceType::class, [ ...$commonChoiceFieldOptions,
...$commonChoiceFieldOptions, 'label' => 'Skipass',
'label' => 'Skipass', 'choices' => $options['selectable_ski_passes'],
'choices' => $options['selectable_ski_passes'], ]);
]); }
} if (0 < count($options['selectable_board'])) {
if (0 < count($options['selectable_board'])) { $form->add('board', ChoiceType::class, [
$form->add('board', ChoiceType::class, [ ...$commonChoiceFieldOptions,
...$commonChoiceFieldOptions, 'label' => 'Verpflegung',
'label' => 'Verpflegung', 'choices' => $options['selectable_board'],
'choices' => $options['selectable_board'], ]);
]); }
} if (0 < count($options['selectable_rentals'])) {
if (0 < count($options['selectable_rentals'])) { $form->add('rentals', ChoiceType::class, [
$form->add('rentals', ChoiceType::class, [ ...$commonChoiceFieldOptions,
...$commonChoiceFieldOptions, 'label' => 'Verleih',
'label' => 'Verleih', 'choices' => $options['selectable_rentals'],
'choices' => $options['selectable_rentals'], ]);
]);
}
} }
// Transportation // Transportation
if (true === $options['transportation_services_mutable']) { $form
$form ->add('transportationServiceTo', ChoiceType::class, [
->add('transportationServiceTo', ChoiceType::class, [ ...$commonChoiceFieldOptions,
...$commonChoiceFieldOptions, 'label' => 'Anreise',
'label' => 'Anreise', 'required' => true,
'multiple' => false, 'multiple' => false,
'choices' => $options['selectable_transportation_services_to'], 'choices' => $options['selectable_transportation_services_to'],
'choice_attr' => function (?Service $service) { 'choice_attr' => function (?Service $service) {
return [ return [
'data-available' => $service->available, 'data-booking-target' => 'field',
'data-availabilities-target' => 'field', 'data-action' => 'select-toggle#toggle booking#toggle',
'data-action' => 'select-toggle#toggle', 'data-select-toggle-value' => $service->subType,
'data-select-toggle-value' => $service->subType, ];
]; },
}, ])
]) ->add('transportationServiceFro', ChoiceType::class, [
->add('transportationServiceFro', ChoiceType::class, [ ...$commonChoiceFieldOptions,
...$commonChoiceFieldOptions, 'label' => 'Rückreise',
'label' => 'Rückreise', 'required' => true,
'multiple' => false, 'multiple' => false,
'choices' => $options['selectable_transportation_services_fro'], 'choices' => $options['selectable_transportation_services_fro'],
]); ])
} ;
// Pickup // Pickup
if (true === $options['pickups_mutable']) { $form
$form ->add('pickup', ChoiceType::class, [
->add('pickup', ChoiceType::class, [ 'label' => 'Zustieg',
'label' => 'Zustieg', 'multiple' => false,
'multiple' => false, 'expanded' => false,
'expanded' => false, 'choices' => $options['selectable_pickups'],
'choices' => $options['selectable_pickups'], 'choice_value' => 'id',
'choice_value' => 'id', 'choice_label' => function (?Pickup $pickup) {
'choice_label' => function (?Pickup $pickup) { if (null === $pickup) {
if (null === $pickup) { return null;
return null; }
}
$price = $pickup->price; $price = $pickup->price;
if (null === $price || 0.0 === $price) { if (null === $price || 0.0 === $price) {
return $pickup->city; return $pickup->city;
} }
return sprintf('%s (%s €)', return sprintf('%s (%s €)',
$pickup->city, $pickup->city,
number_format($price, 2, ',', '.') number_format($price, 2, ',', '.')
); );
}, },
]); ])
} ;
}) })
->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options) { ->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options) {
$data = $event->getData(); $data = $event->getData();
+3 -3
View File
@@ -1,9 +1,9 @@
{% if app.session.flashBag.peek('error')|length > 0 %} {% if app.session.flashBag.peek('error')|length > 0 %}
{% include '_partials/_alert.html.twig' with { level: 'warning', messages: app.flashes('error') } %} {% include '_partials/_toast.html.twig' with { level: 'warning', messages: app.flashes('error') } %}
{% endif %} {% endif %}
{% if app.session.flashBag.peek('success')|length > 0 %} {% if app.session.flashBag.peek('success')|length > 0 %}
{% include '_partials/_alert.html.twig' with { level: 'success', messages: app.flashes('success') } %} {% include '_partials/_toast.html.twig' with { level: 'success', messages: app.flashes('success') } %}
{% endif %} {% endif %}
{% if app.session.flashBag.peek('info')|length > 0 %} {% if app.session.flashBag.peek('info')|length > 0 %}
{% include '_partials/_alert.html.twig' with { level: 'info', messages: app.flashes('info') } %} {% include '_partials/_toast.html.twig' with { level: 'info', messages: app.flashes('info') } %}
{% endif %} {% endif %}
+8
View File
@@ -0,0 +1,8 @@
{% 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 }) }}></div>
{% endfor %}
-1
View File
@@ -12,6 +12,5 @@
</head> </head>
<body class="bg-white text-gray-700 font-sans antialiased text-sm"> <body class="bg-white text-gray-700 font-sans antialiased text-sm">
{% block body %}{% endblock %} {% block body %}{% endblock %}
{% include '_partials/_flashes.html.twig' %}
</body> </body>
</html> </html>
+96 -23
View File
@@ -3,34 +3,57 @@
{% block content %} {% block content %}
{{ form_start(form) }} {{ form_start(form) }}
{% if not form.vars.valid %} {% if not form.vars.valid %}
<article> <div class="rounded-md bg-red-50 p-4 mb-4">
<header> <div class="flex">
<strong>Fehler</strong> <div class="shrink-0">
</header> <svg class="size-5 text-red-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" data-slot="icon">
{{ form_errors(form) }} <path fill-rule="evenodd" d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16ZM8.28 7.22a.75.75 0 0 0-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 1 0 1.06 1.06L10 11.06l1.72 1.72a.75.75 0 1 0 1.06-1.06L11.06 10l1.72-1.72a.75.75 0 0 0-1.06-1.06L10 8.94 8.28 7.22Z" clip-rule="evenodd" />
</article> </svg>
</div>
<div class="ml-3">
{% if form.vars.errors|length > 0 %}
<h3 class="text-sm font-medium text-red-800">
Die Buchung konnte nicht aktualisiert werden
</h3>
<div class="mt-2 text-sm text-red-700">
<ul role="list" class="list-disc space-y-1 pl-5">
{% for error in form.vars.errors %}
<li>
{{ error.message }}
</li>
{% endfor %}
</ul>
</div>
{% else %}
<h3 class="text-sm font-medium text-red-800">
Die Buchung konnte nicht aktualisiert werden. Problematische Eingaben wurden entsprechend markiert.
</h3>
{% endif %}
</div>
</div>
</div>
{% endif %} {% endif %}
<div class="flex flex-col space-y-8 pb-4"> <div class="flex flex-col space-y-8 pb-4" {{ stimulus_controller('booking', { 'availabilities': availabilities.items }) }}>
<div class="p-8 border border-gray-300 rounded-md" <div class="p-8 border border-gray-300 rounded-md"
{{ stimulus_controller('availabilities')|stimulus_controller('toggle', { 'open': true }, { 'closed': 'hidden' }) }}> {{ stimulus_controller('toggle', { 'open': true }, { 'closed': 'hidden' }) }}>
<div role="button" class="flex items-center justify-between" {{ stimulus_action('toggle', 'toggle', 'click') }}> <div role="button" class="flex items-center justify-between" {{ stimulus_action('toggle', 'toggle', 'click') }}>
<span class="block text-3xl font-semibold">Reisedaten</span> <span class="block text-2xl font-semibold">Reisedaten</span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6" {{ stimulus_target('toggle', 'icon') }}> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6" {{ stimulus_target('toggle', 'icon') }}>
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" /> <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg> </svg>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-x-8 gap-y-4 pt-4" {{ stimulus_target('toggle', 'toggle') }}> <div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-3 gap-x-8 gap-y-4 pt-4" {{ stimulus_target('toggle', 'toggle') }}>
<div> <div>
<h3 class="text-lg font-semibold"> <h3 class="text-lg font-semibold">
Destination Reise
</h3> </h3>
{{ bookingData.travel }} {{ bookingData.travel }}
</div> </div>
<div> <div>
<h3 class="text-lg font-semibold"> <h3 class="text-lg font-semibold">
Reisedatum Reisezeitraum
</h3> </h3>
{{ bookingData.travelDate|date('d.m.Y') }} {{ travelData.dateFrom|date('d.m.Y') }} - {{ travelData.dateTo|date('d.m.Y') }}
</div> </div>
<div> <div>
<h3 class="text-lg font-semibold"> <h3 class="text-lg font-semibold">
@@ -38,6 +61,24 @@
</h3> </h3>
{{ bookingData.bookingDate|date('d.m.Y') }} {{ bookingData.bookingDate|date('d.m.Y') }}
</div> </div>
<div>
<h3 class="text-lg font-semibold">
Anmelder
</h3>
{{ bookingData.applicant.firstName }} {{ bookingData.applicant.name }}
</div>
<div>
<h3 class="text-lg font-semibold">
Anzahl Teilnehmer
</h3>
{{ bookingData.participants|length }}
</div>
<div>
<h3 class="text-lg font-semibold">
Gesamtpreis
</h3>
{{ bookingData.totalPrice|format_currency('EUR') }}
</div>
<div> <div>
<h3 class="text-lg font-semibold"> <h3 class="text-lg font-semibold">
Inklusivleistungen Inklusivleistungen
@@ -52,39 +93,65 @@
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
<div>
<h3 class="text-lg font-semibold">
Aktualisierung möglich bis
</h3>
<ul class="list-disc pl-4">
<li>
Unterkunft: {{ mutableData.items['accommodation'].mutableBefore|date('d.m.Y') }}
</li>
<li>
Teilnehmer: {{ mutableData.items['participant_data'].mutableBefore|date('d.m.Y') }}
</li>
<li>
Zusatzleistungen: {{ mutableData.items['additional_services'].mutableBefore|date('d.m.Y') }}
</li>
<li>
Beförderung: {{ mutableData.items['transportation'].mutableBefore|date('d.m.Y') }}
</li>
<li>
Zustiege: {{ mutableData.items['pickup'].mutableBefore|date('d.m.Y') }}
</li>
</ul>
</div>
</div> </div>
</div> </div>
{% for child in form.participants %} {% for child in form.participants %}
{% set participant = child.vars.data %} {% set participant = child.vars.data %}
<div class="{{ html_classes('p-8 border rounded-md', { 'border-gray-300': child.vars.valid, 'border-red-500': not child.vars.valid }) }}" {{ stimulus_controller('availabilities')|stimulus_controller('toggle', {'open': child.vars.valid == false}, { 'closed': 'hidden' }) }}> <div class="{{ html_classes('p-8 border rounded-md', { 'border-gray-300': child.vars.valid, 'border-red-500': not child.vars.valid }) }}" {{ stimulus_controller('toggle', {'open': child.vars.valid == false}, { 'closed': 'hidden' }) }}>
<div role="button" class="flex items-center justify-between" {{ stimulus_action('toggle', 'toggle', 'click') }}> <div role="button" class="flex items-center justify-between" {{ stimulus_action('toggle', 'toggle', 'click') }}>
<div class="text-xl font-semibold">Teilnehmer:in {{ participant.index }}: {{ participant.firstName }} {{ participant.lastName }}</div> <div class="text-2xl font-semibold">Teilnehmer:in {{ participant.index }}: {{ participant.firstName }} {{ participant.lastName }}</div>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6" {{ stimulus_target('toggle', 'icon') }}> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6" {{ stimulus_target('toggle', 'icon') }}>
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" /> <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg> </svg>
</div> </div>
<div class="hidden" {{ stimulus_target('toggle', 'toggle') }}> <div class="hidden" {{ stimulus_target('toggle', 'toggle') }}>
<div class="flex flex-col space-y-4 pt-4"> <div class="flex flex-col space-y-4 pt-4">
<div> <div class="relative">
<h3 class="text-xl font-semibold pb-2"> <h3 class="text-xl font-semibold pb-2">
Persönliche Daten Persönliche Daten
</h3> </h3>
<fieldset class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-x-8 gap-y-4 pb-4"> <fieldset class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-4 pb-4">
{{ form_row(child.firstName) }} {{ form_row(child.firstName) }}
{{ form_row(child.lastName) }} {{ form_row(child.lastName) }}
{{ form_row(child.gender) }} {{ form_row(child.gender) }}
{{ form_row(child.dateOfBirth) }} {{ form_row(child.dateOfBirth) }}
{{ form_row(child.nationality) }} {{ form_row(child.nationality) }}
</fieldset> </fieldset>
<fieldset class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-x-8 gap-y-4 pb-4"> <fieldset class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-4 pb-4">
{{ form_row(child.email) }} {{ form_row(child.email) }}
{{ form_row(child.mobile) }} {{ form_row(child.mobile) }}
</fieldset> </fieldset>
<fieldset class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-x-8 gap-y-4"> <fieldset class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-4">
{{ form_row(child.height) }} {{ form_row(child.height) }}
{{ form_row(child.shoeSize) }} {{ form_row(child.shoeSize) }}
{{ form_row(child.weight) }} {{ form_row(child.weight) }}
</fieldset> </fieldset>
{% if not travelData.participantDataMutable %}
<div class="absolute top-0 left-0 inset-0 cursor-not-allowed"></div>
{% endif %}
</div> </div>
<div> <div>
<h3 class="text-xl font-semibold pb-2"> <h3 class="text-xl font-semibold pb-2">
@@ -93,11 +160,11 @@
{% set room = bookingData.roomForParticipant(participant.index) %} {% set room = bookingData.roomForParticipant(participant.index) %}
{{ room.label }} ({{ room.individualPrice[participant.index]|format_currency('EUR') }}) {{ room.label }} ({{ room.individualPrice[participant.index]|format_currency('EUR') }})
</div> </div>
<div> <div class="relative">
<h3 class="text-xl font-semibold pb-2"> <h3 class="text-xl font-semibold pb-2">
Leistungen Leistungen
</h3> </h3>
<fieldset class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-x-8 gap-y-4"> <fieldset class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-4">
{% if child.courses is defined %} {% if child.courses is defined %}
{{ form_row(child.courses) }} {{ form_row(child.courses) }}
{% endif %} {% endif %}
@@ -114,12 +181,15 @@
{{ form_row(child.rentals) }} {{ form_row(child.rentals) }}
{% endif %} {% endif %}
</fieldset> </fieldset>
{% if not travelData.additionalServicesMutable %}
<div class="absolute top-0 left-0 inset-0 cursor-not-allowed"></div>
{% endif %}
</div> </div>
<div> <div class="relative">
<h3 class="text-xl font-semibold pb-2"> <h3 class="text-xl font-semibold pb-2">
Hin-/Rückreise Hin-/Rückreise
</h3> </h3>
<fieldset class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-x-8 gap-y-4" {{ stimulus_controller('select-toggle', { 'show': ['BUS'] }, { 'hidden': 'hidden' }) }}> <fieldset class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-4" {{ stimulus_controller('select-toggle', { 'show': ['BUS'] }, { 'hidden': 'hidden' }) }}>
{{ form_row(child.transportationServiceTo) }} {{ form_row(child.transportationServiceTo) }}
{{ form_row(child.transportationServiceFro) }} {{ form_row(child.transportationServiceFro) }}
{% if child.pickup is defined %} {% if child.pickup is defined %}
@@ -128,6 +198,9 @@
</div> </div>
{% endif %} {% endif %}
</fieldset> </fieldset>
{% if not travelData.transportationServicesMutable %}
<div class="absolute top-0 left-0 inset-0 cursor-not-allowed"></div>
{% endif %}
</div> </div>
<div> <div>
<h3 class="text-xl font-semibold pb-2"> <h3 class="text-xl font-semibold pb-2">
+2 -11
View File
@@ -98,7 +98,7 @@
<div class="flex h-6 items-center"> <div class="flex h-6 items-center">
{{ form_widget(form) }} {{ form_widget(form) }}
</div> </div>
<div class="{{ html_classes('ml-3 leading-6', { 'text-red-500': errors|length }) }}"> <div class="{{ html_classes('ml-1 leading-6', { 'text-red-500': errors|length }) }}">
<label for="{{ form.vars.id }}"> <label for="{{ form.vars.id }}">
{{ form.vars.label }} {{ form.vars.label }}
</label> </label>
@@ -125,16 +125,7 @@
{%- block choice_widget_expanded -%} {%- block choice_widget_expanded -%}
{%- for child in form %} {%- for child in form %}
<div class="relative flex items-start pb-2"> {{- form_row(child) -}}
<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 -%} {% endfor -%}
{%- endblock choice_widget_expanded -%} {%- endblock choice_widget_expanded -%}
+1 -1
View File
@@ -11,11 +11,11 @@
{% endif %} {% endif %}
</div> </div>
<div id="content"> <div id="content">
{% include '_partials/_flashes.html.twig' %}
{% block content %}{% endblock %} {% block content %}{% endblock %}
</div> </div>
<div class="htmx-indicator fixed top-0 left-0 inset-0 z-10 bg-white/90 flex items-center justify-center" <div class="htmx-indicator fixed top-0 left-0 inset-0 z-10 bg-white/90 flex items-center justify-center"
id="loading-indicator"> id="loading-indicator">
{% include '_partials/_spinner.html.twig' with { 'class': 'w-8 h-8'} %} {% include '_partials/_spinner.html.twig' with { 'class': 'w-8 h-8'} %}
</div> </div>
{% include '_partials/_flashes.html.twig' %}
</div> </div>
+30 -28
View File
@@ -6,55 +6,57 @@
<h2 class="text-2xl font-semibold pb-4"> <h2 class="text-2xl font-semibold pb-4">
Persönliche Daten Persönliche Daten
</h2> </h2>
{{ form_start(form) }} {{ form_start(personalDataForm) }}
<div class="grid grid-cols-2 gap-x-8 gap-y-4 pb-4"> <div class="grid grid-cols-2 gap-x-8 gap-y-4 pb-4">
{{ form_row(form.firstName) }} {{ form_row(personalDataForm.firstName) }}
{{ form_row(form.name) }} {{ form_row(personalDataForm.name) }}
{{ form_row(form.gender) }} {{ form_row(personalDataForm.gender) }}
{{ form_row(form.dateOfBirth) }} {{ form_row(personalDataForm.dateOfBirth) }}
</div> </div>
<h3 class="text-xl font-semibold pb-2"> <h3 class="text-xl font-semibold pb-2">
Anschrift Anschrift
</h3> </h3>
<div class="grid grid-cols-2 gap-x-8 gap-y-4 pb-4"> <div class="grid grid-cols-2 gap-x-8 gap-y-4 pb-4">
<div class="col-span-2"> <div class="col-span-2">
{{ form_row(form.street) }} {{ form_row(personalDataForm.street) }}
</div> </div>
{{ form_row(form.postCode) }} {{ form_row(personalDataForm.postCode) }}
{{ form_row(form.city) }} {{ form_row(personalDataForm.city) }}
{{ form_row(form.country) }} {{ form_row(personalDataForm.country) }}
</div> </div>
<h3 class="text-xl font-semibold pb-2"> <h3 class="text-xl font-semibold pb-2">
Kontakt Kontakt
</h3> </h3>
<div class="grid grid-cols-2 gap-x-8 gap-y-4 pb-4"> <div class="grid grid-cols-2 gap-x-8 gap-y-4 pb-4">
<div class="col-span-2"> <div class="col-span-2">
{{ form_row(form.email) }} {{ form_row(personalDataForm.email) }}
</div> </div>
{{ form_row(form.phone) }} {{ form_row(personalDataForm.phone) }}
{{ form_row(form.mobile) }} {{ form_row(personalDataForm.mobile) }}
</div> </div>
<button type="submit" class="btn"> <button type="submit" class="btn">
Speichern Speichern
</button> </button>
{{ form_rest(form) }} {{ form_rest(personalDataForm) }}
{{ form_end(form) }} {{ form_end(personalDataForm) }}
</div> </div>
<div> <div>
<h2 class="text-2xl font-semibold pb-4"> <div>
Newsletter <h2 class="text-2xl font-semibold pb-4">
</h2> Newsletter
<p class="pb-4"> </h2>
Du bist aktuell {% if not personalData.communication.newsletter %}<strong>nicht</strong> {% endif%} zum Newsletter <p class="pb-4">
angemeldet. Du bist aktuell {% if not personalData.communication.newsletter %}<strong>nicht</strong> {% endif%} zum Newsletter
</p> angemeldet.
<button type="button" class="btn" hx-post="{{ path('app_personal_data_newsletter') }}" hx-target="#app"> </p>
{% if personalData.communication.newsletter %} <button type="button" class="btn" hx-post="{{ path('app_personal_data_newsletter') }}" hx-target="#app" hx-swap="outerHTML">
vom Newsletter abmelden {% if personalData.communication.newsletter %}
{% else %} vom Newsletter abmelden
zum Newsletter anmelden {% else %}
{% endif %} zum Newsletter anmelden
</button> {% endif %}
</button>
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}