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
+2 -1
View File
@@ -7,4 +7,5 @@ htmx.config.includeIndicatorStyles = false
htmx.config.historyEnabled = false
htmx.config.historyCacheSize = 0
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/button.css";
@import "components/menu.css";
/*@import "components/toast.css";*/
/*@import "components/tooltip.css";*/
@import "components/toast.css";
@import "components/tooltip.css";
+26
View File
@@ -6,6 +6,10 @@
"": {
"name": "myep-next",
"license": "WTFPL",
"dependencies": {
"tippy.js": "^6.3.7",
"toastify-js": "^1.12.0"
},
"devDependencies": {
"@babel/core": "^7.17.0",
"@babel/preset-env": "^7.16.0",
@@ -1807,6 +1811,15 @@
"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": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@@ -6485,6 +6498,14 @@
"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": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
@@ -6506,6 +6527,11 @@
"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": {
"version": "0.1.13",
"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",
"watch": "encore dev --watch",
"build": "encore production --progress"
},
"dependencies": {
"tippy.js": "^6.3.7",
"toastify-js": "^1.12.0"
}
}
@@ -2,6 +2,7 @@
namespace App\BusProNet\ApiResponseParser;
use App\BusProNet\Model\CrmAction;
use App\BusProNet\Model\CrmAttributes;
use App\BusProNet\Model\CrmSelection;
use App\BusProNet\Model\CrmSelectionGroup;
@@ -18,11 +19,13 @@ class CrmAttributesResponseParser
public function parse(\SimpleXMLElement $xml): CrmAttributes
{
$groups = [];
$actions = [];
$isAdmin = $isManager = $isHouseManager = $isTeamer = false;
$hotelCode = null;
foreach ($xml->selektionsmerkmale->selektionsgruppe as $item) {
$group = new CrmSelectionGroup();
$group->id = (int) $item->attributes()['id'];
$group->label = (string) $item->attributes()['bezeichnung'];
$attributes = [];
@@ -32,6 +35,7 @@ class CrmAttributesResponseParser
$attribute = new CrmSelection();
$attribute->id = (int) $subItemAttributes['id'];
$attribute->label = $attributeLabel;
$attribute->mutable = $this->stringToBool((string) $subItemAttributes['aenderbar']);
$attribute->selected = $this->stringToBool((string) $subItemAttributes['auswahl']);
if (1 === preg_match('/^Hausleitung ([A-Z0-9]+)$/', $attributeLabel, $matches) && true === $attribute->selected) {
@@ -54,8 +58,21 @@ class CrmAttributesResponseParser
$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->selectionGroups = $groups;
$response->crmActions = $actions;
$response->admin = $isAdmin;
$response->manager = $isManager;
$response->houseManager = $isHouseManager;
@@ -64,4 +81,4 @@ class CrmAttributesResponseParser
return $response;
}
}
}
+9 -1
View File
@@ -2,10 +2,18 @@
namespace App\BusProNet\Model;
class Availability
class Availability implements \JsonSerializable
{
public ?int $serviceId = null;
public ?string $status = null;
public ?int $available = 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
{
public ?array $selectionGroups = null;
public ?array $crmActions = null;
public bool $admin = false;
public bool $manager = false;
public bool $houseManager = false;
public bool $teamer = false;
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 ?string $label = null;
public bool $mutable = false;
public bool $selected = false;
}
+12
View File
@@ -19,4 +19,16 @@ class CrmSelectionGroup
public ?int $id = null;
public ?string $label = 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())
;
$form = $this->createForm(PersonalDataType::class, $personalData, [
$personalDataForm = $this->createForm(PersonalDataType::class, $personalData, [
'attr' => ['novalidate' => 'novalidate'],
'hx_post' => $this->generateUrl('app_personal_data'),
'hx_target' => '#app',
]);
$form->handleRequest($request);
$personalDataForm->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
if ($personalDataForm->isSubmitted() && $personalDataForm->isValid()) {
try {
$this->apiClient->updatePersonalData($bpnUser->getEmail(), $bpnUser->getPassword(), $personalData);
$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', [
'personalData' => $personalData,
'form' => $form->createView(),
'personalDataForm' => $personalDataForm->createView(),
]);
}
+2 -2
View File
@@ -24,10 +24,10 @@ class ParticipantData
public ?string $gender = 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;
#[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;
#[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
{
if (true === $options['personal_data_mutable']) {
$builder
->add('firstName', TextType::class, [
'label' => 'Vorname',
])
->add('lastName', TextType::class, [
'label' => 'Nachname',
])
->add('dateOfBirth', BirthdayType::class, [
'label' => 'Geburtsdatum',
'html5' => true,
'widget' => 'single_text',
'input' => 'datetime_immutable',
])
->add('gender', ChoiceType::class, [
'label' => 'Geschlecht',
'required' => false,
'placeholder' => 'keine Angabe',
'choices' => [
'männlich' => 'M',
'weiblich' => 'W',
'divers' => 'D',
],
])
->add('nationality', CountryType::class, [
'label' => 'Nationalität',
'property' => 'nationality',
'preferred_choices' => ['D', 'A', 'CH'],
])
->add('email', EmailType::class, [
'label' => 'E-Mail',
])
->add('mobile', TextType::class, [
'label' => 'Telefon (mobil)',
])
->add('height', IntegerType::class, [
'label' => 'Körpergröße [cm]',
'required' => false,
])
->add('shoeSize', IntegerType::class, [
'label' => 'Schuhgröße',
'required' => false,
])
->add('weight', IntegerType::class, [
'label' => 'Gewicht [kg]',
'required' => false,
])
;
}
$builder
->add('firstName', TextType::class, [
'label' => 'Vorname',
])
->add('lastName', TextType::class, [
'label' => 'Nachname',
])
->add('dateOfBirth', BirthdayType::class, [
'label' => 'Geburtsdatum',
'html5' => true,
'widget' => 'single_text',
'input' => 'datetime_immutable',
])
->add('gender', ChoiceType::class, [
'label' => 'Geschlecht',
'required' => false,
'placeholder' => 'keine Angabe',
'choices' => [
'männlich' => 'M',
'weiblich' => 'W',
'divers' => 'D',
],
])
->add('nationality', CountryType::class, [
'label' => 'Nationalität',
'property' => 'nationality',
'preferred_choices' => ['D', 'A', 'CH'],
])
->add('email', EmailType::class, [
'label' => 'E-Mail',
])
->add('mobile', TextType::class, [
'label' => 'Telefon (mobil)',
])
->add('height', IntegerType::class, [
'label' => 'Körpergröße [cm]',
'required' => false,
])
->add('shoeSize', IntegerType::class, [
'label' => 'Schuhgröße',
'required' => false,
])
->add('weight', IntegerType::class, [
'label' => 'Gewicht [kg]',
'required' => false,
])
;
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($options) {
/** @var ParticipantData $participantData */
@@ -99,8 +97,8 @@ class ParticipantType extends AbstractType
},
'choice_attr' => function (?Service $service) {
$attributes = [
'data-available' => $service->available,
'data-availabilities-target' => 'field',
'data-booking-target' => 'field',
'data-action' => 'booking#toggle',
];
// forcibly select mandatory services
@@ -131,96 +129,93 @@ class ParticipantType extends AbstractType
];
// Additional services
if (true === $options['additional_services_mutable']) {
if (0 < count($options['selectable_courses'])) {
$form->add('courses', ChoiceType::class, [
...$commonChoiceFieldOptions,
'label' => 'Kurse',
'choices' => $options['selectable_courses'],
]);
}
if (0 < count($options['selectable_services'])) {
$form->add('additionalServices', ChoiceType::class, [
...$commonChoiceFieldOptions,
'label' => 'Zusatzleistungen',
'choices' => $options['selectable_services'],
]);
}
if (0 < count($options['selectable_ski_passes'])) {
$form->add('skiPass', ChoiceType::class, [
...$commonChoiceFieldOptions,
'label' => 'Skipass',
'choices' => $options['selectable_ski_passes'],
]);
}
if (0 < count($options['selectable_board'])) {
$form->add('board', ChoiceType::class, [
...$commonChoiceFieldOptions,
'label' => 'Verpflegung',
'choices' => $options['selectable_board'],
]);
}
if (0 < count($options['selectable_rentals'])) {
$form->add('rentals', ChoiceType::class, [
...$commonChoiceFieldOptions,
'label' => 'Verleih',
'choices' => $options['selectable_rentals'],
]);
}
if (0 < count($options['selectable_courses'])) {
$form->add('courses', ChoiceType::class, [
...$commonChoiceFieldOptions,
'label' => 'Kurse',
'choices' => $options['selectable_courses'],
]);
}
if (0 < count($options['selectable_services'])) {
$form->add('additionalServices', ChoiceType::class, [
...$commonChoiceFieldOptions,
'label' => 'Zusatzleistungen',
'choices' => $options['selectable_services'],
]);
}
if (0 < count($options['selectable_ski_passes'])) {
$form->add('skiPass', ChoiceType::class, [
...$commonChoiceFieldOptions,
'label' => 'Skipass',
'choices' => $options['selectable_ski_passes'],
]);
}
if (0 < count($options['selectable_board'])) {
$form->add('board', ChoiceType::class, [
...$commonChoiceFieldOptions,
'label' => 'Verpflegung',
'choices' => $options['selectable_board'],
]);
}
if (0 < count($options['selectable_rentals'])) {
$form->add('rentals', ChoiceType::class, [
...$commonChoiceFieldOptions,
'label' => 'Verleih',
'choices' => $options['selectable_rentals'],
]);
}
// Transportation
if (true === $options['transportation_services_mutable']) {
$form
->add('transportationServiceTo', ChoiceType::class, [
...$commonChoiceFieldOptions,
'label' => 'Anreise',
'multiple' => false,
'choices' => $options['selectable_transportation_services_to'],
'choice_attr' => function (?Service $service) {
return [
'data-available' => $service->available,
'data-availabilities-target' => 'field',
'data-action' => 'select-toggle#toggle',
'data-select-toggle-value' => $service->subType,
];
},
])
->add('transportationServiceFro', ChoiceType::class, [
...$commonChoiceFieldOptions,
'label' => 'Rückreise',
'multiple' => false,
'choices' => $options['selectable_transportation_services_fro'],
]);
}
$form
->add('transportationServiceTo', ChoiceType::class, [
...$commonChoiceFieldOptions,
'label' => 'Anreise',
'required' => true,
'multiple' => false,
'choices' => $options['selectable_transportation_services_to'],
'choice_attr' => function (?Service $service) {
return [
'data-booking-target' => 'field',
'data-action' => 'select-toggle#toggle booking#toggle',
'data-select-toggle-value' => $service->subType,
];
},
])
->add('transportationServiceFro', ChoiceType::class, [
...$commonChoiceFieldOptions,
'label' => 'Rückreise',
'required' => true,
'multiple' => false,
'choices' => $options['selectable_transportation_services_fro'],
])
;
// Pickup
if (true === $options['pickups_mutable']) {
$form
->add('pickup', ChoiceType::class, [
'label' => 'Zustieg',
'multiple' => false,
'expanded' => false,
'choices' => $options['selectable_pickups'],
'choice_value' => 'id',
'choice_label' => function (?Pickup $pickup) {
if (null === $pickup) {
return null;
}
$form
->add('pickup', ChoiceType::class, [
'label' => 'Zustieg',
'multiple' => false,
'expanded' => false,
'choices' => $options['selectable_pickups'],
'choice_value' => 'id',
'choice_label' => function (?Pickup $pickup) {
if (null === $pickup) {
return null;
}
$price = $pickup->price;
$price = $pickup->price;
if (null === $price || 0.0 === $price) {
return $pickup->city;
}
if (null === $price || 0.0 === $price) {
return $pickup->city;
}
return sprintf('%s (%s €)',
$pickup->city,
number_format($price, 2, ',', '.')
);
},
]);
}
return sprintf('%s (%s €)',
$pickup->city,
number_format($price, 2, ',', '.')
);
},
])
;
})
->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options) {
$data = $event->getData();
+3 -3
View File
@@ -1,9 +1,9 @@
{% 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 %}
{% 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 %}
{% 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 %}
+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>
<body class="bg-white text-gray-700 font-sans antialiased text-sm">
{% block body %}{% endblock %}
{% include '_partials/_flashes.html.twig' %}
</body>
</html>
+96 -23
View File
@@ -3,34 +3,57 @@
{% block content %}
{{ form_start(form) }}
{% if not form.vars.valid %}
<article>
<header>
<strong>Fehler</strong>
</header>
{{ form_errors(form) }}
</article>
<div class="rounded-md bg-red-50 p-4 mb-4">
<div class="flex">
<div class="shrink-0">
<svg class="size-5 text-red-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" data-slot="icon">
<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" />
</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 %}
<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"
{{ 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') }}>
<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') }}>
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg>
</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>
<h3 class="text-lg font-semibold">
Destination
Reise
</h3>
{{ bookingData.travel }}
</div>
<div>
<h3 class="text-lg font-semibold">
Reisedatum
Reisezeitraum
</h3>
{{ bookingData.travelDate|date('d.m.Y') }}
{{ travelData.dateFrom|date('d.m.Y') }} - {{ travelData.dateTo|date('d.m.Y') }}
</div>
<div>
<h3 class="text-lg font-semibold">
@@ -38,6 +61,24 @@
</h3>
{{ bookingData.bookingDate|date('d.m.Y') }}
</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>
<h3 class="text-lg font-semibold">
Inklusivleistungen
@@ -52,39 +93,65 @@
{% endfor %}
</ul>
</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>
{% for child in form.participants %}
{% 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 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') }}>
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg>
</div>
<div class="hidden" {{ stimulus_target('toggle', 'toggle') }}>
<div class="flex flex-col space-y-4 pt-4">
<div>
<div class="relative">
<h3 class="text-xl font-semibold pb-2">
Persönliche Daten
</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.lastName) }}
{{ form_row(child.gender) }}
{{ form_row(child.dateOfBirth) }}
{{ form_row(child.nationality) }}
</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.mobile) }}
</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.shoeSize) }}
{{ form_row(child.weight) }}
</fieldset>
{% if not travelData.participantDataMutable %}
<div class="absolute top-0 left-0 inset-0 cursor-not-allowed"></div>
{% endif %}
</div>
<div>
<h3 class="text-xl font-semibold pb-2">
@@ -93,11 +160,11 @@
{% set room = bookingData.roomForParticipant(participant.index) %}
{{ room.label }} ({{ room.individualPrice[participant.index]|format_currency('EUR') }})
</div>
<div>
<div class="relative">
<h3 class="text-xl font-semibold pb-2">
Leistungen
</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 %}
{{ form_row(child.courses) }}
{% endif %}
@@ -114,12 +181,15 @@
{{ form_row(child.rentals) }}
{% endif %}
</fieldset>
{% if not travelData.additionalServicesMutable %}
<div class="absolute top-0 left-0 inset-0 cursor-not-allowed"></div>
{% endif %}
</div>
<div>
<div class="relative">
<h3 class="text-xl font-semibold pb-2">
Hin-/Rückreise
</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.transportationServiceFro) }}
{% if child.pickup is defined %}
@@ -128,6 +198,9 @@
</div>
{% endif %}
</fieldset>
{% if not travelData.transportationServicesMutable %}
<div class="absolute top-0 left-0 inset-0 cursor-not-allowed"></div>
{% endif %}
</div>
<div>
<h3 class="text-xl font-semibold pb-2">
+2 -11
View File
@@ -98,7 +98,7 @@
<div class="flex h-6 items-center">
{{ form_widget(form) }}
</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 }}">
{{ form.vars.label }}
</label>
@@ -125,16 +125,7 @@
{%- block choice_widget_expanded -%}
{%- for child in form %}
<div class="relative flex items-start pb-2">
<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>
{{- form_row(child) -}}
{% endfor -%}
{%- endblock choice_widget_expanded -%}
+1 -1
View File
@@ -11,11 +11,11 @@
{% endif %}
</div>
<div id="content">
{% include '_partials/_flashes.html.twig' %}
{% block content %}{% endblock %}
</div>
<div class="htmx-indicator fixed top-0 left-0 inset-0 z-10 bg-white/90 flex items-center justify-center"
id="loading-indicator">
{% include '_partials/_spinner.html.twig' with { 'class': 'w-8 h-8'} %}
</div>
{% include '_partials/_flashes.html.twig' %}
</div>
+30 -28
View File
@@ -6,55 +6,57 @@
<h2 class="text-2xl font-semibold pb-4">
Persönliche Daten
</h2>
{{ form_start(form) }}
{{ form_start(personalDataForm) }}
<div class="grid grid-cols-2 gap-x-8 gap-y-4 pb-4">
{{ form_row(form.firstName) }}
{{ form_row(form.name) }}
{{ form_row(form.gender) }}
{{ form_row(form.dateOfBirth) }}
{{ form_row(personalDataForm.firstName) }}
{{ form_row(personalDataForm.name) }}
{{ form_row(personalDataForm.gender) }}
{{ form_row(personalDataForm.dateOfBirth) }}
</div>
<h3 class="text-xl font-semibold pb-2">
Anschrift
</h3>
<div class="grid grid-cols-2 gap-x-8 gap-y-4 pb-4">
<div class="col-span-2">
{{ form_row(form.street) }}
{{ form_row(personalDataForm.street) }}
</div>
{{ form_row(form.postCode) }}
{{ form_row(form.city) }}
{{ form_row(form.country) }}
{{ form_row(personalDataForm.postCode) }}
{{ form_row(personalDataForm.city) }}
{{ form_row(personalDataForm.country) }}
</div>
<h3 class="text-xl font-semibold pb-2">
Kontakt
</h3>
<div class="grid grid-cols-2 gap-x-8 gap-y-4 pb-4">
<div class="col-span-2">
{{ form_row(form.email) }}
{{ form_row(personalDataForm.email) }}
</div>
{{ form_row(form.phone) }}
{{ form_row(form.mobile) }}
{{ form_row(personalDataForm.phone) }}
{{ form_row(personalDataForm.mobile) }}
</div>
<button type="submit" class="btn">
Speichern
</button>
{{ form_rest(form) }}
{{ form_end(form) }}
{{ form_rest(personalDataForm) }}
{{ form_end(personalDataForm) }}
</div>
<div>
<h2 class="text-2xl font-semibold pb-4">
Newsletter
</h2>
<p class="pb-4">
Du bist aktuell {% if not personalData.communication.newsletter %}<strong>nicht</strong> {% endif%} zum Newsletter
angemeldet.
</p>
<button type="button" class="btn" hx-post="{{ path('app_personal_data_newsletter') }}" hx-target="#app">
{% if personalData.communication.newsletter %}
vom Newsletter abmelden
{% else %}
zum Newsletter anmelden
{% endif %}
</button>
<div>
<h2 class="text-2xl font-semibold pb-4">
Newsletter
</h2>
<p class="pb-4">
Du bist aktuell {% if not personalData.communication.newsletter %}<strong>nicht</strong> {% endif%} zum Newsletter
angemeldet.
</p>
<button type="button" class="btn" hx-post="{{ path('app_personal_data_newsletter') }}" hx-target="#app" hx-swap="outerHTML">
{% if personalData.communication.newsletter %}
vom Newsletter abmelden
{% else %}
zum Newsletter anmelden
{% endif %}
</button>
</div>
</div>
</div>
{% endblock %}