wip: group inquiries and bookings

This commit is contained in:
Björn Fromme committed 2026-07-14 11:29:43 +02:00
1 parent 6a6051e92b
commit 505bda46cf
115 files changed
+8107 -424

No files matched your search

@@ -0,0 +1,166 @@
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
static targets = ['mount', 'error', 'message', 'resetButton', 'day', 'dateFrom', 'dateTo', 'submit']
static values = {
hotelCode: String,
selectedFrom: String,
selectedTo: String,
calendarRefreshUrl: String,
}
connect() {
this.startDate = null
this.endDate = null
this.sessionRestored = false
// Re-apply highlights after every HTMX month swap (navigation or initial load).
this.mountTarget.addEventListener('htmx:afterSwap', () => requestAnimationFrame(() => this.initAfterLoad()))
this.mountTarget.addEventListener('htmx:responseError', () => this.showError())
}
initAfterLoad() {
// Restore from session values exactly once on the first swap.
if (!this.sessionRestored) {
this.sessionRestored = true
const from = this.selectedFromValue
const to = this.selectedToValue
if (from && to) {
this.startDate = from
this.endDate = to
if (this.hasResetButtonTarget) this.resetButtonTarget.classList.remove('hidden')
}
}
this.updateDisplay()
}
selectDate(event) {
const btn = event.currentTarget
btn.blur()
const date = btn.dataset.date
const status = btn.dataset.status
if (!status || status === 'blocked' || status === 'past') return
if (this.startDate && this.endDate) {
this.resetSelection()
}
if (!this.startDate) {
if (status === 'checkout-only') {
this.showMessage('Bitte wähle einen Anreisetag.')
return
}
this.startDate = date
this.endDate = null
this.updateDisplay()
this.showMessage('Bitte wähle das Abreisedatum.')
return
}
if (date === this.startDate) {
this.resetSelection()
return
}
const [from, to] = date < this.startDate ? [date, this.startDate] : [this.startDate, date]
if (this.hasBlockedInRange(from, to)) {
this.resetSelection()
this.showMessage('Dieser Zeitraum enthält nicht verfügbare Daten.')
return
}
const startBtn = this.dayTargets.find(b => b.dataset.date === from)
const minNights = parseInt(startBtn?.dataset.minNights) || 0
const nights = (new Date(to) - new Date(from)) / 86400000
if (minNights > 0 && nights < minNights) {
this.showMessage(`Weniger als ${minNights} Nächte nur auf Anfrage.`)
} else {
this.hideMessage()
}
this.startDate = from
this.endDate = to
this.updateDisplay()
this.activateSubmit(from, to)
this.saveAndRefreshSummary(from, to)
this.dispatch('range-select', { detail: { dateFrom: from, dateTo: to } })
}
hasBlockedInRange(from, to) {
for (const btn of this.dayTargets) {
const d = btn.dataset.date
const s = btn.dataset.status
if (d >= from && d < to && (s === 'blocked' || s === 'past' || s === 'checkout-only')) return true
}
return false
}
updateDisplay() {
for (const btn of this.dayTargets) {
const d = btn.dataset.date
btn.classList.remove('pc-day--pending-start', 'pc-day--selected-start', 'pc-day--selected-end', 'pc-day--in-range')
if (!this.startDate) continue
if (d === this.startDate) {
btn.classList.add(this.endDate ? 'pc-day--selected-start' : 'pc-day--pending-start')
} else if (this.endDate) {
if (d === this.endDate) btn.classList.add('pc-day--selected-end')
else if (d > this.startDate && d < this.endDate) btn.classList.add('pc-day--in-range')
}
}
}
activateSubmit(dateFrom, dateTo) {
if (this.hasDateFromTarget) this.dateFromTarget.value = dateFrom
if (this.hasDateToTarget) this.dateToTarget.value = dateTo
if (this.hasSubmitTarget) {
this.submitTarget.disabled = false
this.submitTarget.classList.remove('opacity-50', 'cursor-not-allowed', 'pointer-events-none')
}
if (this.hasResetButtonTarget) this.resetButtonTarget.classList.remove('hidden')
}
resetSelection() {
this.startDate = null
this.endDate = null
this.updateDisplay()
this.hideMessage()
if (this.hasDateFromTarget) this.dateFromTarget.value = ''
if (this.hasDateToTarget) this.dateToTarget.value = ''
if (this.hasSubmitTarget) {
this.submitTarget.disabled = true
this.submitTarget.classList.add('opacity-50', 'cursor-not-allowed', 'pointer-events-none')
}
if (this.hasResetButtonTarget) this.resetButtonTarget.classList.add('hidden')
this.saveAndRefreshSummary()
}
reset() {
this.resetSelection()
}
saveAndRefreshSummary(dateFrom = '', dateTo = '') {
if (!this.hasCalendarRefreshUrlValue) return
htmx.ajax('POST', this.calendarRefreshUrlValue, {
values: { date_from: dateFrom, date_to: dateTo },
swap: 'none',
target: this.element,
})
}
showMessage(text) {
if (this.hasMessageTarget) this.messageTarget.textContent = text
}
hideMessage() {
if (this.hasMessageTarget) this.messageTarget.textContent = ''
}
showError() {
if (this.hasErrorTarget) this.errorTarget.classList.remove('hidden')
}
}
-42
View File
@@ -1,42 +0,0 @@
import { Controller } from '@hotwired/stimulus'
import Sortable from 'sortablejs'
import { useFetch } from '../mixins/use_fetch'
/* stimulusFetch: 'lazy' */
export default class extends Controller {
static targets = [ 'item', 'handle', 'positionField', 'positionLabel' ]
static values = { url: String }
connect() {
useFetch(this)
new Sortable(this.element, {
item: '[data-sortable-target="item"]',
handle: '[data-sortable-target="handle"]',
sort: true,
onEnd: () => {
this.updateOrdering()
if (this.urlValue) {
this.saveOrdering()
}
}
})
}
updateOrdering() {
this.positionFieldTargets.forEach((field, index) => {
field.value = index + 1
})
this.positionLabelTargets.forEach((label, index) => {
label.innerText = index + 1
})
}
saveOrdering() {
const body = new FormData()
this.positionFieldTargets.forEach(field => {
body.append(`sorting[${field.dataset.id}]`, field.value)
})
fetch(this.urlValue, this.getInitObject('post', body))
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 839 KiB

+4
View File
@@ -53,6 +53,10 @@
background-image: url('../images/bg_5.jpg'), linear-gradient(190deg, rgba(24, 82, 123, 1) 0%, rgba(22, 88, 131, 1) 62%, rgba(0, 112, 224, 1) 100%);;
}
.bg-outer--summer {
background-image: url('../images/bg_summer.jpg'), linear-gradient(190deg, rgba(24, 82, 123, 1) 0%, rgba(22, 88, 131, 1) 62%, rgba(0, 112, 224, 1) 100%);;
}
.sbw .bg-outer--2 {
background-image: url('../images/bg_7.jpg'), linear-gradient(190deg, rgba(24, 82, 123, 1) 0%, rgba(22, 88, 131, 1) 62%, rgba(0, 112, 224, 1) 100%);;
}
+1
View File
@@ -1,6 +1,7 @@
@import "components/button.css";
@import "components/forms.css";
@import "components/pagination.css";
@import "components/price_calendar.css";
@import "components/toast.css";
@import "components/tooltip.css";
@import "components/typography.css";
@@ -0,0 +1,41 @@
.pc-day {
@apply relative w-full text-center py-1 px-0.5 text-sm leading-tight rounded text-white cursor-pointer;
}
.pc-day--out-of-month {
@apply pointer-events-none opacity-0;
}
.pc-day[data-status="ok"] {
@apply bg-emerald-500;
}
.pc-day[data-status="blocked-to-ok"] {
background: linear-gradient(to bottom right, theme('colors.gray.400') 50%, theme('colors.emerald.500') 50%);
}
.pc-day[data-status="checkout-only"] {
background: linear-gradient(to bottom right, theme('colors.emerald.500') 50%, theme('colors.gray.400') 50%);
}
.pc-day[data-status="past"] {
@apply bg-transparent text-gray-400 cursor-default;
}
.pc-day[data-status="blocked"] {
@apply bg-gray-400 cursor-default;
}
.pc-day[data-status=""], .pc-day:not([data-status]) {
@apply text-gray-400 cursor-default;
}
.pc-day.pc-day--pending-start,
.pc-day.pc-day--selected-start,
.pc-day.pc-day--selected-end {
@apply bg-none outline-none bg-secondary text-gray-700;
}
.pc-day.pc-day--in-range {
@apply bg-none bg-secondary text-gray-700 opacity-55;
}