feat: implement screendesign
@@ -1,9 +1,8 @@
|
||||
import './bootstrap.js'
|
||||
import './styles/app.css'
|
||||
|
||||
import '@iframe-resizer/child'
|
||||
|
||||
import htmx from 'htmx.org'
|
||||
|
||||
window.htmx = htmx
|
||||
htmx.config.includeIndicatorStyles = false
|
||||
htmx.config.historyEnabled = false
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import {Controller} from '@hotwired/stimulus'
|
||||
|
||||
export default class extends Controller {
|
||||
static values = {url: String, alt: String}
|
||||
static classes = ['image']
|
||||
|
||||
connect() {
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
img.classList.add(...this.imageClasses)
|
||||
this.element.innerHTML = ''
|
||||
this.element.appendChild(img)
|
||||
}
|
||||
img.alt = this.altValue
|
||||
img.src = this.urlValue
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Controller } from '@hotwired/stimulus'
|
||||
|
||||
export default class extends Controller {
|
||||
static values = { offsetTop: { type: Number}}
|
||||
connect() {
|
||||
if ('parentIframe' in window) {
|
||||
window.iFrameResizer = {
|
||||
onReady: function () {
|
||||
window.parentIFrame.scrollTo(0, this.offsetTopValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,11 +13,17 @@ export default class extends Controller {
|
||||
this.boundHandleBeforeRequest = this.handleBeforeRequest.bind(this)
|
||||
this.boundHandleAfterRequest = this.handleAfterRequest.bind(this)
|
||||
this.boundHandleTimeout = this.handleTimeout.bind(this)
|
||||
this.boundHandlePageShow = this.handlePageShow.bind(this)
|
||||
this.boundHandleHistoryRestore = this.handleHistoryRestore.bind(this)
|
||||
|
||||
// Listen to HTMX events
|
||||
document.body.addEventListener('htmx:beforeRequest', this.boundHandleBeforeRequest)
|
||||
document.body.addEventListener('htmx:afterRequest', this.boundHandleAfterRequest)
|
||||
document.body.addEventListener('htmx:timeout', this.boundHandleTimeout)
|
||||
document.body.addEventListener('htmx:historyRestore', this.boundHandleHistoryRestore)
|
||||
|
||||
// Listen for browser back/forward navigation
|
||||
window.addEventListener('pageshow', this.boundHandlePageShow)
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
@@ -25,6 +31,8 @@ export default class extends Controller {
|
||||
document.body.removeEventListener('htmx:beforeRequest', this.boundHandleBeforeRequest)
|
||||
document.body.removeEventListener('htmx:afterRequest', this.boundHandleAfterRequest)
|
||||
document.body.removeEventListener('htmx:timeout', this.boundHandleTimeout)
|
||||
document.body.removeEventListener('htmx:historyRestore', this.boundHandleHistoryRestore)
|
||||
window.removeEventListener('pageshow', this.boundHandlePageShow)
|
||||
|
||||
// Clear any pending timeout
|
||||
if (this.debounceTimeout) {
|
||||
@@ -76,6 +84,18 @@ export default class extends Controller {
|
||||
alert('Die Anfrage hat zu lange gedauert. Bitte versuchen Sie es erneut. Falls das Problem weiterhin besteht, kontaktieren Sie bitte unseren Support.')
|
||||
}
|
||||
|
||||
handlePageShow(event) {
|
||||
// event.persisted is true when page is restored from bfcache (back/forward navigation)
|
||||
if (event.persisted) {
|
||||
this.hide()
|
||||
}
|
||||
}
|
||||
|
||||
handleHistoryRestore(event) {
|
||||
// HTMX history restore - hide loading indicator
|
||||
this.hide()
|
||||
}
|
||||
|
||||
show() {
|
||||
this.isVisible = true
|
||||
this.indicatorTarget.classList.remove(this.hiddenClass)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Controller } from '@hotwired/stimulus'
|
||||
import {Controller} from '@hotwired/stimulus'
|
||||
|
||||
export default class extends Controller {
|
||||
static classes = ['closed']
|
||||
static targets = ['toggle', 'icon']
|
||||
static classes = ['closed', 'open', 'iconOpen']
|
||||
static targets = ['content', 'icon', 'container']
|
||||
static values = {
|
||||
open: {
|
||||
type: Boolean,
|
||||
@@ -15,7 +15,7 @@ export default class extends Controller {
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.target = this.hasToggleTarget ? this.toggleTarget : this.element
|
||||
this.target = this.hasContentTarget ? this.contentTarget : this.element
|
||||
|
||||
// Restore state from storage if storageKey is provided
|
||||
if (this.hasStorageKey()) {
|
||||
@@ -39,7 +39,16 @@ export default class extends Controller {
|
||||
this.target.classList.toggle(this.closedClass, false === open)
|
||||
|
||||
if (this.hasIconTarget) {
|
||||
this.iconTarget.classList.toggle('rotate-90', true === open)
|
||||
let iconClass = this.hasIconOpenClass ? this.iconOpenClass : 'rotate-90'
|
||||
this.iconTarget.classList.toggle(iconClass, true === open)
|
||||
}
|
||||
|
||||
// Toggle class on container element (or controller element) if specified
|
||||
if (this.hasOpenClass) {
|
||||
const container = this.hasContainerTarget ? this.containerTarget : this.element
|
||||
this.openClasses.forEach(cls => {
|
||||
container.classList.toggle(cls, true === open)
|
||||
})
|
||||
}
|
||||
|
||||
// Save state to storage if storageKey is provided
|
||||
|
||||
|
After Width: | Height: | Size: 284 KiB |
|
After Width: | Height: | Size: 245 KiB |
|
After Width: | Height: | Size: 313 KiB |
|
After Width: | Height: | Size: 417 KiB |
|
After Width: | Height: | Size: 384 KiB |
@@ -1,236 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<symbol id="icon-dots" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 12.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 18.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Z" />
|
||||
</symbol>
|
||||
<symbol id="icon-feedback" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 01.865-.501 48.172 48.172 0 003.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z" />
|
||||
</symbol>
|
||||
<symbol id="icon-login" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" />
|
||||
</symbol>
|
||||
<symbol id="icon-logout" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
|
||||
</symbol>
|
||||
<symbol id="icon-alert" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
|
||||
</symbol>
|
||||
<symbol id="icon-close" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</symbol>
|
||||
<symbol id="icon-back" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 15L3 9m0 0l6-6M3 9h12a6 6 0 010 12h-3" />
|
||||
</symbol>
|
||||
<symbol id="icon-list" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zM3.75 12h.007v.008H3.75V12zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm-.375 5.25h.007v.008H3.75v-.008zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
|
||||
</symbol>
|
||||
<symbol id="icon-menu" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
||||
</symbol>
|
||||
<symbol id="icon-edit" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" />
|
||||
</symbol>
|
||||
<symbol id="icon-plus" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</symbol>
|
||||
<symbol id="icon-minus" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
|
||||
</symbol>
|
||||
<symbol id="icon-search" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
|
||||
</symbol>
|
||||
<symbol id="icon-user" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
|
||||
</symbol>
|
||||
<symbol id="icon-users" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
|
||||
</symbol>
|
||||
<symbol id="icon-delete" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
|
||||
</symbol>
|
||||
<symbol id="icon-check" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
</symbol>
|
||||
<symbol id="icon-upload" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/>
|
||||
</symbol>
|
||||
<symbol id="icon-download" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||
</symbol>
|
||||
<symbol id="icon-sort" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9" />
|
||||
</symbol>
|
||||
<symbol id="icon-arrow-left" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15m0 0l6.75 6.75M4.5 12l6.75-6.75" />
|
||||
</symbol>
|
||||
<symbol id="icon-arrow-right" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75" />
|
||||
</symbol>
|
||||
<symbol id="icon-document" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
||||
</symbol>
|
||||
<symbol id="icon-filter" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 01-.659 1.591l-5.432 5.432a2.25 2.25 0 00-.659 1.591v2.927a2.25 2.25 0 01-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 00-.659-1.591L3.659 7.409A2.25 2.25 0 013 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0112 3z" />
|
||||
</symbol>
|
||||
<symbol id="icon-refresh" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
|
||||
</symbol>
|
||||
<symbol id="icon-settings" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</symbol>
|
||||
<symbol id="icon-at" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" d="M16.5 12a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0zm0 0c0 1.657 1.007 3 2.25 3S21 13.657 21 12a9 9 0 10-2.636 6.364M16.5 12V8.25" />
|
||||
</symbol>
|
||||
<symbol id="icon-mail" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" />
|
||||
</symbol>
|
||||
<symbol id="icon-house" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 21v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21m0 0h4.5V3.545M12.75 21h7.5V10.75M2.25 21h1.5m18 0h-18M2.25 9l4.5-1.636M18.75 3l-1.5.545m0 6.205l3 1m1.5.5l-1.5-.5M6.75 7.364V3h-3v18m3-13.636l10.5-3.819" />
|
||||
</symbol>
|
||||
<symbol id="icon-phone" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 6.75c0 8.284 6.716 15 15 15h2.25a2.25 2.25 0 002.25-2.25v-1.372c0-.516-.351-.966-.852-1.091l-4.423-1.106c-.44-.11-.902.055-1.173.417l-.97 1.293c-.282.376-.769.542-1.21.38a12.035 12.035 0 01-7.143-7.143c-.162-.441.004-.928.38-1.21l1.293-.97c.363-.271.527-.734.417-1.173L6.963 3.102a1.125 1.125 0 00-1.091-.852H4.5A2.25 2.25 0 002.25 4.5v2.25z" />
|
||||
</symbol>
|
||||
<symbol id="icon-mobile" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 1.5H8.25A2.25 2.25 0 0 0 6 3.75v16.5a2.25 2.25 0 0 0 2.25 2.25h7.5A2.25 2.25 0 0 0 18 20.25V3.75a2.25 2.25 0 0 0-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3" />
|
||||
</symbol>
|
||||
<symbol id="icon-calendar" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5m-9-6h.008v.008H12v-.008zM12 15h.008v.008H12V15zm0 2.25h.008v.008H12v-.008zM9.75 15h.008v.008H9.75V15zm0 2.25h.008v.008H9.75v-.008zM7.5 15h.008v.008H7.5V15zm0 2.25h.008v.008H7.5v-.008zm6.75-4.5h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008V15zm0 2.25h.008v.008h-.008v-.008zm2.25-4.5h.008v.008H16.5v-.008zm0 2.25h.008v.008H16.5V15z" />
|
||||
</symbol>
|
||||
<symbol id="icon-hand" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10.05 4.575a1.575 1.575 0 10-3.15 0v3m3.15-3v-1.5a1.575 1.575 0 013.15 0v1.5m-3.15 0l.075 5.925m3.075.75V4.575m0 0a1.575 1.575 0 013.15 0V15M6.9 7.575a1.575 1.575 0 10-3.15 0v8.175a6.75 6.75 0 006.75 6.75h2.018a5.25 5.25 0 003.712-1.538l1.732-1.732a5.25 5.25 0 001.538-3.712l.003-2.024a.668.668 0 01.198-.471 1.575 1.575 0 10-2.228-2.228 3.818 3.818 0 00-1.12 2.687M6.9 7.575V12m6.27 4.318A4.49 4.49 0 0116.35 15m.002 0h-.002" />
|
||||
</symbol>
|
||||
<symbol id="icon-copy" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6" />
|
||||
</symbol>
|
||||
<symbol id="icon-photo" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
|
||||
</symbol>
|
||||
<symbol id="icon-print" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
||||
<path d="M6.72 13.829c-.24.03-.48.062-.72.096m.72-.096a42.415 42.415 0 0110.56 0m-10.56 0L6.34 18m10.94-4.171c.24.03.48.062.72.096m-.72-.096L17.66 18m0 0l.229 2.523a1.125 1.125 0 01-1.12 1.227H7.231c-.662 0-1.18-.568-1.12-1.227L6.34 18m11.318 0h1.091A2.25 2.25 0 0021 15.75V9.456c0-1.081-.768-2.015-1.837-2.175a48.055 48.055 0 00-1.913-.247M6.34 18H5.25A2.25 2.25 0 013 15.75V9.456c0-1.081.768-2.015 1.837-2.175a48.041 48.041 0 011.913-.247m10.5 0a48.536 48.536 0 00-10.5 0m10.5 0V3.375c0-.621-.504-1.125-1.125-1.125h-8.25c-.621 0-1.125.504-1.125 1.125v3.659M18 10.5h.008v.008H18V10.5zm-3 0h.008v.008H15V10.5z" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</symbol>
|
||||
<symbol id="icon-locked" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
|
||||
</symbol>
|
||||
<symbol id="icon-unlocked" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 10.5V6.75a4.5 4.5 0 119 0v3.75M3.75 21.75h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H3.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
|
||||
</symbol>
|
||||
<symbol id="icon-mask" fill="currentColor" stroke="none" viewBox="0 0 576 512">
|
||||
<!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
|
||||
<path d="M288 64C39.52 64 0 182.1 0 273.5C0 379.5 78.8 448 176 448c27.33 0 51.21-6.516 66.11-36.79l19.93-40.5C268.3 358.6 278.1 352.4 288 352.1c9.9 .3711 19.7 6.501 25.97 18.63l19.93 40.5C348.8 441.5 372.7 448 400 448c97.2 0 176-68.51 176-174.5C576 182.1 536.5 64 288 64zM400 400c-18.12 0-19.56-2.924-23.04-9.986l-20.35-41.33c-13.87-26.86-38.85-43.53-66.82-44.57L288 304L286.2 304.1c-27.96 1.049-52.94 17.71-67.24 45.41l-19.93 40.49C195.6 397.1 194.1 400 176 400c-75.36 0-128-52.04-128-126.5C48 229.3 48 112 288 112s240 117.3 240 161.5C528 347.1 475.4 400 400 400zM160 192C124.7 192 96 220.7 96 256s28.65 64 64 64c35.35 0 64-28.65 64-64S195.3 192 160 192zM416 192c-35.35 0-64 28.65-64 64s28.65 64 64 64c35.35 0 64-28.65 64-64S451.3 192 416 192z"/>
|
||||
</symbol>
|
||||
<symbol id="icon-excel" fill="currentColor" stroke="none" viewBox="0 0 384 512">
|
||||
<!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
|
||||
<path d="M365.3 93.38l-74.63-74.64C278.6 6.742 262.3 0 245.4 0H64C28.65 0 0 28.65 0 64l.0065 384c0 35.34 28.65 64 64 64H320c35.2 0 64-28.8 64-64V138.6C384 121.7 377.3 105.4 365.3 93.38zM336 448c0 8.836-7.164 16-16 16H64.02c-8.838 0-16-7.164-16-16L48 64.13c0-8.836 7.164-16 16-16h160L224 128c0 17.67 14.33 32 32 32h79.1V448zM229.1 233.3L192 280.9L154.9 233.3C146.8 222.8 131.8 220.9 121.3 229.1C110.8 237.2 108.9 252.3 117.1 262.8L161.6 320l-44.53 57.25c-8.156 10.47-6.25 25.56 4.188 33.69C125.7 414.3 130.8 416 135.1 416c7.156 0 14.25-3.188 18.97-9.25L192 359.1l37.06 47.65C233.8 412.8 240.9 416 248 416c5.125 0 10.31-1.656 14.72-5.062c10.44-8.125 12.34-23.22 4.188-33.69L222.4 320l44.53-57.25c8.156-10.47 6.25-25.56-4.188-33.69C252.2 220.9 237.2 222.8 229.1 233.3z"/>
|
||||
</symbol>
|
||||
<symbol id="icon-pdf" fill="currentColor" stroke="none" viewBox="0 0 384 512">
|
||||
<!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
|
||||
<path d="M365.3 93.38l-74.63-74.64C278.6 6.742 262.3 0 245.4 0H64C28.65 0 0 28.65 0 64l.0065 384c0 35.34 28.65 64 64 64H320c35.2 0 64-28.8 64-64V138.6C384 121.7 377.3 105.4 365.3 93.38zM336 448c0 8.836-7.164 16-16 16H64.02c-8.838 0-16-7.164-16-16L48 64.13c0-8.836 7.164-16 16-16h160L224 128c0 17.67 14.33 32 32 32h79.1V448zM202 286.1c.877-2.688 1.74-5.398 2.582-8.145c1.434-5.762 7.488-31.54 7.488-52.47C212.1 207 197.1 192 178.6 192C160.1 192 145.1 207 145.1 225.5c0 .2969 .1641 28.81 13.85 62.3c-7.035 19.36-15.57 38.8-25.41 57.93c-21.49 10.11-39.24 22.23-52.8 36.07c-6.234 6.438-9.367 14.74-9.367 24.72c0 18.45 15.01 33.46 33.46 33.46c10.8 0 20.98-5.227 27.22-13.98c7.322-10.28 18.38-26.9 30.47-48.95c15.8-6.352 33.88-11.72 53.88-16c13.55 9.578 28.9 17.29 45.71 22.95c4.527 1.551 9.402 2.348 14.43 2.348c20.26 0 36.13-16.19 36.13-36.86c0-20.33-16.54-36.87-36.87-36.87h-3.705c-2.727 .125-20.51 1.141-45.37 5.367C216.9 308.9 208.6 298.3 202 286.1zM110.2 410.4c-3.273 4.688-12.03 2.777-12.03-5.312c0-1.754 .6289-3.43 1.729-4.555c9.02-9.219 19.94-17.05 31.85-23.72C122.3 393.1 114.3 404.7 110.2 410.4zM178.6 218.8c3.693 0 6.703 3.008 6.703 6.703c0 15.21-4.109 34.84-5.746 42.1C172.1 245 171.9 227.2 171.9 225.5C171.9 221.8 174.9 218.8 178.6 218.8zM162.3 348.3c6.611-13.48 13.22-28.46 19.38-44.7c6.389 10.92 14.56 21.86 24.96 31.97C192.6 338.8 177.4 342.9 162.3 348.3zM272.4 339.5h3.352c5.539 0 10.05 4.5 10.05 10.79c0 5.129-4.176 9.32-9.32 9.32c-2.029 0-4.059-.3164-5.852-.9414c-12.33-4.137-23.11-9.32-32.54-15.19C258.3 340.3 272.1 339.5 272.4 339.5z"/>
|
||||
</symbol>
|
||||
<symbol id="icon-file" fill="currentColor" stroke="none" viewBox="0 0 384 512">
|
||||
<!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
|
||||
<path d="M365.3 93.38l-74.63-74.64C278.6 6.743 262.3 0 245.4 0L64-.0001c-35.35 0-64 28.65-64 64l.0065 384c0 35.35 28.65 64 64 64H320c35.2 0 64-28.8 64-64V138.6C384 121.7 377.3 105.4 365.3 93.38zM320 464H64.02c-8.836 0-15.1-7.163-16-15.1L48 64.13c-.0004-8.837 7.163-16 16-16h160L224 128c0 17.67 14.33 32 32 32h79.1v288C336 456.8 328.8 464 320 464z"/>
|
||||
</symbol>
|
||||
<symbol id="icon-sigma" fill="currentColor" stroke="none" viewBox="0 0 384 512">
|
||||
<!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
|
||||
<path d="M335.1 136l.0005-56H83.59l165 158.7C253.3 243.2 255.1 249.5 255.1 256s-2.656 12.78-7.375 17.31L83.59 432h252.4l-.0005-56c0-13.25 10.75-24 24-24C373.2 352 384 362.8 384 376v80c0 13.25-10.75 24-24 24H23.99c-9.782 0-18.59-5.938-22.25-15.03s-1.438-19.47 5.625-26.28L197.4 256L7.364 73.31C.3015 66.5-1.917 56.13 1.739 47.03S14.21 32 23.99 32h336C373.2 32 384 42.75 384 56v80C384 149.3 373.2 160 359.1 160C346.7 160 335.1 149.3 335.1 136z"/>
|
||||
</symbol>
|
||||
<symbol id="icon-percent" fill="currentColor" stroke="none" viewBox="0 0 384 512">
|
||||
<!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
|
||||
<path d="M376.1 71.03c-9.375-9.375-24.56-9.375-33.94 0l-336 336c-9.375 9.375-9.375 24.56 0 33.94C11.72 445.7 17.84 448 24 448s12.28-2.344 16.97-7.031l336-336C386.3 95.59 386.3 80.41 376.1 71.03zM64 176c26.51 0 48-21.49 48-48S90.51 80 64 80C37.49 80 16 101.5 16 128S37.49 176 64 176zM320 336c-26.51 0-48 21.49-48 48s21.49 48 48 48c26.51 0 48-21.49 48-48S346.5 336 320 336z"/>
|
||||
</symbol>
|
||||
<symbol id="icon-eye" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</symbol>
|
||||
<symbol id="icon-profile" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M7.864 4.243A7.5 7.5 0 0119.5 10.5c0 2.92-.556 5.709-1.568 8.268M5.742 6.364A7.465 7.465 0 004.5 10.5a7.464 7.464 0 01-1.15 3.993m1.989 3.559A11.209 11.209 0 008.25 10.5a3.75 3.75 0 117.5 0c0 .527-.021 1.049-.064 1.565M12 10.5a14.94 14.94 0 01-3.6 9.75m6.633-4.596a18.666 18.666 0 01-2.485 5.33" />
|
||||
</symbol>
|
||||
<symbol id="icon-chart" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5M9 11.25v1.5M12 9v3.75m3-6v6" />
|
||||
</symbol>
|
||||
<symbol id="icon-info" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
|
||||
</symbol>
|
||||
<symbol id="icon-star" fill="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z" />
|
||||
</symbol>
|
||||
<symbol id="icon-cancel" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</symbol>
|
||||
<symbol id="icon-bell" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0" />
|
||||
</symbol>
|
||||
<symbol id="icon-folder" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
||||
<path d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</symbol>
|
||||
<symbol id="icon-power" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5.636 5.636a9 9 0 1 0 12.728 0M12 3v9" />
|
||||
</symbol>
|
||||
<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-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" />
|
||||
<path d="M18 17m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
||||
<path d="M4 17h-2v-11a1 1 0 0 1 1 -1h14a5 7 0 0 1 5 7v5h-2m-4 0h-8" />
|
||||
<path d="M16 5l1.5 7l4.5 0" />
|
||||
<path d="M2 10l15 0" />
|
||||
<path d="M7 5l0 5" />
|
||||
<path d="M12 5l0 5" />
|
||||
</symbol>
|
||||
<symbol id="icon-ski" fill="currentColor" stroke="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||
<path d="M380.7 48a48 48 0 1 1 96 0 48 48 0 1 1 -96 0zM2.7 268.9c6.1-11.8 20.6-16.3 32.4-10.2L239.8 365l49.6-74.5-82.7-87.9c-9.5-10.1-14.4-22.4-15.1-34.9l87.7 42 49.4 52.5c12.8 13.6 14.5 34.1 4.2 49.6l-50.2 75.4 135.8 70.5c13.6 7.1 29.8 7.2 43.6 .3l15.2-7.6c11.9-5.9 26.3-1.1 32.2 10.7s1.1 26.3-10.7 32.2l-15.2 7.6c-27.5 13.7-59.9 13.5-87.2-.7L12.9 301.3C1.2 295.2-3.4 280.7 2.7 268.9zM118.9 65.6L137 74.2l8.7-17.4c4-7.9 13.6-11.1 21.5-7.2s11.1 13.6 7.2 21.5l-8.5 16.9 55.5 26.6c1.7-.9 3.5-1.7 5.4-2.5l80.9-32.4c32.4-13 68.6 6.5 75.6 40.7l12.3 59.7 62.9 30.1c12 5.7 17 20 11.3 32s-20 17-32 11.3l-54.2-25.9c-1-.3-1.9-.6-2.8-1l-229.1-110-9.2 18.4c-4 7.9-13.6 11.1-21.5 7.2s-11.1-13.6-7.2-21.5l9-18-17.6-8.4c-8-3.8-11.3-13.4-7.5-21.3s13.4-11.3 21.3-7.5zm217.3 64.7c-1-4.9-6.2-7.7-10.8-5.8l-45.7 18.3 65.5 31.5-9-43.9z"/>
|
||||
</symbol>
|
||||
<symbol id="icon-snowboard" fill="currentColor" stroke="nonw" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||
<path d="M204.3 3c11.6-6.4 26.2-2.3 32.6 9.3l31.9 57.4c1.3 2.3 3.6 3.8 6.2 4.1l33.8 3.4c24.1 2.4 46.9 12 65.4 27.6L503.5 213.6c10.1 8.5 11.4 23.7 2.9 33.8s-23.7 11.4-33.8 2.9l-68.2-57.5-82.3 43.9 24.7 18.9c24.9 19 34.6 51.9 24.1 81.4l-28.2 79c-1.4 3.8-3.6 7-6.3 9.6l93.3 35.7c4.6 1.8 9.4 2.6 14.3 2.6H472c13.3 0 24 10.7 24 24s-10.7 24-24 24H443.8c-10.8 0-21.4-2-31.5-5.8L60.1 371.3c-11.5-4.4-22-11.2-30.8-20L7 329c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l22.4 22.4c4 4 8.7 7.1 14 9.1l19.4 7.4c-2.9-11.4 2.9-23.4 14-28.1L192 272l0-68.2c0-21.2 12-40.6 31-50.1L284.4 123l-14.1-1.4c-18.3-1.8-34.5-12.5-43.4-28.5L195 35.7c-6.4-11.6-2.3-26.2 9.3-32.6zm91.8 407.2c-.3-3.4 .1-6.9 1.3-10.3l28.2-79c3.5-9.8 .3-20.8-8-27.1L240 234.4l0 42.9c0 16.1-9.7 30.7-24.6 36.9L134 348.2l162.1 62.1zM259.6 189l20.5 15.7 84.6-45.1-17.2-14.5L259.6 189zM384 48a48 48 0 1 1 96 0 48 48 0 1 1 -96 0z"/>
|
||||
</symbol>
|
||||
<symbol id="icon-hourglass" fill="currentColor" stroke="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512">
|
||||
<!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||
<path d="M24 0C10.7 0 0 10.7 0 24S10.7 48 24 48h8V67c0 40.3 16 79 44.5 107.5L158.1 256 76.5 337.5C48 366 32 404.7 32 445v19H24c-13.3 0-24 10.7-24 24s10.7 24 24 24H360c13.3 0 24-10.7 24-24s-10.7-24-24-24h-8V445c0-40.3-16-79-44.5-107.5L225.9 256l81.5-81.5C336 146 352 107.3 352 67V48h8c13.3 0 24-10.7 24-24s-10.7-24-24-24H24zM192 289.9l81.5 81.5C293 391 304 417.4 304 445v19H80V445c0-27.6 11-54 30.5-73.5L192 289.9zm0-67.9l-81.5-81.5C91 121 80 94.6 80 67V48H304V67c0 27.6-11 54-30.5 73.5L192 222.1z"/>
|
||||
</symbol>
|
||||
<symbol id="icon-flag-a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480"><g fill-rule="evenodd"><path fill="#fff" d="M640 480H0V0h640z"/><path fill="#ed2939" d="M640 480H0V320h640zm0-319.9H0V.1h640z"/></g></symbol>
|
||||
<symbol id="icon-flag-ch" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480"><g fill-rule="evenodd" stroke-width="1pt"><path fill="#d52b1e" d="M0 0h640v480H0z"/><g fill="#fff"><path d="M170 195h300v90H170z"/><path d="M275 90h90v300h-90z"/></g></g></symbol>
|
||||
<symbol id="icon-flag-d" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480"><path fill="#ffce00" d="M0 320h640v160H0z"/><path d="M0 0h640v160H0z"/><path fill="#d00" d="M0 160h640v160H0z"/></symbol>
|
||||
<symbol id="icon-flag-f" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480"><g fill-rule="evenodd" stroke-width="1pt"><path fill="#fff" d="M0 0h640v480H0z"/><path fill="#00267f" d="M0 0h213.3v480H0z"/><path fill="#f31830" d="M426.7 0H640v480H426.7z"/></g></symbol>
|
||||
<symbol id="icon-flag-i" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480"><g fill-rule="evenodd" stroke-width="1pt"><path fill="#fff" d="M0 0h640v480H0z"/><path fill="#009246" d="M0 0h213.3v480H0z"/><path fill="#ce2b37" d="M426.7 0H640v480H426.7z"/></g></symbol>
|
||||
<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
|
||||
<symbol id="icon-spinner" viewBox="0 0 44 44" xmlns="http://www.w3.org/2000/svg" stroke="currentColor">
|
||||
<g fill="none" fill-rule="evenodd" stroke-width="2">
|
||||
<circle cx="22" cy="22" r="1">
|
||||
<animate attributeName="r"
|
||||
begin="0s" dur="1.8s"
|
||||
values="1; 20"
|
||||
calcMode="spline"
|
||||
keyTimes="0; 1"
|
||||
keySplines="0.165, 0.84, 0.44, 1"
|
||||
repeatCount="indefinite" />
|
||||
<animate attributeName="stroke-opacity"
|
||||
begin="0s" dur="1.8s"
|
||||
values="1; 0"
|
||||
calcMode="spline"
|
||||
keyTimes="0; 1"
|
||||
keySplines="0.3, 0.61, 0.355, 1"
|
||||
repeatCount="indefinite" />
|
||||
</circle>
|
||||
<circle cx="22" cy="22" r="1">
|
||||
<animate attributeName="r"
|
||||
begin="-0.9s" dur="1.8s"
|
||||
values="1; 20"
|
||||
calcMode="spline"
|
||||
keyTimes="0; 1"
|
||||
keySplines="0.165, 0.84, 0.44, 1"
|
||||
repeatCount="indefinite" />
|
||||
<animate attributeName="stroke-opacity"
|
||||
begin="-0.9s" dur="1.8s"
|
||||
values="1; 0"
|
||||
calcMode="spline"
|
||||
keyTimes="0; 1"
|
||||
keySplines="0.3, 0.61, 0.355, 1"
|
||||
repeatCount="indefinite" />
|
||||
</circle>
|
||||
</g>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 31 KiB |
@@ -40,3 +40,19 @@
|
||||
.htmx-request.htmx-indicator{
|
||||
@apply visible;
|
||||
}
|
||||
|
||||
.bg-outer {
|
||||
@apply bg-blend-multiply bg-right-top bg-cover;
|
||||
}
|
||||
|
||||
.bg-outer--1 {
|
||||
background-image: url('../images/bg_4.jpg'), linear-gradient(311.39deg, #0070E0 10.55%, #165883 53.93%, #18527B 80.73%);
|
||||
}
|
||||
|
||||
.bg-outer--2 {
|
||||
background-image: url('../images/bg_5.jpg'), linear-gradient(311.39deg, #0070E0 10.55%, #165883 53.93%, #18527B 80.73%);
|
||||
}
|
||||
|
||||
.bg-inner {
|
||||
background-image: linear-gradient(311.39deg, #0070E0cc 16.35%, #165883cc 41.93%, #18527Bcc 56.29%);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
@import "components/typography.css";
|
||||
@import "components/forms.css";
|
||||
@import "components/button.css";
|
||||
@import "components/menu.css";
|
||||
@import "components/tooltip.css";
|
||||
@import "components/table-responsive.css";
|
||||
@import "components/toast.css";
|
||||
@import "components/pagination.css";
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
.button {
|
||||
@apply inline-flex items-center justify-center space-x-2 px-4 py-2 md:px-8 cursor-pointer;
|
||||
@apply inline-flex items-center justify-center space-x-2 h-10 leading-10 px-4 md:px-8 cursor-pointer;
|
||||
@apply uppercase leading-none text-center;
|
||||
@apply transition-colors outline-none focus:outline-2 focus:outline-offset-2 focus:outline-primary-light;
|
||||
@apply rounded-md;
|
||||
}
|
||||
|
||||
.button--small {
|
||||
@@ -12,38 +13,24 @@
|
||||
@apply block w-full;
|
||||
}
|
||||
|
||||
.bg-button {
|
||||
@apply bg-none bg-primary-dark text-white;
|
||||
@apply hover:bg-badge-gradient;
|
||||
.button--secondary {
|
||||
@apply bg-none bg-primary-light text-white;
|
||||
@apply hover:bg-primary-light/80;
|
||||
}
|
||||
|
||||
|
||||
.bg-button--active,
|
||||
.bg-button--secondary {
|
||||
@apply bg-badge-gradient text-white;
|
||||
@apply hover:bg-none hover:bg-pink;
|
||||
.button--primary {
|
||||
@apply bg-pink text-white;
|
||||
@apply hover:bg-none hover:bg-pink/80;
|
||||
}
|
||||
|
||||
.bg-button--muted {
|
||||
.button--muted {
|
||||
@apply bg-zinc-300 hover:bg-zinc-300;
|
||||
}
|
||||
|
||||
.bg-button--dark {
|
||||
.button--dark {
|
||||
@apply bg-primary-dark;
|
||||
}
|
||||
|
||||
.bg-button--light {
|
||||
@apply bg-white border-2;
|
||||
@apply font-bold;
|
||||
@apply border-primary-light text-primary;
|
||||
}
|
||||
|
||||
.bg-button--light:hover,
|
||||
.bg-button--light.button--active {
|
||||
@apply bg-white;
|
||||
@apply border-secondary text-secondary;
|
||||
}
|
||||
|
||||
.button[disabled] {
|
||||
@apply cursor-not-allowed;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,13 @@ label.required:after {
|
||||
}
|
||||
|
||||
.form-field {
|
||||
@apply border-zinc-400 focus:border-zinc-800 ring-0 focus:outline-2 focus:outline-offset-2 focus:outline-primary-light mt-1 block w-full;
|
||||
@apply border-zinc-400 ring-0 mt-1 block w-full rounded-md;
|
||||
@apply focus:outline-primary-light focus:border-zinc-800 focus:outline-2 focus:outline-offset-2;
|
||||
}
|
||||
|
||||
#participant-form .form-field,
|
||||
#form-payment .form-field {
|
||||
@apply bg-primary-bg border-primary-bg;
|
||||
}
|
||||
|
||||
.form-field--has-error {
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
.menu--main {
|
||||
@apply flex items-center m-0 divide-x divide-white;
|
||||
}
|
||||
|
||||
.menu--main a {
|
||||
@apply block py-2 px-4 text-white hover:bg-secondary hover:text-zinc-800 text-sm md:text-base;
|
||||
}
|
||||
|
||||
.menu--main .current a {
|
||||
@apply bg-secondary text-zinc-800;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
:root {
|
||||
--pagination-skew: 2rem;
|
||||
--pagination-border-width: 1px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
@apply flex h-12 lg:h-16 overflow-hidden bg-primary-bg shadow-md;
|
||||
}
|
||||
|
||||
.pagination-item {
|
||||
flex: 1 1 0;
|
||||
margin-right: calc(-1 * var(--pagination-skew));
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.pagination-item:last-child {
|
||||
@apply mr-0;
|
||||
}
|
||||
|
||||
/* Outer clip-path for button shape */
|
||||
.pagination-item:first-child {
|
||||
clip-path: polygon(0 0, calc(100% - var(--pagination-skew)) 0, 100% 100%, 0 100%);
|
||||
}
|
||||
|
||||
.pagination-item:not(:first-child):not(:last-child) {
|
||||
clip-path: polygon(0 0, calc(100% - var(--pagination-skew)) 0, 100% 100%, var(--pagination-skew) 100%);
|
||||
}
|
||||
|
||||
.pagination-item:last-child {
|
||||
clip-path: polygon(0 0, 100% 0, 100% 100%, var(--pagination-skew) 100%);
|
||||
}
|
||||
|
||||
/* Border and content wrappers */
|
||||
.pagination-item__border {
|
||||
@apply w-full h-full flex items-center justify-center bg-primary-dark/10;
|
||||
}
|
||||
|
||||
.pagination-item__inner {
|
||||
@apply w-full h-full flex items-center justify-center;
|
||||
@apply lg:text-2xl font-bold;
|
||||
}
|
||||
|
||||
a.pagination-item__border > .pagination-item__inner:hover {
|
||||
@apply bg-primary-light text-white;
|
||||
}
|
||||
|
||||
/* Inner clip-path for border effect */
|
||||
.pagination-item:first-child .pagination-item__inner {
|
||||
clip-path: polygon(0 0, calc(100% - var(--pagination-skew)) 0, 100% 100%, 0 100%);
|
||||
}
|
||||
|
||||
.pagination-item:not(:first-child) .pagination-item__inner {
|
||||
/* No left/bottom-left border on overlapping edge to prevent double borders */
|
||||
clip-path: polygon(0 0, calc(100% - var(--pagination-skew) - var(--pagination-border-width)) 0, calc(100% - var(--pagination-border-width)) 100%, var(--pagination-skew) 100%);
|
||||
}
|
||||
|
||||
.pagination-item:last-child .pagination-item__inner {
|
||||
/* No left/bottom-left border on overlapping edge, no right border */
|
||||
clip-path: polygon(0 0, 100% 0, 100% 100%, var(--pagination-skew) 100%);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
.responsive {
|
||||
@apply text-sm md:text-base;
|
||||
}
|
||||
|
||||
.responsive thead {
|
||||
@apply hidden md:table-header-group;
|
||||
}
|
||||
|
||||
.responsive tbody tr {
|
||||
@apply block mb-2 md:table-row;
|
||||
}
|
||||
|
||||
.responsive td {
|
||||
@apply grid grid-cols-2 gap-x-2 last:col-span-2 md:table-cell;
|
||||
}
|
||||
|
||||
.responsive td:before {
|
||||
content: attr(data-label);
|
||||
@apply block font-bold last:hidden md:hidden;
|
||||
}
|
||||
@@ -14,7 +14,6 @@
|
||||
"doctrine/doctrine-bundle": "^2.13",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.3",
|
||||
"doctrine/orm": "^3.3",
|
||||
"knplabs/knp-menu-bundle": "^3.4",
|
||||
"league/flysystem-bundle": "^3.4",
|
||||
"league/flysystem-sftp-v3": "^3.29",
|
||||
"league/oauth2-server-bundle": "^1.0",
|
||||
|
||||
@@ -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": "44832f4adf896d77d2fd6eec915e7739",
|
||||
"content-hash": "cdf22f45562d34493b3d37f46bea70bc",
|
||||
"packages": [
|
||||
{
|
||||
"name": "brick/math",
|
||||
@@ -1454,147 +1454,6 @@
|
||||
],
|
||||
"time": "2025-03-06T22:45:56+00:00"
|
||||
},
|
||||
{
|
||||
"name": "knplabs/knp-menu",
|
||||
"version": "v3.8.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/KnpLabs/KnpMenu.git",
|
||||
"reference": "79d325909a1d428a93f1a0f55e90177830e283bb"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/KnpLabs/KnpMenu/zipball/79d325909a1d428a93f1a0f55e90177830e283bb",
|
||||
"reference": "79d325909a1d428a93f1a0f55e90177830e283bb",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.1"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/http-foundation": "<5.4",
|
||||
"twig/twig": "<2.16"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^2.1",
|
||||
"phpunit/phpunit": "^9.6",
|
||||
"psr/container": "^1.0 || ^2.0",
|
||||
"symfony/http-foundation": "^5.4 || ^6.0 || ^7.0",
|
||||
"symfony/phpunit-bridge": "^7.0",
|
||||
"symfony/routing": "^5.4 || ^6.0 || ^7.0",
|
||||
"twig/twig": "^2.16 || ^3.0"
|
||||
},
|
||||
"suggest": {
|
||||
"twig/twig": "for the TwigRenderer and the integration with your templates"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Knp\\Menu\\": "src/Knp/Menu"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "KnpLabs",
|
||||
"homepage": "https://knplabs.com"
|
||||
},
|
||||
{
|
||||
"name": "Christophe Coevoet",
|
||||
"email": "[email protected]"
|
||||
},
|
||||
{
|
||||
"name": "The Community",
|
||||
"homepage": "https://github.com/KnpLabs/KnpMenu/contributors"
|
||||
}
|
||||
],
|
||||
"description": "An object oriented menu library",
|
||||
"homepage": "https://knplabs.com",
|
||||
"keywords": [
|
||||
"menu",
|
||||
"tree"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/KnpLabs/KnpMenu/issues",
|
||||
"source": "https://github.com/KnpLabs/KnpMenu/tree/v3.8.0"
|
||||
},
|
||||
"time": "2025-06-13T15:03:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "knplabs/knp-menu-bundle",
|
||||
"version": "v3.7.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/KnpLabs/KnpMenuBundle.git",
|
||||
"reference": "aa22e57f8f41c34ad5e382aae4d0c12998c0eb5a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/KnpLabs/KnpMenuBundle/zipball/aa22e57f8f41c34ad5e382aae4d0c12998c0eb5a",
|
||||
"reference": "aa22e57f8f41c34ad5e382aae4d0c12998c0eb5a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"knplabs/knp-menu": "^3.8",
|
||||
"php": "^8.1",
|
||||
"symfony/config": "^6.4 | ^7.0 | ^8.0",
|
||||
"symfony/dependency-injection": "^6.4 | ^7.0 | ^8.0",
|
||||
"symfony/deprecation-contracts": "^2.5 | ^3.3",
|
||||
"symfony/http-kernel": "^6.4 | ^7.0 | ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.5 | ^11.5 | ^12.4",
|
||||
"symfony/expression-language": "^6.4 | ^7.0 | ^8.0",
|
||||
"symfony/phpunit-bridge": "^7.0 | ^8.0",
|
||||
"symfony/templating": "^6.4 | ^7.0 | ^8.0"
|
||||
},
|
||||
"type": "symfony-bundle",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Knp\\Bundle\\MenuBundle\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Knplabs",
|
||||
"homepage": "http://knplabs.com"
|
||||
},
|
||||
{
|
||||
"name": "Christophe Coevoet",
|
||||
"email": "[email protected]"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://github.com/KnpLabs/KnpMenuBundle/contributors"
|
||||
}
|
||||
],
|
||||
"description": "This bundle provides an integration of the KnpMenu library",
|
||||
"keywords": [
|
||||
"menu"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/KnpLabs/KnpMenuBundle/issues",
|
||||
"source": "https://github.com/KnpLabs/KnpMenuBundle/tree/v3.7.0"
|
||||
},
|
||||
"time": "2025-11-30T08:30:04+00:00"
|
||||
},
|
||||
{
|
||||
"name": "lcobucci/clock",
|
||||
"version": "3.3.1",
|
||||
|
||||
@@ -13,7 +13,6 @@ return [
|
||||
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
|
||||
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
|
||||
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
|
||||
Knp\Bundle\MenuBundle\KnpMenuBundle::class => ['all' => true],
|
||||
League\FlysystemBundle\FlysystemBundle::class => ['all' => true],
|
||||
League\Bundle\OAuth2ServerBundle\LeagueOAuth2ServerBundle::class => ['all' => true],
|
||||
Zenstruck\ScheduleBundle\ZenstruckScheduleBundle::class => ['all' => true],
|
||||
|
||||
@@ -45,14 +45,6 @@ services:
|
||||
arguments:
|
||||
$intlExtension: '@twig.extension.intl'
|
||||
|
||||
App\Menu\MenuBuilder:
|
||||
arguments:
|
||||
$factory: '@knp_menu.factory'
|
||||
tags:
|
||||
- name: knp_menu.menu_builder
|
||||
method: createMainMenu
|
||||
alias: main
|
||||
|
||||
App\Command\GenerateKeysCommand:
|
||||
arguments:
|
||||
$path: '%path_to_keys%'
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
"name": "myep-next",
|
||||
"license": "WTFPL",
|
||||
"dependencies": {
|
||||
"@iframe-resizer/child": "^5.3.2",
|
||||
"tippy.js": "^6.3.7",
|
||||
"toastify-js": "^1.12.0"
|
||||
},
|
||||
@@ -1638,19 +1637,6 @@
|
||||
"@hotwired/stimulus": ">= 3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@iframe-resizer/child": {
|
||||
"version": "5.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@iframe-resizer/child/-/child-5.5.7.tgz",
|
||||
"integrity": "sha512-+/t5E9/wbB+sWg6xM6fRWvNZ48WcZSJXHwXt58EdsWnb1vW+N4wf0Tp6Bwq9JPN4GR64yjXAhb0bBq67ECr/lg==",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"auto-console-group": "1.2.11"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://iframe-resizer.com/pricing/"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/schemas": {
|
||||
"version": "29.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
|
||||
@@ -2750,12 +2736,6 @@
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/auto-console-group": {
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/auto-console-group/-/auto-console-group-1.2.11.tgz",
|
||||
"integrity": "sha512-/RFCswabfQZR4CDYser0V+AC+6+Q1ro2+RrP0AinOXDYnWsm3w14uc2MVdEaaSCDfpiTtETfahE9N7z9Yb1JiA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/autoprefixer": {
|
||||
"version": "10.4.22",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz",
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
"cy:open": "cypress open"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iframe-resizer/child": "^5.3.2",
|
||||
"tippy.js": "^6.3.7",
|
||||
"toastify-js": "^1.12.0"
|
||||
}
|
||||
|
||||
@@ -68,17 +68,30 @@ class BookingDataProcessor
|
||||
$participantData = ParticipantDto::fromPersonalData($participant);
|
||||
$participantData->index = $index;
|
||||
|
||||
// First participant (applicant): copy address from applicant if participant address is empty
|
||||
// BPN API may return full address only in <anmelder> but minimal/empty address in <teilnehmer id="1">
|
||||
// Only copy if first participant has no street (indicating empty/incomplete address)
|
||||
// This allows applicant and first participant to be different people with different addresses
|
||||
if (0 === $index && null !== $booking->applicant->address) {
|
||||
$isEmpty = null === $participantData->address
|
||||
|| null === $participantData->address->street
|
||||
|| '' === trim($participantData->address->street);
|
||||
// First participant (applicant): copy data from applicant if participant data is empty
|
||||
// BPN API may return full data only in <anmelder> but minimal/empty data in <teilnehmer id="1">
|
||||
if (0 === $index) {
|
||||
// Copy address if first participant has no street (indicating empty/incomplete address)
|
||||
// This allows applicant and first participant to be different people with different addresses
|
||||
if (null !== $booking->applicant->address) {
|
||||
$isEmpty = null === $participantData->address
|
||||
|| null === $participantData->address->street
|
||||
|| '' === trim($participantData->address->street);
|
||||
|
||||
if ($isEmpty) {
|
||||
$participantData->address = clone $booking->applicant->address;
|
||||
if ($isEmpty) {
|
||||
$participantData->address = clone $booking->applicant->address;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy body dimensions from applicant if not present in participant
|
||||
if (null === $participantData->height && null !== $booking->applicant->height) {
|
||||
$participantData->height = $booking->applicant->height;
|
||||
}
|
||||
if (null === $participantData->weight && null !== $booking->applicant->weight) {
|
||||
$participantData->weight = $booking->applicant->weight;
|
||||
}
|
||||
if (null === $participantData->shoeSize && null !== $booking->applicant->shoeSize) {
|
||||
$participantData->shoeSize = $booking->applicant->shoeSize;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -304,6 +304,17 @@ class BookingPayloadBuilder
|
||||
}
|
||||
}
|
||||
|
||||
// Add body dimensions
|
||||
if (null !== $participant->height) {
|
||||
$participantData['sonstiges1'] = $participant->height;
|
||||
}
|
||||
if (null !== $participant->weight) {
|
||||
$participantData['sonstiges2'] = $participant->weight;
|
||||
}
|
||||
if (null !== $participant->shoeSize) {
|
||||
$participantData['sonstiges3'] = $participant->shoeSize;
|
||||
}
|
||||
|
||||
// Add wishes (room remarks and license plate)
|
||||
if (null !== $participant->remarksRoom || null !== $participant->licensePlate) {
|
||||
$participantData['wünsche'] = [];
|
||||
|
||||
@@ -20,7 +20,7 @@ class PersonalDataSynchronizer
|
||||
/**
|
||||
* Updates participant personal data from form input.
|
||||
*
|
||||
* Only processes participants with status 'F' (active/confirmed participants).
|
||||
* Processes all active participants (status 'F' or 'A'). Skips canceled participants (status 'S').
|
||||
* Updates all personal data fields and communication information.
|
||||
*
|
||||
* IMPORTANT: The applicant's address must never be modified. This method updates
|
||||
@@ -32,7 +32,8 @@ class PersonalDataSynchronizer
|
||||
public function updateParticipantPersonalData(array $participants, Booking $bookingData): void
|
||||
{
|
||||
foreach ($participants as $participant) {
|
||||
if ('F' !== $participant->status) {
|
||||
// Skip canceled participants (status 'S')
|
||||
if ('S' === $participant->status) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ class PersonalData
|
||||
public ?string $name = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['Default', 'personal_data'])]
|
||||
public string $firstName = '';
|
||||
public ?string $firstName = '';
|
||||
|
||||
public ?string $salutation = null;
|
||||
public ?string $title = null;
|
||||
@@ -55,6 +55,11 @@ class PersonalData
|
||||
$this->communication = new Communication();
|
||||
}
|
||||
|
||||
public function getFullName(): string
|
||||
{
|
||||
return sprintf('%s %s', $this->firstName, $this->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the personal data to API payload format.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Account;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
class IndexController extends AbstractController
|
||||
{
|
||||
#[Route('/account', name: 'app_account')]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
public function index(): Response
|
||||
{
|
||||
return $this->render('account/index.html.twig');
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
namespace App\Controller\Account;
|
||||
|
||||
use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\Exception\ApiClientException;
|
||||
@@ -98,7 +98,7 @@ class PersonalDataController extends AbstractController
|
||||
return $this->redirectToRoute('app_personal_data');
|
||||
}
|
||||
|
||||
return $this->render('personal_data/index.html.twig', [
|
||||
return $this->render('account/personal_data.html.twig', [
|
||||
'personalData' => $personalData,
|
||||
'personalDataForm' => $personalDataForm->createView(),
|
||||
]);
|
||||
@@ -9,6 +9,7 @@ use App\Exception\HotelNotFoundException;
|
||||
use App\Exception\HotelNotInTravelException;
|
||||
use App\Exception\NoRoomsAvailableException;
|
||||
use App\Exception\TravelNotFoundException;
|
||||
use App\Htmx\HxTrait;
|
||||
use App\Service\BookingService;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -24,6 +25,8 @@ use Symfony\Component\Routing\Attribute\Route;
|
||||
*/
|
||||
class IndexController extends AbstractController
|
||||
{
|
||||
use HxTrait;
|
||||
|
||||
public function __construct(
|
||||
private readonly BookingService $bookingService,
|
||||
private readonly AgencyLoader $agencyLoader,
|
||||
@@ -113,18 +116,22 @@ class IndexController extends AbstractController
|
||||
#[Route('/bookings/cancel', name: 'app_booking_cancel')]
|
||||
public function cancel(Request $request): Response
|
||||
{
|
||||
// Clear the booking session
|
||||
$this->bookingService->clearBookingSession($request);
|
||||
if (Request::METHOD_POST === $request->getMethod()) {
|
||||
// Clear the booking session
|
||||
$this->bookingService->clearBookingSession($request);
|
||||
|
||||
// Clear the security target path to prevent redirect loop after login
|
||||
// Without this, logging in after cancel would redirect back to a stale booking URL
|
||||
$request->getSession()->remove('_security.main.target_path');
|
||||
// Clear the security target path to prevent redirect loop after login
|
||||
// Without this, logging in after cancel would redirect back to a stale booking URL
|
||||
$request->getSession()->remove('_security.main.target_path');
|
||||
|
||||
// Add a flash message to inform the user
|
||||
$this->addFlash('info', 'Buchung abgebrochen.');
|
||||
// Add a flash message to inform the user
|
||||
$this->addFlash('info', 'Buchung abgebrochen.');
|
||||
|
||||
// Redirect to login page
|
||||
return $this->redirectToRoute('app_login');
|
||||
// Redirect to login page
|
||||
return $this->hxRedirect($request, $this->generateUrl('app_login'));
|
||||
}
|
||||
|
||||
return $this->render('booking/modal_cancel.html.twig');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,7 +17,8 @@ class SecurityController extends AbstractController
|
||||
public function login(AuthenticationUtils $authenticationUtils, Request $request, BookingService $bookingService): Response
|
||||
{
|
||||
// Check if this is a booking flow (BookingDto exists in session)
|
||||
$isBookingFlow = null !== $bookingService->getBookingDto($request, BookingService::BOOKING_CREATE_KEY);
|
||||
$bookingDto = $bookingService->getBookingDto($request, BookingService::BOOKING_CREATE_KEY);
|
||||
$isBookingFlow = null !== $bookingDto;
|
||||
|
||||
// If authenticated and in booking flow, proceed to Step 1
|
||||
if (null !== $this->getUser() && true === $isBookingFlow) {
|
||||
@@ -52,6 +53,9 @@ class SecurityController extends AbstractController
|
||||
return $this->render($template, [
|
||||
'last_username' => $lastUsername,
|
||||
'error' => $error,
|
||||
'travel_title' => $bookingDto?->travel->label,
|
||||
'travel_date_from' => $bookingDto?->travel->dateFrom,
|
||||
'travel_date_to' => $bookingDto?->travel->dateTo,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Model;
|
||||
|
||||
use App\BusProNet\Model\BankAccount;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
@@ -11,11 +12,11 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
*/
|
||||
class BankAccountDto
|
||||
{
|
||||
#[Assert\NotBlank(message: 'Bitte geben Sie Ihre IBAN ein.')]
|
||||
#[Assert\NotBlank(message: 'Bitte gib deine IBAN ein.')]
|
||||
#[Assert\Iban(message: 'Die eingegebene IBAN ist ungültig.')]
|
||||
public ?string $iban = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'Bitte geben Sie den Kontoinhaber ein.')]
|
||||
#[Assert\NotBlank(message: 'Bitte gib den Kontoinhaber ein.')]
|
||||
#[Assert\Length(
|
||||
min: 2,
|
||||
max: 70,
|
||||
@@ -30,10 +31,10 @@ class BankAccountDto
|
||||
)]
|
||||
public ?string $bankName = null;
|
||||
|
||||
#[Assert\IsTrue(message: 'Bitte akzeptieren Sie das SEPA-Mandat.')]
|
||||
#[Assert\IsTrue(message: 'Bitte akzeptiere das SEPA-Mandat.')]
|
||||
public bool $sepaMandateAccepted = false;
|
||||
|
||||
public static function fromBankAccount(\App\BusProNet\Model\BankAccount $bankAccount): static
|
||||
public static function fromBankAccount(BankAccount $bankAccount): static
|
||||
{
|
||||
$instance = new static();
|
||||
$instance->iban = $bankAccount->iban;
|
||||
|
||||
@@ -41,7 +41,7 @@ class BookingDto
|
||||
|
||||
#[Assert\Choice(
|
||||
choices: [Constants::PAYMENT_METHOD_TRANSFER, Constants::PAYMENT_METHOD_DEBIT],
|
||||
message: 'Bitte wählen Sie eine gültige Zahlungsart.'
|
||||
message: 'Bitte wähle eine gültige Zahlungsart.'
|
||||
)]
|
||||
public ?string $paymentMethod = Constants::PAYMENT_METHOD_TRANSFER;
|
||||
|
||||
@@ -255,7 +255,7 @@ class BookingDto
|
||||
}
|
||||
|
||||
if (null === $this->bankAccount) {
|
||||
$context->buildViolation('Bitte geben Sie Ihre Bankverbindung an.')
|
||||
$context->buildViolation('Bitte gib deine Bankverbindung an.')
|
||||
->atPath('bankAccount')
|
||||
->addViolation();
|
||||
|
||||
@@ -263,19 +263,19 @@ class BookingDto
|
||||
}
|
||||
|
||||
if (null === $this->bankAccount->iban || '' === trim($this->bankAccount->iban)) {
|
||||
$context->buildViolation('Bitte geben Sie Ihre IBAN ein.')
|
||||
$context->buildViolation('Bitte gib deine IBAN ein.')
|
||||
->atPath('bankAccount.iban')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if (null === $this->bankAccount->accountHolder || '' === trim($this->bankAccount->accountHolder)) {
|
||||
$context->buildViolation('Bitte geben Sie den Kontoinhaber ein.')
|
||||
$context->buildViolation('Bitte gib den Kontoinhaber ein.')
|
||||
->atPath('bankAccount.accountHolder')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if (false === $this->bankAccount->sepaMandateAccepted) {
|
||||
$context->buildViolation('Bitte akzeptieren Sie das SEPA-Mandat.')
|
||||
$context->buildViolation('Bitte akzeptiere das SEPA-Mandat.')
|
||||
->atPath('bankAccount.sepaMandateAccepted')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ class BookingSummaryDto
|
||||
/**
|
||||
* @param array<int, RoomSelectionDto> $selectedRooms Selected room DTOs from booking
|
||||
* @param int $participantCount Total participant count from room capacity
|
||||
* @param string $totalPrice Formatted total price (e.g., "1.234,56 €")
|
||||
* @param float $totalPrice Total price before voucher deductions
|
||||
* @param float $payableAmount Amount after voucher deductions
|
||||
* @param array<array{room: mixed, count: int}> $groupedSelectedRooms Rooms grouped by participant assignments
|
||||
* @param array<int, int> $assignmentCounts Room ID to participant count mapping
|
||||
* @param array $pricingData Detailed pricing breakdown
|
||||
@@ -24,7 +25,8 @@ class BookingSummaryDto
|
||||
public function __construct(
|
||||
public readonly array $selectedRooms,
|
||||
public readonly int $participantCount,
|
||||
public readonly string $totalPrice,
|
||||
public readonly float $totalPrice,
|
||||
public readonly float $payableAmount,
|
||||
public readonly array $groupedSelectedRooms,
|
||||
public readonly array $assignmentCounts,
|
||||
public readonly array $pricingData,
|
||||
|
||||
@@ -35,7 +35,6 @@ class PersonalDataType extends AbstractType
|
||||
->add('email', EmailType::class, [
|
||||
'label' => 'E-Mail',
|
||||
'property_path' => 'communication.email',
|
||||
'sanitize_html' => true,
|
||||
])
|
||||
->add('phone', TextType::class, [
|
||||
'label' => 'Telefon',
|
||||
|
||||
@@ -29,10 +29,10 @@ class RegistrationType extends AbstractType
|
||||
])
|
||||
->add('name', TextType::class, [
|
||||
'label' => 'Nachname',
|
||||
'sanitize_html' => true,
|
||||
])
|
||||
->add('email', EmailType::class, [
|
||||
'label' => 'E-Mail',
|
||||
'sanitize_html' => true,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class RoomSelectType extends AbstractType
|
||||
@@ -20,15 +22,8 @@ class RoomSelectType extends AbstractType
|
||||
$data = $event->getData();
|
||||
$form = $event->getForm();
|
||||
|
||||
// Build label with pricing
|
||||
$label = 'Anzahl '.$data->roomLabel;
|
||||
if (null !== $data->roomPrice) {
|
||||
$formattedPrice = number_format((float) $data->roomPrice, 2, ',', '.');
|
||||
$label .= sprintf(' (€%s pro Person)', $formattedPrice);
|
||||
}
|
||||
|
||||
$form->add('quantity', StepSelectChoiceType::class, [
|
||||
'label' => $label,
|
||||
'label' => false,
|
||||
'required' => true,
|
||||
'min_value' => 0,
|
||||
'max_value' => $data->maxQuantity,
|
||||
@@ -37,6 +32,19 @@ class RoomSelectType extends AbstractType
|
||||
});
|
||||
}
|
||||
|
||||
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
$data = $form->getData();
|
||||
|
||||
$view->vars['label_room'] = $data->roomLabel;
|
||||
$view->vars['label_price'] = null;
|
||||
|
||||
if (null !== $data->roomPrice) {
|
||||
$formattedPrice = number_format((float) $data->roomPrice, 2, ',', '.');
|
||||
$view->vars['label_price'] = sprintf(' %s € pro Person', $formattedPrice);
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
|
||||
@@ -167,7 +167,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
$participantIndex
|
||||
),
|
||||
'choice_value' => 'id',
|
||||
'choice_label' => fn (?Service $service) => $this->formatServiceLabelWithPrice($service),
|
||||
'choice_label' => fn (?Service $service) => $service?->label,
|
||||
'choice_attr' => function (?Service $service) use ($bookingDto, $participantIndex) {
|
||||
if (null === $service) {
|
||||
return [];
|
||||
@@ -205,7 +205,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
$participantIndex
|
||||
),
|
||||
'choice_value' => 'id',
|
||||
'choice_label' => fn (?Service $service) => $this->formatServiceLabelWithPrice($service),
|
||||
'choice_label' => fn (?Service $service) => $service?->label,
|
||||
'choice_attr' => function (?Service $service) use ($bookingDto, $participantIndex) {
|
||||
if (null === $service) {
|
||||
return [];
|
||||
@@ -251,7 +251,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
$participantIndex
|
||||
),
|
||||
'choice_value' => 'id',
|
||||
'choice_label' => fn (?Service $service) => $this->formatServiceLabelWithPrice($service),
|
||||
'choice_label' => fn (?Service $service) => $service?->label,
|
||||
'choice_attr' => function (?Service $service) use ($bookingDto, $participantIndex) {
|
||||
if (null === $service) {
|
||||
return [];
|
||||
@@ -294,7 +294,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
$participantIndex
|
||||
),
|
||||
'choice_value' => 'id',
|
||||
'choice_label' => fn (?Service $service) => $this->formatServiceLabelWithPrice($service),
|
||||
'choice_label' => fn (?Service $service) => $service?->label,
|
||||
'choice_attr' => function (?Service $service) use ($bookingDto, $participantIndex) {
|
||||
if (null === $service) {
|
||||
return [];
|
||||
@@ -353,7 +353,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
$participantIndex
|
||||
),
|
||||
'choice_value' => 'id',
|
||||
'choice_label' => fn (?Service $service) => $this->formatServiceLabelWithPrice($service),
|
||||
'choice_label' => fn (?Service $service) => $service?->label,
|
||||
'choice_attr' => function (?Service $service) use ($bookingDto, $participantIndex) {
|
||||
if (null === $service) {
|
||||
return [];
|
||||
@@ -396,7 +396,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
$bookingDto,
|
||||
$participantIndex
|
||||
),
|
||||
'choice_label' => fn (Service $service) => $this->formatTransportationServiceLabel($service),
|
||||
'choice_label' => fn (Service $service) => $service?->label,
|
||||
'choice_value' => 'id',
|
||||
'expanded' => true,
|
||||
'multiple' => false,
|
||||
@@ -427,7 +427,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
$this->fieldOptionProviders['transportationInbound'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [
|
||||
'label' => 'Rückfahrt',
|
||||
'choices' => $bookingDto->travel->getTransportationServicesByDirection(DirectionMapper::INBOUND_TRAVEL),
|
||||
'choice_label' => fn (Service $service) => $this->formatTransportationServiceLabel($service),
|
||||
'choice_label' => fn (Service $service) => $service?->label,
|
||||
'choice_value' => 'id',
|
||||
'expanded' => true,
|
||||
'multiple' => false,
|
||||
@@ -653,41 +653,6 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
return sprintf('%s (€%s)', $service->label, number_format($service->price, 2, ',', '.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format transportation service labels with type indicator and pricing.
|
||||
*
|
||||
* Creates user-friendly labels for transportation services that include:
|
||||
* - Transportation type icon (🚌 for bus, 🚗 for car)
|
||||
* - Service name
|
||||
* - Pricing (with discount indication for negative prices)
|
||||
* - Availability warning for limited services
|
||||
*
|
||||
* @param Service $service The transportation service to format
|
||||
*
|
||||
* @return string The formatted transportation service label
|
||||
*/
|
||||
private function formatTransportationServiceLabel(Service $service): string
|
||||
{
|
||||
$label = $service->label;
|
||||
|
||||
if (null === $service->price || 0.0 === $service->price) {
|
||||
return $service->label;
|
||||
}
|
||||
|
||||
if ($service->price > 0) {
|
||||
$label .= sprintf(' (€%s)', number_format($service->price, 2, ',', '.'));
|
||||
} else {
|
||||
$label .= sprintf(' (-%s€ Rabatt)', number_format(abs($service->price), 2, ',', '.'));
|
||||
}
|
||||
|
||||
// Add availability warning if limited
|
||||
if (null !== $service->available && $service->available <= 5) {
|
||||
$label .= sprintf(' (nur %d verfügbar)', $service->available);
|
||||
}
|
||||
|
||||
return $label;
|
||||
}
|
||||
|
||||
private function formatPickupLabelWithPrice(?Pickup $pickup): string
|
||||
{
|
||||
if (null === $pickup) {
|
||||
@@ -935,11 +900,6 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
*/
|
||||
private function filterTransportationChoices(array $services, BookingDto $bookingDto, int $participantIndex): array
|
||||
{
|
||||
// Only apply filtering in create mode
|
||||
if (BookingDto::MODE_CREATE !== $bookingDto->getMode()) {
|
||||
return $services;
|
||||
}
|
||||
|
||||
// Separate PKW/CAR from other services (BUS, etc.)
|
||||
$pkwServices = [];
|
||||
$otherServices = [];
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Menu;
|
||||
|
||||
use Knp\Menu\FactoryInterface;
|
||||
use Knp\Menu\ItemInterface;
|
||||
|
||||
class MenuBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private readonly FactoryInterface $factory,
|
||||
) {
|
||||
}
|
||||
|
||||
public function createMainMenu(): ItemInterface
|
||||
{
|
||||
$menu = $this->factory->createItem('root', [
|
||||
'childrenAttributes' => [
|
||||
'class' => 'menu menu--main',
|
||||
],
|
||||
]);
|
||||
|
||||
$menu->addChild('Meine Daten', [
|
||||
'route' => 'app_personal_data',
|
||||
'linkAttributes' => [
|
||||
'data-action' => 'loading#toggle',
|
||||
],
|
||||
]);
|
||||
$menu->addChild('Meine Buchungen', [
|
||||
'route' => 'app_bookings',
|
||||
'linkAttributes' => [
|
||||
'data-action' => 'loading#toggle',
|
||||
],
|
||||
'extras' => [
|
||||
'routes' => [
|
||||
'app_booking_edit',
|
||||
],
|
||||
],
|
||||
]);
|
||||
$menu->addChild('Logout', [
|
||||
'route' => 'app_logout',
|
||||
'linkAttributes' => [
|
||||
'data-action' => 'loading#toggle',
|
||||
],
|
||||
]);
|
||||
|
||||
return $menu;
|
||||
}
|
||||
}
|
||||
@@ -112,7 +112,7 @@ class BpnAuthenticator extends AbstractLoginFormAuthenticator implements Authent
|
||||
return new RedirectResponse($targetPath);
|
||||
}
|
||||
|
||||
return new RedirectResponse($this->urlGenerator->generate('app_personal_data'));
|
||||
return new RedirectResponse($this->urlGenerator->generate('app_account'));
|
||||
}
|
||||
|
||||
private function collectRoles(CrmAttributes $crmAttributes): array
|
||||
|
||||
@@ -69,10 +69,14 @@ class BookingSummaryDataService
|
||||
// Calculate participant count from room capacity (source of truth)
|
||||
$participantCount = $this->calculateParticipantCountFromRooms($bookingDto);
|
||||
|
||||
// Calculate payable amount after voucher deductions
|
||||
$payableAmount = $this->calculatePayableAmount($bookingDto, $pricingData['grandTotal'], $participantPrices);
|
||||
|
||||
return new BookingSummaryDto(
|
||||
selectedRooms: $selectedRooms,
|
||||
participantCount: $participantCount,
|
||||
totalPrice: number_format($totalPrice, 2, ',', '.').' €',
|
||||
totalPrice: $pricingData['grandTotal'],
|
||||
payableAmount: $payableAmount,
|
||||
groupedSelectedRooms: $groupedSelectedRooms,
|
||||
assignmentCounts: $roomCounts,
|
||||
pricingData: $pricingData,
|
||||
@@ -81,13 +85,35 @@ class BookingSummaryDataService
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates participant count from room selections.
|
||||
* Calculates the payable amount after voucher deductions.
|
||||
*
|
||||
* This is the source of truth for participant count, calculated by
|
||||
* multiplying each selected room's quantity by its maximum capacity (maxPax).
|
||||
* @param array<int, float> $participantPrices Prices per participant for percentage voucher calculation
|
||||
*/
|
||||
private function calculatePayableAmount(BookingDto $bookingDto, float $grandTotal, array $participantPrices): float
|
||||
{
|
||||
$acceptedVouchers = $bookingDto->getAcceptedVouchers($participantPrices);
|
||||
|
||||
if (null === $acceptedVouchers) {
|
||||
return $grandTotal;
|
||||
}
|
||||
|
||||
return max(0.0, $grandTotal - $acceptedVouchers->getTotalDiscount());
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates participant count.
|
||||
*
|
||||
* In edit mode, counts actual participants. In create mode, calculates
|
||||
* from room selections by multiplying quantity by maximum capacity (maxPax).
|
||||
*/
|
||||
private function calculateParticipantCountFromRooms(BookingDto $bookingDto): int
|
||||
{
|
||||
// In edit mode, use actual participant count
|
||||
if (BookingDto::MODE_EDIT === $bookingDto->getMode()) {
|
||||
return count($bookingDto->participants);
|
||||
}
|
||||
|
||||
// In create mode, calculate from room selections
|
||||
$totalCapacity = 0;
|
||||
$availableRooms = $bookingDto->travel->getAvailableRooms();
|
||||
|
||||
|
||||
@@ -24,12 +24,11 @@ class AppExtension extends AbstractExtension
|
||||
public function getFunctions(): array
|
||||
{
|
||||
return [
|
||||
new TwigFunction('icon', [AppRuntime::class, 'renderIcon'], ['needs_environment' => true, 'is_safe' => ['html']]),
|
||||
new TwigFunction('is_participant_eligible', [AppRuntime::class, 'isParticipantEligible']),
|
||||
new TwigFunction('qa_attribute', [AppRuntime::class, 'renderQaAttribute'], ['is_safe' => ['html']]),
|
||||
new TwigFunction('is_static_text', [AppRuntime::class, 'isStaticText']),
|
||||
new TwigFunction('is_hidden', [AppRuntime::class, 'isHidden']),
|
||||
new TwigFunction('gravatar_url', [AppRuntime::class, 'getGravatarUrl']),
|
||||
new TwigFunction('collect_invalid_field_labels', [AppRuntime::class, 'collectInvalidFieldLabels']),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,14 +52,6 @@ class AppRuntime implements RuntimeExtensionInterface
|
||||
return $this->intlExtension->formatCurrency($amount, 'EUR');
|
||||
}
|
||||
|
||||
public function renderIcon(Environment $environment, string $icon, string $classes = 'w-5 h-5'): string
|
||||
{
|
||||
return $environment->render('_partials/_icon.html.twig', [
|
||||
'icon' => $icon,
|
||||
'class' => $classes,
|
||||
]);
|
||||
}
|
||||
|
||||
public function mapStatus(string $status): string
|
||||
{
|
||||
$status = strtoupper($status);
|
||||
@@ -171,18 +163,35 @@ class AppRuntime implements RuntimeExtensionInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a Gravatar URL for the given email address.
|
||||
* Recursively collects labels of invalid form fields.
|
||||
*
|
||||
* @param string $email The email address
|
||||
* @param int $size The size of the avatar in pixels (default: 80)
|
||||
* @param FormView $form The form view to check
|
||||
*
|
||||
* @return string The Gravatar URL
|
||||
* @return array<string> Array of invalid field labels
|
||||
*/
|
||||
public function getGravatarUrl(string $email, int $size = 80): string
|
||||
public function collectInvalidFieldLabels(FormView $form): array
|
||||
{
|
||||
$hash = md5(strtolower(trim($email)));
|
||||
$labels = [];
|
||||
|
||||
return sprintf('https://www.gravatar.com/avatar/%s?s=%d&d=404', $hash, $size);
|
||||
foreach ($form->children as $child) {
|
||||
$hasErrors = \count($child->vars['errors']) > 0;
|
||||
$hasInvalidChildren = false === $child->vars['valid'];
|
||||
|
||||
if ($hasErrors || $hasInvalidChildren) {
|
||||
$hasChildren = \count($child->children) > 0;
|
||||
$isExpanded = $child->vars['expanded'] ?? false;
|
||||
|
||||
if ($hasChildren && false === $isExpanded && false === $hasErrors) {
|
||||
// Nested form without own errors (e.g., address, bodyDimensions) - recurse
|
||||
$labels = array_merge($labels, $this->collectInvalidFieldLabels($child));
|
||||
} else {
|
||||
// Leaf field, expanded choice, or compound field with own errors
|
||||
$labels[] = $child->vars['label'] ?? $child->vars['name'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $labels;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -47,9 +47,6 @@
|
||||
".php-cs-fixer.dist.php"
|
||||
]
|
||||
},
|
||||
"knplabs/knp-menu-bundle": {
|
||||
"version": "v3.4.2"
|
||||
},
|
||||
"league/flysystem-bundle": {
|
||||
"version": "3.4",
|
||||
"recipe": {
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
'./templates/**/*.twig',
|
||||
'./src/**/*.php',
|
||||
],
|
||||
safelist: ['rotate-90'],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: '2rem',
|
||||
content: [
|
||||
'./templates/**/*.twig',
|
||||
'./src/**/*.php',
|
||||
],
|
||||
safelist: ['rotate-90'],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: '2rem',
|
||||
},
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Lato', 'sans-serif'],
|
||||
},
|
||||
colors: {
|
||||
primary: '#3d8ccb',
|
||||
secondary: '#f9c700',
|
||||
'primary-bg': '#ebf3fa',
|
||||
'primary-light': '#0070e0',
|
||||
'primary-medium': '#165883',
|
||||
'primary-dark': '#18527b',
|
||||
pink: '#fa1a8c',
|
||||
},
|
||||
backgroundImage: {
|
||||
'badge-gradient': 'linear-gradient(120deg, rgba(150,1,103,1) 0%, rgba(158,3,106,1) 40%, rgba(250,26,140,1) 100%)',
|
||||
'brand-gradient': 'linear-gradient(to top right, #009fff, #165883, #18527b)',
|
||||
},
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Lato', 'sans-serif'],
|
||||
},
|
||||
colors: {
|
||||
primary: '#3d8ccb',
|
||||
secondary: '#f9c700',
|
||||
'primary-bg': '#ebf3fa',
|
||||
'primary-light': '#0080ff',
|
||||
'primary-medium': '#165883',
|
||||
'primary-dark': '#18527b',
|
||||
pink: '#fa1a8c',
|
||||
},
|
||||
backgroundImage: {
|
||||
'badge-gradient': 'linear-gradient(120deg, rgba(150,1,103,1) 0%, rgba(158,3,106,1) 40%, rgba(250,26,140,1) 100%)',
|
||||
'brand-gradient': 'linear-gradient(to top right, #009fff, #165883, #18527b)',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
require('@tailwindcss/forms'),
|
||||
],
|
||||
plugins: [
|
||||
require('@tailwindcss/forms'),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
{% if modal is not defined %}
|
||||
{% set modal = false %}
|
||||
{% endif %}
|
||||
<div class="{{ html_classes('rounded-md mb-4', { 'py-4': modal == true, 'p-4': modal == false, 'bg-red-50': level == 'error', 'bg-green-50': level == 'success', 'bg-blue-50': level == 'info' }) }}">
|
||||
<div class="flex">
|
||||
<div class="shrink-0">
|
||||
{% if level == 'error' %}
|
||||
<svg class="size-5 text-red-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<div class="{{ html_classes('rounded-md mb-4', { 'p-4': modal == false, 'bg-red-500': level == 'error', 'bg-green-500': level == 'success', 'bg-yellow-500': level == 'warning', 'bg-primary-light': level == 'info' }) }}">
|
||||
<div class="flex items-center">
|
||||
<div class="shrink-0 text-white">
|
||||
{% if level == 'error' or level == 'warning' %}
|
||||
<svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<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>
|
||||
{% elseif level == 'success' %}
|
||||
<svg class="size-5 text-green-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm3.857-9.809a.75.75 0 0 0-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 1 0-1.06 1.061l2.5 2.5a.75.75 0 0 0 1.137-.089l4-5.5Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
{% else %}
|
||||
<svg class="size-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0Zm-7-4a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM9 9a.75.75 0 0 0 0 1.5h.253a.25.25 0 0 1 .244.304l-.459 2.066A1.75 1.75 0 0 0 10.747 15H11a.75.75 0 0 0 0-1.5h-.253a.25.25 0 0 1-.244-.304l.459-2.066A1.75 1.75 0 0 0 9.253 9H9Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
{% if title is defined %}
|
||||
<div class="{{ html_classes('text-sm', { 'text-red-700': level == 'error', 'text-green-800': level == 'success', 'text-blue-800': level == 'info', 'mb-2': messages is defined }) }}">
|
||||
<div class="text-white">
|
||||
{{ title }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if messages is defined %}
|
||||
<div class="{{ html_classes('text-sm', { 'text-red-700': level == 'error', 'text-green-800': level == 'success', 'text-blue-800': level == 'info' }) }}">
|
||||
<ul role="list" class="list-disc space-y-1 pl-4">
|
||||
<div class="text-white">
|
||||
<ul role="list" class="space-y-1">
|
||||
{% for message in messages %}
|
||||
<li>
|
||||
{{ message|raw }}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<div class="flex items-center justify-between">
|
||||
<a href="https://www.ep-reisen.de" title="zur E&P Website">
|
||||
<img src="{{ asset('build/images/logo.svg') }}" alt="" class="h-10 my-2 w-auto">
|
||||
</a>
|
||||
<div class="flex items-center space-x-2">
|
||||
{% if is_granted('ROLE_USER') %}
|
||||
<a href="{{ path('app_logout') }}" title="aus MyE&P abmelden">
|
||||
<svg class="w-10 h-10 text-primary-dark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><polyline points="112 40 48 40 48 216 112 216" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="112" y1="128" x2="224" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><polyline points="184 88 224 128 184 168" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
</a>
|
||||
<a href="{{ path('app_account') }}" title="zu meinem Account">
|
||||
<svg class="w-10 h-10 text-primary-dark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><circle cx="128" cy="128" r="96" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><circle cx="128" cy="120" r="40" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M63.8,199.37a72,72,0,0,1,128.4,0" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ path('app_login') }}" title="zu meinem Account">
|
||||
<svg class="w-10 h-10 text-primary-dark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><circle cx="128" cy="128" r="96" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><circle cx="128" cy="120" r="40" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M63.8,199.37a72,72,0,0,1,128.4,0" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,4 +1,39 @@
|
||||
<div class="invisible fixed top-0 left-0 inset-0 z-10 backdrop-blur-sm flex items-center justify-center transition-all duration-100"
|
||||
<div class="invisible fixed top-0 left-0 inset-0 z-50 backdrop-blur-sm flex items-center justify-center transition-all duration-100"
|
||||
{{ stimulus_target('loading', 'indicator') }}>
|
||||
{% include '_partials/_spinner.html.twig' with { 'class': 'text-primary-light w-32 h-32 lg:w-48 lg:h-48'} %}
|
||||
<svg class="text-primary-light w-32 h-32 lg:w-48 lg:h-48" viewBox="0 0 44 44" xmlns="http://www.w3.org/2000/svg" stroke="currentColor">
|
||||
<g fill="none" fill-rule="evenodd" stroke-width="2">
|
||||
<circle cx="22" cy="22" r="1">
|
||||
<animate attributeName="r"
|
||||
begin="0s" dur="1.8s"
|
||||
values="1; 20"
|
||||
calcMode="spline"
|
||||
keyTimes="0; 1"
|
||||
keySplines="0.165, 0.84, 0.44, 1"
|
||||
repeatCount="indefinite" />
|
||||
<animate attributeName="stroke-opacity"
|
||||
begin="0s" dur="1.8s"
|
||||
values="1; 0"
|
||||
calcMode="spline"
|
||||
keyTimes="0; 1"
|
||||
keySplines="0.3, 0.61, 0.355, 1"
|
||||
repeatCount="indefinite" />
|
||||
</circle>
|
||||
<circle cx="22" cy="22" r="1">
|
||||
<animate attributeName="r"
|
||||
begin="-0.9s" dur="1.8s"
|
||||
values="1; 20"
|
||||
calcMode="spline"
|
||||
keyTimes="0; 1"
|
||||
keySplines="0.165, 0.84, 0.44, 1"
|
||||
repeatCount="indefinite" />
|
||||
<animate attributeName="stroke-opacity"
|
||||
begin="-0.9s" dur="1.8s"
|
||||
values="1; 0"
|
||||
calcMode="spline"
|
||||
keyTimes="0; 1"
|
||||
keySplines="0.3, 0.61, 0.355, 1"
|
||||
repeatCount="indefinite" />
|
||||
</circle>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
<div class="fixed inset-0 w-full h-full z-50" {{ stimulus_controller('modal') }} {{ stimulus_action('modal', 'close', 'modal-close@window') }}>
|
||||
<div class="absolute inset-0 w-full h-full bg-black/80" {{ stimulus_action('modal', 'close', 'click') }}></div>
|
||||
<div class="absolute inset-0 backdrop-blur-sm" {{ stimulus_action('modal', 'close', 'click') }}></div>
|
||||
<div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 px-4 w-full max-w-2xl">
|
||||
<div class="{{ html_classes('relative py-8 rounded-md', { 'bg-red-50': level == 'error', 'bg-green-50': level == 'success', 'bg-blue-50': level == 'info' }) }}">
|
||||
<div class="{{ html_classes('flex justify-between pb-4 px-8', { 'text-red-800': level == 'error', 'text-green-800': level == 'success', 'text-blue-800': level == 'info' }) }}">
|
||||
<div class="text-2xl font-medium">
|
||||
<div class="{{ html_classes('relative py-4 rounded-md', { 'bg-red-500': level == 'error', 'bg-green-500': level == 'success', 'bg-primary-light': level == 'info' }) }}">
|
||||
<div class="flex justify-between pb-4 px-4">
|
||||
<div class="text-2xl font-medium text-white">
|
||||
{% block title %}{% endblock %}
|
||||
</div>
|
||||
<button type="button"
|
||||
class="inline-block"
|
||||
class="inline-block bg-gray-200 rounded-md p-1"
|
||||
{{ stimulus_action('modal', 'close') }}>
|
||||
{{ icon('close', 'w-6 h-6')}}
|
||||
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="200" y1="56" x2="56" y2="200" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="200" y1="200" x2="56" y2="56" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="max-h-96 overflow-y-auto">
|
||||
<div class="{{ html_classes('px-8', { 'text-red-700': level == 'error', 'text-green-700': level == 'success', 'text-blue-700': level == 'info' }) }}">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
<div class="px-4 max-h-96 overflow-y-auto">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
|
||||
<svg class="{{ class }}" viewBox="0 0 44 44" xmlns="http://www.w3.org/2000/svg" stroke="currentColor">
|
||||
<g fill="none" fill-rule="evenodd" stroke-width="2">
|
||||
<circle cx="22" cy="22" r="1">
|
||||
<animate attributeName="r"
|
||||
begin="0s" dur="1.8s"
|
||||
values="1; 20"
|
||||
calcMode="spline"
|
||||
keyTimes="0; 1"
|
||||
keySplines="0.165, 0.84, 0.44, 1"
|
||||
repeatCount="indefinite" />
|
||||
<animate attributeName="stroke-opacity"
|
||||
begin="0s" dur="1.8s"
|
||||
values="1; 0"
|
||||
calcMode="spline"
|
||||
keyTimes="0; 1"
|
||||
keySplines="0.3, 0.61, 0.355, 1"
|
||||
repeatCount="indefinite" />
|
||||
</circle>
|
||||
<circle cx="22" cy="22" r="1">
|
||||
<animate attributeName="r"
|
||||
begin="-0.9s" dur="1.8s"
|
||||
values="1; 20"
|
||||
calcMode="spline"
|
||||
keyTimes="0; 1"
|
||||
keySplines="0.165, 0.84, 0.44, 1"
|
||||
repeatCount="indefinite" />
|
||||
<animate attributeName="stroke-opacity"
|
||||
begin="-0.9s" dur="1.8s"
|
||||
values="1; 0"
|
||||
calcMode="spline"
|
||||
keyTimes="0; 1"
|
||||
keySplines="0.3, 0.61, 0.355, 1"
|
||||
repeatCount="indefinite" />
|
||||
</circle>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,16 @@
|
||||
{# Validation error alert box #}
|
||||
{# Parameters:
|
||||
- form: The form to check for validation errors
|
||||
- message: Optional custom error message
|
||||
- showLabels: Whether to show the list of invalid field labels (default: true)
|
||||
#}
|
||||
{% macro validation_alert(form, message, showLabels = true) %}
|
||||
{% if not form.vars.valid %}
|
||||
{% set errorLabels = showLabels ? collect_invalid_field_labels(form) : [] %}
|
||||
{% include '_partials/_alert.html.twig' with {
|
||||
level: 'error',
|
||||
title: message|default('Du hast nicht alle Pflichtfelder ausgefüllt oder einzelne Angaben sind ungültig:'),
|
||||
messages: [errorLabels | join(', ')]
|
||||
} %}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
@@ -0,0 +1,33 @@
|
||||
{% extends 'layout.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="px-4 lg:px-8 py-8 lg:py-16">
|
||||
<h1 class="text-white uppercase pb-4">
|
||||
Mein<br>Account
|
||||
</h1>
|
||||
</div>
|
||||
<div class="p-4 ld:p-8">
|
||||
<nav>
|
||||
<ul class="divide-y divide-primary-bg/40">
|
||||
<li class="py-4">
|
||||
<a href="{{ path('app_bookings') }}" class="flex items-center space-x-2 group" {{ stimulus_action('loading', 'toggle') }}>
|
||||
<svg class="w-8 h-8 text-white group-hover:text-primary-light" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="128" y1="128" x2="216" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="128" y1="64" x2="216" y2="64" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="128" y1="192" x2="216" y2="192" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><polyline points="40 64 56 80 88 48" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><polyline points="40 128 56 144 88 112" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><polyline points="40 192 56 208 88 176" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
<span class="text-xl uppercase text-white group-hover:text-primary-light">Meine Buchungen</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="py-4">
|
||||
<a href="{{ path('app_personal_data') }}" class="flex items-center space-x-2 group" {{ stimulus_action('loading', 'toggle') }}>
|
||||
<svg class="w-8 h-8 text-white group-hover:text-primary-light" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M160,224c3.67-13.8,16.6-24,32-24s28.33,10.2,32,24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><circle cx="192" cy="176" r="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M120,208H40a8,8,0,0,1-8-8V64a8,8,0,0,1,8-8H93.33a8,8,0,0,1,4.8,1.6l27.74,20.8a8,8,0,0,0,4.8,1.6H216a8,8,0,0,1,8,8v32" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
<span class="text-xl uppercase text-white group-hover:text-primary-light">Meine Daten</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="py-4">
|
||||
<a href="{{ path('app_logout') }}" class="flex items-center space-x-2 group">
|
||||
<svg class="w-8 h-8 text-white group-hover:text-primary-light" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><polyline points="112 40 48 40 48 216 112 216" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="112" y1="128" x2="224" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><polyline points="184 88 224 128 184 168" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
<span class="text-xl uppercase text-white group-hover:text-primary-light">abmelden</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,68 @@
|
||||
{% extends 'layout.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="px-4 lg:px-8 py-8 lg:py-16">
|
||||
<h1 class="text-white uppercase pb-4">
|
||||
Meine<br>Daten
|
||||
</h1>
|
||||
</div>
|
||||
<div class="p-4 ld:p-8">
|
||||
<div class="divide-y divide-primary-bg/40">
|
||||
<div class="grid md:grid-cols-2 gap-x-8 gap-y-4 pb-8">
|
||||
<div class="text-white">
|
||||
<strong>Name</strong>: {{ personalData.fullName }}
|
||||
<br>
|
||||
<strong>Gender</strong>: {{ personalData.gender|map_gender }}
|
||||
<br>
|
||||
<strong>Geburtsdatum</strong>: {{ personalData.dateOfBirth|date('d.m.Y') }}
|
||||
<br>
|
||||
<strong>Nationalität</strong>: {{ personalData.nationality|map_nationality|default('-') }}
|
||||
</div>
|
||||
<div id="newsletter">
|
||||
<p class="pb-4 text-white">
|
||||
Du bist aktuell {% if not personalData.communication.newsletter %}<strong>nicht</strong> {% endif%} zum Newsletter
|
||||
angemeldet.
|
||||
</p>
|
||||
<button type="button"
|
||||
class="relative button button--primary"
|
||||
hx-post="{{ path('app_personal_data_newsletter') }}"
|
||||
hx-target="#newsletter"
|
||||
hx-select="#newsletter"
|
||||
hx-swap="outerHTML">
|
||||
{% if personalData.communication.newsletter %}jetzt abmelden{% else %}jetzt anmelden{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-8">
|
||||
<h2 class="text-white uppercase text-xl">
|
||||
Kontaktdaten
|
||||
</h2>
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
{{ form_start(personalDataForm, { 'attr': { 'data-action': 'loading#toggle' } }) }}
|
||||
<div class="grid md:grid-cols-2 gap-x-8 gap-y-4 pb-4">
|
||||
<div>
|
||||
{{ form_row(personalDataForm.street, { 'label_attr': { 'class': 'text-white' } }) }}
|
||||
{{ form_row(personalDataForm.postCode, { 'label_attr': { 'class': 'text-white' } }) }}
|
||||
{{ form_row(personalDataForm.city, { 'label_attr': { 'class': 'text-white' } }) }}
|
||||
{{ form_row(personalDataForm.country, { 'label_attr': { 'class': 'text-white' } }) }}
|
||||
</div>
|
||||
<div>
|
||||
{{ form_row(personalDataForm.email, { 'label_attr': { 'class': 'text-white' } }) }}
|
||||
{{ form_row(personalDataForm.phone, { 'label_attr': { 'class': 'text-white' } }) }}
|
||||
{{ form_row(personalDataForm.mobile, { 'label_attr': { 'class': 'text-white' } }) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<a href="{{ path('app_account') }}" class="button button--secondary">
|
||||
zurück
|
||||
</a>
|
||||
<button type="submit" class="button button--primary">
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
{{ form_rest(personalDataForm) }}
|
||||
{{ form_end(personalDataForm) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -2,6 +2,7 @@
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}MyE&P.next{% endblock %}</title>
|
||||
{% block stylesheets %}
|
||||
{{ encore_entry_link_tags('app') }}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<div class="mt-8 pt-4 border-t border-gray-200 text-center">
|
||||
<a href="{{ path('app_booking_cancel') }}" class="text-sm text-gray-600 hover:text-gray-900 hover:underline">
|
||||
Buchung abbrechen und zur Startseite zurückkehren
|
||||
</a>
|
||||
</div>
|
||||
@@ -0,0 +1,75 @@
|
||||
{%- block form_row -%}
|
||||
{%- set row_attr = row_attr|merge({ 'class': (row_attr.class|default('') ~ ' mb-4')|trim }) -%}
|
||||
{%- set widget_attr = {} -%}
|
||||
{%- if help is not empty -%}
|
||||
{%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%}
|
||||
{%- endif -%}
|
||||
{%- if form.vars.expanded is defined and form.vars.expanded -%}
|
||||
<div class="pb-4">
|
||||
<fieldset class="border border-primary-bg">
|
||||
<legend class="{{ html_classes('w-full bg-primary-bg p-2 font-semibold uppercase', { 'text-red-500': form.vars.errors | length > 0 }) }}">
|
||||
{{ label }}
|
||||
{%- if required -%}<span>*</span>{%- endif -%}
|
||||
</legend>
|
||||
{{- form_widget(form, widget_attr) -}}
|
||||
</fieldset>
|
||||
{{- form_errors(form) -}}
|
||||
{{- form_help(form) -}}
|
||||
</div>
|
||||
{%- else -%}
|
||||
<div{% with {attr: row_attr} %}{{ block('attributes') }}{% endwith %}>
|
||||
{{- form_label(form) -}}
|
||||
{{- form_widget(form, widget_attr) -}}
|
||||
{{- form_errors(form) -}}
|
||||
{{- form_help(form) -}}
|
||||
</div>
|
||||
{%- endif -%}
|
||||
{%- endblock form_row -%}
|
||||
|
||||
{%- block choice_widget_expanded -%}
|
||||
<table class="w-full table-fixed border-collapse">
|
||||
{%- for child in form %}
|
||||
{% set choiceData = form.vars.choices[loop.index0].data %}
|
||||
{% set description = child.vars.attr['data-description']|default(null) %}
|
||||
{% set isReadonly = child.vars.attr.readonly is defined %}
|
||||
{%- set child_attr = {} -%}
|
||||
{%- if attr['hx-trigger'] is defined -%}
|
||||
{%- set child_attr = {
|
||||
'hx-trigger': attr['hx-trigger'],
|
||||
'hx-post': attr['hx-post'],
|
||||
'hx-swap': attr['hx-swap']
|
||||
} -%}
|
||||
{%- endif -%}
|
||||
<tr>
|
||||
<td class="border border-primary-bg p-2 align-top">
|
||||
<div>{{- child.vars.label -}}</div>
|
||||
{%- if description is not null -%}
|
||||
<div {{ stimulus_controller('tooltip') }}>
|
||||
<button type="button" data-tooltip-target="trigger" class="text-sm text-primary-light">
|
||||
Details
|
||||
</button>
|
||||
<template data-tooltip-target="template">
|
||||
<div class="text-sm">{{ description|raw }}</div>
|
||||
</template>
|
||||
</div>
|
||||
{%- endif -%}
|
||||
</td>
|
||||
{% if choiceData %}
|
||||
<td class="border border-primary-bg p-2 align-top w-24 text-right whitespace-nowrap">
|
||||
{% if choiceData.price %}
|
||||
{{ choiceData.price | format_currency('EUR') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="border border-primary-bg p-2 align-top w-12 text-center">
|
||||
{% set childTooltip = child.vars.attr['data-tooltip']|default(null) %}
|
||||
<div class="{{ html_classes({ 'cursor-not-allowed': isReadonly }) }}"{% if childTooltip is not null %} {{ stimulus_controller('tooltip', { 'content': childTooltip }) }}{% endif %}>
|
||||
<div class="{{ isReadonly ? 'pointer-events-none' : '' }}">
|
||||
{{- form_widget(child, { 'attr': child_attr }) -}}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor -%}
|
||||
</table>
|
||||
{%- endblock choice_widget_expanded -%}
|
||||
@@ -0,0 +1,30 @@
|
||||
{% set current_step = current_step ?? 1 %}
|
||||
{% set step_routes = {
|
||||
1: 'app_booking_create_step_1',
|
||||
2: 'app_booking_create_step_2',
|
||||
3: 'app_booking_create_step_3',
|
||||
4: 'app_booking_create_step_4'
|
||||
} %}
|
||||
<nav class="pagination">
|
||||
{% for step in 1..4 %}
|
||||
<div class="pagination-item">
|
||||
{% if step < current_step %}
|
||||
<a href="{{ path(step_routes[step]) }}"
|
||||
class="pagination-item__border" {{ stimulus_action('loading', 'toggle') }}>
|
||||
<span class="pagination-item__inner bg-primary-bg text-gray-800">
|
||||
{{ step }}
|
||||
</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="pagination-item__border">
|
||||
<span class="{{ html_classes('pagination-item__inner', {
|
||||
'bg-primary-light text-white': step == current_step,
|
||||
'bg-primary-bg text-gray-800': step != current_step
|
||||
}) }}">
|
||||
{{ step }}
|
||||
</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
@@ -4,66 +4,44 @@
|
||||
{% set errorMessages = cardData.errorMessages|default([]) %}
|
||||
{% set mode = mode|default('create') %}
|
||||
|
||||
<div id="participant-card-{{ index }}"
|
||||
class="{{ html_classes('border rounded p-4', { 'border-gray-400 bg-gray-50': isCanceled, 'border-red-700': not isValid }) }}">
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-shrink-0"
|
||||
{{ stimulus_controller('gravatar', {
|
||||
url: gravatar_url(cardData.email),
|
||||
alt: cardData.name
|
||||
}, {
|
||||
image: 'w-12 h-12 rounded-full'
|
||||
}) }}
|
||||
{{ qa_attribute('participant-avatar', index) }}>
|
||||
{{ icon('user', 'w-12 h-12 text-gray-400') }}
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="font-semibold {{ isCanceled ? 'text-gray-600' : '' }}" {{ qa_attribute('participant-name', index) }}>
|
||||
{{ cardData.name }}
|
||||
</h3>
|
||||
{% if isCanceled %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-700 text-white" {{ qa_attribute('participant-canceled', index) }}>
|
||||
storniert
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if not isValid %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-700 text-white" {{ qa_attribute('participant-invalid', index) }}>
|
||||
unvollständige oder fehlerhafte Daten
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p class="text-sm text-gray-600" {{ qa_attribute('participant-room-name', index) }}>{{ cardData.roomName }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-4" id="participant-card-{{ index }}">
|
||||
<div class="flex items-start space-x-4">
|
||||
<div class="{{ html_classes('w-12 h-12 inline-flex flex-shrink-0 items-center justify-center rounded-full text-white text-2xl font-bold', { 'bg-primary-dark': isValid, 'bg-red-500': not isValid }) }}">
|
||||
{{ participantNumber }}
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="font-medium {{ isCanceled ? 'text-gray-500' : '' }}" {{ qa_attribute('participant-price', index) }}>{{ cardData.price }}</span>
|
||||
<div class="flex-1 leading-5">
|
||||
<div class="font-semibold mb-1">
|
||||
{{ cardData.name }}
|
||||
</div>
|
||||
{% if isCanceled %}
|
||||
<button type="button"
|
||||
class="button bg-button bg-button--secondary opacity-50 cursor-not-allowed"
|
||||
disabled
|
||||
title="Stornierte Teilnehmer können nicht bearbeitet werden">
|
||||
Bearbeiten
|
||||
</button>
|
||||
<div class="text-xs font-medium text-gray-700" {{ qa_attribute('participant-canceled', index) }}>
|
||||
storniert
|
||||
</div>
|
||||
{% elseif not isValid %}
|
||||
<div class="text-xs font-medium text-red-500" {{ qa_attribute('participant-invalid', index) }}>
|
||||
unvollständige/fehlerhafte Daten
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-gray-600" {{ qa_attribute('participant-room-name', index) }}>
|
||||
{{ cardData.roomName }}
|
||||
<br>
|
||||
{{ cardData.price }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="flex flex-col justify-center">
|
||||
{% if not isCanceled %}
|
||||
{% if mode == 'edit' %}
|
||||
<a href="{{ path('app_booking_edit_participant', {id: bookingId, index: index}) }}"
|
||||
class="button bg-button bg-button--secondary"
|
||||
data-action="click->loading#show"
|
||||
{{ qa_attribute('btn-edit-participant', index) }}>
|
||||
Bearbeiten
|
||||
</a>
|
||||
{% set url = path('app_booking_edit_participant', {id: bookingId, index: index}) %}
|
||||
{% else %}
|
||||
<a href="{{ path('app_booking_create_step_2_participant', {index: index}) }}"
|
||||
class="button bg-button bg-button--secondary"
|
||||
data-action="click->loading#show"
|
||||
{{ qa_attribute('btn-edit-participant', index) }}>
|
||||
Bearbeiten
|
||||
</a>
|
||||
{% set url = path('app_booking_create_step_2_participant', {index: index}) %}
|
||||
{% endif %}
|
||||
<a href="{{ url }}"
|
||||
class="button button--small button--primary"
|
||||
data-action="click->loading#show"
|
||||
{{ qa_attribute('btn-edit-participant', index) }}>
|
||||
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M92.69,216H48a8,8,0,0,1-8-8V163.31a8,8,0,0,1,2.34-5.65L165.66,34.34a8,8,0,0,1,11.31,0L221.66,79a8,8,0,0,1,0,11.31L98.34,213.66A8,8,0,0,1,92.69,216Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="136" y1="64" x2="192" y2="120" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="164" y1="92" x2="68" y2="188" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="95.49" y1="215.49" x2="40.51" y2="160.51" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
{% import _self as macros %}
|
||||
|
||||
{# Macro to render a form field or static value based on field state #}
|
||||
{% macro field_or_static(form, fieldName, label, staticValue, bookingDto, participantIndex, options = {}) %}
|
||||
{% if form[fieldName] is defined %}
|
||||
@@ -27,384 +25,519 @@
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{# Specialized macro for checkbox fields (like rental insurance) that need manual fieldset wrapping #}
|
||||
{% macro checkbox_field(form, fieldName, label, options = {}, undefinedLabel = 'Nicht wählbar') %}
|
||||
{% if form[fieldName] is defined %}
|
||||
<fieldset class="mb-1">
|
||||
<legend class="font-semibold mb-1">{{ label }}</legend>
|
||||
{{ form_row(form[fieldName], options) }}
|
||||
</fieldset>
|
||||
{% else %}
|
||||
<fieldset class="mb-1">
|
||||
<legend class="font-semibold mb-1">{{ label }}</legend>
|
||||
{% if undefinedLabel %}
|
||||
<div class="text-sm text-gray-500">{{ undefinedLabel }}</div>
|
||||
{# Macro to render a service choice table row with optional price and tooltip #}
|
||||
{% macro service_choice_row(child, price, tooltipText, tooltipLabel, widgetAttr = {}) %}
|
||||
{% set isReadonly = child.vars.attr.readonly is defined %}
|
||||
<tr>
|
||||
<td class="border border-primary-bg p-2 align-top">
|
||||
<div>{{ child.vars.label }}</div>
|
||||
{%- if tooltipText is not null -%}
|
||||
<div {{ stimulus_controller('tooltip') }}>
|
||||
<button type="button" data-tooltip-target="trigger" class="text-sm text-primary-light">
|
||||
{{ tooltipLabel|default('Details') }}
|
||||
</button>
|
||||
<template data-tooltip-target="template">
|
||||
<div class="text-sm">{{ tooltipText|raw }}</div>
|
||||
</template>
|
||||
</div>
|
||||
{%- endif -%}
|
||||
</td>
|
||||
<td class="border border-primary-bg p-2 align-top w-24 text-right whitespace-nowrap">
|
||||
{% if price %}
|
||||
{{ price | format_currency('EUR') }}
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="border border-primary-bg p-2 align-top w-12 text-center">
|
||||
{% set childTooltip = child.vars.attr['data-tooltip']|default(null) %}
|
||||
<div class="{{ html_classes({ 'cursor-not-allowed': isReadonly }) }}"{% if childTooltip is not null %} {{ stimulus_controller('tooltip', { 'content': childTooltip }) }}{% endif %}>
|
||||
<div class="{{ isReadonly ? 'pointer-events-none' : '' }}">
|
||||
{{ form_widget(child, { 'attr': widgetAttr }) }}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endmacro %}
|
||||
|
||||
{# Macro to render a checkbox table row (for trailing checkboxes like rental insurance, parking, bulk insurance) #}
|
||||
{% macro checkbox_row(checkboxField, label, price, tooltipText, tooltipLabel, widgetAttr = {}) %}
|
||||
{% set isReadonly = checkboxField.vars.attr.readonly is defined %}
|
||||
<tr>
|
||||
<td class="border border-primary-bg p-2 align-top">
|
||||
<div>{{ label }}</div>
|
||||
{%- if tooltipText is not null -%}
|
||||
<div {{ stimulus_controller('tooltip') }}>
|
||||
<button type="button" data-tooltip-target="trigger" class="text-sm text-primary-light">
|
||||
{{ tooltipLabel|default('Info') }}
|
||||
</button>
|
||||
<template data-tooltip-target="template">
|
||||
<div class="text-sm">{{ tooltipText|raw }}</div>
|
||||
</template>
|
||||
</div>
|
||||
{%- endif -%}
|
||||
</td>
|
||||
<td class="border border-primary-bg p-2 align-top w-24 text-right whitespace-nowrap">
|
||||
{% if price %}
|
||||
{{ price | format_currency('EUR') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="border border-primary-bg p-2 align-top w-12 text-center">
|
||||
{% set fieldTooltip = checkboxField.vars.attr['data-tooltip']|default(null) %}
|
||||
<div class="{{ html_classes({ 'cursor-not-allowed': isReadonly }) }}"{% if fieldTooltip is not null %} {{ stimulus_controller('tooltip', { 'content': fieldTooltip }) }}{% endif %}>
|
||||
<div class="{{ isReadonly ? 'pointer-events-none' : '' }}">
|
||||
{{ form_widget(checkboxField, { 'attr': widgetAttr }) }}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{# Macro to render insurance choice table row with product info links #}
|
||||
{% macro insurance_choice_row(child, price, urlsProductInfo, widgetAttr = {}) %}
|
||||
{% set isReadonly = child.vars.attr.readonly is defined %}
|
||||
<tr>
|
||||
<td class="border border-primary-bg p-2 align-top">
|
||||
<div>{{ child.vars.label }}</div>
|
||||
{%- if urlsProductInfo is not empty -%}
|
||||
<div {{ stimulus_controller('tooltip') }}>
|
||||
<button type="button" data-tooltip-target="trigger" class="text-sm text-primary-light">
|
||||
Details
|
||||
</button>
|
||||
<template data-tooltip-target="template">
|
||||
<div class="text-sm">
|
||||
{%- for url in urlsProductInfo -%}
|
||||
<a href="{{ url }}" target="_blank" rel="noopener noreferrer" class="block">
|
||||
Produktinformation {{ urlsProductInfo|length > 1 ? loop.index : '' }}
|
||||
</a>
|
||||
{%- endfor -%}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
{%- endif -%}
|
||||
</td>
|
||||
<td class="border border-primary-bg p-2 align-top w-24 text-right whitespace-nowrap">
|
||||
{% if price %}
|
||||
{{ price | format_currency('EUR') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="border border-primary-bg p-2 align-top w-12 text-center">
|
||||
{% set childTooltip = child.vars.attr['data-tooltip']|default(null) %}
|
||||
<div class="{{ html_classes({ 'cursor-not-allowed': isReadonly }) }}"{% if childTooltip is not null %} {{ stimulus_controller('tooltip', { 'content': childTooltip }) }}{% endif %}>
|
||||
<div class="{{ isReadonly ? 'pointer-events-none' : '' }}">
|
||||
{{ form_widget(child, { 'attr': widgetAttr }) }}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endmacro %}
|
||||
|
||||
{# Standalone participant form view (replaces main content area) #}
|
||||
{% block participant_form %}
|
||||
{% import _self as macros %}
|
||||
<div id="participant-form-view">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="flex-shrink-0"
|
||||
{{ stimulus_controller('gravatar', {
|
||||
url: gravatar_url(form.vars.data.participant.email ?? ''),
|
||||
alt: participantIndex == 0 ? 'Anmelder:in' : 'Teilnehmer:in ' ~ (participantIndex + 1)
|
||||
}, {
|
||||
image: 'w-16 h-16 rounded-full'
|
||||
}) }}>
|
||||
{{ icon('user', 'w-16 h-16 text-gray-400') }}
|
||||
{% form_theme form 'booking/_form_theme.html.twig' %}
|
||||
{% import _self as macros %}
|
||||
|
||||
<div class="flex items-center gap-4 pb-4 mb-4 border-b border-primary-bg">
|
||||
<div class="w-12 h-12 inline-flex flex-shrink-0 items-center justify-center rounded-full bg-primary-dark text-white text-2xl font-bold">
|
||||
{{ participantIndex + 1 }}
|
||||
</div>
|
||||
<div class="text-2xl">
|
||||
{{ participantIndex == 0 ? 'Anmelder:in' : 'Teilnehmer:in ' ~ (participantIndex + 1) }}
|
||||
</div>
|
||||
<h2 class="text-2xl">{{ participantIndex == 0 ? 'Anmelder:in' : 'Teilnehmer:in ' ~ (participantIndex + 1) }}</h2>
|
||||
</div>
|
||||
|
||||
{{ form_start(form, {
|
||||
'attr': {
|
||||
'novalidate': 'novalidate',
|
||||
'hx-post': path(submitRouteName, submitRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content',
|
||||
'hx-swap': 'innerHTML'
|
||||
}
|
||||
}) }}
|
||||
<div id="participant-form" class="space-y-4">
|
||||
{# Personal data section #}
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{{ macros.field_or_static(form, 'firstName', 'Vorname', form.vars.data.participant.firstName, bookingDto, participantIndex) }}
|
||||
{{ macros.field_or_static(form, 'lastName', 'Nachname', form.vars.data.participant.lastName, bookingDto, participantIndex) }}
|
||||
<div id="participant-form" class="space-y-4">
|
||||
|
||||
{{ macros.field_or_static(
|
||||
form,
|
||||
'dateOfBirth',
|
||||
'Geburtsdatum',
|
||||
form.vars.data.participant.dateOfBirth ? form.vars.data.participant.dateOfBirth|date('d.m.Y') : null,
|
||||
bookingDto,
|
||||
participantIndex,
|
||||
{
|
||||
'attr': {
|
||||
'hx-trigger': 'change',
|
||||
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content',
|
||||
'hx-swap': 'innerHTML'
|
||||
}
|
||||
}
|
||||
) }}
|
||||
{% from '_partials/_validation_errors.html.twig' import validation_alert %}
|
||||
{{ validation_alert(form) }}
|
||||
|
||||
{% set genderLabel = form.vars.data.participant.gender == 'M' ? 'männlich' : (form.vars.data.participant.gender == 'W' ? 'weiblich' : (form.vars.data.participant.gender == 'D' ? 'divers' : null)) %}
|
||||
{{ macros.field_or_static(form, 'gender', 'Geschlecht', genderLabel, bookingDto, participantIndex) }}
|
||||
{# Eligibility checks #}
|
||||
{% set participantData = form.vars.data.participant %}
|
||||
{% set hasDateOfBirth = participantData and participantData.dateOfBirth %}
|
||||
{% set isEligible = hasDateOfBirth and is_participant_eligible(bookingDto, participantIndex) %}
|
||||
|
||||
{{ macros.field_or_static(form, 'nationality', 'Nationalität', form.vars.data.participant.nationality|map_nationality, bookingDto, participantIndex) }}
|
||||
</div>
|
||||
{% if not hasDateOfBirth %}
|
||||
{% include '_partials/_alert.html.twig' with {
|
||||
level: 'info',
|
||||
messages: ['Leistungen sind erst nach Angabe des Geburtsdatums buchbar']
|
||||
} %}
|
||||
{% elseif not isEligible %}
|
||||
{% include '_partials/_alert.html.twig' with {
|
||||
level: 'error',
|
||||
messages: ['Buchung wegen des Alters von Teilnehmer:in ' ~ (participantIndex + 1) ~ ' nicht möglich']
|
||||
} %}
|
||||
{% endif %}
|
||||
|
||||
{# Personal data section #}
|
||||
<div class="grid lg:grid-cols-2 lg:gap-x-8">
|
||||
{{ macros.field_or_static(form, 'firstName', 'Vorname', form.vars.data.participant.firstName, bookingDto, participantIndex) }}
|
||||
{{ macros.field_or_static(form, 'lastName', 'Nachname', form.vars.data.participant.lastName, bookingDto, participantIndex) }}
|
||||
{{ macros.field_or_static(
|
||||
form,
|
||||
'dateOfBirth',
|
||||
'Geburtsdatum',
|
||||
form.vars.data.participant.dateOfBirth ? form.vars.data.participant.dateOfBirth|date('d.m.Y') : null,
|
||||
bookingDto,
|
||||
participantIndex,
|
||||
{
|
||||
'attr': {
|
||||
'hx-trigger': 'change',
|
||||
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content',
|
||||
'hx-swap': 'innerHTML'
|
||||
}
|
||||
}
|
||||
) }}
|
||||
|
||||
{% set genderLabel = form.vars.data.participant.gender == 'M' ? 'männlich' : (form.vars.data.participant.gender == 'W' ? 'weiblich' : (form.vars.data.participant.gender == 'D' ? 'divers' : null)) %}
|
||||
{{ macros.field_or_static(form, 'gender', 'Geschlecht', genderLabel, bookingDto, participantIndex) }}
|
||||
|
||||
{{ macros.field_or_static(form, 'nationality', 'Nationalität', form.vars.data.participant.nationality|map_nationality, bookingDto, participantIndex) }}
|
||||
|
||||
{# Contact information #}
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{{ macros.field_or_static(form, 'email', 'E-Mail', form.vars.data.participant.email, bookingDto, participantIndex) }}
|
||||
{{ macros.field_or_static(form, 'mobile', 'Telefon (mobil)', form.vars.data.participant.mobile, bookingDto, participantIndex) }}
|
||||
</div>
|
||||
|
||||
{# Address #}
|
||||
{% if form.address is defined %}
|
||||
{# Address field is in form - render as editable form fields #}
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{{ form_row(form.address.street) }}
|
||||
{{ form_row(form.address.postCode) }}
|
||||
{{ form_row(form.address.city) }}
|
||||
{{ form_row(form.address.country) }}
|
||||
</div>
|
||||
{% elseif is_static_text('address', bookingDto, participantIndex) %}
|
||||
{# Address excluded from form but should render as static text #}
|
||||
{# For applicant (index 0), use booking.applicant.address which has full data from BPN #}
|
||||
{# For other participants, use participants[index].address (may only have country in BPN response) #}
|
||||
{% set addressSource = (0 == participantIndex and bookingDto.booking) ? bookingDto.booking.applicant : bookingDto.participants[participantIndex] %}
|
||||
<div>
|
||||
<label class="font-semibold mb-1 block">Adresse</label>
|
||||
{% if addressSource.address %}
|
||||
<div class="text-sm text-gray-600">
|
||||
{% if addressSource.address.street %}{{ addressSource.address.street }}<br>{% endif %}
|
||||
{% if addressSource.address.postCode or addressSource.address.city %}
|
||||
{{ addressSource.address.postCode }} {{ addressSource.address.city }}<br>
|
||||
{% endif %}
|
||||
{% if addressSource.address.country %}{{ addressSource.address.country|map_country }}{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-sm text-gray-600">-</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Room assignment - hidden until date of birth is provided, or shown as static text in edit mode #}
|
||||
{% if form.assignedRoomId is defined or is_static_text('assignedRoomId', bookingDto, participantIndex) %}
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{% set assignedRoom = bookingDto.travel.getRoomById(form.vars.data.participant.assignedRoomId) %}
|
||||
{% set roomLabel = assignedRoom ? assignedRoom.label : 'Kein Zimmer zugewiesen' %}
|
||||
{{ macros.field_or_static(form, 'assignedRoomId', 'Zimmer', roomLabel, bookingDto, participantIndex, {
|
||||
'attr': {
|
||||
'hx-trigger': 'change',
|
||||
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content',
|
||||
'hx-swap': 'innerHTML'
|
||||
}
|
||||
}) }}
|
||||
{% if form.remarksRoom is defined %}
|
||||
{{ form_row(form.remarksRoom) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Eligibility checks #}
|
||||
{% set participantData = form.vars.data.participant %}
|
||||
{% set hasDateOfBirth = participantData and participantData.dateOfBirth %}
|
||||
{% set isEligible = hasDateOfBirth and is_participant_eligible(bookingDto, participantIndex) %}
|
||||
|
||||
{% if not hasDateOfBirth %}
|
||||
<div class="my-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 text-blue-600 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<p class="text-sm text-blue-800">
|
||||
Leistungen sind erst nach Angabe des Geburtsdatums buchbar
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% elseif not isEligible %}
|
||||
<div class="my-4 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 text-red-600 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<p class="text-sm text-red-800">
|
||||
Buchung wegen des Alters von Teilnehmer:in {{ participantIndex + 1 }} nicht möglich
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Service selection #}
|
||||
{% if isEligible %}
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{{ macros.service_field(form, 'skiPass', 'Skipass', {
|
||||
'attr': {
|
||||
'hx-trigger': 'change',
|
||||
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content', 'hx-swap': 'innerHTML'
|
||||
}
|
||||
}) }}
|
||||
|
||||
{{ macros.service_field(form, 'courses', 'Kurse', {
|
||||
'attr': {
|
||||
'hx-trigger': 'change',
|
||||
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content', 'hx-swap': 'innerHTML'
|
||||
}
|
||||
}) }}
|
||||
|
||||
{{ macros.service_field(form, 'additionalServices', 'Zusatzleistungen', {
|
||||
'attr': {
|
||||
'hx-trigger': 'change',
|
||||
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content', 'hx-swap': 'innerHTML'
|
||||
}
|
||||
}) }}
|
||||
|
||||
{{ macros.service_field(form, 'board', 'Verpflegung', {
|
||||
'attr': {
|
||||
'hx-trigger': 'change',
|
||||
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content', 'hx-swap': 'innerHTML'
|
||||
}
|
||||
}) }}
|
||||
|
||||
<div>
|
||||
{{ macros.service_field(form, 'rentals', 'Leihmaterial', {
|
||||
'attr': {
|
||||
'hx-trigger': 'change',
|
||||
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content', 'hx-swap': 'innerHTML'
|
||||
}
|
||||
}, 'Bitte zuerst den Skipass auswählen') }}
|
||||
|
||||
{{ macros.checkbox_field(form, 'rentalInsurance', null, {
|
||||
'attr': {
|
||||
'hx-trigger': 'change',
|
||||
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content', 'hx-swap': 'innerHTML'
|
||||
}
|
||||
}, null) }}
|
||||
</div>
|
||||
|
||||
{% if form.bodyDimensions is defined %}
|
||||
<div class="grid gap-y-4">
|
||||
{{ form_row(form.bodyDimensions.height) }}
|
||||
{{ form_row(form.bodyDimensions.shoeSize) }}
|
||||
{{ form_row(form.bodyDimensions.weight) }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div>{# empty slot to maintain grid layout #}</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="col-span-2">
|
||||
{# Insurance display for edit mode (read-only) #}
|
||||
{% if bookingDto.mode == constant('App\\Form\\Model\\BookingDto::MODE_EDIT') and form.vars.data.participant.insurance %}
|
||||
<fieldset class="mb-1">
|
||||
<legend class="font-semibold mb-1">Reiseversicherung</legend>
|
||||
<div class="text-sm text-gray-600">
|
||||
{{ form.vars.data.participant.insurance.label }}
|
||||
{% if form.vars.data.participant.insurance.price and form.vars.data.participant.insurance.price > 0 %}
|
||||
<span class="text-gray-500">(€{{ form.vars.data.participant.insurance.price|number_format(2, ',', '.') }})</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
{# Insurance field OR assigned insurance display for dependent participants (create mode) #}
|
||||
{% else %}
|
||||
{% set showBulkInsurance = participantIndex > 0 and bookingDto.participants[0].bulkInsuranceBooking %}
|
||||
|
||||
{% if showBulkInsurance %}
|
||||
<fieldset class="mb-1">
|
||||
<legend class="font-semibold mb-1">Reiseversicherung</legend>
|
||||
<div class="text-sm text-gray-600">
|
||||
{% set applicantInsurance = bookingDto.participants[0].insurance %}
|
||||
{% if applicantInsurance %}
|
||||
{{ applicantInsurance.label }}
|
||||
{% if applicantInsurance.price and applicantInsurance.price > 0 %}
|
||||
<span class="text-gray-500">(€{{ applicantInsurance.price|number_format(2, ',', '.') }})</span>
|
||||
{% endif %}
|
||||
<span class="italic text-gray-500 ml-2">– wie Anmelder</span>
|
||||
{% else %}
|
||||
<span class="italic">wie Anmelder</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</fieldset>
|
||||
{% else %}
|
||||
<fieldset class="mb-1">
|
||||
<legend class="font-semibold mb-1">Reiseversicherung</legend>
|
||||
|
||||
{# Bulk insurance booking checkbox (applicant only) #}
|
||||
{% if form.bulkInsuranceBooking is defined %}
|
||||
{{ form_row(form.bulkInsuranceBooking) }}
|
||||
{% endif %}
|
||||
|
||||
{% if form.insurance is defined %}
|
||||
{{ form_row(form.insurance, {
|
||||
'attr': {
|
||||
'hx-trigger': 'change',
|
||||
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content', 'hx-swap': 'innerHTML'
|
||||
},
|
||||
'label': false
|
||||
}) }}
|
||||
{% else %}
|
||||
<div class="text-sm text-gray-500">Nicht wählbar</div>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{# Transportation Services Section #}
|
||||
<div class="mt-6 border-t pt-4">
|
||||
<h3 class="font-semibold text-lg mb-4">Anreise</h3>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{% if form.transportationOutbound is defined %}
|
||||
{{ form_row(form.transportationOutbound, {
|
||||
'attr': {
|
||||
'hx-trigger': 'change',
|
||||
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content', 'hx-swap': 'innerHTML'
|
||||
}
|
||||
}) }}
|
||||
{% endif %}
|
||||
{% if form.transportationInbound is defined %}
|
||||
{{ form_row(form.transportationInbound, {
|
||||
'attr': {
|
||||
'hx-trigger': 'change',
|
||||
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content', 'hx-swap': 'innerHTML'
|
||||
}
|
||||
}) }}
|
||||
{% endif %}
|
||||
{% if form.pickup is defined %}
|
||||
<div class="col-span-2 mt-4">
|
||||
{{ form_row(form.pickup, {
|
||||
'attr': {
|
||||
'hx-trigger': 'change',
|
||||
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content', 'hx-swap': 'innerHTML'
|
||||
}
|
||||
}) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.parking is defined %}
|
||||
<div class="mt-4">
|
||||
{{ form_row(form.parking, {
|
||||
'attr': {
|
||||
'hx-trigger': 'change',
|
||||
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content', 'hx-swap': 'innerHTML'
|
||||
}
|
||||
}) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.licensePlate is defined %}
|
||||
<div class="mt-4">
|
||||
{{ form_row(form.licensePlate) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Voucher fields - available for all participants regardless of eligibility #}
|
||||
{% if form.purchaseVoucherCode is defined or form.promoVoucherCode is defined %}
|
||||
<div class="mt-6 border-t pt-4">
|
||||
<h3 class="font-semibold text-lg mb-4">Gutscheine</h3>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{% if form.purchaseVoucherCode is defined %}
|
||||
{{ form_row(form.purchaseVoucherCode) }}
|
||||
{% endif %}
|
||||
{% if form.promoVoucherCode is defined %}
|
||||
{{ form_row(form.promoVoucherCode) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ macros.field_or_static(form, 'email', 'E-Mail', form.vars.data.participant.email, bookingDto, participantIndex) }}
|
||||
{{ macros.field_or_static(form, 'mobile', 'Telefon (mobil)', form.vars.data.participant.mobile, bookingDto, participantIndex) }}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between mt-8">
|
||||
{% if cancelRouteName is defined %}
|
||||
<a href="{{ path(cancelRouteName, cancelRouteParams|default({})) }}"
|
||||
class="button bg-button bg-button--secondary"
|
||||
data-action="click->loading#show"
|
||||
{{ qa_attribute('btn-cancel') }}>
|
||||
Abbrechen
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ path('app_booking_create_step_2') }}"
|
||||
class="button bg-button bg-button--secondary"
|
||||
data-action="click->loading#show"
|
||||
{{ qa_attribute('btn-cancel') }}>
|
||||
Abbrechen
|
||||
</a>
|
||||
{# Address #}
|
||||
{% if form.address is defined %}
|
||||
<div class="grid lg:grid-cols-2 lg:gap-x-8">
|
||||
{# Address field is in form - render as editable form fields #}
|
||||
{{ form_row(form.address.street) }}
|
||||
{{ form_row(form.address.postCode) }}
|
||||
{{ form_row(form.address.city) }}
|
||||
{{ form_row(form.address.country) }}
|
||||
</div>
|
||||
{% elseif is_static_text('address', bookingDto, participantIndex) %}
|
||||
{# Address excluded from form but should render as static text #}
|
||||
{# For applicant (index 0), use booking.applicant.address which has full data from BPN #}
|
||||
{# For other participants, use participants[index].address (may only have country in BPN response) #}
|
||||
{% set addressSource = (0 == participantIndex and bookingDto.booking) ? bookingDto.booking.applicant : bookingDto.participants[participantIndex] %}
|
||||
<div>
|
||||
<label class="font-semibold mb-1 block">Adresse</label>
|
||||
{% if addressSource.address %}
|
||||
<div class="text-sm text-gray-600">
|
||||
{% if addressSource.address.street %}{{ addressSource.address.street }}<br>{% endif %}
|
||||
{% if addressSource.address.postCode or addressSource.address.city %}
|
||||
{{ addressSource.address.postCode }} {{ addressSource.address.city }}<br>
|
||||
{% endif %}
|
||||
{% if addressSource.address.country %}{{ addressSource.address.country|map_country }}{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-sm text-gray-600">-</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<button type="submit" class="button bg-button bg-button--secondary" {{ qa_attribute('btn-submit') }}>
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
{# Room assignment - hidden until date of birth is provided, or shown as static text in edit mode #}
|
||||
{% if form.assignedRoomId is defined or is_static_text('assignedRoomId', bookingDto, participantIndex) %}
|
||||
{% set assignedRoom = bookingDto.travel.getRoomById(form.vars.data.participant.assignedRoomId) %}
|
||||
{% set roomLabel = assignedRoom ? assignedRoom.label : 'Kein Zimmer zugewiesen' %}
|
||||
<div class="grid lg:grid-cols-2 lg:gap-x-8">
|
||||
{{ macros.field_or_static(form, 'assignedRoomId', 'Zimmer', roomLabel, bookingDto, participantIndex, {
|
||||
'attr': {
|
||||
'hx-trigger': 'change',
|
||||
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content',
|
||||
'hx-swap': 'innerHTML'
|
||||
}
|
||||
}) }}
|
||||
{% if form.remarksRoom is defined %}
|
||||
{{ form_row(form.remarksRoom) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Service selection #}
|
||||
{% if isEligible %}
|
||||
{{ macros.service_field(form, 'skiPass', 'Skipass', {
|
||||
'attr': {
|
||||
'hx-trigger': 'change',
|
||||
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content', 'hx-swap': 'innerHTML'
|
||||
}
|
||||
}) }}
|
||||
|
||||
{{ macros.service_field(form, 'courses', 'Kurse', {
|
||||
'attr': {
|
||||
'hx-trigger': 'change',
|
||||
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content', 'hx-swap': 'innerHTML'
|
||||
}
|
||||
}) }}
|
||||
|
||||
{{ macros.service_field(form, 'additionalServices', 'Zusatzleistungen', {
|
||||
'attr': {
|
||||
'hx-trigger': 'change',
|
||||
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content', 'hx-swap': 'innerHTML'
|
||||
}
|
||||
}) }}
|
||||
|
||||
{{ macros.service_field(form, 'board', 'Verpflegung', {
|
||||
'attr': {
|
||||
'hx-trigger': 'change',
|
||||
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content', 'hx-swap': 'innerHTML'
|
||||
}
|
||||
}) }}
|
||||
|
||||
{% set htmxAttr = {
|
||||
'hx-trigger': 'change',
|
||||
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content',
|
||||
'hx-swap': 'innerHTML'
|
||||
} %}
|
||||
<div class="pb-4">
|
||||
<fieldset class="border border-primary-bg">
|
||||
<legend class="{{ html_classes('w-full bg-primary-bg p-2 font-semibold uppercase', { 'text-red-500': form.rentals is defined and not form.rentals.vars.valid }) }}">
|
||||
Leihmaterial
|
||||
{% if form.rentals is defined and form.rentals.vars.required %}<span>*</span>{% endif %}
|
||||
</legend>
|
||||
|
||||
{% if form.rentals is defined %}
|
||||
<table class="w-full table-fixed border-collapse">
|
||||
{% for child in form.rentals %}
|
||||
{% set choiceData = form.rentals.vars.choices[loop.index0].data %}
|
||||
{{ macros.service_choice_row(child, choiceData.price|default(null), child.vars.attr['data-description']|default(null), 'Details', htmxAttr) }}
|
||||
{% endfor %}
|
||||
|
||||
{% if form.rentalInsurance is defined %}
|
||||
{% set rentalInsuranceService = bookingDto.travel.getAdditionalServicesBySubTypes(constant('App\\BusProNet\\Constants::TOKEN_RENTAL_INSURANCE'), true, true)|first %}
|
||||
{{ macros.checkbox_row(
|
||||
form.rentalInsurance,
|
||||
constant('App\\BusProNet\\Constants::SERVICE_LABELS')[constant('App\\BusProNet\\Constants::TOKEN_RENTAL_INSURANCE')],
|
||||
rentalInsuranceService.price|default(null),
|
||||
form.rentalInsurance.vars.help|default(null),
|
||||
'Info',
|
||||
htmxAttr
|
||||
) }}
|
||||
{% endif %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="p-2 text-sm text-gray-500">Bitte zuerst den Skipass auswählen</div>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
{% if form.rentals is defined %}
|
||||
{{ form_errors(form.rentals) }}
|
||||
{{ form_help(form.rentals) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if form.bodyDimensions is defined %}
|
||||
<div class="grid lg:grid-cols-3 lg:gap-x-4">
|
||||
{{ form_row(form.bodyDimensions.height) }}
|
||||
{{ form_row(form.bodyDimensions.shoeSize) }}
|
||||
{{ form_row(form.bodyDimensions.weight) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Insurance display for edit mode (read-only) - only show if insurance is selected #}
|
||||
{% if bookingDto.mode == constant('App\\Form\\Model\\BookingDto::MODE_EDIT') %}
|
||||
{% if form.vars.data.participant.insurance %}
|
||||
{% set editInsurance = form.vars.data.participant.insurance %}
|
||||
<div class="pb-4">
|
||||
<fieldset class="border border-primary-bg">
|
||||
<legend class="w-full bg-primary-bg p-2 font-semibold uppercase">
|
||||
Reiseversicherung
|
||||
</legend>
|
||||
<table class="w-full table-fixed border-collapse">
|
||||
<tr>
|
||||
<td class="border border-primary-bg p-2 align-top">
|
||||
<div>{{ editInsurance.label }}</div>
|
||||
{%- if editInsurance.getAllUrlsProductInfo() is not empty -%}
|
||||
<div {{ stimulus_controller('tooltip') }}>
|
||||
<button type="button" data-tooltip-target="trigger" class="text-sm text-primary-light">
|
||||
Details
|
||||
</button>
|
||||
<template data-tooltip-target="template">
|
||||
<div class="text-sm">
|
||||
{%- for url in editInsurance.getAllUrlsProductInfo() -%}
|
||||
<a href="{{ url }}" target="_blank" rel="noopener noreferrer" class="block">
|
||||
Produktinformation {{ editInsurance.getAllUrlsProductInfo()|length > 1 ? loop.index : '' }}
|
||||
</a>
|
||||
{%- endfor -%}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
{%- endif -%}
|
||||
</td>
|
||||
<td class="border border-primary-bg p-2 align-top w-24 text-right whitespace-nowrap">
|
||||
{% if editInsurance.price and editInsurance.price > 0 %}
|
||||
{{ editInsurance.price | format_currency('EUR') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="border border-primary-bg p-2 align-top w-12"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
</div>
|
||||
{% endif %}
|
||||
{# In edit mode without insurance: don't show the section at all #}
|
||||
|
||||
{# Insurance field OR assigned insurance display for dependent participants (create mode) #}
|
||||
{% else %}
|
||||
{% set showBulkInsurance = participantIndex > 0 and bookingDto.participants[0].bulkInsuranceBooking %}
|
||||
|
||||
{% if showBulkInsurance %}
|
||||
{% set applicantInsurance = bookingDto.participants[0].insurance %}
|
||||
<div class="pb-4">
|
||||
<fieldset class="border border-primary-bg">
|
||||
<legend class="w-full bg-primary-bg p-2 font-semibold uppercase">
|
||||
Reiseversicherung* <span class="font-normal normal-case text-sm text-gray-600 italic">– wie Anmelder</span>
|
||||
</legend>
|
||||
{% if applicantInsurance %}
|
||||
<table class="w-full table-fixed border-collapse">
|
||||
<tr>
|
||||
<td class="border border-primary-bg p-2 align-top">
|
||||
<div>{{ applicantInsurance.label }}</div>
|
||||
{%- if applicantInsurance.getAllUrlsProductInfo() is not empty -%}
|
||||
<div {{ stimulus_controller('tooltip') }}>
|
||||
<button type="button" data-tooltip-target="trigger" class="text-sm text-primary-light">
|
||||
Details
|
||||
</button>
|
||||
<template data-tooltip-target="template">
|
||||
<div class="text-sm">
|
||||
{%- for url in applicantInsurance.getAllUrlsProductInfo() -%}
|
||||
<a href="{{ url }}" target="_blank" rel="noopener noreferrer" class="block">
|
||||
Produktinformation {{ applicantInsurance.getAllUrlsProductInfo()|length > 1 ? loop.index : '' }}
|
||||
</a>
|
||||
{%- endfor -%}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
{%- endif -%}
|
||||
</td>
|
||||
<td class="border border-primary-bg p-2 align-top w-24 text-right whitespace-nowrap">
|
||||
{% if applicantInsurance.price and applicantInsurance.price > 0 %}
|
||||
{{ applicantInsurance.price | format_currency('EUR') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="border border-primary-bg p-2 align-top w-12"></td>
|
||||
</tr>
|
||||
</table>
|
||||
{% else %}
|
||||
<table class="w-full table-fixed border-collapse">
|
||||
<tr>
|
||||
<td class="border border-primary-bg p-2 align-top text-sm text-gray-500 italic">wie Anmelder</td>
|
||||
<td class="border border-primary-bg p-2 align-top w-24"></td>
|
||||
<td class="border border-primary-bg p-2 align-top w-12"></td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pb-4">
|
||||
<fieldset class="border border-primary-bg">
|
||||
<legend class="{{ html_classes('w-full bg-primary-bg p-2 font-semibold uppercase', { 'text-red-500': form.insurance is defined and not form.insurance.vars.valid }) }}">
|
||||
Reiseversicherung*
|
||||
</legend>
|
||||
{% if form.insurance is defined %}
|
||||
<table class="w-full table-fixed border-collapse">
|
||||
{% if form.bulkInsuranceBooking is defined %}
|
||||
{{ macros.checkbox_row(
|
||||
form.bulkInsuranceBooking,
|
||||
form.bulkInsuranceBooking.vars.label,
|
||||
null,
|
||||
form.bulkInsuranceBooking.vars.attr['data-description']|default(null),
|
||||
'Info',
|
||||
htmxAttr
|
||||
) }}
|
||||
{% endif %}
|
||||
{% for child in form.insurance %}
|
||||
{% set choiceData = form.insurance.vars.choices[loop.index0].data %}
|
||||
{{ macros.insurance_choice_row(child, choiceData.price|default(null), choiceData ? choiceData.getAllUrlsProductInfo() : [], htmxAttr) }}
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="p-2 text-sm text-gray-500">Nicht wählbar</div>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
{% if form.insurance is defined %}
|
||||
{{ form_errors(form.insurance) }}
|
||||
{{ form_help(form.insurance) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{# Transportation Services Section #}
|
||||
<h3 class="font-semibold text-lg mb-4">Anreise</h3>
|
||||
{% if form.transportationOutbound is defined %}
|
||||
<div class="pb-4">
|
||||
<fieldset class="border border-primary-bg">
|
||||
<legend class="{{ html_classes('w-full bg-primary-bg p-2 font-semibold uppercase', { 'text-red-500': not form.transportationOutbound.vars.valid }) }}">
|
||||
{{ form.transportationOutbound.vars.label }}
|
||||
{% if form.transportationOutbound.vars.required %}<span>*</span>{% endif %}
|
||||
</legend>
|
||||
|
||||
<table class="w-full table-fixed border-collapse">
|
||||
{% for child in form.transportationOutbound %}
|
||||
{% set choiceData = form.transportationOutbound.vars.choices[loop.index0].data %}
|
||||
{{ macros.service_choice_row(child, choiceData.price|default(null), child.vars.attr['data-description']|default(null), 'Details', htmxAttr) }}
|
||||
{% endfor %}
|
||||
|
||||
{% if form.parking is defined %}
|
||||
{% set parkingService = bookingDto.travel.getAdditionalServicesBySubTypes(constant('App\\BusProNet\\Constants::TOKEN_PARKING'), true)|first %}
|
||||
{{ macros.checkbox_row(
|
||||
form.parking,
|
||||
'Parkplatz',
|
||||
parkingService.price|default(null),
|
||||
null,
|
||||
null,
|
||||
htmxAttr
|
||||
) }}
|
||||
{% endif %}
|
||||
</table>
|
||||
</fieldset>
|
||||
{{ form_errors(form.transportationOutbound) }}
|
||||
{{ form_help(form.transportationOutbound) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.transportationInbound is defined %}
|
||||
{{ form_row(form.transportationInbound, {
|
||||
'attr': {
|
||||
'hx-trigger': 'change',
|
||||
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content', 'hx-swap': 'innerHTML'
|
||||
}
|
||||
}) }}
|
||||
{% endif %}
|
||||
{% if form.pickup is defined %}
|
||||
{{ form_row(form.pickup, {
|
||||
'attr': {
|
||||
'hx-trigger': 'change',
|
||||
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content', 'hx-swap': 'innerHTML'
|
||||
}
|
||||
}) }}
|
||||
{% endif %}
|
||||
{% if form.licensePlate is defined %}
|
||||
{{ form_row(form.licensePlate) }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{# Voucher fields - available for all participants regardless of eligibility #}
|
||||
{% if form.purchaseVoucherCode is defined or form.promoVoucherCode is defined %}
|
||||
<h3 class="font-semibold text-lg mb-4">
|
||||
Gutscheine
|
||||
</h3>
|
||||
<div class="grid lg:grid-cols-2 lg:gap-x-4">
|
||||
{% if form.purchaseVoucherCode is defined %}
|
||||
{{ form_row(form.purchaseVoucherCode) }}
|
||||
{% endif %}
|
||||
{% if form.promoVoucherCode is defined %}
|
||||
{{ form_row(form.promoVoucherCode) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{# Sidebar summary with conditional OOB swap - only render for HTMX requests #}
|
||||
{% if htmx_oob_swap|default(false) %}
|
||||
{% block booking_summary %}
|
||||
<div id="booking-summary" hx-swap-oob="true">
|
||||
<div id="booking-summary" hx-swap-oob="innerHTML">
|
||||
{% include 'booking/_summary.html.twig' with {
|
||||
'bookingCreateDto': bookingDto,
|
||||
'summaryData': summaryData,
|
||||
|
||||
@@ -1,193 +1,240 @@
|
||||
<div class="booking-summary p-8 bg-gray-50 rounded-lg border sticky top-4">
|
||||
<h3 class="font-bold text-xl mb-6 text-gray-800">Buchungsübersicht</h3>
|
||||
|
||||
{# Inquiry booking notification #}
|
||||
{% if bookingCreateDto is defined and bookingCreateDto.isInquiryBooking() %}
|
||||
<div class="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded text-sm text-yellow-800">
|
||||
<span class="font-semibold">Anfragebuchung:</span> Diese Reise ist ausgebucht. Ihre Buchung wird als Anfrage verarbeitet.
|
||||
<div class="bg-white h-full flex flex-col lg:border-l lg:border-primary-bg">
|
||||
{# Header - always visible #}
|
||||
<div class="shrink-0 px-4 lg:px-8 py-4 lg:flex lg:flex-col lg:justify-center shadow-md relative z-10">
|
||||
<div class="flex items-center justify-between pb-2">
|
||||
<span class="block uppercase font-semibold">Gesamtpreis</span>
|
||||
<span class="font-semibold">{{ summaryData.payableAmount|format_currency('EUR') }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Mutability information (edit mode only) #}
|
||||
{% if bookingCreateDto.mode == constant('App\\Form\\Model\\BookingDto::MODE_EDIT') and mutableData %}
|
||||
<div class="mb-6 pb-4 border-b border-gray-200">
|
||||
<h4 class="font-semibold text-lg mb-3 text-gray-800">Aktualisierung möglich bis</h4>
|
||||
<ul class="space-y-1 text-sm text-gray-600">
|
||||
<li class="flex items-start">
|
||||
<span class="mr-2">•</span>
|
||||
<span>
|
||||
<span class="font-medium">Zusatzleistungen:</span>
|
||||
{% if mutableData.items['additional_services'].mutable %}
|
||||
{{ mutableData.items['additional_services'].mutableBefore|date('d.m.Y') }}
|
||||
{% else %}
|
||||
<span class="text-red-600">nicht mehr möglich</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<span class="mr-2">•</span>
|
||||
<span>
|
||||
<span class="font-medium">Beförderung:</span>
|
||||
{% if mutableData.items['transportation'].mutable %}
|
||||
{{ mutableData.items['transportation'].mutableBefore|date('d.m.Y') }}
|
||||
{% else %}
|
||||
<span class="text-red-600">nicht mehr möglich</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<span class="mr-2">•</span>
|
||||
<span>
|
||||
<span class="font-medium">Zustiege:</span>
|
||||
{% if mutableData.items['pickup'].mutable %}
|
||||
{{ mutableData.items['pickup'].mutableBefore|date('d.m.Y') }}
|
||||
{% else %}
|
||||
<span class="text-red-600">nicht mehr möglich</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Travel Information #}
|
||||
<div class="mb-6 pb-4 border-b border-gray-200">
|
||||
{% if summaryData.cmsData.hotel.images is defined %}
|
||||
<img src="{{ summaryData.cmsData.hotel.images.resized.l[0].url }}" alt="{{ summaryData.cmsData.hotel.images.resized.s[0].alt }}" class="w-full rounded mb-4">
|
||||
{% endif %}
|
||||
<div class="space-y-2 text-sm text-gray-600">
|
||||
<div><span class="font-medium">Reise:</span> {{ bookingCreateDto.travel.label }}</div>
|
||||
{% if summaryData.cmsData.region is defined %}
|
||||
<div><span class="font-medium">Gebiet:</span> {{ summaryData.cmsData.region.name }}</div>
|
||||
{% endif %}
|
||||
{% if summaryData.cmsData.country is defined %}
|
||||
<div><span class="font-medium">Land:</span> {{ summaryData.cmsData.country.name }}</div>
|
||||
{% endif %}
|
||||
<div><span class="font-medium">Zeitraum:</span> {{ bookingCreateDto.travel.dateFrom|date('d.m.Y') }} - {{ bookingCreateDto.travel.dateTo|date('d.m.Y') }}</div>
|
||||
<div><span class="font-medium">Unterkunft:</span> {{ bookingCreateDto.travel.hotel.name }}{% if summaryData.cmsData.hotel.address is defined %}, {{ summaryData.cmsData.hotel.address | replace({"\n": ', '}) }}{% endif %}</div>
|
||||
<div><span class="font-medium">Anzahl Teilnehmer:</span> {{ summaryData.participantCount }}</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="block uppercase font-semibold">Buchungsübersicht</span>
|
||||
<button type="button"
|
||||
class="lg:hidden button button--small bg-primary-light"
|
||||
data-action="toggle#toggle">
|
||||
<svg class="w-4 h-4 text-white transition-transform"
|
||||
data-toggle-target="icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><polyline points="208 96 128 176 48 96" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Rooms Section #}
|
||||
{% if summaryData.pricingData.rooms is not empty %}
|
||||
<div class="mb-6 pb-4 border-b border-gray-200">
|
||||
<h4 class="font-semibold text-lg mb-3 text-gray-800">Unterkunft</h4>
|
||||
<div class="space-y-2">
|
||||
{% for roomPricing in summaryData.pricingData.rooms %}
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="text-sm text-gray-600">
|
||||
<span class="font-medium">{{ roomPricing.quantity }}x {{ roomPricing.label }}</span>
|
||||
{% if summaryData.assignmentCounts[roomPricing.roomId] is defined %}
|
||||
<span class="block text-xs text-gray-500">{{ summaryData.assignmentCounts[roomPricing.roomId] }} belegt</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-gray-900">
|
||||
€{{ roomPricing.totalPrice|number_format(2, ',', '.') }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
€{{ roomPricing.unitPrice|number_format(2, ',', '.') }} pro Person
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{# Content - toggleable on mobile, always visible on desktop #}
|
||||
<div class="hidden lg:block flex-1 min-h-0 bg-white shadow-lg lg:shadow-none overflow-y-auto px-4 lg:px-8 py-8"
|
||||
data-toggle-target="content">
|
||||
{# Travel Information #}
|
||||
<div class="pb-4">
|
||||
<table>
|
||||
<tr>
|
||||
<th class="text-left align-top border-r-2 border-gray-300 pr-2">
|
||||
Reise
|
||||
</th>
|
||||
<td class="pl-2">
|
||||
{{ bookingCreateDto.travel.label }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-left align-top border-r-2 border-gray-300 pr-2">
|
||||
Zeitraum
|
||||
</th>
|
||||
<td class="pl-2">
|
||||
{{ bookingCreateDto.travel.dateFrom|date('d.m.Y') }} - {{ bookingCreateDto.travel.dateTo|date('d.m.Y') }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-left align-top border-r-2 border-gray-300 pr-2">
|
||||
Unterkunft
|
||||
</th>
|
||||
<td class="pl-2">
|
||||
{{ bookingCreateDto.travel.hotel.name }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-left align-top border-r-2 border-gray-300 pr-2">
|
||||
Teilnehmer
|
||||
</th>
|
||||
<td class="pl-2">
|
||||
{{ summaryData.participantCount }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Services Section #}
|
||||
{% if summaryData.pricingData.services is not empty %}
|
||||
<div class="mb-6">
|
||||
<h4 class="font-semibold text-lg mb-3 text-gray-800">Leistungen</h4>
|
||||
{% for serviceGroup in summaryData.pricingData.services %}
|
||||
<div class="mb-4 last:mb-0">
|
||||
<h5 class="font-medium text-sm text-gray-700 mb-2 uppercase tracking-wide">{{ serviceGroup.groupName }}</h5>
|
||||
<div class="space-y-1 ml-4">
|
||||
{% for servicePricing in serviceGroup.services %}
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<span class="text-gray-600">
|
||||
{{ servicePricing.participantCount }}x {{ servicePricing.label }}
|
||||
</span>
|
||||
<span class="font-medium text-gray-900">
|
||||
€{{ servicePricing.totalPrice|number_format(2, ',', '.') }}
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% include 'booking/_summary_hotel.html.twig' %}
|
||||
|
||||
{# Inquiry booking notification #}
|
||||
{% if bookingCreateDto is defined and bookingCreateDto.isInquiryBooking() %}
|
||||
<div>
|
||||
Deine Buchung wird als Anfrage verarbeitet.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Mutability information (edit mode only) #}
|
||||
{% if bookingCreateDto.mode == constant('App\\Form\\Model\\BookingDto::MODE_EDIT') and mutableData %}
|
||||
<div class="border border-primary-bg mb-4">
|
||||
<div class="p-2 bg-primary-bg uppercase font-semibold">
|
||||
Aktualisierung möglich bis
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<table class="w-full table-fixed">
|
||||
<tr>
|
||||
<td class="p-2 align-top">
|
||||
Zusatzleistungen
|
||||
</td>
|
||||
<td class="p-2 align-top w-24 text-right whitespace-nowrap">
|
||||
{% if mutableData.items['additional_services'].mutable %}
|
||||
{{ mutableData.items['additional_services'].mutableBefore|date('d.m.Y') }}
|
||||
{% else %}
|
||||
nicht mehr möglich
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="p-2 align-top">
|
||||
Beförderung
|
||||
</td>
|
||||
<td class="p-2 align-top w-24 text-right whitespace-nowrap">
|
||||
{% if mutableData.items['transportation'].mutable %}
|
||||
{{ mutableData.items['transportation'].mutableBefore|date('d.m.Y') }}
|
||||
{% else %}
|
||||
nicht mehr möglich
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="p-2 align-top">
|
||||
Zustiege
|
||||
</td>
|
||||
<td class="p-2 align-top w-24 text-right whitespace-nowrap">
|
||||
{% if mutableData.items['pickup'].mutable %}
|
||||
{{ mutableData.items['pickup'].mutableBefore|date('d.m.Y') }}
|
||||
{% else %}
|
||||
nicht mehr möglich
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Surcharges Section (Edit mode only) #}
|
||||
{% if summaryData.pricingData.surcharges is defined and summaryData.pricingData.surcharges is not empty %}
|
||||
<div class="mb-6 pb-4 border-b border-gray-200">
|
||||
<h4 class="font-semibold text-lg mb-3 text-gray-800">Zuschläge</h4>
|
||||
<div class="space-y-1">
|
||||
{% for surchargePricing in summaryData.pricingData.surcharges %}
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<span class="text-gray-600">
|
||||
{{ surchargePricing.participantCount }}x {{ surchargePricing.label }}
|
||||
</span>
|
||||
<span class="font-medium text-gray-900">
|
||||
€{{ surchargePricing.totalPrice|number_format(2, ',', '.') }}
|
||||
</span>
|
||||
{# Rooms Section #}
|
||||
{% if summaryData.pricingData.rooms is not empty %}
|
||||
<div class="border border-primary-bg mb-4">
|
||||
<div class="p-2 bg-primary-bg uppercase font-semibold">
|
||||
Unterkunft
|
||||
</div>
|
||||
<table class="w-full table-fixed">
|
||||
{% for roomPricing in summaryData.pricingData.rooms %}
|
||||
<tr>
|
||||
<td class="p-2 align-top">
|
||||
<div>{{ roomPricing.quantity }}x {{ roomPricing.label }}</div>
|
||||
{% if summaryData.assignmentCounts[roomPricing.roomId] is defined %}
|
||||
<div class="text-sm text-gray-600">{{ summaryData.assignmentCounts[roomPricing.roomId] }}x Erwachsener</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="p-2 align-top w-24 text-right whitespace-nowrap">
|
||||
{{ roomPricing.totalPrice | format_currency('EUR') }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Services Section #}
|
||||
{% if summaryData.pricingData.services is not empty %}
|
||||
<div class="border border-primary-bg mb-4">
|
||||
<div class="p-2 bg-primary-bg uppercase font-semibold">
|
||||
Leistungen
|
||||
</div>
|
||||
{% for serviceGroup in summaryData.pricingData.services %}
|
||||
<div class="p-2 text-primary-dark/70 uppercase font-semibold">
|
||||
{{ serviceGroup.groupName }}
|
||||
</div>
|
||||
<table class="w-full table-fixed">
|
||||
{% for servicePricing in serviceGroup.services %}
|
||||
<tr>
|
||||
<td class="px-2 pb-2 align-top">
|
||||
{{ servicePricing.participantCount }}x {{ servicePricing.label }}
|
||||
</td>
|
||||
<td class="px-2 pb-2 align-top w-24 text-right whitespace-nowrap">
|
||||
{{ servicePricing.totalPrice | format_currency('EUR') }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{# Total Section #}
|
||||
{% if summaryData.pricingData.grandTotal is defined and summaryData.pricingData.grandTotal > 0 %}
|
||||
<div class="pt-4 border-t-2 border-gray-300">
|
||||
{# Surcharges Section (Edit mode only) #}
|
||||
{% if summaryData.pricingData.surcharges is defined and summaryData.pricingData.surcharges is not empty %}
|
||||
<div class="border border-primary-bg mb-4">
|
||||
<div class="p-2 bg-primary-bg uppercase font-semibold">
|
||||
Zu-/Abschläge
|
||||
</div>
|
||||
<table class="w-full table-fixed">
|
||||
{% for surchargePricing in summaryData.pricingData.surcharges %}
|
||||
<tr>
|
||||
<td class="px-2 pb-2 align-top">
|
||||
{{ surchargePricing.participantCount }}x {{ surchargePricing.label }}
|
||||
</td>
|
||||
<td class="px-2 pb-2 align-top w-24 text-right whitespace-nowrap">
|
||||
{{ surchargePricing.totalPrice|format_currency('EUR') }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Total Section #}
|
||||
{% if summaryData.totalPrice > 0 %}
|
||||
{# Show subtotal and voucher discounts when vouchers are applied #}
|
||||
{% set acceptedVouchers = bookingCreateDto.getAcceptedVouchers() %}
|
||||
{% if acceptedVouchers and acceptedVouchers.hasVouchers() %}
|
||||
{# Subtotal before vouchers #}
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="text-gray-600">Gesamtpreis:</span>
|
||||
<span class="text-gray-900">
|
||||
€{{ summaryData.pricingData.grandTotal|number_format(2, ',', '.') }}
|
||||
</span>
|
||||
<div class="border border-primary-bg mb-4">
|
||||
<div class="p-2 bg-primary-bg uppercase font-semibold">
|
||||
Kostenübersicht
|
||||
</div>
|
||||
<table class="w-full table-fixed">
|
||||
{# Subtotal before vouchers #}
|
||||
<tr>
|
||||
<td class="px-2 pb-2 align-top">
|
||||
Gesamtpreis
|
||||
</td>
|
||||
<td class="px-2 pb-2 align-top w-24 text-right whitespace-nowrap">
|
||||
{{ summaryData.totalPrice|format_currency('EUR') }}
|
||||
</td>
|
||||
</tr>
|
||||
{# Voucher discounts breakdown #}
|
||||
{% for voucher in acceptedVouchers.vouchers %}
|
||||
<tr>
|
||||
<td class="px-2 pb-2 align-top">
|
||||
{% if voucher.promotional %}
|
||||
Aktionsgutschein ({{ voucher.code }})
|
||||
{% elseif voucher.goodwill %}
|
||||
Kulanzgutschein ({{ voucher.code }})
|
||||
{% elseif voucher.purchase %}
|
||||
Kaufgutschein ({{ voucher.code }})
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-2 pb-2 align-top w-24 text-right whitespace-nowrap">
|
||||
- {{ voucher.amount|format_currency('EUR') }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{# Voucher discounts breakdown #}
|
||||
<div class="mb-3 space-y-1">
|
||||
{% for voucher in acceptedVouchers.vouchers %}
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<span class="text-gray-600">
|
||||
{% if voucher.promotional %}
|
||||
Aktionsgutschein ({{ voucher.code }})
|
||||
{% elseif voucher.goodwill %}
|
||||
Kulanzgutschein ({{ voucher.code }})
|
||||
{% elseif voucher.purchase %}
|
||||
Kaufgutschein ({{ voucher.code }})
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="font-medium text-green-600">
|
||||
-€{{ voucher.amount|number_format(2, ',', '.') }}
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# Amount to pay after vouchers #}
|
||||
<div class="flex justify-between items-center pt-2 border-t border-gray-200">
|
||||
<span class="font-bold text-lg text-gray-800">Zu zahlen:</span>
|
||||
<span class="font-bold text-xl text-gray-900" {{ qa_attribute('summary-total') }}>
|
||||
€{{ (summaryData.pricingData.grandTotal - acceptedVouchers.totalDiscount)|number_format(2, ',', '.') }}
|
||||
</span>
|
||||
<div class="flex items-center justify-between px-2 pb-2">
|
||||
<span class="block uppercase font-semibold">Zu zahlen</span>
|
||||
<span class="font-semibold">{{ summaryData.payableAmount|format_currency('EUR') }}</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="font-bold text-lg text-gray-800">Gesamtpreis:</span>
|
||||
<span class="font-bold text-xl text-gray-900" {{ qa_attribute('summary-total') }}>
|
||||
€{{ summaryData.pricingData.grandTotal|number_format(2, ',', '.') }}
|
||||
</span>
|
||||
<div class="flex items-center justify-between pb-2">
|
||||
<span class="block uppercase font-semibold">Gesamtpreis</span>
|
||||
<span class="font-semibold">{{ summaryData.payableAmount|format_currency('EUR') }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{% if summaryData.cmsData.hotel.images is defined %}
|
||||
<div class="grid grid-cols-3 gap-x-4 py-4 border-t border-primary-bg">
|
||||
<img src="{{ summaryData.cmsData.hotel.images.resized.l[0].url }}"
|
||||
alt="{{ summaryData.cmsData.hotel.images.resized.l[0].alt }}"
|
||||
class="block w-full h-auto">
|
||||
<div class="col-span-2">
|
||||
<span class="block font-semibold uppercase">{{ summaryData.cmsData.hotel.name }}</span>
|
||||
{% if summaryData.cmsData.hotel.address is defined %}
|
||||
{{ summaryData.cmsData.hotel.address | nl2br }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -1,98 +1,96 @@
|
||||
{% extends 'layout.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
<div class="px-4 lg:px-8 py-8 lg:py-16">
|
||||
|
||||
<h1 class="mb-4">
|
||||
Neue Buchung
|
||||
</h1>
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-6">
|
||||
<h2 class="text-blue-900 font-semibold text-lg mb-3">
|
||||
Optional: Anmelden für schnelleres Buchen
|
||||
</h2>
|
||||
<p class="text-blue-800 mb-2">
|
||||
Melde dich an, um deine persönlichen Daten automatisch in die Buchung zu übernehmen.
|
||||
Dies ist <strong>vollständig optional</strong> – du kannst auch ohne Anmeldung als Gast fortfahren.
|
||||
</p>
|
||||
</div>
|
||||
<h1 class="text-white uppercase pb-4 mb-8 border-b border-primary-bg/40">
|
||||
Neue<br>Buchung
|
||||
</h1>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{# Login Form #}
|
||||
<div>
|
||||
<h3 class="mb-4 font-semibold text-lg">
|
||||
Mit bestehendem Account anmelden
|
||||
</h3>
|
||||
|
||||
{% if error %}
|
||||
{% include '_partials/_alert.html.twig' with { 'level': 'error', messages: [ error.messageKey|trans(error.messageData, 'security') ] } %}
|
||||
{% endif %}
|
||||
|
||||
<form action="{{ path('app_login') }}" method="post" {{ stimulus_action('loading', 'toggle') }}>
|
||||
<div class="mb-4">
|
||||
<label for="username" class="mb-1 font-semibold">
|
||||
E-Mail:
|
||||
</label>
|
||||
<input type="email"
|
||||
id="username"
|
||||
name="_username"
|
||||
value="{{ last_username }}"
|
||||
class="form-field"
|
||||
required
|
||||
autocomplete="username">
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="password" class="mb-1 font-semibold">
|
||||
Passwort:
|
||||
</label>
|
||||
<input type="password"
|
||||
id="password"
|
||||
name="_password"
|
||||
class="form-field"
|
||||
required
|
||||
autocomplete="current-password">
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col space-y-2">
|
||||
<button type="submit" class="button bg-button">
|
||||
Anmelden und fortfahren
|
||||
</button>
|
||||
<a href="{{ path('app_reset_password') }}"
|
||||
class="text-sm text-center text-blue-600 hover:underline">
|
||||
Passwort vergessen?
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
|
||||
</form>
|
||||
<div class="text-white uppercase pb-4 ld:pb-8">
|
||||
{{ travel_title }}
|
||||
<br>
|
||||
{{ travel_date_from | date('d.m.Y') }} - {{ travel_date_to | date('d.m.Y') }}
|
||||
</div>
|
||||
|
||||
{# Continue as Guest #}
|
||||
<div>
|
||||
<h3 class="mb-4 font-semibold text-lg">
|
||||
Als Gast fortfahren
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div>
|
||||
<a href="{{ path('app_booking_create_step_1') }}" class="button button--secondary mb-4">
|
||||
Als Gast fortfahren
|
||||
</a>
|
||||
|
||||
<p class="mb-4 text-gray-700">
|
||||
Du kannst auch ohne Anmeldung buchen. Deine persönlichen Daten gibst du dann im nächsten Schritt ein.
|
||||
</p>
|
||||
|
||||
<a href="{{ path('app_booking_create_step_1') }}" class="button bg-button bg-button--secondary w-full block text-center">
|
||||
Als Gast fortfahren
|
||||
</a>
|
||||
|
||||
<div class="mt-6 p-4 bg-gray-50 border border-gray-200 rounded">
|
||||
<h4 class="font-semibold mb-2 text-sm">
|
||||
<h2 class="text-base text-white">
|
||||
Noch kein Account?
|
||||
</h4>
|
||||
<p class="text-sm text-gray-700">
|
||||
</h2>
|
||||
<p class="text-white">
|
||||
Nach Abschluss deiner Buchung wird automatisch ein Account für dich erstellt.
|
||||
Du erhältst dann Zugangsdaten per E-Mail und kannst deine Buchungen jederzeit verwalten.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-base text-white">
|
||||
Mit bestehendem Account anmelden
|
||||
</h2>
|
||||
|
||||
{% include 'booking/_cancel_link.html.twig' %}
|
||||
{% if error %}
|
||||
{% include '_partials/_alert.html.twig' with { 'level': 'error', messages: [ error.messageKey|trans(error.messageData, 'security') ] } %}
|
||||
{% endif %}
|
||||
|
||||
<div class="pb-4">
|
||||
<form action="{{ path('app_login') }}" method="post" {{ stimulus_action('loading', 'toggle') }}>
|
||||
<div class="mb-4">
|
||||
<label for="username" class="mb-1 font-semibold text-white">
|
||||
E-Mail:
|
||||
</label>
|
||||
<input type="email"
|
||||
id="username"
|
||||
name="_username"
|
||||
value="{{ last_username }}"
|
||||
class="form-field"
|
||||
required
|
||||
autocomplete="username">
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="password" class="mb-1 font-semibold text-white">
|
||||
Passwort:
|
||||
</label>
|
||||
<input type="password"
|
||||
id="password"
|
||||
name="_password"
|
||||
class="form-field"
|
||||
required
|
||||
autocomplete="current-password">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="button button--primary">
|
||||
Anmelden und fortfahren
|
||||
</button>
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<a href="{{ path('app_reset_password') }}"
|
||||
class="text-sm underline text-white">
|
||||
Passwort vergessen?
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block footer %}
|
||||
<div class="shrink-0 bg-primary-dark px-4 lg:px-8 py-2 lg:py-4">
|
||||
<button type="button"
|
||||
hx-get="{{ path('app_booking_cancel') }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend"
|
||||
class="inline-flex items-center justify-center bg-gray-200 rounded-md w-10 h-10">
|
||||
<svg class="w-8 h-8" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="200" y1="56" x2="56" y2="200" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="200" y1="200" x2="56" y2="56" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -5,34 +5,16 @@
|
||||
{% block body %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="max-w-lg mx-auto">
|
||||
<div class="bg-red-50 border border-red-200 rounded-lg p-6">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800">
|
||||
Booking Error
|
||||
</h3>
|
||||
<div class="mt-2 text-sm text-red-700">
|
||||
{% for flash_message in app.flashes('error') %}
|
||||
<p>{{ flash_message }}</p>
|
||||
{% else %}
|
||||
<p>Es ist ein Fehler beim Starten des Buchungsvorgangs aufgetreten.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<div class="flex space-x-2">
|
||||
<a href="https://www.ep-reisen.de" class="button bg-button bg-button--secondary">
|
||||
Zur Startseite
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% set errorMessages = app.flashes('error')|default(['Es ist ein Fehler beim Starten des Buchungsvorgangs aufgetreten.']) %}
|
||||
{% include '_partials/_alert.html.twig' with {
|
||||
level: 'error',
|
||||
title: 'Booking Error',
|
||||
messages: errorMessages
|
||||
} %}
|
||||
|
||||
<a href="https://www.ep-reisen.de" class="button button--secondary">
|
||||
Zur Startseite
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,64 +1,105 @@
|
||||
{% extends 'layout.html.twig' %}
|
||||
{% extends 'layout_booking.html.twig' %}
|
||||
|
||||
{% macro stepFormField(form) %}
|
||||
<div class="grid grid-cols-3 pb-2">
|
||||
<div class="col-span-2 pr-2">
|
||||
<div class="font-semibold">
|
||||
{{ form.vars.label_room }}
|
||||
</div>
|
||||
<div class="text-gray-500">
|
||||
{{ form.vars.label_price }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{{ form_row(form, {
|
||||
'attr': {
|
||||
'hx-post': path('app_booking_create_step_1_refresh'),
|
||||
'hx-swap': 'none',
|
||||
'hx-trigger': 'change delay:300ms',
|
||||
}
|
||||
}) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% block content %}
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
<h1>Neue Buchung</h1>
|
||||
<div class="grid grid-cols-3 gap-8">
|
||||
<div id="form-wrapper" class="col-span-2">
|
||||
<h2 class="pb-4">
|
||||
Unterkunft
|
||||
</h2>
|
||||
{{ form_start(form) }}
|
||||
{% do form.roomSelections.setRendered %}
|
||||
{# This block contains the room selection form fields #}
|
||||
{% block room_selection_form %}
|
||||
<div id="room-selection-form"{% if htmx_oob_swap is defined and htmx_oob_swap %} hx-swap-oob="true"{% endif %}>
|
||||
{{ form_errors(form) }}
|
||||
{% if groupedRooms.by_room is not empty %}
|
||||
<h3>
|
||||
Zimmer
|
||||
</h3>
|
||||
{% for roomId, room in groupedRooms.by_room %}
|
||||
{{ form_row(form.roomSelections[roomId], {
|
||||
'attr': {
|
||||
'hx-post': path('app_booking_create_step_1_refresh'),
|
||||
'hx-swap': 'none',
|
||||
'hx-trigger': 'change delay:300ms',
|
||||
}
|
||||
}) }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if groupedRooms.by_pax is not empty %}
|
||||
<h3>
|
||||
Betten
|
||||
</h3>
|
||||
{% for roomId, room in groupedRooms.by_pax %}
|
||||
{{ form_row(form.roomSelections[roomId], {
|
||||
'attr': {
|
||||
'hx-post': path('app_booking_create_step_1_refresh'),
|
||||
'hx-swap': 'none',
|
||||
'hx-trigger': 'change delay:300ms'
|
||||
}
|
||||
}) }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{{ form_start(form, {
|
||||
'attr': {
|
||||
'class': 'flex-1 flex flex-col min-h-0',
|
||||
'hx-post': path('app_booking_create_step_1'),
|
||||
'hx-target': '#form-wrapper',
|
||||
'hx-swap': 'innerHTML'
|
||||
}
|
||||
}) }}
|
||||
{# Pagination - mobile only (above summary) #}
|
||||
<div class="lg:hidden">
|
||||
{% include 'booking/_pagination.html.twig' with { 'current_step': 1 } %}
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col lg:grid lg:grid-cols-5 min-h-0 relative">
|
||||
{% block booking_summary %}
|
||||
<div id="booking-summary"
|
||||
class="order-1 lg:order-2 lg:flex-1 lg:min-h-0 lg:col-span-2"
|
||||
{{ stimulus_controller('toggle', { 'open': false }, { 'closed': 'hidden', 'open': 'absolute inset-0 z-20', 'iconOpen': 'rotate-180' }) }}
|
||||
{% if htmx_oob_swap is defined and htmx_oob_swap %}hx-swap-oob="true"{% endif %}>
|
||||
{% include 'booking/_summary.html.twig' with {
|
||||
'bookingCreateDto': bookingCreateDto,
|
||||
'summaryData': summaryData
|
||||
} %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block form %}
|
||||
<div id="form-wrapper"
|
||||
class="flex-1 order-2 lg:order-1 lg:col-span-3 bg-primary-bg flex flex-col min-h-0">
|
||||
{# Flash messages - must be inside form-wrapper for HTMX swap to display them #}
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
{# Pagination - desktop only (fixed above scrollable content) #}
|
||||
<div class="hidden lg:block shrink-0">
|
||||
{% include 'booking/_pagination.html.twig' with { 'current_step': 1 } %}
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto px-4 lg:px-8 py-8">
|
||||
<h2 class="pb-4">
|
||||
Unterkunft
|
||||
</h2>
|
||||
{% do form.roomSelections.setRendered %}
|
||||
{% block room_selection_form %}
|
||||
<div id="room-selection-form"{% if htmx_oob_swap is defined and htmx_oob_swap %} hx-swap-oob="true"{% endif %}>
|
||||
{{ form_errors(form) }}
|
||||
{% if groupedRooms.by_room is not empty %}
|
||||
<h3>
|
||||
Zimmer
|
||||
</h3>
|
||||
{% for roomId, room in groupedRooms.by_room %}
|
||||
{{ _self.stepFormField(form.roomSelections[roomId]) }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if groupedRooms.by_pax is not empty %}
|
||||
<h3>
|
||||
Betten
|
||||
</h3>
|
||||
{% for roomId, room in groupedRooms.by_pax %}
|
||||
{{ _self.stepFormField(form.roomSelections[roomId]) }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
<div class="flex justify-end pt-4">
|
||||
<button type="submit" class="button bg-button bg-button--secondary" {{ qa_attribute('btn-submit')}} {{ stimulus_action('loading', 'toggle') }}>Weiter</button>
|
||||
</div>
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
{% block booking_summary %}
|
||||
<div id="booking-summary"{% if htmx_oob_swap is defined and htmx_oob_swap %} hx-swap-oob="true"{% endif %}>
|
||||
{% include 'booking/_summary.html.twig' with {
|
||||
'bookingCreateDto': bookingCreateDto,
|
||||
'summaryData': summaryData
|
||||
} %}
|
||||
<div class="shrink-0 bg-primary-dark px-4 lg:px-8 py-2 lg:py-4 z-20">
|
||||
<div class="flex justify-between" hx-disinherit="*">
|
||||
<button type="button"
|
||||
hx-get="{{ path('app_booking_cancel') }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend"
|
||||
class="inline-flex items-center justify-center bg-gray-200 rounded-md w-10 h-10">
|
||||
<svg class="w-8 h-8" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="200" y1="56" x2="56" y2="200" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="200" y1="200" x2="56" y2="56" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
</button>
|
||||
<button type="submit" class="button button--primary" {{ qa_attribute('btn-submit')}} {{ stimulus_action('loading', 'toggle') }}>
|
||||
Weiter zu Schritt 2
|
||||
</button>
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
{% include 'booking/_cancel_link.html.twig' %}
|
||||
</div>
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,74 +1,80 @@
|
||||
{% extends 'layout.html.twig' %}
|
||||
{% extends 'layout_booking.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
<h1>Neue Buchung</h1>
|
||||
{{ form_start(form, {
|
||||
'attr': {
|
||||
'class': 'flex-1 flex flex-col min-h-0',
|
||||
'hx-post': path('app_booking_create_step_2'),
|
||||
'hx-target': '#main-content',
|
||||
'hx-swap': 'innerHTML scroll:top'
|
||||
}
|
||||
}) }}
|
||||
{# Pagination - mobile only (above summary) #}
|
||||
<div class="lg:hidden">
|
||||
{% include 'booking/_pagination.html.twig' with { 'current_step': 2 } %}
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col lg:grid lg:grid-cols-5 min-h-0 relative">
|
||||
{% block booking_summary %}
|
||||
<div id="booking-summary"
|
||||
class="order-1 lg:order-2 lg:flex-1 lg:min-h-0 lg:col-span-2"
|
||||
{{ stimulus_controller('toggle', { 'open': false }, { 'closed': 'hidden', 'open': 'absolute inset-0 z-20', 'iconOpen': 'rotate-180' }) }}
|
||||
{% if htmx_oob_swap|default(false) %} hx-swap-oob="true"{% endif %}>
|
||||
{% include 'booking/_summary.html.twig' with {
|
||||
'bookingCreateDto': bookingDto,
|
||||
'summaryData': summaryData
|
||||
} %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block participant_cards %}
|
||||
<div id="main-content"
|
||||
class="flex-1 order-2 lg:order-1 lg:col-span-3 bg-primary-bg flex flex-col min-h-0">
|
||||
{# Pagination - desktop only (fixed above scrollable content) #}
|
||||
<div class="hidden lg:block shrink-0">
|
||||
{% include 'booking/_pagination.html.twig' with { 'current_step': 2 } %}
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto px-4 lg:px-8 py-8">
|
||||
<h2 class="pb-4">
|
||||
Teilnehmer
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-3 gap-8">
|
||||
{# Main content area - cards grid #}
|
||||
{% block participant_cards %}
|
||||
<div id="main-content" class="col-span-2">
|
||||
{{ form_start(form, {
|
||||
'method': 'POST',
|
||||
'action': path('app_booking_create_step_2'),
|
||||
'attr': {
|
||||
'hx-post': path('app_booking_create_step_2'),
|
||||
'hx-target': '#main-content',
|
||||
'hx-swap': 'innerHTML'
|
||||
}
|
||||
}) }}
|
||||
{# Display form-level validation errors #}
|
||||
{% if form.vars.submitted and not form.vars.valid %}
|
||||
{% include '_partials/_alert.html.twig' with {
|
||||
level: 'error',
|
||||
title: 'Bitte überprüfe die Teilnehmerdaten',
|
||||
messages: ['Einige Teilnehmer haben noch unvollständige oder fehlerhafte Angaben. Bitte bearbeite die markierten Teilnehmer.']
|
||||
} %}
|
||||
{% endif %}
|
||||
|
||||
<h2>Teilnehmer</h2>
|
||||
|
||||
{# Display form-level validation errors #}
|
||||
{% if form.vars.submitted and not form.vars.valid %}
|
||||
<div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg" {{ qa_attribute('alert-form-errors') }}>
|
||||
<div class="flex items-start">
|
||||
<svg class="w-5 h-5 text-red-600 mr-2 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<p class="font-semibold text-red-800 mb-1">Bitte überprüfe die Teilnehmerdaten</p>
|
||||
<p class="text-sm text-red-700">Einige Teilnehmer haben noch unvollständige oder fehlerhafte Angaben. Bitte bearbeite die markierten Teilnehmer.</p>
|
||||
<div class="divide-y divide-gray-200">
|
||||
{% for cardData in cardsData %}
|
||||
{% include 'booking/_participant_card.html.twig' with {
|
||||
'cardData': cardData,
|
||||
'index': loop.index0,
|
||||
'participantNumber': loop.index,
|
||||
'mode': 'create'
|
||||
} %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div id="participant-cards-grid" class="space-y-4">
|
||||
{% for cardData in cardsData %}
|
||||
{% include 'booking/_participant_card.html.twig' with {
|
||||
'cardData': cardData,
|
||||
'index': loop.index0,
|
||||
'mode': 'create'
|
||||
} %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between mt-8">
|
||||
<a href="{{ path('app_booking_create_step_1') }}" class="button bg-button bg-button--secondary" {{ qa_attribute('btn-back') }} {{ stimulus_action('loading', 'toggle') }}>
|
||||
Zurück
|
||||
</a>
|
||||
<button type="submit" class="button bg-button bg-button--secondary" {{ qa_attribute('btn-submit') }} {{ stimulus_action('loading', 'toggle') }}>
|
||||
Weiter
|
||||
{% endblock %}
|
||||
</div>
|
||||
<div class="shrink-0 bg-primary-dark px-4 lg:px-8 py-2 lg:py-4 z-20">
|
||||
<div class="flex justify-between" hx-disinherit="*">
|
||||
<button type="button"
|
||||
hx-get="{{ path('app_booking_cancel') }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend"
|
||||
class="inline-flex items-center justify-center bg-gray-200 rounded-md w-10 h-10">
|
||||
<svg class="w-8 h-8" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="200" y1="56" x2="56" y2="200" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="200" y1="200" x2="56" y2="56" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
</button>
|
||||
<button type="submit" class="button button--primary" {{ qa_attribute('btn-submit') }} {{ stimulus_action('loading', 'toggle') }}>
|
||||
Weiter zu Schritt 3
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{# Sidebar summary #}
|
||||
{% block booking_summary %}
|
||||
<div id="booking-summary"{% if htmx_oob_swap|default(false) %} hx-swap-oob="true"{% endif %}>
|
||||
{% include 'booking/_summary.html.twig' with {
|
||||
'bookingCreateDto': bookingDto,
|
||||
'summaryData': summaryData
|
||||
} %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
{% include 'booking/_cancel_link.html.twig' %}
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,23 +1,54 @@
|
||||
{% extends 'layout.html.twig' %}
|
||||
{% extends 'layout_booking.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
<h1>Neue Buchung</h1>
|
||||
|
||||
<div class="grid grid-cols-3 gap-8">
|
||||
{# Main content area - participant form #}
|
||||
<div id="main-content" class="col-span-2">
|
||||
{% include 'booking/_participant_form.html.twig' %}
|
||||
{{ form_start(form, {
|
||||
'attr': {
|
||||
'class': 'flex-1 flex flex-col min-h-0',
|
||||
'novalidate': 'novalidate',
|
||||
'hx-post': path(submitRouteName, submitRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content',
|
||||
'hx-swap': 'innerHTML scroll:top'
|
||||
}
|
||||
}) }}
|
||||
{# Pagination - mobile only (above form) #}
|
||||
<div class="lg:hidden">
|
||||
{% include 'booking/_pagination.html.twig' with { 'current_step': 2 } %}
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col lg:grid lg:grid-cols-5 min-h-0 relative">
|
||||
{# Summary - hidden on mobile, visible on desktop #}
|
||||
<div id="booking-summary"
|
||||
class="hidden lg:block lg:order-2 lg:flex-1 lg:min-h-0 lg:col-span-2">
|
||||
{% include 'booking/_summary.html.twig' with {
|
||||
'bookingCreateDto': bookingDto,
|
||||
'summaryData': summaryData
|
||||
} %}
|
||||
</div>
|
||||
|
||||
{# Sidebar summary #}
|
||||
<div id="booking-summary">
|
||||
{% include 'booking/_summary.html.twig' with {
|
||||
'bookingCreateDto': bookingDto,
|
||||
'summaryData': summaryData
|
||||
} %}
|
||||
{# Form area - full width mobile, 3 cols desktop #}
|
||||
<div class="flex-1 order-1 lg:col-span-3 bg-white flex flex-col min-h-0">
|
||||
{# Pagination - desktop only (fixed above scrollable content) #}
|
||||
<div class="hidden lg:block shrink-0">
|
||||
{% include 'booking/_pagination.html.twig' with { 'current_step': 2 } %}
|
||||
</div>
|
||||
<div id="main-content" class="flex-1 overflow-y-auto px-4 lg:px-8 py-8">
|
||||
{# Flash messages - must be inside main-content for HTMX swap to display them #}
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
{% include 'booking/_participant_form.html.twig' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'booking/_cancel_link.html.twig' %}
|
||||
<div class="shrink-0 bg-primary-dark px-4 lg:px-8 py-2 lg:py-4 z-20">
|
||||
<div class="flex justify-between">
|
||||
<a href="{{ path('app_booking_create_step_2') }}"
|
||||
class="inline-flex items-center justify-center bg-gray-200 rounded-md w-10 h-10"
|
||||
{{ stimulus_action('loading', 'toggle') }}>
|
||||
<svg class="w-8 h-8" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="200" y1="56" x2="56" y2="200" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="200" y1="200" x2="56" y2="56" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
</a>
|
||||
<button type="submit" class="button button--primary" {{ qa_attribute('btn-submit') }} {{ stimulus_action('loading', 'toggle') }}>
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,68 +1,92 @@
|
||||
{% extends 'layout.html.twig' %}
|
||||
{% extends 'layout_booking.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Neue Buchung</h1>
|
||||
{{ form_start(form, {
|
||||
'attr': {
|
||||
'class': 'flex-1 flex flex-col min-h-0',
|
||||
'novalidate': 'novalidate',
|
||||
'hx-post': path('app_booking_create_step_3'),
|
||||
'hx-target': '#form-wrapper',
|
||||
'hx-select': '#form-wrapper',
|
||||
'hx-swap': 'outerHTML'
|
||||
}
|
||||
}) }}
|
||||
{# Pagination - mobile only (above summary) #}
|
||||
<div class="lg:hidden">
|
||||
{% include 'booking/_pagination.html.twig' with { 'current_step': 3 } %}
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col lg:grid lg:grid-cols-3 min-h-0 relative">
|
||||
{% block booking_summary %}
|
||||
<div id="booking-summary"
|
||||
class="order-1 lg:order-2 lg:flex-1 lg:min-h-0"
|
||||
{{ stimulus_controller('toggle', { 'open': false }, { 'closed': 'hidden', 'open': 'absolute inset-0 z-20', 'iconOpen': 'rotate-180' }) }}
|
||||
{% if htmx_oob_swap is defined and htmx_oob_swap %}hx-swap-oob="true"{% endif %}>
|
||||
{% include 'booking/_summary.html.twig' with {
|
||||
'bookingCreateDto': bookingCreateDto,
|
||||
'summaryData': summaryData
|
||||
} %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block form %}
|
||||
<div id="form-wrapper"
|
||||
class="flex-1 order-2 lg:order-1 lg:col-span-2 bg-white flex flex-col min-h-0">
|
||||
{# Flash messages - must be inside form-wrapper for HTMX swap to display them #}
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
{# Pagination - desktop only (fixed above scrollable content) #}
|
||||
<div class="hidden lg:block shrink-0">
|
||||
{% include 'booking/_pagination.html.twig' with { 'current_step': 3 } %}
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto px-4 lg:px-8 py-8">
|
||||
<h2 class="pb-4">
|
||||
Zahlungsart
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-3 gap-8">
|
||||
<div id="form-wrapper" class="col-span-2">
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
{# Payment method selection with HTMX #}
|
||||
<div id="form-payment"
|
||||
hx-post="{{ path('app_booking_create_step_3_refresh') }}"
|
||||
hx-trigger="change"
|
||||
hx-target="#form-payment"
|
||||
hx-select="#form-payment"
|
||||
hx-swap="outerHTML">
|
||||
{{ form_row(form.paymentMethod) }}
|
||||
|
||||
<h2 class="mb-6">Zahlungsart</h2>
|
||||
{# Bank account section - conditionally rendered #}
|
||||
{% if form.bankAccount is defined %}
|
||||
<h3 class="font-semibold text-lg mb-4">
|
||||
Bankverbindung
|
||||
</h3>
|
||||
|
||||
{{ form_start(form, {
|
||||
'attr': {
|
||||
'novalidate': 'novalidate',
|
||||
'hx-post': path('app_booking_create_step_3'),
|
||||
'hx-target': '#form-wrapper',
|
||||
'hx-select': '#form-wrapper',
|
||||
'hx-swap': 'outerHTML'
|
||||
}
|
||||
}) }}
|
||||
<div class="space-y-4">
|
||||
{{ form_row(form.bankAccount.iban) }}
|
||||
{{ form_row(form.bankAccount.accountHolder) }}
|
||||
{{ form_row(form.bankAccount.bankName) }}
|
||||
|
||||
{# Payment method selection with HTMX #}
|
||||
<div id="form-payment"
|
||||
hx-post="{{ path('app_booking_create_step_3_refresh') }}"
|
||||
hx-trigger="change"
|
||||
hx-target="#form-payment"
|
||||
hx-select="#form-payment"
|
||||
hx-swap="outerHTML">
|
||||
{{ form_row(form.paymentMethod) }}
|
||||
|
||||
{# Bank account section - conditionally rendered #}
|
||||
{% if form.bankAccount is defined %}
|
||||
<div class="border border-gray-300 rounded-lg p-4 bg-gray-50 mt-4">
|
||||
<h3 class="font-semibold text-lg mb-4">Bankverbindung</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
{{ form_row(form.bankAccount.iban) }}
|
||||
{{ form_row(form.bankAccount.accountHolder) }}
|
||||
{{ form_row(form.bankAccount.bankName) }}
|
||||
|
||||
<div class="mt-6 p-4 bg-blue-50 border border-blue-200 rounded">
|
||||
{{ form_row(form.bankAccount.sepaMandateAccepted, {
|
||||
'label_attr': {'class': 'text-sm'}
|
||||
}) }}
|
||||
</div>
|
||||
<div class="mt-6 p-4 bg-blue-50 border border-blue-200 rounded">
|
||||
{{ form_row(form.bankAccount.sepaMandateAccepted, {
|
||||
'label_attr': {'class': 'text-sm'}
|
||||
}) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between pt-6">
|
||||
<a href="{{ path('app_booking_create_step_2') }}" class="button bg-button bg-button--secondary" {{ stimulus_action('loading', 'toggle') }}>Zurück</a>
|
||||
<button type="submit" class="button bg-button bg-button--secondary" {{ stimulus_action('loading', 'toggle') }}>Weiter</button>
|
||||
</div>
|
||||
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
<div id="booking-summary">
|
||||
{% include 'booking/_summary.html.twig' with {
|
||||
'bookingCreateDto': bookingCreateDto,
|
||||
'summaryData': summaryData
|
||||
} %}
|
||||
<div class="shrink-0 bg-primary-dark px-4 lg:px-8 py-2 lg:py-4 z-20">
|
||||
<div class="flex justify-between" hx-disinherit="*">
|
||||
<button type="button"
|
||||
hx-get="{{ path('app_booking_cancel') }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend"
|
||||
class="inline-flex items-center justify-center bg-gray-200 rounded-md w-10 h-10">
|
||||
<svg class="w-8 h-8" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="200" y1="56" x2="56" y2="200" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="200" y1="200" x2="56" y2="56" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
</button>
|
||||
<button type="submit" class="button button--primary" {{ qa_attribute('btn-submit') }} {{ stimulus_action('loading', 'toggle') }}>
|
||||
Weiter zu Schritt 4
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'booking/_cancel_link.html.twig' %}
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
|
||||
@@ -3,37 +3,56 @@
|
||||
{% block title %}Buchung erfolgreich{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-2xl mx-auto text-center py-12">
|
||||
<h1 class="text-3xl font-bold mb-4">Buchung erfolgreich abgeschlossen</h1>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 px-4 lg:px-8 py-8 lg:py-16">
|
||||
<div>
|
||||
<h1 class="text-white uppercase pb-4 mb-8 border-b border-primary-bg/40">
|
||||
Vielen Dank
|
||||
<br>
|
||||
für deine Buchung
|
||||
</h1>
|
||||
|
||||
<p class="text-xl mb-8">
|
||||
Deine Buchungsnummer lautet <strong class="font-mono">{{ bookingNumber }}</strong>
|
||||
</p>
|
||||
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-8">
|
||||
<p class="text-gray-700">
|
||||
Du erhältst in Kürze eine Bestätigungs-E-Mail mit allen Details zu Deiner Buchung.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{% if app.user %}
|
||||
{# Authenticated user - show account-related actions #}
|
||||
<div class="space-y-4">
|
||||
<a href="{{ path('app_personal_data') }}" class="button bg-button bg-button--primary inline-block">
|
||||
Zu meinen persönlichen Daten
|
||||
</a>
|
||||
<a href="{{ path('app_bookings') }}" class="button bg-button bg-button--secondary inline-block ml-2">
|
||||
Meine Buchungen anzeigen
|
||||
</a>
|
||||
<a href="{{ path('app_logout') }}" class="button bg-button bg-button--outline inline-block ml-2">
|
||||
Abmelden
|
||||
</a>
|
||||
<div class="text-xl text-white mb-8">
|
||||
Deine Buchungsnummer lautet
|
||||
<br>
|
||||
<strong class="font-mono">{{ bookingNumber }}</strong>
|
||||
</div>
|
||||
{% else %}
|
||||
{# Guest user - show simple homepage link #}
|
||||
<a href="https://www.ep-reisen.de" class="button bg-button bg-button--primary">
|
||||
Zurück zur Startseite
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<div class="pb-8">
|
||||
{% include '_partials/_alert.html.twig' with {
|
||||
level: 'info',
|
||||
messages: ['Du erhältst in Kürze eine Bestätigungs-E-Mail mit allen Details.']
|
||||
} %}
|
||||
</div>
|
||||
|
||||
{% if app.user %}
|
||||
{# Authenticated user - show account-related actions #}
|
||||
<ul class="divide-y divide-primary-bg/40">
|
||||
<li class="py-4">
|
||||
<a href="{{ path('app_bookings') }}" class="flex items-center space-x-2 group" {{ stimulus_action('loading', 'toggle') }}>
|
||||
<svg class="w-8 h-8 text-white group-hover:text-primary-light" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="128" y1="128" x2="216" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="128" y1="64" x2="216" y2="64" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="128" y1="192" x2="216" y2="192" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><polyline points="40 64 56 80 88 48" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><polyline points="40 128 56 144 88 112" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><polyline points="40 192 56 208 88 176" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
<span class="text-xl uppercase text-white group-hover:text-primary-light">Meine Buchungen</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="py-4">
|
||||
<a href="{{ path('app_personal_data') }}" class="flex items-center space-x-2 group" {{ stimulus_action('loading', 'toggle') }}>
|
||||
<svg class="w-8 h-8 text-white group-hover:text-primary-light" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M160,224c3.67-13.8,16.6-24,32-24s28.33,10.2,32,24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><circle cx="192" cy="176" r="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M120,208H40a8,8,0,0,1-8-8V64a8,8,0,0,1,8-8H93.33a8,8,0,0,1,4.8,1.6l27.74,20.8a8,8,0,0,0,4.8,1.6H216a8,8,0,0,1,8,8v32" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
<span class="text-xl uppercase text-white group-hover:text-primary-light">Meine Daten</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="py-4">
|
||||
<a href="{{ path('app_logout') }}" class="flex items-center space-x-2 group">
|
||||
<svg class="w-8 h-8 text-white group-hover:text-primary-light" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><polyline points="112 40 48 40 48 216 112 216" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="112" y1="128" x2="224" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><polyline points="184 88 224 128 184 168" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
<span class="text-xl uppercase text-white group-hover:text-primary-light">abmelden</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% else %}
|
||||
{# Guest user - show simple homepage link #}
|
||||
<a href="https://www.ep-reisen.de" class="button button--primary">
|
||||
Zurück zur Startseite
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,109 +1,108 @@
|
||||
{% extends 'layout.html.twig' %}
|
||||
{% extends 'layout_booking.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
|
||||
{# Header with reload button #}
|
||||
<div class="flex justify-between items-center pb-8">
|
||||
<h1 class="text-3xl font-semibold">Buchung bearbeiten</h1>
|
||||
|
||||
<button type="button"
|
||||
hx-post="{{ path('app_booking_edit_reload', {id: bookingData.id}) }}"
|
||||
hx-confirm="Alle nicht gespeicherten Änderungen gehen verloren. Fortfahren?"
|
||||
{{ stimulus_action('loading', 'toggle') }}
|
||||
class="px-4 py-2 bg-gray-200 hover:bg-gray-300 text-gray-800 rounded transition-colors">
|
||||
Änderungen verwerfen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{# Grid layout with 2/3 cards + 1/3 summary #}
|
||||
<div class="grid grid-cols-3 gap-8">
|
||||
{# Main content area - cards grid #}
|
||||
<div id="main-content" class="col-span-2">
|
||||
{% block participant_cards %}
|
||||
{{ form_start(form) }}
|
||||
<div>
|
||||
<h2>Teilnehmer</h2>
|
||||
|
||||
{% if isDirty %}
|
||||
<div class="mb-6 p-4 bg-yellow-50 border border-yellow-400 rounded-lg">
|
||||
<div class="flex items-start">
|
||||
<svg class="w-6 h-6 text-yellow-600 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
||||
</svg>
|
||||
<div>
|
||||
<h3 class="font-semibold text-yellow-800">Ungespeicherte Änderungen</h3>
|
||||
<p class="text-yellow-700 text-sm mt-1">
|
||||
Du hast Änderungen an der Buchung vorgenommen.
|
||||
Bitte denke daran, abschließend den 'Buchung aktualisieren' Button zu klicken.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Display validation errors #}
|
||||
{% if hasValidationErrors|default(false) %}
|
||||
<div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<div class="flex items-start">
|
||||
<svg class="w-5 h-5 text-red-600 mr-2 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<p class="font-semibold text-red-800 mb-1">Bitte überprüfe die Teilnehmerdaten</p>
|
||||
<p class="text-sm text-red-700">Einige Teilnehmer haben noch unvollständige oder fehlerhafte Angaben. Bitte bearbeite die markierten Teilnehmer.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div id="participant-cards-grid" class="space-y-4">
|
||||
{% for participant in bookingDto.participants %}
|
||||
{% set isCanceled = (bookingData.participantsStatus[loop.index0] ?? null) == 'S' %}
|
||||
{% include 'booking/_participant_card.html.twig' with {
|
||||
'cardData': cardsData[loop.index0],
|
||||
'index': loop.index0,
|
||||
'mode': 'edit',
|
||||
'isCanceled': isCanceled,
|
||||
'bookingId': bookingData.id
|
||||
} %}
|
||||
{% endfor %}
|
||||
{{ form_start(form, {
|
||||
'attr': {
|
||||
'class': 'flex-1 flex flex-col min-h-0'
|
||||
}
|
||||
}) }}
|
||||
<div class="flex-1 flex flex-col lg:grid lg:grid-cols-5 min-h-0 relative">
|
||||
{# Sidebar summary #}
|
||||
{% block booking_summary %}
|
||||
<div id="booking-summary"
|
||||
class="order-1 lg:order-2 lg:flex-1 lg:min-h-0 lg:col-span-2"
|
||||
{{ stimulus_controller('toggle', { 'open': false }, { 'closed': 'hidden', 'open': 'absolute inset-0 z-20', 'iconOpen': 'rotate-180' }) }}
|
||||
{% if htmx_oob_swap is defined and htmx_oob_swap %}hx-swap-oob="true"{% endif %}>
|
||||
{% include 'booking/_summary.html.twig' with {
|
||||
'bookingCreateDto': bookingDto,
|
||||
'summaryData': summaryData,
|
||||
'mutableData': mutableData|default(null)
|
||||
} %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
<div class="flex justify-between mt-8">
|
||||
<button type="button"
|
||||
hx-post="{{ path('app_booking_edit_cancel', {id: bookingData.id}) }}"
|
||||
{{ stimulus_action('loading', 'toggle') }}
|
||||
class="button bg-button bg-button--secondary">
|
||||
Zurück
|
||||
</button>
|
||||
{# Main content area #}
|
||||
<div id="main-content"
|
||||
class="flex-1 order-2 lg:order-1 lg:col-span-3 bg-primary-bg flex flex-col min-h-0">
|
||||
{# Header - desktop only #}
|
||||
<div class="hidden lg:flex shrink-0 px-4 lg:px-8 h-16 items-center justify-between shadow-md relative z-10 bg-white">
|
||||
<h1 class="text-xl font-semibold uppercase">
|
||||
Buchung bearbeiten
|
||||
</h1>
|
||||
{% if isDirty %}
|
||||
<button type="submit"
|
||||
<button type="button"
|
||||
hx-post="{{ path('app_booking_edit_reload', {id: bookingData.id}) }}"
|
||||
hx-confirm="Alle nicht gespeicherten Änderungen gehen verloren. Fortfahren?"
|
||||
{{ stimulus_action('loading', 'toggle') }}
|
||||
{% if hasValidationErrors|default(false) %}
|
||||
disabled
|
||||
title="Bitte behebe zuerst alle Validierungsfehler"
|
||||
{% endif %}
|
||||
class="button bg-button bg-button--secondary {{ hasValidationErrors|default(false) ? 'opacity-50 cursor-not-allowed' : '' }}">
|
||||
Buchung aktualisieren
|
||||
class="button button--small button--secondary">
|
||||
Änderungen verwerfen
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Scrollable content #}
|
||||
<div class="flex-1 overflow-y-auto px-4 lg:px-8 py-8">
|
||||
{% block participant_cards %}
|
||||
<h2 class="pb-4">Teilnehmer</h2>
|
||||
|
||||
{% if isDirty %}
|
||||
{% include '_partials/_alert.html.twig' with {
|
||||
level: 'warning',
|
||||
title: 'Ungespeicherte Änderungen',
|
||||
messages: ['Du hast Änderungen an der Buchung vorgenommen. Bitte denke daran, abschließend den \'Buchung aktualisieren\' Button zu klicken.']
|
||||
} %}
|
||||
{% endif %}
|
||||
|
||||
{# Display validation errors #}
|
||||
{% if hasValidationErrors|default(false) %}
|
||||
{% include '_partials/_alert.html.twig' with {
|
||||
level: 'error',
|
||||
title: 'Bitte überprüfe die Teilnehmerdaten',
|
||||
messages: ['Einige Teilnehmer haben noch unvollständige oder fehlerhafte Angaben. Bitte bearbeite die markierten Teilnehmer.']
|
||||
} %}
|
||||
{% endif %}
|
||||
|
||||
<div id="participant-cards-grid" class="space-y-4">
|
||||
{% for participant in bookingDto.participants %}
|
||||
{% set isCanceled = (bookingData.participantsStatus[loop.index0] ?? null) == 'S' %}
|
||||
{% include 'booking/_participant_card.html.twig' with {
|
||||
'cardData': cardsData[loop.index0],
|
||||
'index': loop.index0,
|
||||
'participantNumber': loop.index,
|
||||
'mode': 'edit',
|
||||
'isCanceled': isCanceled,
|
||||
'bookingId': bookingData.id
|
||||
} %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
{# Sidebar summary #}
|
||||
{% block booking_summary %}
|
||||
<div id="booking-summary"{% if htmx_oob_swap is defined and htmx_oob_swap %} hx-swap-oob="true"{% endif %}>
|
||||
{% include 'booking/_summary.html.twig' with {
|
||||
'bookingCreateDto': bookingDto,
|
||||
'summaryData': summaryData,
|
||||
'mutableData': mutableData|default(null)
|
||||
} %}
|
||||
{# Fixed footer #}
|
||||
<div class="shrink-0 bg-primary-dark px-4 lg:px-8 py-2 lg:py-4 z-20">
|
||||
<div class="flex justify-between">
|
||||
<button type="button"
|
||||
hx-post="{{ path('app_booking_edit_cancel', {id: bookingData.id}) }}"
|
||||
{{ stimulus_action('loading', 'toggle') }}
|
||||
class="inline-flex items-center justify-center bg-gray-200 rounded-md w-10 h-10">
|
||||
<svg class="w-8 h-8" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="200" y1="56" x2="56" y2="200" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="200" y1="200" x2="56" y2="56" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
</button>
|
||||
{% if isDirty %}
|
||||
<button type="submit"
|
||||
{{ stimulus_action('loading', 'toggle') }}
|
||||
{% if hasValidationErrors|default(false) %}
|
||||
disabled
|
||||
title="Bitte behebe zuerst alle Validierungsfehler"
|
||||
{% endif %}
|
||||
class="button button--primary {{ hasValidationErrors|default(false) ? 'opacity-50 cursor-not-allowed' : '' }}">
|
||||
Buchung aktualisieren
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,24 +1,52 @@
|
||||
{% extends 'layout.html.twig' %}
|
||||
{% extends 'layout_booking.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
<h1>Buchung bearbeiten</h1>
|
||||
{{ form_start(form, {
|
||||
'attr': {
|
||||
'class': 'flex-1 flex flex-col min-h-0',
|
||||
'novalidate': 'novalidate',
|
||||
'hx-post': path(submitRouteName, submitRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content',
|
||||
'hx-swap': 'innerHTML scroll:top'
|
||||
}
|
||||
}) }}
|
||||
<div class="flex-1 flex flex-col lg:grid lg:grid-cols-5 min-h-0 relative">
|
||||
{# Summary - hidden on mobile, visible on desktop #}
|
||||
<div id="booking-summary"
|
||||
class="hidden lg:block lg:order-2 lg:flex-1 lg:min-h-0 lg:col-span-2">
|
||||
{% include 'booking/_summary.html.twig' with {
|
||||
'bookingCreateDto': bookingDto,
|
||||
'summaryData': summaryData,
|
||||
'mutableData': mutableData|default(null)
|
||||
} %}
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-8">
|
||||
{# Main content area - participant form #}
|
||||
<div id="main-content" class="col-span-2">
|
||||
{% include 'booking/_participant_form.html.twig' %}
|
||||
{# Form area - full width mobile, 3 cols desktop #}
|
||||
<div class="flex-1 order-1 lg:col-span-3 bg-white flex flex-col min-h-0">
|
||||
{# Header - desktop only #}
|
||||
<div class="hidden lg:flex shrink-0 px-4 lg:px-8 h-16 items-center shadow-md relative z-10 bg-white">
|
||||
<h1 class="text-xl font-semibold uppercase">Buchung bearbeiten</h1>
|
||||
</div>
|
||||
<div id="main-content" class="flex-1 overflow-y-auto px-4 lg:px-8 py-8">
|
||||
{# Flash messages - must be inside main-content for HTMX swap to display them #}
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
{% include 'booking/_participant_form.html.twig' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Sidebar summary #}
|
||||
<div id="booking-summary">
|
||||
{% include 'booking/_summary.html.twig' with {
|
||||
'bookingCreateDto': bookingDto,
|
||||
'summaryData': summaryData,
|
||||
'mutableData': mutableData|default(null)
|
||||
} %}
|
||||
<div class="shrink-0 bg-primary-dark px-4 lg:px-8 py-2 lg:py-4 z-20">
|
||||
<div class="flex justify-between">
|
||||
<a href="{{ path('app_booking_edit', {id: bookingDto.booking.id}) }}"
|
||||
class="inline-flex items-center justify-center bg-gray-200 rounded-md w-10 h-10"
|
||||
{{ stimulus_action('loading', 'toggle') }}
|
||||
{{ qa_attribute('btn-cancel') }}>
|
||||
<svg class="w-8 h-8" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="200" y1="56" x2="56" y2="200" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="200" y1="200" x2="56" y2="56" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
</a>
|
||||
<button type="submit" class="button button--primary" {{ qa_attribute('btn-submit') }} {{ stimulus_action('loading', 'toggle') }}>
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'booking/_cancel_link.html.twig' %}
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,92 +1,105 @@
|
||||
{% extends 'layout.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="text-2xl font-semibold pb-4">
|
||||
Meine Buchungen
|
||||
</h1>
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
<table class="w-full mb-4 responsive">
|
||||
<thead>
|
||||
<tr class="bg-zinc-200">
|
||||
<th class="border-r border-white/50 px-2 py-1 md:p-2 text-left text-sm md:text-base" scope="col">
|
||||
Reise
|
||||
</th>
|
||||
<th class="border-r border-white/50 px-2 py-1 md:p-2 text-sm md:text-base" scope="col">
|
||||
Reisedatum
|
||||
</th>
|
||||
<th class="border-r border-white/50 px-2 py-1 md:p-2 text-sm md:text-base" scope="col">
|
||||
Buchungsnummer
|
||||
</th>
|
||||
<th class="border-r border-white/50 px-2 py-1 md:p-2 text-sm md:text-base" scope="col">
|
||||
Reisepreis
|
||||
</th>
|
||||
<th class="border-r border-white/50 px-2 py-1 md:p-2 text-sm md:text-base" scope="col">
|
||||
offen
|
||||
</th>
|
||||
<th class="border-r border-white/50 px-2 py-1 md:p-2 text-sm md:text-base" scope="col">
|
||||
Status
|
||||
</th>
|
||||
<th scope="col">
|
||||
<span class="sr-only">Aktionen</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for booking in bookings %}
|
||||
<tr class="odd:bg-zinc-50 hover:bg-zinc-100 odd:hover:bg-zinc-100">
|
||||
<td class="border-r border-zinc-200 px-2 py-1 md:p-2"
|
||||
data-label="Reise">
|
||||
{{ booking.travelName }}
|
||||
</td>
|
||||
<td class="border-r border-zinc-200 px-2 py-1 md:p-2"
|
||||
data-label="Buchungsdatum">
|
||||
{% if booking.travelData %}
|
||||
{{ booking.travelData.dateFrom|date('d.m.Y') }} - {{ booking.travelData.dateTo|date('d.m.Y') }}
|
||||
{% else %}
|
||||
{{ booking.travelDate|date('d.m.Y') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="border-r border-zinc-200 px-2 py-1 md:p-2"
|
||||
data-label="Buchungsnummer">
|
||||
{{ booking.bookingNumber }}
|
||||
</td>
|
||||
<td class="border-r border-zinc-200 px-2 py-1 md:p-2"
|
||||
data-label="Reisepreis">
|
||||
{{ booking.price|format_currency('EUR') }}
|
||||
</td>
|
||||
<td class="border-r border-zinc-200 px-2 py-1 md:p-2"
|
||||
data-label="offen">
|
||||
{{ booking.balance ? booking.balance|format_currency('EUR') : '-' }}
|
||||
</td>
|
||||
<td class="border-r border-zinc-200 px-2 py-1 md:p-2"
|
||||
data-label="Status">
|
||||
{{ booking.status|map_status }}
|
||||
</td>
|
||||
<td class="px-2 py-1 md:p-2">
|
||||
<div class="flex md:flex-col space-x-2 md:space-x-0 md:space-y-1">
|
||||
{% if booking.editable %}
|
||||
<a href="{{ path('app_booking_edit', { 'id': booking.id }) }}"
|
||||
{{ stimulus_action('loading', 'toggle') }}
|
||||
class="button bg-button bg-button--secondary button--small">
|
||||
bearbeiten
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{{ path('app_booking_invoice', { 'id': booking.id }) }}"
|
||||
class="button bg-button button--small"
|
||||
target="_blank">
|
||||
Rechnung
|
||||
</a>
|
||||
{% if booking.travelInfoUrl is not null %}
|
||||
<a href="{{ booking.travelInfoUrl }}"
|
||||
class="button bg-button bg-pink button--small"
|
||||
target="_blank">
|
||||
Reiseinfos
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="flex-1 flex flex-col min-h-0">
|
||||
{# Header #}
|
||||
<div class="shrink-0 px-4 lg:px-8 py-8 lg:py-16">
|
||||
<h1 class="text-white uppercase pb-4">
|
||||
Meine<br>Buchungen
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{# Scrollable content area #}
|
||||
<div class="flex-1 overflow-y-auto p-4 lg:p-8 bg-primary-bg">
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
|
||||
<div class="space-y-8">
|
||||
{% for booking in bookings %}
|
||||
<div class="grid md:grid-cols-2 gap-4 p-2 bg-white rounded-md">
|
||||
<div class="flex items-start space-x-4">
|
||||
<div class="w-12 h-12 inline-flex flex-shrink-0 items-center justify-center rounded-full bg-primary-dark text-white text-2xl font-bold">
|
||||
{{ loop.index }}
|
||||
</div>
|
||||
<div class="text-xl font-semibold">
|
||||
{{ booking.travelName }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<table class="mb-4">
|
||||
<tr>
|
||||
<th class="text-left border-r-2 border-gray-300 pr-2">
|
||||
Reisedatum
|
||||
</th>
|
||||
<td class="pl-2">
|
||||
{% if booking.travelData %}
|
||||
{{ booking.travelData.dateFrom|date('d.m.Y') }} - {{ booking.travelData.dateTo|date('d.m.Y') }}
|
||||
{% else %}
|
||||
{{ booking.travelDate|date('d.m.Y') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-left border-r-2 border-gray-300 pr-2">
|
||||
Buchungsnr.
|
||||
</th>
|
||||
<td class="pl-2">
|
||||
{{ booking.bookingNumber }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-left border-r-2 border-gray-300">
|
||||
Reisepreis
|
||||
</th>
|
||||
<td class="pl-2">
|
||||
{{ booking.price|format_currency('EUR') }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-left border-r-2 border-gray-300">
|
||||
offen
|
||||
</th>
|
||||
<td class="pl-2">
|
||||
{{ booking.balance ? booking.balance|format_currency('EUR') : '-' }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-left border-r-2 border-gray-300">
|
||||
Status
|
||||
</th>
|
||||
<td class="pl-2">
|
||||
{{ booking.status|map_status }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="flex space-x-2">
|
||||
{% if booking.editable %}
|
||||
<a href="{{ path('app_booking_edit', { 'id': booking.id }) }}"
|
||||
{{ stimulus_action('loading', 'toggle') }}
|
||||
class="button button--primary button--small"
|
||||
title="Buchung bearbeiten">
|
||||
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M92.69,216H48a8,8,0,0,1-8-8V163.31a8,8,0,0,1,2.34-5.65L165.66,34.34a8,8,0,0,1,11.31,0L221.66,79a8,8,0,0,1,0,11.31L98.34,213.66A8,8,0,0,1,92.69,216Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="136" y1="64" x2="192" y2="120" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="164" y1="92" x2="68" y2="188" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="95.49" y1="215.49" x2="40.51" y2="160.51" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{{ path('app_booking_invoice', { 'id': booking.id }) }}"
|
||||
class="button button--secondary button--small"
|
||||
title="Rechnung herunterladen"
|
||||
target="_blank">
|
||||
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M64,56H40A16,16,0,0,0,24,72h0A16,16,0,0,0,40,88H56a16,16,0,0,1,16,16h0a16,16,0,0,1-16,16H28" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="48" y1="48" x2="48" y2="56" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="48" y1="120" x2="48" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M96,56H224V192a8,8,0,0,1-8,8H40a8,8,0,0,1-8-8V152" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="104" y1="104" x2="224" y2="104" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="80" y1="152" x2="224" y2="152" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="168" y1="104" x2="168" y2="200" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
</a>
|
||||
{% if booking.travelInfoUrl is not null %}
|
||||
<a href="{{ booking.travelInfoUrl }}"
|
||||
class="button button--secondary button--small"
|
||||
title="zu den Reiseinfos"
|
||||
target="_blank">
|
||||
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><circle cx="128" cy="128" r="96" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M120,120a8,8,0,0,1,8,8v40a8,8,0,0,0,8,8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><circle cx="124" cy="84" r="12"/></svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{% extends 'htmx_modal.html.twig' %}
|
||||
|
||||
{% block title %}Sicher?{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pb-8">
|
||||
Willst du den Buchungsvorgang wirklich abbrechen?
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<button type="button"
|
||||
hx-post="{{ path('app_booking_cancel') }}"
|
||||
hx-target="body"
|
||||
class="button button--small button--primary">
|
||||
Ja
|
||||
</button>
|
||||
<button type="button"
|
||||
{{ stimulus_action('modal', 'close') }}
|
||||
class="button button--small button--secondary">
|
||||
Nein
|
||||
</button>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -25,7 +25,7 @@
|
||||
{% endmacro %}
|
||||
|
||||
{%- block form_row -%}
|
||||
{%- set row_attr = row_attr|merge({ 'class': (row_attr.class|default('') ~ ' mb-1')|trim }) -%}
|
||||
{%- set row_attr = row_attr|merge({ 'class': (row_attr.class|default('') ~ ' mb-4')|trim }) -%}
|
||||
{%- set widget_attr = {} -%}
|
||||
{%- if help is not empty -%}
|
||||
{%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%}
|
||||
@@ -52,9 +52,9 @@
|
||||
{%- block form_label -%}
|
||||
{% set class = 'font-semibold' %}
|
||||
{% if errors|length %}
|
||||
{% set class = class ~ ' text-red-700' %}
|
||||
{% set class = class ~ ' text-red-500' %}
|
||||
{% endif %}
|
||||
{% set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ class)|trim}) %}
|
||||
{% set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ ' ' ~ class)|trim}) %}
|
||||
{{ parent() }}
|
||||
{%- endblock form_label -%}
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
{%- if errors|length > 0 -%}
|
||||
<ul class="pb-2">
|
||||
{%- for error in errors -%}
|
||||
<li class="text-red-700">{{ error.message }}</li>
|
||||
<li class="text-red-500">{{ error.message }}</li>
|
||||
{%- endfor -%}
|
||||
</ul>
|
||||
{%- endif -%}
|
||||
@@ -155,7 +155,7 @@
|
||||
<div class="flex h-6 items-center">
|
||||
{{ form_widget(form) }}
|
||||
</div>
|
||||
<div class="{{ html_classes('ml-2 leading-6', { 'text-red-700': errors|length }) }}">
|
||||
<div class="{{ html_classes('ml-2 leading-6', { 'text-red-500': errors|length }) }}">
|
||||
{{ form.vars.label | raw }}
|
||||
{{- form_errors(form) -}}
|
||||
</div>
|
||||
@@ -178,7 +178,7 @@
|
||||
<div class="flex h-6 items-center">
|
||||
{{ form_widget(form) }}
|
||||
</div>
|
||||
<div class="{{ html_classes('ml-2 leading-6', { 'text-red-700': errors|length }) }}">
|
||||
<div class="{{ html_classes('ml-2 leading-6', { 'text-red-500': errors|length }) }}">
|
||||
{{ form.vars.label | raw }}
|
||||
{{- form_errors(form) -}}
|
||||
</div>
|
||||
@@ -236,7 +236,7 @@
|
||||
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') }}
|
||||
<svg class="w-4 h-4 mt-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><circle cx="128" cy="128" r="12"/><circle cx="128" cy="60" r="12"/><circle cx="128" cy="196" r="12"/></svg>
|
||||
</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 %}
|
||||
@@ -325,13 +325,13 @@
|
||||
'data-step-input-target': 'field',
|
||||
'class': 'hidden',
|
||||
} }) }}
|
||||
<div class="flex space-x-2">
|
||||
<button type="button" class="button button--small bg-button" {{ stimulus_target('step-input', 'buttonDec') }} {{ stimulus_action('step-input', 'decrease') }}>
|
||||
-
|
||||
<div class="flex items-center justify-between">
|
||||
<button type="button" class="button button--small bg-white" {{ stimulus_target('step-input', 'buttonDec') }} {{ stimulus_action('step-input', 'decrease') }}>
|
||||
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="40" y1="128" x2="216" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
</button>
|
||||
<div {{ stimulus_target('step-input', 'display') }}></div>
|
||||
<button type="button" class="button button--small bg-button--secondary" {{ stimulus_target('step-input', 'buttonInc') }} {{ stimulus_action('step-input', 'increase') }}>
|
||||
+
|
||||
<div class="px-2" {{ stimulus_target('step-input', 'display') }}></div>
|
||||
<button type="button" class="button button--small button--primary" {{ stimulus_target('step-input', 'buttonInc') }} {{ stimulus_action('step-input', 'increase') }}>
|
||||
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="40" y1="128" x2="216" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="128" y1="40" x2="128" y2="216" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<div id="htmx-modal" class="fixed inset-0 w-full h-full z-50" {{ stimulus_controller('modal') }} {{ stimulus_action('modal', 'close', 'modal-close@window') }}>
|
||||
<div class="absolute inset-0 w-full h-full bg-black/80" {{ stimulus_action('modal', 'close', 'click') }}></div>
|
||||
<div id="htmx-modal"
|
||||
class="fixed inset-0 w-full h-full z-50" {{ stimulus_controller('modal') }} {{ stimulus_action('modal', 'close', 'modal-close@window') }}>
|
||||
<div class="absolute inset-0 w-full h-full backdrop-blur-sm" {{ stimulus_action('modal', 'close', 'click') }}></div>
|
||||
<div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 px-4 w-full max-w-2xl">
|
||||
<div class="relative bg-white py-8 rounded-md">
|
||||
<div class="flex justify-between pb-4 px-8">
|
||||
@@ -7,9 +8,9 @@
|
||||
{% block title %}{% endblock %}
|
||||
</div>
|
||||
<button type="button"
|
||||
class="inline-block"
|
||||
class="inline-flex items-center justify-center bg-gray-200 rounded-md w-10 h-10"
|
||||
{{ stimulus_action('modal', 'close') }}>
|
||||
{{ icon('close', 'w-6 h-6')}}
|
||||
<svg class="w-8 h-8" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="200" y1="56" x2="56" y2="200" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="200" y1="200" x2="56" y2="56" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="max-h-96 overflow-y-scroll">
|
||||
@@ -17,10 +18,6 @@
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="htmx-indicator absolute top-0 left-0 inset-0 bg-white/90 flex items-center justify-center"
|
||||
id="htmx-modal-indicator">
|
||||
{% include '_partials/_spinner.html.twig' with { 'class': 'w-8 h-8'} %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,26 +1,21 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
{# Global toast controller - persists across all HTMX swaps and page navigations #}
|
||||
<div {{ stimulus_controller('toast') }}></div>
|
||||
|
||||
<div class="{{ html_classes('pb-8', { 'pt-24': is_granted('ROLE_USER') }) }}" {{ stimulus_controller('loading', [], { 'hidden': 'invisible' }) }}>
|
||||
<div class="fixed inset-x-0 top-0 z-20">
|
||||
<div class="max-w-screen-xl mx-auto flex items-center justify-between bg-primary-dark text-white">
|
||||
{% if is_granted('ROLE_USER') %}
|
||||
<nav class="w-full flex items-center relative">
|
||||
<div class="flex-1 block text-xl px-4">My E&P</div>
|
||||
<div>
|
||||
{{ knp_menu_render(knp_menu_get('main')) }}
|
||||
</div>
|
||||
</nav>
|
||||
{% endif %}
|
||||
<div class="w-screen h-screen {% block background %}bg-outer bg-outer--2{% endblock %}" {{ stimulus_controller('loading', [], { 'hidden': 'invisible' }) }}>
|
||||
<div class="max-w-screen-xl h-screen ml-auto flex flex-col overflow-hidden bg-inner">
|
||||
<div class="shrink-0 z-20 py-2 px-4 bg-white shadow-md">
|
||||
{% block header %}
|
||||
{% include '_partials/_header.html.twig' %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col min-h-0 relative z-10">
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% block footer %}{% endblock %}
|
||||
</div>
|
||||
<div class="max-w-screen-xl mx-auto">
|
||||
{% block content %}{% endblock %}
|
||||
{% include '_partials/_loading_indicator.html.twig' %}
|
||||
</div>
|
||||
<div data-iframe-size></div>
|
||||
{% include '_partials/_loading_indicator.html.twig' %}
|
||||
</div>
|
||||
<div {{ stimulus_controller('toast') }}></div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
<div class="w-screen h-screen {% block background %}bg-outer bg-outer--2{% endblock %}" {{ stimulus_controller('loading', [], { 'hidden': 'invisible' }) }}>
|
||||
<div class="max-w-screen-xl h-screen ml-auto flex flex-col overflow-hidden bg-inner">
|
||||
<div class="shrink-0 z-20 py-2 px-4 bg-white shadow-md">
|
||||
{% block header %}
|
||||
{% include '_partials/_header.html.twig' %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col min-h-0 relative z-10">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% include '_partials/_loading_indicator.html.twig' %}
|
||||
</div>
|
||||
<div {{ stimulus_controller('toast') }}></div>
|
||||
{% endblock %}
|
||||
@@ -1,97 +0,0 @@
|
||||
{% extends 'layout.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="grid md:grid-cols-2 gap-x-16 gap-y-4">
|
||||
<div>
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
{{ form_start(personalDataForm, { 'attr': { 'data-action': 'loading#toggle' } }) }}
|
||||
<div class="pb-4">
|
||||
<button type="submit" class="button bg-button">
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
<h1 class="text-3xl font-semibold pb-2">
|
||||
Persönliche Daten
|
||||
</h1>
|
||||
<div class="grid grid-cols-2 gap-x-8 gap-y-4 pb-4">
|
||||
<div>
|
||||
<p class="text-lg font-semibold">
|
||||
Vorname
|
||||
</p>
|
||||
{{ personalData.firstName }}
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-lg font-semibold">
|
||||
Nachname
|
||||
</p>
|
||||
{{ personalData.name }}
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-lg font-semibold">
|
||||
Gender
|
||||
</p>
|
||||
{{ personalData.gender|map_gender }}
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-lg font-semibold">
|
||||
Geburtsdatum
|
||||
</p>
|
||||
{{ personalData.dateOfBirth|date('d.m.Y') }}
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-lg font-semibold">
|
||||
Nationaliät
|
||||
</p>
|
||||
{{ personalData.nationality|map_nationality|default('-') }}
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="text-xl font-semibold pb-2">
|
||||
Anschrift
|
||||
</h2>
|
||||
<div class="grid grid-cols-2 gap-x-8 gap-y-4 pb-4">
|
||||
<div class="col-span-2">
|
||||
{{ form_row(personalDataForm.street) }}
|
||||
</div>
|
||||
{{ form_row(personalDataForm.postCode) }}
|
||||
{{ form_row(personalDataForm.city) }}
|
||||
{{ form_row(personalDataForm.country) }}
|
||||
</div>
|
||||
<h2 class="text-xl font-semibold pb-2">
|
||||
Kontakt
|
||||
</h2>
|
||||
<div class="grid grid-cols-2 gap-x-8 gap-y-4 pb-4">
|
||||
<div class="col-span-2">
|
||||
{{ form_row(personalDataForm.email) }}
|
||||
</div>
|
||||
{{ form_row(personalDataForm.phone) }}
|
||||
{{ form_row(personalDataForm.mobile) }}
|
||||
</div>
|
||||
<button type="submit" class="button bg-button">
|
||||
Speichern
|
||||
</button>
|
||||
{{ form_rest(personalDataForm) }}
|
||||
{{ form_end(personalDataForm) }}
|
||||
</div>
|
||||
<div>
|
||||
<div id="newsletter">
|
||||
<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="relative button bg-button bg-button--secondary"
|
||||
hx-post="{{ path('app_personal_data_newsletter') }}"
|
||||
hx-target="#newsletter"
|
||||
hx-select="#newsletter"
|
||||
hx-indicator="#button-indicator"
|
||||
hx-swap="innerHTML">
|
||||
{% if personalData.communication.newsletter %}jetzt abmelden{% else %}jetzt anmelden{% endif %}
|
||||
<span class="htmx-indicator absolute top-0 left-0 inset-0 bg-secondary/80 flex items-center justify-center" id="button-indicator">{% include '_partials/_spinner.html.twig' with { 'class': 'w-6 h-6 text-white' } %}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,28 +1,30 @@
|
||||
{% extends 'layout.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="mb-4">
|
||||
Registrierung MyE&P
|
||||
</h1>
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
{{ form_start(form, { 'attr': { 'data-action': 'loading#toggle' } }) }}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 mb-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 px-4 lg:px-8 py-8 lg:py-16">
|
||||
<div>
|
||||
{{ form_row(form.firstName) }}
|
||||
{{ form_row(form.name) }}
|
||||
{{ form_row(form.email) }}
|
||||
{{ form_row(form.gender) }}
|
||||
<h1 class="text-white uppercase pb-4 mb-8 border-b border-primary-bg/40">
|
||||
Registrierung
|
||||
<br>
|
||||
MyE&P
|
||||
</h1>
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
<div class="pb-4">
|
||||
{{ form_start(form, { 'attr': { 'data-action': 'loading#toggle' } }) }}
|
||||
{{ form_row(form.firstName, { 'label_attr': { 'class': 'text-white' } }) }}
|
||||
{{ form_row(form.name, { 'label_attr': { 'class': 'text-white' } }) }}
|
||||
{{ form_row(form.email, { 'label_attr': { 'class': 'text-white' } }) }}
|
||||
{{ form_row(form.gender, { 'label_attr': { 'class': 'text-white' } }) }}
|
||||
<button type="submit" class="button button--primary">
|
||||
Jetzt registrieren
|
||||
</button>
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
<a href="{{ path('app_login') }}"
|
||||
class="text-white underline">
|
||||
Zum Login
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col lg:flex-row space-y-2 lg:space-y-0 lg:space-x-2">
|
||||
<button type="submit" class="button bg-button">
|
||||
registrieren
|
||||
</button>
|
||||
<a href="{{ path('app_login') }}"
|
||||
class="button bg-button bg-button--secondary">
|
||||
zum Login
|
||||
</a>
|
||||
</div>
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
@@ -1,27 +1,33 @@
|
||||
{% extends 'layout.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="mb-4">
|
||||
Passwort vergessen
|
||||
</h1>
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
{{ form_start(form, { 'attr': { 'data-action': 'loading#toggle' } }) }}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 mb-4">
|
||||
{{ form_row(form.email) }}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 px-4 lg:px-8 py-8 lg:py-16">
|
||||
<div>
|
||||
<h1 class="text-white uppercase pb-4 mb-8 border-b border-primary-bg/40">
|
||||
Passwort
|
||||
<br>
|
||||
zurücksetzen
|
||||
</h1>
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
<div class="pb-4">
|
||||
{{ form_start(form, { 'attr': { 'data-action': 'loading#toggle' } }) }}
|
||||
{{ form_row(form.email, { 'label_attr': { 'class': 'text-white' } }) }}
|
||||
<button type="submit" class="button button--primary">
|
||||
Passwort zurücksetzen
|
||||
</button>
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<a href="{{ path('app_login') }}"
|
||||
class="text-white underline">
|
||||
Zum Login
|
||||
</a>
|
||||
<a href="{{ path('app_registration') }}"
|
||||
class="text-white underline">
|
||||
Jetzt registrieren
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col lg:flex-row space-y-2 lg:space-y-0 lg:space-x-2 mt-4">
|
||||
<button type="submit" class="button bg-button">
|
||||
Passwort erneuern
|
||||
</button>
|
||||
<a href="{{ path('app_login') }}"
|
||||
class="button bg-button bg-button--secondary">
|
||||
zum Login
|
||||
</a>
|
||||
<a href="{{ path('app_registration') }}"
|
||||
class="button bg-button bg-button--secondary">
|
||||
registrieren
|
||||
</a>
|
||||
</div>
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
@@ -1,73 +1,77 @@
|
||||
{% extends 'layout.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="mb-4">
|
||||
Login MyE&P
|
||||
</h1>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 px-4 lg:px-8 py-8 lg:py-16">
|
||||
<div>
|
||||
<h1 class="text-white uppercase pb-4 mb-8 border-b border-primary-bg/40">
|
||||
Login<br>MyE&P
|
||||
</h1>
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
{% if error %}
|
||||
{% include '_partials/_alert.html.twig' with { 'level': 'error', messages: [ error.messageKey|trans(error.messageData, 'security') ] } %}
|
||||
{% endif %}
|
||||
<form action="{{ path('app_login') }}" method="post" {{ stimulus_action('loading', 'toggle') }}>
|
||||
<div class="mb-4">
|
||||
<label for="username" class="mb-1 font-semibold">
|
||||
E-Mail:
|
||||
</label>
|
||||
<input type="email"
|
||||
id="username"
|
||||
name="_username"
|
||||
value="{{ last_username }}"
|
||||
class="form-field"
|
||||
required
|
||||
autocomplete="username">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="password" class="mb-1 font-semibold">
|
||||
Passwort:
|
||||
</label>
|
||||
<input type="password"
|
||||
id="password"
|
||||
name="_password"
|
||||
class="form-field"
|
||||
required
|
||||
autocomplete="current-password">
|
||||
</div>
|
||||
<div class="flex flex-col lg:flex-row space-y-2 lg:space-y-0 lg:space-x-2 mt-4">
|
||||
<button type="submit" class="button bg-button">
|
||||
Login
|
||||
</button>
|
||||
<a href="{{ path('app_reset_password') }}"
|
||||
class="button bg-button bg-button--secondary">
|
||||
Passwort vergessen?
|
||||
</a>
|
||||
<a href="{{ path('app_registration') }}"
|
||||
class="button bg-button bg-button--secondary">
|
||||
registrieren
|
||||
</a>
|
||||
</div>
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
|
||||
</form>
|
||||
<div class="pb-4">
|
||||
<form action="{{ path('app_login') }}" method="post" {{ stimulus_action('loading', 'toggle') }}>
|
||||
<div class="mb-4">
|
||||
<label for="username" class="mb-1 font-semibold text-white">
|
||||
E-Mail:
|
||||
</label>
|
||||
<input type="email"
|
||||
id="username"
|
||||
name="_username"
|
||||
value="{{ last_username }}"
|
||||
class="form-field"
|
||||
required
|
||||
autocomplete="username">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="password" class="mb-1 font-semibold text-white">
|
||||
Passwort:
|
||||
</label>
|
||||
<input type="password"
|
||||
id="password"
|
||||
name="_password"
|
||||
class="form-field"
|
||||
required
|
||||
autocomplete="current-password">
|
||||
</div>
|
||||
<div class="flex flex-col lg:flex-row space-y-2 lg:space-y-0 lg:space-x-2 mt-4">
|
||||
<button type="submit" class="button button--primary">
|
||||
Login
|
||||
</button>
|
||||
</div>
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
|
||||
</form>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<a href="{{ path('app_reset_password') }}"
|
||||
class="text-white underline">
|
||||
Passwort vergessen?
|
||||
</a>
|
||||
<a href="{{ path('app_registration') }}"
|
||||
class="text-white underline">
|
||||
Jetzt registrieren
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="mb-2">
|
||||
<h3 class="mb-2 text-white">
|
||||
Neu!
|
||||
</h3>
|
||||
<p class="mb-4">
|
||||
<p class="mb-4 text-white">
|
||||
Melde dich jetzt bei MyEP an und du kannst jederzeit auf deine aktuellen Buchungen zugreifen und
|
||||
diese abändern. Egal ob du einen Kurs oder eine Skipasserweiterung hinzufügen/entfernen möchtest,
|
||||
oder das vegetarische Essen auswählen willst. Im neuen Portal ist das nun kostenlos bis 7 Tage vor
|
||||
Reisebeginn möglich!
|
||||
</p>
|
||||
<h3 class="mb-2">
|
||||
<h3 class="mb-2 text-white">
|
||||
Noch keine Zugangsdaten?
|
||||
</h3>
|
||||
<p class="mb-2">
|
||||
<p class="mb-2 text-white">
|
||||
Dann gib deine E-Mail ein, mit der Du gebucht hast und klicke auf "Passwort vergessen"! Und wir
|
||||
senden dir ein E-Mail mit einem Passwort-Link!
|
||||
</p>
|
||||
<p>
|
||||
<p class="text-white">
|
||||
Ein LogIn ist nur möglich wenn Eure Daten schon durch eine Buchung oder Anfrage bei uns
|
||||
angelegt wurden. Wenn Ihr auch Kunde werden möchtet, dann schickt uns Eure Interessen über unser
|
||||
<a href="https://www.ep-reisen.de/skireisen/unternehmen/kontakt/kontaktformular/" title="Kontaktformular" class="underline">Kontaktformular</a>.
|
||||
|
||||
@@ -90,8 +90,9 @@ class ParticipantEditDtoTest extends TestCase
|
||||
$this->createAdultParticipant('[email protected]'),
|
||||
]);
|
||||
|
||||
// Validate second participant (index 1) - applicant (index 0) is exempt from email uniqueness
|
||||
$wrapper = new ParticipantEditDto(
|
||||
participant: $bookingDto->participants[0],
|
||||
participant: $bookingDto->participants[1],
|
||||
bookingContext: $bookingDto,
|
||||
);
|
||||
|
||||
@@ -187,8 +188,9 @@ class ParticipantEditDtoTest extends TestCase
|
||||
$this->createAdultParticipant('[email protected]'),
|
||||
]);
|
||||
|
||||
// Validate second participant (index 1) - applicant (index 0) is exempt from email uniqueness
|
||||
$wrapper = new ParticipantEditDto(
|
||||
participant: $bookingDto->participants[0],
|
||||
participant: $bookingDto->participants[1],
|
||||
bookingContext: $bookingDto,
|
||||
);
|
||||
|
||||
@@ -206,14 +208,15 @@ class ParticipantEditDtoTest extends TestCase
|
||||
public function testWhitespaceNormalizationInEmailComparison(): void
|
||||
{
|
||||
$participant1 = $this->createAdultParticipant('[email protected]');
|
||||
$participant1->email = ' [email protected] ';
|
||||
|
||||
$participant2 = $this->createAdultParticipant('[email protected]');
|
||||
$participant2->email = ' [email protected] ';
|
||||
|
||||
$bookingDto = $this->createBookingDtoWithParticipants([$participant1, $participant2]);
|
||||
|
||||
// Validate second participant (index 1) - applicant (index 0) is exempt from email uniqueness
|
||||
$wrapper = new ParticipantEditDto(
|
||||
participant: $bookingDto->participants[0],
|
||||
participant: $bookingDto->participants[1],
|
||||
bookingContext: $bookingDto,
|
||||
);
|
||||
|
||||
@@ -236,16 +239,16 @@ class ParticipantEditDtoTest extends TestCase
|
||||
$this->createAdultParticipant('[email protected]'),
|
||||
]);
|
||||
|
||||
// Validate first participant - should fail
|
||||
// Validate first participant (applicant) - should pass (applicant is exempt from email uniqueness)
|
||||
$wrapper1 = new ParticipantEditDto(
|
||||
participant: $bookingDto->participants[0],
|
||||
bookingContext: $bookingDto,
|
||||
);
|
||||
|
||||
$violations1 = $this->validator->validate($wrapper1, null, ['booking_create']);
|
||||
$this->assertCount(1, $violations1);
|
||||
$this->assertCount(0, $violations1);
|
||||
|
||||
// Validate second participant - should fail
|
||||
// Validate second participant - should fail (duplicate with applicant and third participant)
|
||||
$wrapper2 = new ParticipantEditDto(
|
||||
participant: $bookingDto->participants[1],
|
||||
bookingContext: $bookingDto,
|
||||
@@ -254,7 +257,7 @@ class ParticipantEditDtoTest extends TestCase
|
||||
$violations2 = $this->validator->validate($wrapper2, null, ['booking_create']);
|
||||
$this->assertCount(1, $violations2);
|
||||
|
||||
// Validate third participant - should fail
|
||||
// Validate third participant - should fail (duplicate with applicant and second participant)
|
||||
$wrapper3 = new ParticipantEditDto(
|
||||
participant: $bookingDto->participants[2],
|
||||
bookingContext: $bookingDto,
|
||||
@@ -310,14 +313,15 @@ class ParticipantEditDtoTest extends TestCase
|
||||
|
||||
$bookingDto->participants = [$participant1, $participant2];
|
||||
|
||||
// Validate second participant (index 1) - applicant (index 0) is exempt from email uniqueness
|
||||
$wrapper = new ParticipantEditDto(
|
||||
participant: $bookingDto->participants[0],
|
||||
participant: $bookingDto->participants[1],
|
||||
bookingContext: $bookingDto,
|
||||
);
|
||||
|
||||
$violations = $this->validator->validate($wrapper, null, ['booking_edit']);
|
||||
|
||||
// Should have uniqueness violation (edit mode with immutable applicant = strict validation)
|
||||
// Should have uniqueness violation (edit mode still validates email uniqueness for non-applicants)
|
||||
$this->assertCount(1, $violations);
|
||||
}
|
||||
|
||||
|
||||