feat: groups price calculator admin crud, booking/offer flow and api
This commit is contained in:
@@ -48,6 +48,15 @@ MAILJET_DEFAULT_LIST_ID=
|
|||||||
MAILJET_WEBHOOK_BASIC_PASSWORD=secret
|
MAILJET_WEBHOOK_BASIC_PASSWORD=secret
|
||||||
APP_NEWSLETTER_CONFIRMATION_TTL_HOURS=1
|
APP_NEWSLETTER_CONFIRMATION_TTL_HOURS=1
|
||||||
|
|
||||||
|
# Dedicated signing secret for accommodation booking customer access links (kept separate
|
||||||
|
# from APP_SECRET so it can be rotated independently). Unlike lazily-used API keys, UriSigner
|
||||||
|
# validates this is non-empty at construction time, so a real dev value is required here
|
||||||
|
# (override in .env.local/production secrets as usual).
|
||||||
|
ACCOMMODATION_OFFER_LINK_SECRET=abf8ec28cc2162d9121d35d6f8cbf7c29728cdacce0303e22e1bac854501728a
|
||||||
|
|
||||||
|
BPN_CONNECT_BASE_URL=https://bpn-connect.ep-reisen.app
|
||||||
|
BPN_CONNECT_API_KEY=
|
||||||
|
|
||||||
APP_BPN_USER=
|
APP_BPN_USER=
|
||||||
APP_BPN_PASSWORD=
|
APP_BPN_PASSWORD=
|
||||||
APP_BPN_IP=
|
APP_BPN_IP=
|
||||||
@@ -72,20 +81,26 @@ APP_TRAVEL_PREFER_REMOTE=false
|
|||||||
APP_TRAVEL_ENABLE_FALLBACK=true
|
APP_TRAVEL_ENABLE_FALLBACK=true
|
||||||
APP_TRAVEL_SNAPSHOT_RETENTION_BUFFER_DAYS=14
|
APP_TRAVEL_SNAPSHOT_RETENTION_BUFFER_DAYS=14
|
||||||
|
|
||||||
# Feature Flags
|
|
||||||
FEATURE_TRAVEL_SNAPSHOT=true
|
|
||||||
|
|
||||||
# Emails
|
# Emails
|
||||||
APP_DEFAULT_EMAIL_FROM=[email protected]
|
APP_DEFAULT_EMAIL_FROM=[email protected]
|
||||||
APP_DEFAULT_EMAIL_TO=[email protected]
|
APP_DEFAULT_EMAIL_TO=[email protected]
|
||||||
|
ACCOMMODATION_INQUIRY_EMAIL=[email protected]
|
||||||
|
|
||||||
|
# Global common defaults
|
||||||
|
APP_SEASON_WINTER_FROM=2026-10-01
|
||||||
|
APP_SEASON_SUMMER_FROM=2026-05-01
|
||||||
|
APP_RUNNING_COSTS_FACTOR_EUR=2.75
|
||||||
|
APP_RUNNING_COSTS_FACTOR_CHF=3.2
|
||||||
|
APP_UNDERSUBSCRIPTION_SURCHARGE_30_EUR=5.0
|
||||||
|
APP_UNDERSUBSCRIPTION_SURCHARGE_30_CHF=5.0
|
||||||
|
APP_UNDERSUBSCRIPTION_SURCHARGE_40_EUR=2.5
|
||||||
|
APP_UNDERSUBSCRIPTION_SURCHARGE_40_CHF=2.5
|
||||||
|
|
||||||
# Booking Configuration
|
# Booking Configuration
|
||||||
# Status for new bookings: 'F' (Fixed/Final), 'O' (Option - requires agency confirmation)
|
# Status for new bookings: 'F' (Fixed/Final), 'O' (Option - requires agency confirmation)
|
||||||
# Use 'O' during beta phase, switch to 'F' for production
|
# Use 'O' during beta phase, switch to 'F' for production
|
||||||
DEFAULT_BOOKING_STATUS=F
|
DEFAULT_BOOKING_STATUS=F
|
||||||
|
|
||||||
API_KEYS=
|
|
||||||
|
|
||||||
XML_EXPORT_PATH="%kernel.project_dir%/var/xmlexport"
|
XML_EXPORT_PATH="%kernel.project_dir%/var/xmlexport"
|
||||||
XML_EXPORT_CONTINGENTS_PATH="%kernel.project_dir%/var/xmlexportzimmer"
|
XML_EXPORT_CONTINGENTS_PATH="%kernel.project_dir%/var/xmlexportzimmer"
|
||||||
JSON_EXPORT_PATH="%kernel.project_dir%/var/jsonexport"
|
JSON_EXPORT_PATH="%kernel.project_dir%/var/jsonexport"
|
||||||
|
|||||||
@@ -121,6 +121,19 @@ Authorization: Bearer {{$auth.token("oauth2_newsletter")}}
|
|||||||
"listIds": [10321569,10321382,10321383]
|
"listIds": [10321569,10321382,10321383]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
### API contingent calendar
|
||||||
|
# @no-cookie-jar
|
||||||
|
GET {{base_url}}/api/contingents/calendar?hotelCode=DGS&dateFrom=2026-06-01&dateTo=2026-06-30
|
||||||
|
Accept: application/json
|
||||||
|
Authorization: Bearer {{$auth.token("oauth2_api")}}
|
||||||
|
|
||||||
|
### API contintent prices
|
||||||
|
# @no-cookie-jar
|
||||||
|
GET {{base_url}}/api/contingents/prices?hotelCode=DGS&year=2026
|
||||||
|
Accept: application/json
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer {{$auth.token("oauth2_api")}}
|
||||||
|
|
||||||
### API pickups planning webhook
|
### API pickups planning webhook
|
||||||
# @no-cookie-jar
|
# @no-cookie-jar
|
||||||
POST {{base_url}}/api/pickups-planning
|
POST {{base_url}}/api/pickups-planning
|
||||||
@@ -136,3 +149,10 @@ GET {{base_url}}/api/pickups-planning/DPWWTS191125
|
|||||||
Accept: application/json
|
Accept: application/json
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
Authorization: Bearer {{$auth.token("oauth2_api")}}
|
Authorization: Bearer {{$auth.token("oauth2_api")}}
|
||||||
|
|
||||||
|
### API accommodation booking
|
||||||
|
# @no-cookie-jar
|
||||||
|
GET {{base_url}}/api/accommodation-bookings/e4591076-eb65-41e8-a115-09d18d15604a
|
||||||
|
Accept: application/json
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer {{$auth.token("oauth2_api")}}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import htmx from 'htmx.org'
|
||||||
|
htmx.config.includeIndicatorStyles = false
|
||||||
|
htmx.config.historyCacheSize = 0
|
||||||
|
htmx.config.allowScriptTags = false
|
||||||
|
|
||||||
|
import './bootstrap.js'
|
||||||
|
|
||||||
|
import './styles/admin.css'
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { Controller } from '@hotwired/stimulus'
|
||||||
|
import { useFetch } from '../mixins/use_fetch'
|
||||||
|
import Autocomplete from '@trevoreyre/autocomplete-js'
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
|
||||||
|
static targets = [ 'input', 'field', 'resetButton', 'searchIcon' ]
|
||||||
|
|
||||||
|
static values = { url: String, choices: Array, selected: Boolean }
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
useFetch(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
const searchHandler = input => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
// Check input minimum length
|
||||||
|
if (input.length < 2) {
|
||||||
|
return resolve([])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search choices if provided
|
||||||
|
if (this.choicesValue.length > 0) {
|
||||||
|
return resolve(this.choicesValue.filter(choice => {
|
||||||
|
return -1 !== choice.text.toLowerCase().search(input.toLowerCase())
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use API otherwise
|
||||||
|
let data = { search: input }
|
||||||
|
fetch(this.urlValue, { method: 'POST', body: JSON.stringify(data), credentials: 'include' })
|
||||||
|
.then(this.checkStatus)
|
||||||
|
.then(this.parseJSON)
|
||||||
|
.then(data => {
|
||||||
|
resolve(data)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitHandler = result => {
|
||||||
|
this.fieldTarget.value = result.value
|
||||||
|
this.selectedValue = true
|
||||||
|
this.element.dispatchEvent(new CustomEvent('select', {
|
||||||
|
detail: { result }
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
this.autocomplete = new Autocomplete(this.element, {
|
||||||
|
search: searchHandler,
|
||||||
|
getResultValue: result => result.text,
|
||||||
|
submitOnEnter: true,
|
||||||
|
onSubmit: submitHandler,
|
||||||
|
debounceTime: 250,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.fieldTarget.value) {
|
||||||
|
this.selectedValue = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// prevent form submission when selecting entry in autocomplete list with enter key
|
||||||
|
this.element.addEventListener('keypress', e => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.inputTarget.value = null
|
||||||
|
this.fieldTarget.value = null
|
||||||
|
this.selectedValue = false
|
||||||
|
this.element.dispatchEvent(new CustomEvent('reset'))
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedValueChanged() {
|
||||||
|
this.resetButtonTarget.classList.toggle('hidden', false === this.selectedValue)
|
||||||
|
this.searchIconTarget.classList.toggle('hidden', this.selectedValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.autocomplete.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
|
||||||
|
static values = {
|
||||||
|
content: String,
|
||||||
|
label: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.originalLabel = this.element.innerText
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
clearTimeout(this.resetTimer)
|
||||||
|
}
|
||||||
|
|
||||||
|
copy() {
|
||||||
|
navigator.clipboard.writeText(this.contentValue).then(() => {
|
||||||
|
this.dispatch('copied', {
|
||||||
|
detail: {
|
||||||
|
content: this.contentValue,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('Failed to copy to clipboard:', error)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.hasLabelValue) {
|
||||||
|
this.element.innerText = this.labelValue
|
||||||
|
clearTimeout(this.resetTimer)
|
||||||
|
this.resetTimer = setTimeout(() => {
|
||||||
|
this.element.innerText = this.originalLabel
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { Controller } from '@hotwired/stimulus'
|
||||||
|
import { Calendar } from 'vanilla-calendar-pro'
|
||||||
|
|
||||||
|
/* stimulusFetch: 'lazy' */
|
||||||
|
export default class extends Controller {
|
||||||
|
|
||||||
|
static targets = ['field', 'hiddenField']
|
||||||
|
static values = {
|
||||||
|
minDate: String,
|
||||||
|
maxDate: String,
|
||||||
|
disableWeekends: Boolean,
|
||||||
|
disabledDates: Array,
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
const options = {
|
||||||
|
inputMode: true,
|
||||||
|
locale: 'de-DE',
|
||||||
|
selectedTheme: 'light',
|
||||||
|
selectedDates: this.hiddenFieldTarget.value ? [this.hiddenFieldTarget.value] : [],
|
||||||
|
onClickDate: (self) => {
|
||||||
|
const iso = self.context.selectedDates[0] ?? null
|
||||||
|
this.hiddenFieldTarget.value = iso ?? ''
|
||||||
|
if (iso) {
|
||||||
|
const [year, month, day] = iso.split('-')
|
||||||
|
this.fieldTarget.value = `${day}.${month}.${year}`
|
||||||
|
} else {
|
||||||
|
this.fieldTarget.value = ''
|
||||||
|
}
|
||||||
|
this.element.dispatchEvent(new CustomEvent('datepicker:picked', {
|
||||||
|
detail: { date: this.fieldTarget.value }
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
onHide: () => {
|
||||||
|
this.element.dispatchEvent(new CustomEvent('datepicker:closed'))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.minDateValue) options.dateMin = this.minDateValue
|
||||||
|
if (this.maxDateValue) options.dateMax = this.maxDateValue
|
||||||
|
if (this.disableWeekendsValue) options.disableWeekdays = [0, 6]
|
||||||
|
if (this.disabledDatesValue.length) options.disableDates = this.disabledDatesValue
|
||||||
|
|
||||||
|
this.picker = new Calendar(this.fieldTarget, options)
|
||||||
|
this.picker.init()
|
||||||
|
|
||||||
|
this.handleManualInput = () => {
|
||||||
|
const value = this.fieldTarget.value.trim()
|
||||||
|
if (!value) {
|
||||||
|
this.hiddenFieldTarget.value = ''
|
||||||
|
this.picker.set({ selectedDates: [] }, { dates: true })
|
||||||
|
} else {
|
||||||
|
const match = value.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/)
|
||||||
|
if (match) {
|
||||||
|
const [, day, month, year] = match
|
||||||
|
const iso = `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`
|
||||||
|
this.hiddenFieldTarget.value = iso
|
||||||
|
this.picker.set({ selectedDates: [iso] }, { dates: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.element.dispatchEvent(new CustomEvent('datepicker:closed'))
|
||||||
|
}
|
||||||
|
this.fieldTarget.addEventListener('change', this.handleManualInput)
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.fieldTarget.removeEventListener('change', this.handleManualInput)
|
||||||
|
this.picker.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { Controller } from '@hotwired/stimulus'
|
||||||
|
import GLightbox from 'glightbox'
|
||||||
|
|
||||||
|
/* stimulusFetch: 'lazy' */
|
||||||
|
export default class extends Controller {
|
||||||
|
|
||||||
|
static targets = ['item']
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.lightbox = GLightbox({
|
||||||
|
elements: this.itemTargets.map((el) => ({
|
||||||
|
href: el.dataset.galleryHref,
|
||||||
|
type: 'image',
|
||||||
|
alt: el.dataset.galleryAlt ?? '',
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
|
||||||
|
this.itemTargets.forEach((el, index) => {
|
||||||
|
el.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
this.lightbox.openAt(index)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.lightbox.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Controller } from '@hotwired/stimulus'
|
||||||
|
import { useClickOutside } from 'stimulus-use'
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
|
||||||
|
static targets = [ 'label', 'dropdown', 'choice' ]
|
||||||
|
static values = { placeholder: String }
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.update()
|
||||||
|
useClickOutside(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
this.dropdownTarget.classList.toggle('hidden')
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
let labels = []
|
||||||
|
|
||||||
|
this.choiceTargets.forEach((choice) => {
|
||||||
|
if (choice.checked) {
|
||||||
|
labels.push(choice.dataset.label)
|
||||||
|
}
|
||||||
|
this.labelTarget.innerText = labels.join(', ')
|
||||||
|
})
|
||||||
|
|
||||||
|
if (0 === labels.length) {
|
||||||
|
this.labelTarget.innerText = this.placeholderValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clickOutside(e) {
|
||||||
|
this.dropdownTarget.classList.toggle('hidden', true)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,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')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Controller } from '@hotwired/stimulus'
|
import { Controller } from '@hotwired/stimulus'
|
||||||
import tippy from 'tippy.js'
|
import tippy, { hideAll } from 'tippy.js'
|
||||||
|
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static targets = ['trigger', 'template']
|
static targets = ['trigger', 'template']
|
||||||
@@ -27,6 +27,7 @@ export default class extends Controller {
|
|||||||
allowHTML: true,
|
allowHTML: true,
|
||||||
interactive: true,
|
interactive: true,
|
||||||
appendTo: document.body,
|
appendTo: document.body,
|
||||||
|
onShow(instance) { hideAll({ exclude: instance }) },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.hasTriggerTarget) {
|
if (this.hasTriggerTarget) {
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 839 KiB |
+85
-241
@@ -1,208 +1,69 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg">
|
<svg xmlns="http://www.w3.org/2000/svg">
|
||||||
<symbol id="icon-table" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
<symbol id="icon-table" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3.375 19.5h17.25m-17.25 0a1.125 1.125 0 0 1-1.125-1.125M3.375 19.5h7.5c.621 0 1.125-.504 1.125-1.125m-9.75 0V5.625m0 12.75v-1.5c0-.621.504-1.125 1.125-1.125m18.375 2.625V5.625m0 12.75c0 .621-.504 1.125-1.125 1.125m1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125m0 3.75h-7.5A1.125 1.125 0 0 1 12 18.375m9.75-12.75c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125m19.5 0v1.5c0 .621-.504 1.125-1.125 1.125M2.25 5.625v1.5c0 .621.504 1.125 1.125 1.125m0 0h17.25m-17.25 0h7.5c.621 0 1.125.504 1.125 1.125M3.375 8.25c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125m17.25-3.75h-7.5c-.621 0-1.125.504-1.125 1.125m8.625-1.125c.621 0 1.125.504 1.125 1.125v1.5c0 .621-.504 1.125-1.125 1.125m-17.25 0h7.5m-7.5 0c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125M12 10.875v-1.5m0 1.5c0 .621-.504 1.125-1.125 1.125M12 10.875c0 .621.504 1.125 1.125 1.125m-2.25 0c.621 0 1.125.504 1.125 1.125M13.125 12h7.5m-7.5 0c-.621 0-1.125.504-1.125 1.125M20.625 12c.621 0 1.125.504 1.125 1.125v1.5c0 .621-.504 1.125-1.125 1.125m-17.25 0h7.5M12 14.625v-1.5m0 1.5c0 .621-.504 1.125-1.125 1.125M12 14.625c0 .621.504 1.125 1.125 1.125m-2.25 0c.621 0 1.125.504 1.125 1.125m0 1.5v-1.5m0 0c0-.621.504-1.125 1.125-1.125m0 0h7.5" /></symbol>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.375 19.5h17.25m-17.25 0a1.125 1.125 0 0 1-1.125-1.125M3.375 19.5h7.5c.621 0 1.125-.504 1.125-1.125m-9.75 0V5.625m0 12.75v-1.5c0-.621.504-1.125 1.125-1.125m18.375 2.625V5.625m0 12.75c0 .621-.504 1.125-1.125 1.125m1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125m0 3.75h-7.5A1.125 1.125 0 0 1 12 18.375m9.75-12.75c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125m19.5 0v1.5c0 .621-.504 1.125-1.125 1.125M2.25 5.625v1.5c0 .621.504 1.125 1.125 1.125m0 0h17.25m-17.25 0h7.5c.621 0 1.125.504 1.125 1.125M3.375 8.25c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125m17.25-3.75h-7.5c-.621 0-1.125.504-1.125 1.125m8.625-1.125c.621 0 1.125.504 1.125 1.125v1.5c0 .621-.504 1.125-1.125 1.125m-17.25 0h7.5m-7.5 0c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125M12 10.875v-1.5m0 1.5c0 .621-.504 1.125-1.125 1.125M12 10.875c0 .621.504 1.125 1.125 1.125m-2.25 0c.621 0 1.125.504 1.125 1.125M13.125 12h7.5m-7.5 0c-.621 0-1.125.504-1.125 1.125M20.625 12c.621 0 1.125.504 1.125 1.125v1.5c0 .621-.504 1.125-1.125 1.125m-17.25 0h7.5M12 14.625v-1.5m0 1.5c0 .621-.504 1.125-1.125 1.125M12 14.625c0 .621.504 1.125 1.125 1.125m-2.25 0c.621 0 1.125.504 1.125 1.125m0 1.5v-1.5m0 0c0-.621.504-1.125 1.125-1.125m0 0h7.5" />
|
<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>
|
<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-dots" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
<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>
|
||||||
<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 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>
|
<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-feedback" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
<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>
|
||||||
<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 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>
|
<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-login" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
<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>
|
||||||
<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 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>
|
<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-logout" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
<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>
|
||||||
<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 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>
|
<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-alert" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
<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>
|
||||||
<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 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>
|
<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-close" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
<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>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
<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>
|
<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-back" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
<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>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 15L3 9m0 0l6-6M3 9h12a6 6 0 010 12h-3" />
|
<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>
|
<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-list" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
<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>
|
||||||
<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 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>
|
<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-menu" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
<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>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
<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>
|
<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-edit" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
<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>
|
||||||
<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 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>
|
<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-plus" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
<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>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
<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>
|
<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-minus" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
<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>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
|
<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>
|
<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-search" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
<symbol id="icon-external-link" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M10 6V8H5V19H16V14H18V20C18 20.5523 17.5523 21 17 21H4C3.44772 21 3 20.5523 3 20V7C3 6.44772 3.44772 6 4 6H10ZM21 3V11H19L18.9999 6.413L11.2071 14.2071L9.79289 12.7929L17.5849 5H13V3H21Z"/></symbol>
|
||||||
<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 id="icon-cookie" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M21.1634 10.9711C21.5132 10.8204 21.9579 11.0073 21.9811 11.3875C22.1494 14.1467 21.1794 16.9625 19.071 19.0709C15.1657 22.976 8.83405 22.9761 4.92886 19.0709C1.02367 15.1657 1.02377 8.83405 4.92885 4.9288C7.03704 2.82061 9.8525 1.85015 12.6115 2.01806C12.9916 2.04119 13.1787 2.48598 13.028 2.83576C12.4774 4.11367 12.7239 5.65275 13.7677 6.69657C14.3307 7.25963 15.038 7.58971 15.7706 7.68848C16.0483 7.72594 16.2738 7.95144 16.3113 8.2292C16.4101 8.96178 16.7402 9.66905 17.3032 10.2321C18.347 11.2759 19.8857 11.5218 21.1634 10.9711ZM19.7845 13.8397C19.8558 13.5362 19.6123 13.2603 19.3017 13.2337C18.0569 13.1271 16.8417 12.599 15.889 11.6463C15.3166 11.0739 14.8979 10.4061 14.6309 9.69455C14.5744 9.54422 14.4556 9.42534 14.3052 9.36892C13.5936 9.10186 12.9259 8.68322 12.3535 8.11078C11.4005 7.15778 10.8715 5.94208 10.7648 4.69683C10.7381 4.38618 10.4621 4.14264 10.1586 4.2141C8.76039 4.54327 7.43341 5.25267 6.34306 6.34301C3.21903 9.46721 3.21893 14.5326 6.34307 17.6567C9.46721 20.7809 14.5326 20.7808 17.6568 17.6567C18.7474 16.566 19.4557 15.2384 19.7845 13.8397ZM9.8786 14.1212C10.4644 14.707 10.4644 15.6567 9.8786 16.2425C9.29281 16.8283 8.34307 16.8283 7.75728 16.2425C7.17149 15.6567 7.17149 14.707 7.75728 14.1212C8.34306 13.5354 9.29281 13.5354 9.8786 14.1212ZM10.2321 8.11078C10.8179 8.69657 10.8179 9.64631 10.2321 10.2321C9.64636 10.8179 8.69661 10.8179 8.11083 10.2321C7.52504 9.64631 7.52504 8.69656 8.11083 8.11078C8.69661 7.52499 9.64636 7.52499 10.2321 8.11078ZM15.889 13.7676C16.4748 14.3534 16.4748 15.3032 15.889 15.889C15.3032 16.4747 14.3535 16.4747 13.7677 15.889C13.1819 15.3032 13.1819 14.3534 13.7677 13.7676C14.3535 13.1818 15.3032 13.1818 15.889 13.7676Z"/></symbol>
|
||||||
</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-user" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
<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>
|
||||||
<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 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>
|
<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-users" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
<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>
|
||||||
<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 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>
|
<symbol id="icon-chair" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 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="M288 16c26.5 0 48 21.5 48 48V256h16V64c0-35.3-28.7-64-64-64H160C124.7 0 96 28.7 96 64V256h16V64c0-26.5 21.5-48 48-48H288zM48 168c0-4.4-3.6-8-8-8s-8 3.6-8 8v80c0 4.4 3.6 8 8 8s8-3.6 8-8V168zm368 0c0-4.4-3.6-8-8-8s-8 3.6-8 8v80c0 4.4 3.6 8 8 8s8-3.6 8-8V168zM80 304H368c6.1 0 11.6 3.4 14.3 8.8l16 32c2.5 5 2.2 10.9-.7 15.6s-8.1 7.6-13.6 7.6H224 64c-5.5 0-10.7-2.9-13.6-7.6s-3.2-10.6-.7-15.6l16-32c2.7-5.4 8.3-8.8 14.3-8.8zm152 80H384c11.1 0 21.4-5.7 27.2-15.2s6.4-21.2 1.4-31.1l-16-32C391.2 294.8 380.1 288 368 288H80c-12.1 0-23.2 6.8-28.6 17.7l-16 32c-5 9.9-4.4 21.7 1.4 31.1S52.9 384 64 384H216v64H184c-30.9 0-56 25.1-56 56c0 4.4 3.6 8 8 8s8-3.6 8-8c0-22.1 17.9-40 40-40h32v40c0 4.4 3.6 8 8 8s8-3.6 8-8V464h32c22.1 0 40 17.9 40 40c0 4.4 3.6 8 8 8s8-3.6 8-8c0-30.9-25.1-56-56-56H232V384z"/></symbol>
|
||||||
<symbol id="icon-delete" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
<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>
|
||||||
<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 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>
|
<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-check" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
<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>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
<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>
|
<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-upload" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" stroke="currentColor">
|
<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>
|
||||||
<path d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/>
|
<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>
|
<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-download" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
<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>
|
||||||
<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 id="icon-draggable" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M8.5 7C9.32843 7 10 6.32843 10 5.5C10 4.67157 9.32843 4 8.5 4C7.67157 4 7 4.67157 7 5.5C7 6.32843 7.67157 7 8.5 7ZM8.5 13.5C9.32843 13.5 10 12.8284 10 12C10 11.1716 9.32843 10.5 8.5 10.5C7.67157 10.5 7 11.1716 7 12C7 12.8284 7.67157 13.5 8.5 13.5ZM10 18.5C10 19.3284 9.32843 20 8.5 20C7.67157 20 7 19.3284 7 18.5C7 17.6716 7.67157 17 8.5 17C9.32843 17 10 17.6716 10 18.5ZM15.5 7C16.3284 7 17 6.32843 17 5.5C17 4.67157 16.3284 4 15.5 4C14.6716 4 14 4.67157 14 5.5C14 6.32843 14.6716 7 15.5 7ZM17 12C17 12.8284 16.3284 13.5 15.5 13.5C14.6716 13.5 14 12.8284 14 12C14 11.1716 14.6716 10.5 15.5 10.5C16.3284 10.5 17 11.1716 17 12ZM15.5 20C16.3284 20 17 19.3284 17 18.5C17 17.6716 16.3284 17 15.5 17C14.6716 17 14 17.6716 14 18.5C14 19.3284 14.6716 20 15.5 20Z"/></symbol>
|
||||||
</symbol>
|
<symbol id="icon-discount" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M10.0544 2.0941C11.1756 1.13856 12.8248 1.13855 13.9461 2.09411L15.2941 3.24286C15.4542 3.37935 15.6533 3.46182 15.8631 3.47856L17.6286 3.61945C19.0971 3.73663 20.2633 4.9028 20.3805 6.37131L20.5214 8.13679C20.5381 8.34654 20.6205 8.54568 20.757 8.70585L21.9058 10.0539C22.8614 11.1751 22.8614 12.8243 21.9058 13.9456L20.757 15.2935C20.6206 15.4537 20.538 15.6529 20.5213 15.8627L20.3805 17.6281C20.2633 19.0967 19.0971 20.2628 17.6286 20.3799L15.8631 20.5208C15.6533 20.5376 15.4542 20.6201 15.2941 20.7566L13.9461 21.9053C12.8248 22.8609 11.1756 22.8608 10.0543 21.9053L8.70631 20.7566C8.54615 20.6201 8.34705 20.5376 8.1373 20.5209L6.37184 20.3799C4.9033 20.2627 3.73716 19.0966 3.61997 17.6281L3.47906 15.8627C3.46232 15.6529 3.37983 15.4538 3.24336 15.2936L2.0946 13.9455C1.13905 12.8243 1.13904 11.1752 2.09458 10.0539L3.24334 8.70589C3.37983 8.54573 3.46234 8.34654 3.47907 8.13678L3.61996 6.3713C3.73714 4.90278 4.90327 3.73665 6.3718 3.61946L8.13729 3.47857C8.34705 3.46183 8.54619 3.37935 8.70636 3.24286L10.0544 2.0941ZM12.6488 3.61632C12.2751 3.29782 11.7253 3.29781 11.3516 3.61632L10.0036 4.76509C9.5231 5.17456 8.92568 5.42201 8.29637 5.47223L6.5309 5.61312C6.04139 5.65219 5.65268 6.04089 5.61362 6.53041L5.47272 8.29593C5.4225 8.92521 5.17505 9.52259 4.76559 10.0031L3.61683 11.3511C3.29832 11.7248 3.29831 12.2746 3.61683 12.6483L4.76559 13.9963C5.17506 14.4768 5.4225 15.0743 5.47275 15.7035L5.61363 17.469C5.65268 17.9585 6.04139 18.3473 6.53092 18.3863L8.29636 18.5272C8.92563 18.5774 9.5231 18.8249 10.0036 19.2344L11.3516 20.3831C11.7254 20.7016 12.2751 20.7016 12.6488 20.3831L13.9969 19.2343C14.4773 18.8249 15.0747 18.5774 15.704 18.5272L17.4695 18.3863C17.959 18.3472 18.3478 17.9585 18.3868 17.469L18.5277 15.7035C18.5779 15.0742 18.8253 14.4768 19.2349 13.9964L20.3836 12.6483C20.7022 12.2746 20.7021 11.7249 20.3836 11.3511L19.2348 10.0031C18.8253 9.52259 18.5779 8.92519 18.5277 8.2959L18.3868 6.53041C18.3478 6.0409 17.959 5.65219 17.4695 5.61312L15.704 5.47224C15.0748 5.42203 14.4773 5.17455 13.9968 4.76508L12.6488 3.61632ZM14.8284 7.75718L16.2426 9.1714L9.17154 16.2425L7.75733 14.8282L14.8284 7.75718ZM10.2322 10.232C9.64641 10.8178 8.69667 10.8178 8.11088 10.232C7.52509 9.6463 7.52509 8.69652 8.11088 8.11073C8.69667 7.52494 9.64641 7.52494 10.2322 8.11073C10.818 8.69652 10.818 9.6463 10.2322 10.232ZM13.7677 15.8889C14.3535 16.4747 15.3032 16.4747 15.889 15.8889C16.4748 15.3031 16.4748 14.3534 15.889 13.7676C15.3032 13.1818 14.3535 13.1818 13.7677 13.7676C13.1819 14.3534 13.1819 15.3031 13.7677 15.8889Z"/></symbol>
|
||||||
<symbol id="icon-sort" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
<symbol id="icon-stack" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M20.0833 15.1999L21.2854 15.9212C21.5221 16.0633 21.5989 16.3704 21.4569 16.6072C21.4146 16.6776 21.3557 16.7365 21.2854 16.7787L12.5144 22.0412C12.1977 22.2313 11.8021 22.2313 11.4854 22.0412L2.71451 16.7787C2.47772 16.6366 2.40093 16.3295 2.54301 16.0927C2.58523 16.0223 2.64413 15.9634 2.71451 15.9212L3.9166 15.1999L11.9999 20.0499L20.0833 15.1999ZM20.0833 10.4999L21.2854 11.2212C21.5221 11.3633 21.5989 11.6704 21.4569 11.9072C21.4146 11.9776 21.3557 12.0365 21.2854 12.0787L11.9999 17.6499L2.71451 12.0787C2.47772 11.9366 2.40093 11.6295 2.54301 11.3927C2.58523 11.3223 2.64413 11.2634 2.71451 11.2212L3.9166 10.4999L11.9999 15.3499L20.0833 10.4999ZM12.5144 1.30864L21.2854 6.5712C21.5221 6.71327 21.5989 7.0204 21.4569 7.25719C21.4146 7.32757 21.3557 7.38647 21.2854 7.42869L11.9999 12.9999L2.71451 7.42869C2.47772 7.28662 2.40093 6.97949 2.54301 6.7427C2.58523 6.67232 2.64413 6.61343 2.71451 6.5712L11.4854 1.30864C11.8021 1.11864 12.1977 1.11864 12.5144 1.30864ZM11.9999 3.33233L5.88723 6.99995L11.9999 10.6676L18.1126 6.99995L11.9999 3.33233Z"/></symbol>
|
||||||
<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 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>
|
<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-arrow-left" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
<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>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15m0 0l6.75 6.75M4.5 12l6.75-6.75" />
|
<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>
|
|
||||||
<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-external-link" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<path d="M10 6V8H5V19H16V14H18V20C18 20.5523 17.5523 21 17 21H4C3.44772 21 3 20.5523 3 20V7C3 6.44772 3.44772 6 4 6H10ZM21 3V11H19L18.9999 6.413L11.2071 14.2071L9.79289 12.7929L17.5849 5H13V3H21Z"/>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-cookie" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<path d="M21.1634 10.9711C21.5132 10.8204 21.9579 11.0073 21.9811 11.3875C22.1494 14.1467 21.1794 16.9625 19.071 19.0709C15.1657 22.976 8.83405 22.9761 4.92886 19.0709C1.02367 15.1657 1.02377 8.83405 4.92885 4.9288C7.03704 2.82061 9.8525 1.85015 12.6115 2.01806C12.9916 2.04119 13.1787 2.48598 13.028 2.83576C12.4774 4.11367 12.7239 5.65275 13.7677 6.69657C14.3307 7.25963 15.038 7.58971 15.7706 7.68848C16.0483 7.72594 16.2738 7.95144 16.3113 8.2292C16.4101 8.96178 16.7402 9.66905 17.3032 10.2321C18.347 11.2759 19.8857 11.5218 21.1634 10.9711ZM19.7845 13.8397C19.8558 13.5362 19.6123 13.2603 19.3017 13.2337C18.0569 13.1271 16.8417 12.599 15.889 11.6463C15.3166 11.0739 14.8979 10.4061 14.6309 9.69455C14.5744 9.54422 14.4556 9.42534 14.3052 9.36892C13.5936 9.10186 12.9259 8.68322 12.3535 8.11078C11.4005 7.15778 10.8715 5.94208 10.7648 4.69683C10.7381 4.38618 10.4621 4.14264 10.1586 4.2141C8.76039 4.54327 7.43341 5.25267 6.34306 6.34301C3.21903 9.46721 3.21893 14.5326 6.34307 17.6567C9.46721 20.7809 14.5326 20.7808 17.6568 17.6567C18.7474 16.566 19.4557 15.2384 19.7845 13.8397ZM9.8786 14.1212C10.4644 14.707 10.4644 15.6567 9.8786 16.2425C9.29281 16.8283 8.34307 16.8283 7.75728 16.2425C7.17149 15.6567 7.17149 14.707 7.75728 14.1212C8.34306 13.5354 9.29281 13.5354 9.8786 14.1212ZM10.2321 8.11078C10.8179 8.69657 10.8179 9.64631 10.2321 10.2321C9.64636 10.8179 8.69661 10.8179 8.11083 10.2321C7.52504 9.64631 7.52504 8.69656 8.11083 8.11078C8.69661 7.52499 9.64636 7.52499 10.2321 8.11078ZM15.889 13.7676C16.4748 14.3534 16.4748 15.3032 15.889 15.889C15.3032 16.4747 14.3535 16.4747 13.7677 15.889C13.1819 15.3032 13.1819 14.3534 13.7677 13.7676C14.3535 13.1818 15.3032 13.1818 15.889 13.7676Z"/>
|
|
||||||
</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-chair" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 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="M288 16c26.5 0 48 21.5 48 48V256h16V64c0-35.3-28.7-64-64-64H160C124.7 0 96 28.7 96 64V256h16V64c0-26.5 21.5-48 48-48H288zM48 168c0-4.4-3.6-8-8-8s-8 3.6-8 8v80c0 4.4 3.6 8 8 8s8-3.6 8-8V168zm368 0c0-4.4-3.6-8-8-8s-8 3.6-8 8v80c0 4.4 3.6 8 8 8s8-3.6 8-8V168zM80 304H368c6.1 0 11.6 3.4 14.3 8.8l16 32c2.5 5 2.2 10.9-.7 15.6s-8.1 7.6-13.6 7.6H224 64c-5.5 0-10.7-2.9-13.6-7.6s-3.2-10.6-.7-15.6l16-32c2.7-5.4 8.3-8.8 14.3-8.8zm152 80H384c11.1 0 21.4-5.7 27.2-15.2s6.4-21.2 1.4-31.1l-16-32C391.2 294.8 380.1 288 368 288H80c-12.1 0-23.2 6.8-28.6 17.7l-16 32c-5 9.9-4.4 21.7 1.4 31.1S52.9 384 64 384H216v64H184c-30.9 0-56 25.1-56 56c0 4.4 3.6 8 8 8s8-3.6 8-8c0-22.1 17.9-40 40-40h32v40c0 4.4 3.6 8 8 8s8-3.6 8-8V464h32c22.1 0 40 17.9 40 40c0 4.4 3.6 8 8 8s8-3.6 8-8c0-30.9-25.1-56-56-56H232V384z"/>
|
|
||||||
</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-car" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><circle cx="64" cy="176" r="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"/><line x1="88" y1="176" x2="168" y2="176" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M216,176h24a8,8,0,0,0,8-8V128a8,8,0,0,0-8-8H208L162.34,74.34A8,8,0,0,0,156.69,72H44.28a8,8,0,0,0-6.65,3.56L8,120v48a8,8,0,0,0,8,8H40" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="208" y1="120" x2="8" y2="120" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></symbol>
|
<symbol id="icon-car" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><circle cx="64" cy="176" r="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"/><line x1="88" y1="176" x2="168" y2="176" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M216,176h24a8,8,0,0,0,8-8V128a8,8,0,0,0-8-8H208L162.34,74.34A8,8,0,0,0,156.69,72H44.28a8,8,0,0,0-6.65,3.56L8,120v48a8,8,0,0,0,8,8H40" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="208" y1="120" x2="8" y2="120" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></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-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-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>
|
||||||
@@ -210,41 +71,24 @@
|
|||||||
<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-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>
|
<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>
|
||||||
<symbol id="icon-flag-nl" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480"><g fill-rule="evenodd"><path fill="#21468b" d="M0 320h640v160H0z"/><path fill="#fff" d="M0 160h640v160H0z"/><path fill="#ae1c28" d="M0 0h640v160H0z"/></g></symbol>
|
<symbol id="icon-flag-nl" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480"><g fill-rule="evenodd"><path fill="#21468b" d="M0 320h640v160H0z"/><path fill="#fff" d="M0 160h640v160H0z"/><path fill="#ae1c28" d="M0 0h640v160H0z"/></g></symbol>
|
||||||
<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
|
<symbol id="icon-backpack" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M96,48h64a48,48,0,0,1,48,48V216a8,8,0,0,1-8,8H56a8,8,0,0,1-8-8V96A48,48,0,0,1,96,48Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M80,224V152a16,16,0,0,1,16-16h64a16,16,0,0,1,16,16v72" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M96,48V32a16,16,0,0,1,16-16h32a16,16,0,0,1,16,16V48" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="112" y1="88" x2="144" y2="88" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="80" y1="168" x2="176" y2="168" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="144" y1="168" x2="144" y2="184" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></symbol>
|
||||||
<symbol id="icon-spinner" viewBox="0 0 44 44" xmlns="http://www.w3.org/2000/svg" stroke="currentColor">
|
<symbol id="icon-barricade" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><rect x="24" y="72" width="208" height="88" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="84" y1="72" x2="172" y2="160" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="156" y1="72" x2="232" y2="148" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="24" y1="84" x2="100" y2="160" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="192" y1="160" x2="192" y2="200" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="64" y1="160" x2="64" y2="200" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></symbol>
|
||||||
<g fill="none" fill-rule="evenodd" stroke-width="2">
|
<symbol id="icon-bed" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M112,168V80H216a32,32,0,0,1,32,32v56" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="24" y1="208" x2="24" y2="48" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><polyline points="24 168 248 168 248 208" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="112" y1="80" x2="24" y2="80" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></symbol>
|
||||||
<circle cx="22" cy="22" r="1">
|
<symbol id="icon-boot" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="152" y1="120" x2="112" y2="120" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="152" y1="88" x2="112" y2="88" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M240,168v32a8,8,0,0,1-8,8H205.89a8.08,8.08,0,0,1-3.58-.84l-12.62-6.32a8.08,8.08,0,0,0-3.58-.84H169.89a8.08,8.08,0,0,0-3.58.84l-12.62,6.32a8.08,8.08,0,0,1-3.58.84H121.89a8.08,8.08,0,0,1-3.58-.84l-12.62-6.32a8.08,8.08,0,0,0-3.58-.84H85.89a8.08,8.08,0,0,0-3.58.84l-12.62,6.32a8.08,8.08,0,0,1-3.58.84H40a8,8,0,0,1-8-8V168c9.22-32.06,12-88.65,0-120H144a8,8,0,0,1,8,8v64h40A48,48,0,0,1,240,168Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="32" y1="168" x2="240" y2="168" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></symbol>
|
||||||
<animate attributeName="r"
|
<symbol id="icon-lift" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="16" y1="72" x2="240" y2="32" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><rect x="40" y="104" width="176" height="112" rx="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="40" y1="168" x2="216" y2="168" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="128" y1="52" x2="128" y2="104" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="160" y1="104" x2="160" y2="168" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="96" y1="104" x2="96" y2="168" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></symbol>
|
||||||
begin="0s" dur="1.8s"
|
<symbol id="icon-chat" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M71.58,144,32,176V48a8,8,0,0,1,8-8H168a8,8,0,0,1,8,8v88a8,8,0,0,1-8,8Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M80,144v40a8,8,0,0,0,8,8h96.42L224,224V96a8,8,0,0,0-8-8H176" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></symbol>
|
||||||
values="1; 20"
|
<symbol id="icon-weather" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="87.66" y1="56.73" x2="83.5" y2="33.09" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="56.69" y1="76.46" x2="37.03" y2="62.69" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="48.73" y1="112.31" x2="25.09" y2="116.48" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="123.52" y1="64.69" x2="137.28" y2="45.03" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M96,144a68.06,68.06,0,1,1,68,72H84a44,44,0,1,1,14.2-85.66" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M59.65,135.35a48,48,0,1,1,80.19-50.94" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></symbol>
|
||||||
calcMode="spline"
|
<symbol id="icon-party" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M40.49,205.52,93,61.14a7.79,7.79,0,0,1,12.84-2.85l91.88,91.88A7.79,7.79,0,0,1,194.86,163L50.48,215.51A7.79,7.79,0,0,1,40.49,205.52Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M168,72s0-24,24-24,24-24,24-24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="144" y1="16" x2="144" y2="40" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="216" y1="112" x2="232" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="216" y1="80" x2="240" y2="72" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="78.09" y1="102.09" x2="153.91" y2="177.91" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="101.11" y1="197.11" x2="58.89" y2="154.89" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></symbol>
|
||||||
keyTimes="0; 1"
|
<symbol id="icon-dice" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><rect x="40" y="40" width="176" height="176" rx="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><circle cx="92" cy="84" r="12"/><circle cx="164" cy="84" r="12"/><circle cx="92" cy="128" r="12"/><circle cx="164" cy="128" r="12"/><circle cx="92" cy="172" r="12"/><circle cx="164" cy="172" r="12"/></symbol>
|
||||||
keySplines="0.165, 0.84, 0.44, 1"
|
<symbol id="icon-flag" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="48" y1="224" x2="48" y2="56" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M48,176c64-55.43,112,55.43,176,0V56C160,111.43,112,.57,48,56" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></symbol>
|
||||||
repeatCount="indefinite" />
|
<symbol id="icon-burger" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M48.07,96a8,8,0,0,1-7.89-9.72C46.68,55.55,83.52,32,128,32s81.32,23.55,87.82,54.28A8,8,0,0,1,207.93,96Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><polyline points="24 176 68 160 108 176 148 160 188 176 232 160" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="24" y1="128" x2="232" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M208,168.73V184a32,32,0,0,1-32,32H80a32,32,0,0,1-32-32V167.27" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></symbol>
|
||||||
<animate attributeName="stroke-opacity"
|
<symbol id="icon-handshake" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><polyline points="200 152 160 192 96 176 40 136" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><polyline points="72.68 70.63 128 56 183.32 70.63" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M34.37,60.42,8.85,111.48a8,8,0,0,0,3.57,10.73L40,136,72.68,70.63,45.11,56.85A8,8,0,0,0,34.37,60.42Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M216,136l27.58-13.79a8,8,0,0,0,3.57-10.73L221.63,60.42a8,8,0,0,0-10.74-3.57L183.32,70.63Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M184,72H144L98.34,116.29a8,8,0,0,0,1.38,12.42C117.23,139.9,141,139.13,160,120l40,32,16-16" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><polyline points="124.06 216 82.34 205.57 56 186.75" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></symbol>
|
||||||
begin="0s" dur="1.8s"
|
<symbol id="icon-badge" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="152" y1="112" x2="192" y2="112" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="152" y1="144" x2="192" y2="144" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><rect x="32" y="48" width="192" height="160" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><circle cx="96" cy="120" r="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M64,168c3.55-13.8,17.09-24,32-24s28.46,10.19,32,24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></symbol>
|
||||||
values="1; 0"
|
<symbol id="icon-parking" 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="M104,144h32a28,28,0,0,0,0-56H104v80" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></symbol>
|
||||||
calcMode="spline"
|
<symbol id="icon-drinks" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M56,32H200L176.85,225A8,8,0,0,1,168.9,232H87.1A8,8,0,0,1,79.15,225Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="60.8" y1="72" x2="195.2" y2="72" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></symbol>
|
||||||
keyTimes="0; 1"
|
<symbol id="icon-sauna" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor"><path d="M153.599 76.8c-35.285 0-64 28.706-64 64s28.715 64 64 64 64-28.715 64-64-28.706-64-64-64Zm0 102.4c-21.205 0-38.4-17.195-38.4-38.4s17.195-38.4 38.4-38.4 38.4 17.195 38.4 38.4-17.194 38.4-38.4 38.4Zm152.824 315.623-51.2-140.8c-1.835-5.06-6.639-8.422-12.023-8.422h-89.6V230.4c0-7.074-5.726-12.8-12.8-12.8S128 223.326 128 230.4v128c0 7.074 5.726 12.8 12.8 12.8h93.44l48.137 132.378C284.263 508.775 289.178 512 294.4 512c1.451 0 2.927-.247 4.378-.776 6.638-2.415 10.077-9.754 7.645-16.401ZM209.919 396.8h-107.52c-7.074 0-12.8 5.726-12.8 12.8s5.726 12.8 12.8 12.8h107.52c7.074 0 12.8-5.726 12.8-12.8s-5.726-12.8-12.8-12.8ZM340.36 7.074c-3.157-6.323-10.863-8.875-17.169-5.726-6.323 3.166-8.892 10.846-5.726 17.178 22.886 45.79 12.228 85.726-.111 131.959-7.603 28.424-15.454 57.839-15.991 90.914-.111 7.074 5.521 12.902 12.587 13.013h.213c6.972 0 12.689-5.598 12.8-12.587.486-29.935 7.927-57.796 15.113-84.736 12.552-46.975 25.514-95.547-1.716-150.015Zm65.98 0c-3.174-6.323-10.837-8.875-17.178-5.726-6.323 3.166-8.892 10.846-5.726 17.178 22.903 45.79 12.237 85.726-.111 131.977-13.141 49.203-28.041 104.977-.324 174.251 1.997 5.009 6.81 8.047 11.887 8.047 1.587 0 3.2-.29 4.753-.913 6.562-2.628 9.762-10.078 7.134-16.64-24.559-61.389-11.426-110.575 1.271-158.148C420.599 110.123 433.578 61.551 406.34 7.074Z"/></symbol>
|
||||||
keySplines="0.3, 0.61, 0.355, 1"
|
<symbol id="icon-shower" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><circle cx="52" cy="236" r="12"/><circle cx="84" cy="204" r="12"/><circle cx="20" cy="204" r="12"/><circle cx="52" cy="172" r="12"/><path d="M248,40H216L184,72,161.91,201.31a8,8,0,0,1-13.54,4.34l-98-98a8,8,0,0,1,4.34-13.54L184,72" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></symbol>
|
||||||
repeatCount="indefinite" />
|
<symbol id="icon-wifi" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><circle cx="128" cy="204" r="12"/><path d="M232,93.19a164,164,0,0,0-208,0" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M200,129a116,116,0,0,0-144,0" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M168,165a68,68,0,0,0-80,0" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></symbol>
|
||||||
</circle>
|
<symbol id="icon-mountain" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><circle cx="164" cy="52" r="20" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M8,200,81.1,75.94a8,8,0,0,1,13.8,0L168,200Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="50.35" y1="128" x2="125.65" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M146.61,163.71l33.06-55.79a8,8,0,0,1,13.76,0L248,200H168" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></symbol>
|
||||||
<circle cx="22" cy="22" r="1">
|
<symbol id="icon-spinner" viewBox="0 0 44 44" xmlns="http://www.w3.org/2000/svg" stroke="currentColor"><!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL --><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>
|
||||||
<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>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 55 KiB |
@@ -0,0 +1,29 @@
|
|||||||
|
export const useFetch = controller => {
|
||||||
|
Object.assign(controller, {
|
||||||
|
checkStatus(response) {
|
||||||
|
if (response.status >= 200 && response.status < 300) {
|
||||||
|
return response
|
||||||
|
} else {
|
||||||
|
const error = new Error(response.statusText)
|
||||||
|
error.response = response
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parseJSON(response) {
|
||||||
|
return response.json()
|
||||||
|
},
|
||||||
|
parseHTML(response) {
|
||||||
|
return response.text()
|
||||||
|
},
|
||||||
|
getInitObject(method = 'get', body = null) {
|
||||||
|
return {
|
||||||
|
method,
|
||||||
|
body,
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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%);;
|
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 {
|
.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%);;
|
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,7 +1,9 @@
|
|||||||
@import "components/button.css";
|
@import "components/button.css";
|
||||||
@import "components/datatable.css";
|
|
||||||
@import "components/forms.css";
|
@import "components/forms.css";
|
||||||
|
@import "components/gallery.css";
|
||||||
@import "components/pagination.css";
|
@import "components/pagination.css";
|
||||||
|
@import "components/price_calendar.css";
|
||||||
@import "components/toast.css";
|
@import "components/toast.css";
|
||||||
@import "components/tooltip.css";
|
@import "components/tooltip.css";
|
||||||
@import "components/typography.css";
|
@import "components/typography.css";
|
||||||
|
@import "components/rte-content.css";
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
@import "components/autocomplete.css";
|
||||||
|
@import "components/badge.css";
|
||||||
|
@import "components/button.css";
|
||||||
|
@import "components/datatable.css";
|
||||||
|
@import "components/datepicker.css";
|
||||||
|
@import "components/forms.css";
|
||||||
|
@import "components/pagination.css";
|
||||||
|
@import "components/toast.css";
|
||||||
|
@import "components/tooltip.css";
|
||||||
|
@import "components/typography.css";
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
@import "tailwindcss/base";
|
||||||
|
@import "_base.css";
|
||||||
|
|
||||||
|
@import "tailwindcss/components";
|
||||||
|
@import "_components_admin.css";
|
||||||
|
|
||||||
|
@import "tailwindcss/utilities";
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
.autocomplete {
|
||||||
|
@apply relative w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete[data-loading="true"]:after {
|
||||||
|
@apply block absolute top-0 right-0;
|
||||||
|
@apply absolute w-4 h-4 rounded-full right-0 mr-4;
|
||||||
|
content: "";
|
||||||
|
border: 3px solid rgba(0, 0, 0, 0.12);
|
||||||
|
border-right: 3px solid rgba(0, 0, 0, 0.48);
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
animation: rotate 1s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-icon {
|
||||||
|
@apply absolute top-1/2 right-0 transform -translate-y-1/2 mr-4;
|
||||||
|
}
|
||||||
|
.autocomplete[data-loading="true"] .autocomplete-icon {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-result-list {
|
||||||
|
@apply bg-white shadow-md max-h-64 overflow-y-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-result {
|
||||||
|
@apply p-2;
|
||||||
|
}
|
||||||
|
.autocomplete-result:hover,
|
||||||
|
.autocomplete-result[aria-selected="true"] {
|
||||||
|
@apply bg-gray-200;
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.badge {
|
||||||
|
@apply inline-flex items-center rounded-md px-1.5 py-0.5 text-xs font-medium whitespace-nowrap;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
@import "vanilla-calendar-pro/styles/index.css";
|
||||||
|
@import "vanilla-calendar-pro/styles/themes/light.css";
|
||||||
|
|
||||||
|
[data-vc-theme=light] .vc-date[data-vc-date-selected] .vc-date__btn,
|
||||||
|
[data-vc-theme=light] .vc-date[data-vc-date-selected] .vc-date__btn:hover {
|
||||||
|
background-color: theme('colors.secondary');
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@import "glightbox/dist/css/glightbox.min.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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
.rte-content {
|
||||||
|
p {
|
||||||
|
@apply pb-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
@apply list-disc p-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol {
|
||||||
|
@apply list-decimal p-4;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,6 +56,7 @@
|
|||||||
"symfony/translation": "6.4.*",
|
"symfony/translation": "6.4.*",
|
||||||
"symfony/twig-bundle": "6.4.*",
|
"symfony/twig-bundle": "6.4.*",
|
||||||
"symfony/uid": "6.4.*",
|
"symfony/uid": "6.4.*",
|
||||||
|
"symfony/ux-twig-component": "^2.36",
|
||||||
"symfony/validator": "6.4.*",
|
"symfony/validator": "6.4.*",
|
||||||
"symfony/web-link": "6.4.*",
|
"symfony/web-link": "6.4.*",
|
||||||
"symfony/webpack-encore-bundle": "^2.2",
|
"symfony/webpack-encore-bundle": "^2.2",
|
||||||
|
|||||||
Generated
+102
-13
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "aa69dee5968ec6f7c3f152b8a8a6100d",
|
"content-hash": "f04202a445d42ebff0dd0e8dce335163",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "carbonphp/carbon-doctrine-types",
|
"name": "carbonphp/carbon-doctrine-types",
|
||||||
@@ -3793,16 +3793,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpseclib/phpseclib",
|
"name": "phpseclib/phpseclib",
|
||||||
"version": "3.0.55",
|
"version": "3.0.56",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/phpseclib/phpseclib.git",
|
"url": "https://github.com/phpseclib/phpseclib.git",
|
||||||
"reference": "db9744e6d47e742b1f974e965ad49bdd041105af"
|
"reference": "7adbbe38cde25e2df2116dbf2673c407e24fa305"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/db9744e6d47e742b1f974e965ad49bdd041105af",
|
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/7adbbe38cde25e2df2116dbf2673c407e24fa305",
|
||||||
"reference": "db9744e6d47e742b1f974e965ad49bdd041105af",
|
"reference": "7adbbe38cde25e2df2116dbf2673c407e24fa305",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -3883,7 +3883,7 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/phpseclib/phpseclib/issues",
|
"issues": "https://github.com/phpseclib/phpseclib/issues",
|
||||||
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.55"
|
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.56"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -3899,7 +3899,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2026-06-14T23:24:10+00:00"
|
"time": "2026-08-03T04:36:50+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpstan/phpdoc-parser",
|
"name": "phpstan/phpdoc-parser",
|
||||||
@@ -10080,6 +10080,95 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-12-23T15:07:59+00:00"
|
"time": "2025-12-23T15:07:59+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/ux-twig-component",
|
||||||
|
"version": "v2.36.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/ux-twig-component.git",
|
||||||
|
"reference": "d64b068d8339e905cd48974bdd6e9ba54dc8f247"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/ux-twig-component/zipball/d64b068d8339e905cd48974bdd6e9ba54dc8f247",
|
||||||
|
"reference": "d64b068d8339e905cd48974bdd6e9ba54dc8f247",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.1",
|
||||||
|
"symfony/dependency-injection": "^5.4|^6.0|^7.0|^8.0",
|
||||||
|
"symfony/deprecation-contracts": "^2.2|^3.0",
|
||||||
|
"symfony/event-dispatcher": "^5.4|^6.0|^7.0|^8.0",
|
||||||
|
"symfony/property-access": "^5.4|^6.0|^7.0|^8.0",
|
||||||
|
"twig/twig": "^3.10.3"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"symfony/config": "<5.4.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"symfony/console": "^5.4|^6.0|^7.0|^8.0",
|
||||||
|
"symfony/css-selector": "^5.4|^6.0|^7.0|^8.0",
|
||||||
|
"symfony/dom-crawler": "^5.4|^6.0|^7.0|^8.0",
|
||||||
|
"symfony/framework-bundle": "^5.4|^6.0|^7.0|^8.0",
|
||||||
|
"symfony/phpunit-bridge": "^6.0|^7.0|^8.0",
|
||||||
|
"symfony/stimulus-bundle": "^2.9.1|^3.0",
|
||||||
|
"symfony/twig-bundle": "^5.4|^6.0|^7.0|^8.0",
|
||||||
|
"symfony/webpack-encore-bundle": "^1.15|^2.3.0",
|
||||||
|
"twig/extra-bundle": "^3.10.3",
|
||||||
|
"twig/html-extra": "^3.10.3"
|
||||||
|
},
|
||||||
|
"type": "symfony-bundle",
|
||||||
|
"extra": {
|
||||||
|
"thanks": {
|
||||||
|
"url": "https://github.com/symfony/ux",
|
||||||
|
"name": "symfony/ux"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\UX\\TwigComponent\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Twig components for Symfony",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"components",
|
||||||
|
"symfony-ux",
|
||||||
|
"twig"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/ux-twig-component/tree/v2.36.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nicolas-grekas",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-04-03T05:13:59+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/validator",
|
"name": "symfony/validator",
|
||||||
"version": "v6.4.43",
|
"version": "v6.4.43",
|
||||||
@@ -13028,16 +13117,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "rector/rector",
|
"name": "rector/rector",
|
||||||
"version": "2.5.9",
|
"version": "2.6.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/rectorphp/rector.git",
|
"url": "https://github.com/rectorphp/rector.git",
|
||||||
"reference": "858b1fb95d94f658b54c01080bf7b6ae97274536"
|
"reference": "24ef17c06737a6272806c5cca32b860449efe1ba"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/rectorphp/rector/zipball/858b1fb95d94f658b54c01080bf7b6ae97274536",
|
"url": "https://api.github.com/repos/rectorphp/rector/zipball/24ef17c06737a6272806c5cca32b860449efe1ba",
|
||||||
"reference": "858b1fb95d94f658b54c01080bf7b6ae97274536",
|
"reference": "24ef17c06737a6272806c5cca32b860449efe1ba",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -13076,7 +13165,7 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/rectorphp/rector/issues",
|
"issues": "https://github.com/rectorphp/rector/issues",
|
||||||
"source": "https://github.com/rectorphp/rector/tree/2.5.9"
|
"source": "https://github.com/rectorphp/rector/tree/2.6.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -13084,7 +13173,7 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2026-07-30T22:10:00+00:00"
|
"time": "2026-08-03T09:01:59+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "sebastian/cli-parser",
|
"name": "sebastian/cli-parser",
|
||||||
|
|||||||
@@ -19,4 +19,5 @@ return [
|
|||||||
Flagception\Bundle\FlagceptionBundle\FlagceptionBundle::class => ['all' => true],
|
Flagception\Bundle\FlagceptionBundle\FlagceptionBundle::class => ['all' => true],
|
||||||
Knp\Bundle\MenuBundle\KnpMenuBundle::class => ['all' => true],
|
Knp\Bundle\MenuBundle\KnpMenuBundle::class => ['all' => true],
|
||||||
Knp\Bundle\PaginatorBundle\KnpPaginatorBundle::class => ['all' => true],
|
Knp\Bundle\PaginatorBundle\KnpPaginatorBundle::class => ['all' => true],
|
||||||
|
Symfony\UX\TwigComponent\TwigComponentBundle::class => ['all' => true],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ security:
|
|||||||
users:
|
users:
|
||||||
mailjet:
|
mailjet:
|
||||||
password: '%env(MAILJET_WEBHOOK_BASIC_PASSWORD)%'
|
password: '%env(MAILJET_WEBHOOK_BASIC_PASSWORD)%'
|
||||||
roles: ['ROLE_MAILJET_WEBHOOK']
|
roles: [ ROLE_MAILJET_WEBHOOK ]
|
||||||
|
role_hierarchy:
|
||||||
|
ROLE_ADMIN: [ ROLE_GROUPS_ADMIN ]
|
||||||
|
ROLE_GROUPS_ADMIN: [ ROLE_GROUPS_MANAGER ]
|
||||||
firewalls:
|
firewalls:
|
||||||
dev:
|
dev:
|
||||||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||||
@@ -49,7 +52,6 @@ security:
|
|||||||
access_control:
|
access_control:
|
||||||
- { path: ^/webhooks/mailjet/newsletter$, roles: ROLE_MAILJET_WEBHOOK, requires_channel: https }
|
- { path: ^/webhooks/mailjet/newsletter$, roles: ROLE_MAILJET_WEBHOOK, requires_channel: https }
|
||||||
- { path: ^/authorize, roles: IS_AUTHENTICATED_REMEMBERED, requires_channel: https }
|
- { path: ^/authorize, roles: IS_AUTHENTICATED_REMEMBERED, requires_channel: https }
|
||||||
- { path: ^/admin, roles: ROLE_ADMIN, requires_channel: https }
|
|
||||||
- { path: ^/, roles: PUBLIC_ACCESS, requires_channel: https }
|
- { path: ^/, roles: PUBLIC_ACCESS, requires_channel: https }
|
||||||
|
|
||||||
when@test:
|
when@test:
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
twig_component:
|
||||||
|
anonymous_template_directory: 'components/'
|
||||||
|
defaults:
|
||||||
|
# Namespace & directory for components
|
||||||
|
App\Twig\Components\: 'components/'
|
||||||
@@ -13,6 +13,7 @@ parameters:
|
|||||||
travel_snapshot_retention_buffer_days: '%env(int:APP_TRAVEL_SNAPSHOT_RETENTION_BUFFER_DAYS)%'
|
travel_snapshot_retention_buffer_days: '%env(int:APP_TRAVEL_SNAPSHOT_RETENTION_BUFFER_DAYS)%'
|
||||||
default_email_from: '%env(APP_DEFAULT_EMAIL_FROM)%'
|
default_email_from: '%env(APP_DEFAULT_EMAIL_FROM)%'
|
||||||
default_email_to: '%env(APP_DEFAULT_EMAIL_TO)%'
|
default_email_to: '%env(APP_DEFAULT_EMAIL_TO)%'
|
||||||
|
accommodation_inquiry_email: '%env(ACCOMMODATION_INQUIRY_EMAIL)%'
|
||||||
|
|
||||||
# MailJet list ids and their labels
|
# MailJet list ids and their labels
|
||||||
mailjet_lists:
|
mailjet_lists:
|
||||||
@@ -43,6 +44,10 @@ parameters:
|
|||||||
shoe_size_min: 35
|
shoe_size_min: 35
|
||||||
shoe_size_max: 50
|
shoe_size_max: 50
|
||||||
|
|
||||||
|
# Season data
|
||||||
|
season_winter_from: '%env(APP_SEASON_WINTER_FROM)%'
|
||||||
|
season_summer_from: '%env(APP_SEASON_SUMMER_FROM)%'
|
||||||
|
|
||||||
# domain mapping for theme, gtm id and cmp url
|
# domain mapping for theme, gtm id and cmp url
|
||||||
domain_config:
|
domain_config:
|
||||||
ep-reisen.de:
|
ep-reisen.de:
|
||||||
@@ -116,6 +121,14 @@ services:
|
|||||||
method: createMainMenu
|
method: createMainMenu
|
||||||
alias: admin_main
|
alias: admin_main
|
||||||
|
|
||||||
|
App\Menu\GroupsMenuBuilder:
|
||||||
|
arguments:
|
||||||
|
$factory: '@knp_menu.factory'
|
||||||
|
tags:
|
||||||
|
- name: knp_menu.menu_builder
|
||||||
|
method: createMainMenu
|
||||||
|
alias: groups_main
|
||||||
|
|
||||||
App\Service\BpnXmlSyncManager:
|
App\Service\BpnXmlSyncManager:
|
||||||
arguments:
|
arguments:
|
||||||
$xmlSource: '@xml_source.storage'
|
$xmlSource: '@xml_source.storage'
|
||||||
@@ -250,10 +263,33 @@ services:
|
|||||||
from: '%default_email_from%'
|
from: '%default_email_from%'
|
||||||
to: '%default_email_to%'
|
to: '%default_email_to%'
|
||||||
|
|
||||||
|
App\Service\AccommodationBookingService:
|
||||||
|
arguments:
|
||||||
|
$officeEmail: '%accommodation_inquiry_email%'
|
||||||
|
|
||||||
|
App\Service\GroupsPriceCalculator:
|
||||||
|
arguments:
|
||||||
|
$config:
|
||||||
|
runningCostsEur: '%env(float:APP_RUNNING_COSTS_FACTOR_EUR)%'
|
||||||
|
runningCostsChf: '%env(float:APP_RUNNING_COSTS_FACTOR_CHF)%'
|
||||||
|
undersubscription30Eur: '%env(float:APP_UNDERSUBSCRIPTION_SURCHARGE_30_EUR)%'
|
||||||
|
undersubscription30Chf: '%env(float:APP_UNDERSUBSCRIPTION_SURCHARGE_30_CHF)%'
|
||||||
|
undersubscription40Eur: '%env(float:APP_UNDERSUBSCRIPTION_SURCHARGE_40_EUR)%'
|
||||||
|
undersubscription40Chf: '%env(float:APP_UNDERSUBSCRIPTION_SURCHARGE_40_CHF)%'
|
||||||
|
|
||||||
App\Validator\Constraints\ParticipantValidator:
|
App\Validator\Constraints\ParticipantValidator:
|
||||||
arguments:
|
arguments:
|
||||||
$bodyDimensionRanges: '%body_dimension_ranges%'
|
$bodyDimensionRanges: '%body_dimension_ranges%'
|
||||||
|
|
||||||
|
App\BpnConnect\AbstractApiClient:
|
||||||
|
abstract: true
|
||||||
|
arguments:
|
||||||
|
$baseUrl: '%env(default::BPN_CONNECT_BASE_URL)%'
|
||||||
|
$apiKey: '%env(default::BPN_CONNECT_API_KEY)%'
|
||||||
|
|
||||||
|
App\BpnConnect\ContingentsClient:
|
||||||
|
parent: App\BpnConnect\AbstractApiClient
|
||||||
|
|
||||||
App\Service\MailjetApiClient:
|
App\Service\MailjetApiClient:
|
||||||
arguments:
|
arguments:
|
||||||
$apiKey: '%env(default::MAILJET_API_KEY)%'
|
$apiKey: '%env(default::MAILJET_API_KEY)%'
|
||||||
|
|||||||
+20
@@ -102,6 +102,26 @@ host('staging')
|
|||||||
->set('rsync_src', __DIR__)
|
->set('rsync_src', __DIR__)
|
||||||
->set('rsync', $rsyncOptions)
|
->set('rsync', $rsyncOptions)
|
||||||
->set('cachetool_args', '--web=SymfonyHttpClient --web-path={{current_path}}/public/ --web-url=https://my.ep-reisen.net --web-basic-auth=myep:staging')
|
->set('cachetool_args', '--web=SymfonyHttpClient --web-path={{current_path}}/public/ --web-url=https://my.ep-reisen.net --web-basic-auth=myep:staging')
|
||||||
|
->add('shared_files', [
|
||||||
|
'public/.htpasswd',
|
||||||
|
])
|
||||||
|
;
|
||||||
|
|
||||||
|
host('develop')
|
||||||
|
->setHostname('185.243.135.29')
|
||||||
|
->setRemoteUser('p704161')
|
||||||
|
->setForwardAgent(true)
|
||||||
|
->setSshMultiplexing(true)
|
||||||
|
->setDeployPath('/home/www/p704161/html/myep-develop')
|
||||||
|
->set('writable_mode', 'chmod')
|
||||||
|
->set('http_user', 'p704161')
|
||||||
|
->set('bin/php', '/usr/local/bin/php')
|
||||||
|
->set('rsync_src', __DIR__)
|
||||||
|
->set('rsync', $rsyncOptions)
|
||||||
|
->set('cachetool_args', '--web=SymfonyHttpClient --web-path={{current_path}}/public/ --web-url=https://mydev.ep-reisen.net --web-basic-auth=myep:develop')
|
||||||
|
->add('shared_files', [
|
||||||
|
'public/.htpasswd',
|
||||||
|
])
|
||||||
;
|
;
|
||||||
|
|
||||||
task('deploy', [
|
task('deploy', [
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260625092525 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE TABLE accommodation (id INT AUTO_INCREMENT NOT NULL, created_by_id INT DEFAULT NULL, updated_by_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, code VARCHAR(16) NOT NULL, min_pax_summer INT NOT NULL, max_adolescent_age INT NOT NULL, currency VARCHAR(3) NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', updated_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\', UNIQUE INDEX UNIQ_2D38541277153098 (code), INDEX IDX_2D385412B03A8386 (created_by_id), INDEX IDX_2D385412896DBBDE (updated_by_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||||
|
$this->addSql('CREATE TABLE accommodation_price (id INT AUTO_INCREMENT NOT NULL, accommodation_id INT NOT NULL, created_by_id INT DEFAULT NULL, updated_by_id INT DEFAULT NULL, date_from DATE NOT NULL COMMENT \'(DC2Type:date_immutable)\', date_to DATE NOT NULL COMMENT \'(DC2Type:date_immutable)\', included_pax INT NOT NULL, price_per_night INT NOT NULL, price_additional_person INT NOT NULL, min_nights INT NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', updated_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\', INDEX IDX_C67701238F3692CD (accommodation_id), INDEX IDX_C6770123B03A8386 (created_by_id), INDEX IDX_C6770123896DBBDE (updated_by_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||||
|
$this->addSql('CREATE TABLE additional_service (id INT AUTO_INCREMENT NOT NULL, accommodation_id INT NOT NULL, created_by_id INT DEFAULT NULL, updated_by_id INT DEFAULT NULL, label VARCHAR(255) NOT NULL, price INT NOT NULL, type VARCHAR(20) NOT NULL, date_from DATE NOT NULL COMMENT \'(DC2Type:date_immutable)\', date_to DATE NOT NULL COMMENT \'(DC2Type:date_immutable)\', `grouping` VARCHAR(128) NOT NULL, sorting INT NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', updated_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\', INDEX IDX_E01749268F3692CD (accommodation_id), INDEX IDX_E0174926B03A8386 (created_by_id), INDEX IDX_E0174926896DBBDE (updated_by_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||||
|
$this->addSql('CREATE TABLE board_service (id INT AUTO_INCREMENT NOT NULL, accommodation_id INT NOT NULL, created_by_id INT DEFAULT NULL, updated_by_id INT DEFAULT NULL, label VARCHAR(255) NOT NULL, price INT NOT NULL, date_from DATE NOT NULL COMMENT \'(DC2Type:date_immutable)\', date_to DATE NOT NULL COMMENT \'(DC2Type:date_immutable)\', sorting INT NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', updated_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\', INDEX IDX_A983B2DD8F3692CD (accommodation_id), INDEX IDX_A983B2DDB03A8386 (created_by_id), INDEX IDX_A983B2DD896DBBDE (updated_by_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||||
|
$this->addSql('ALTER TABLE accommodation ADD CONSTRAINT FK_2D385412B03A8386 FOREIGN KEY (created_by_id) REFERENCES user (id) ON DELETE SET NULL');
|
||||||
|
$this->addSql('ALTER TABLE accommodation ADD CONSTRAINT FK_2D385412896DBBDE FOREIGN KEY (updated_by_id) REFERENCES user (id) ON DELETE SET NULL');
|
||||||
|
$this->addSql('ALTER TABLE accommodation_price ADD CONSTRAINT FK_C67701238F3692CD FOREIGN KEY (accommodation_id) REFERENCES accommodation (id)');
|
||||||
|
$this->addSql('ALTER TABLE accommodation_price ADD CONSTRAINT FK_C6770123B03A8386 FOREIGN KEY (created_by_id) REFERENCES user (id) ON DELETE SET NULL');
|
||||||
|
$this->addSql('ALTER TABLE accommodation_price ADD CONSTRAINT FK_C6770123896DBBDE FOREIGN KEY (updated_by_id) REFERENCES user (id) ON DELETE SET NULL');
|
||||||
|
$this->addSql('ALTER TABLE additional_service ADD CONSTRAINT FK_E01749268F3692CD FOREIGN KEY (accommodation_id) REFERENCES accommodation (id)');
|
||||||
|
$this->addSql('ALTER TABLE additional_service ADD CONSTRAINT FK_E0174926B03A8386 FOREIGN KEY (created_by_id) REFERENCES user (id) ON DELETE SET NULL');
|
||||||
|
$this->addSql('ALTER TABLE additional_service ADD CONSTRAINT FK_E0174926896DBBDE FOREIGN KEY (updated_by_id) REFERENCES user (id) ON DELETE SET NULL');
|
||||||
|
$this->addSql('ALTER TABLE board_service ADD CONSTRAINT FK_A983B2DD8F3692CD FOREIGN KEY (accommodation_id) REFERENCES accommodation (id)');
|
||||||
|
$this->addSql('ALTER TABLE board_service ADD CONSTRAINT FK_A983B2DDB03A8386 FOREIGN KEY (created_by_id) REFERENCES user (id) ON DELETE SET NULL');
|
||||||
|
$this->addSql('ALTER TABLE board_service ADD CONSTRAINT FK_A983B2DD896DBBDE FOREIGN KEY (updated_by_id) REFERENCES user (id) ON DELETE SET NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE accommodation DROP FOREIGN KEY FK_2D385412B03A8386');
|
||||||
|
$this->addSql('ALTER TABLE accommodation DROP FOREIGN KEY FK_2D385412896DBBDE');
|
||||||
|
$this->addSql('ALTER TABLE accommodation_price DROP FOREIGN KEY FK_C67701238F3692CD');
|
||||||
|
$this->addSql('ALTER TABLE accommodation_price DROP FOREIGN KEY FK_C6770123B03A8386');
|
||||||
|
$this->addSql('ALTER TABLE accommodation_price DROP FOREIGN KEY FK_C6770123896DBBDE');
|
||||||
|
$this->addSql('ALTER TABLE additional_service DROP FOREIGN KEY FK_E01749268F3692CD');
|
||||||
|
$this->addSql('ALTER TABLE additional_service DROP FOREIGN KEY FK_E0174926B03A8386');
|
||||||
|
$this->addSql('ALTER TABLE additional_service DROP FOREIGN KEY FK_E0174926896DBBDE');
|
||||||
|
$this->addSql('ALTER TABLE board_service DROP FOREIGN KEY FK_A983B2DD8F3692CD');
|
||||||
|
$this->addSql('ALTER TABLE board_service DROP FOREIGN KEY FK_A983B2DDB03A8386');
|
||||||
|
$this->addSql('ALTER TABLE board_service DROP FOREIGN KEY FK_A983B2DD896DBBDE');
|
||||||
|
$this->addSql('DROP TABLE accommodation');
|
||||||
|
$this->addSql('DROP TABLE accommodation_price');
|
||||||
|
$this->addSql('DROP TABLE additional_service');
|
||||||
|
$this->addSql('DROP TABLE board_service');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260625154838 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE accommodation_price ADD price_per_night_override INT DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE accommodation_price DROP price_per_night_override');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260625173941 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE additional_service ADD selection_group VARCHAR(128) DEFAULT NULL, DROP `grouping`');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE additional_service ADD `grouping` VARCHAR(128) NOT NULL, DROP selection_group');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260627121823 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE accommodation_price ADD accept_undersubscription TINYINT(1) NOT NULL');
|
||||||
|
$this->addSql('DROP INDEX IDX_NEWSLETTER_CONSENT_EMAIL ON newsletter_consent');
|
||||||
|
$this->addSql('ALTER TABLE newsletter_opt_in_request RENAME INDEX uniq_70505214b3bc57da TO UNIQ_EC0365DB3BC57DA');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE accommodation_price DROP accept_undersubscription');
|
||||||
|
$this->addSql('CREATE INDEX IDX_NEWSLETTER_CONSENT_EMAIL ON newsletter_consent (email)');
|
||||||
|
$this->addSql('ALTER TABLE newsletter_opt_in_request RENAME INDEX uniq_ec0365db3bc57da TO UNIQ_70505214B3BC57DA');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260627130625 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE accommodation_price ADD price_additional_person_override INT DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE accommodation_price DROP price_additional_person_override');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260630080409 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE accommodation_price ADD season VARCHAR(20) NOT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE accommodation_price DROP season');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260630085856 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('DROP INDEX UNIQ_2D38541277153098 ON accommodation');
|
||||||
|
$this->addSql('ALTER TABLE accommodation ADD cms_code VARCHAR(16) DEFAULT NULL, CHANGE code calendar_code VARCHAR(16) NOT NULL');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX UNIQ_2D3854122583B3CC ON accommodation (calendar_code)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('DROP INDEX UNIQ_2D3854122583B3CC ON accommodation');
|
||||||
|
$this->addSql('ALTER TABLE accommodation DROP cms_code, CHANGE calendar_code code VARCHAR(16) NOT NULL');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX UNIQ_2D38541277153098 ON accommodation (code)');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260702084714 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Replace override price fields with a PriceType column; migrate existing override rows to sibling DISCOUNT records';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE accommodation_price ADD type VARCHAR(20) DEFAULT NULL');
|
||||||
|
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
INSERT INTO accommodation_price
|
||||||
|
(accommodation_id, date_from, date_to, season, included_pax, min_nights,
|
||||||
|
price_per_night, price_additional_person, accept_undersubscription, type,
|
||||||
|
created_at, updated_at, created_by_id, updated_by_id)
|
||||||
|
SELECT accommodation_id, date_from, date_to, season, included_pax, min_nights,
|
||||||
|
price_per_night_override, price_additional_person_override, accept_undersubscription, 'discount',
|
||||||
|
created_at, updated_at, created_by_id, updated_by_id
|
||||||
|
FROM accommodation_price
|
||||||
|
WHERE price_per_night_override IS NOT NULL
|
||||||
|
SQL);
|
||||||
|
|
||||||
|
$this->addSql('ALTER TABLE accommodation_price DROP price_per_night_override, DROP price_additional_person_override');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE accommodation_price ADD price_per_night_override INT DEFAULT NULL, ADD price_additional_person_override INT DEFAULT NULL, DROP type');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260706120805 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE accommodation DROP min_pax_summer');
|
||||||
|
$this->addSql('ALTER TABLE accommodation_price ADD accept_short_term TINYINT(1) NOT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE accommodation ADD min_pax_summer INT NOT NULL');
|
||||||
|
$this->addSql('ALTER TABLE accommodation_price DROP accept_short_term');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260706141648 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add accommodation_inquiry and accommodation_inquiry_additional_service tables';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE TABLE accommodation_inquiry (id INT AUTO_INCREMENT NOT NULL, accommodation_id INT NOT NULL, created_by_id INT DEFAULT NULL, updated_by_id INT DEFAULT NULL, date_from DATE NOT NULL COMMENT \'(DC2Type:date_immutable)\', date_to DATE NOT NULL COMMENT \'(DC2Type:date_immutable)\', pax_count INT NOT NULL, minors_count INT NOT NULL, children_count INT NOT NULL, adolescents_count INT NOT NULL, board_service_label VARCHAR(255) DEFAULT NULL, board_service_price INT DEFAULT NULL, board_service_original_id INT DEFAULT NULL, is_inquiry TINYINT(1) NOT NULL, salutation VARCHAR(10) DEFAULT NULL, first_name VARCHAR(100) NOT NULL, last_name VARCHAR(100) NOT NULL, email VARCHAR(255) NOT NULL, phone VARCHAR(50) DEFAULT NULL, street VARCHAR(255) DEFAULT NULL, zip VARCHAR(20) DEFAULT NULL, city VARCHAR(100) DEFAULT NULL, country VARCHAR(5) DEFAULT NULL, remarks LONGTEXT DEFAULT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', updated_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\', INDEX IDX_160D770A8F3692CD (accommodation_id), INDEX IDX_160D770AB03A8386 (created_by_id), INDEX IDX_160D770A896DBBDE (updated_by_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||||
|
$this->addSql('CREATE TABLE accommodation_inquiry_additional_service (id INT AUTO_INCREMENT NOT NULL, inquiry_id INT NOT NULL, label VARCHAR(255) NOT NULL, price INT NOT NULL, type VARCHAR(20) NOT NULL, original_service_id INT DEFAULT NULL, INDEX IDX_B28A7FD8A7AD6D71 (inquiry_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||||
|
$this->addSql('ALTER TABLE accommodation_booking ADD CONSTRAINT FK_160D770A8F3692CD FOREIGN KEY (accommodation_id) REFERENCES accommodation (id)');
|
||||||
|
$this->addSql('ALTER TABLE accommodation_booking ADD CONSTRAINT FK_160D770AB03A8386 FOREIGN KEY (created_by_id) REFERENCES user (id) ON DELETE SET NULL');
|
||||||
|
$this->addSql('ALTER TABLE accommodation_booking ADD CONSTRAINT FK_160D770A896DBBDE FOREIGN KEY (updated_by_id) REFERENCES user (id) ON DELETE SET NULL');
|
||||||
|
$this->addSql('ALTER TABLE accommodation_inquiry_additional_service ADD CONSTRAINT FK_B28A7FD8A7AD6D71 FOREIGN KEY (inquiry_id) REFERENCES accommodation_booking (id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE accommodation_booking DROP FOREIGN KEY FK_160D770A8F3692CD');
|
||||||
|
$this->addSql('ALTER TABLE accommodation_booking DROP FOREIGN KEY FK_160D770AB03A8386');
|
||||||
|
$this->addSql('ALTER TABLE accommodation_booking DROP FOREIGN KEY FK_160D770A896DBBDE');
|
||||||
|
$this->addSql('ALTER TABLE accommodation_inquiry_additional_service DROP FOREIGN KEY FK_B28A7FD8A7AD6D71');
|
||||||
|
$this->addSql('DROP TABLE accommodation_booking');
|
||||||
|
$this->addSql('DROP TABLE accommodation_inquiry_additional_service');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260707094812 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Drop children_count (4–5 J.) and rename adolescents_count to children_count (now 4–X J.)';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE accommodation_booking DROP COLUMN children_count');
|
||||||
|
$this->addSql('ALTER TABLE accommodation_booking RENAME COLUMN adolescents_count TO children_count');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE accommodation_booking RENAME COLUMN children_count TO adolescents_count');
|
||||||
|
$this->addSql('ALTER TABLE accommodation_booking ADD COLUMN children_count INT NOT NULL DEFAULT 0');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260707150000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add group_name to accommodation_inquiry';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql("ALTER TABLE accommodation_booking ADD COLUMN group_name VARCHAR(255) NOT NULL DEFAULT ''");
|
||||||
|
$this->addSql("ALTER TABLE accommodation_booking ALTER COLUMN group_name DROP DEFAULT");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE accommodation_booking DROP COLUMN group_name');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260708071724 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Replace accommodation_inquiry_additional_service table with JSON column; add uuid unique index';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE accommodation_inquiry_additional_service DROP FOREIGN KEY FK_B28A7FD8A7AD6D71');
|
||||||
|
$this->addSql('DROP TABLE accommodation_inquiry_additional_service');
|
||||||
|
$this->addSql('ALTER TABLE accommodation_booking ADD additional_services JSON NOT NULL');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX UNIQ_160D770AD17F50A6 ON accommodation_booking (uuid)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE TABLE accommodation_inquiry_additional_service (id INT AUTO_INCREMENT NOT NULL, inquiry_id INT NOT NULL, label VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, price INT NOT NULL, type VARCHAR(20) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, original_service_id INT DEFAULT NULL, INDEX IDX_B28A7FD8A7AD6D71 (inquiry_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB COMMENT = \'\' ');
|
||||||
|
$this->addSql('ALTER TABLE accommodation_inquiry_additional_service ADD CONSTRAINT FK_B28A7FD8A7AD6D71 FOREIGN KEY (inquiry_id) REFERENCES accommodation_booking (id) ON UPDATE NO ACTION ON DELETE NO ACTION');
|
||||||
|
$this->addSql('DROP INDEX UNIQ_160D770AD17F50A6 ON accommodation_booking');
|
||||||
|
$this->addSql('ALTER TABLE accommodation_booking DROP additional_services');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260708081559 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE additional_service ADD description LONGTEXT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE board_service ADD description LONGTEXT DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE additional_service DROP description');
|
||||||
|
$this->addSql('ALTER TABLE board_service DROP description');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260708102807 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE accommodation_booking ADD discount INT DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE accommodation_booking DROP discount');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260709133922 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Drop manual sorting column from board_service/additional_service; they are now ordered by dateFrom then label.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE additional_service DROP sorting');
|
||||||
|
$this->addSql('ALTER TABLE board_service DROP sorting');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE additional_service ADD sorting INT NOT NULL');
|
||||||
|
$this->addSql('ALTER TABLE board_service ADD sorting INT NOT NULL');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260709141929 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add nullable accepted_at column to accommodation_booking for audit tracking of inquiry-to-booking acceptance.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE
|
||||||
|
accommodation_booking
|
||||||
|
ADD
|
||||||
|
accepted_at DATETIME DEFAULT NULL COMMENT '(DC2Type:datetime_immutable)'
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE accommodation_booking DROP accepted_at');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260709162705 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add nullable access_link_issued_at column to accommodation_booking for the customer-facing offer/accept access link.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE
|
||||||
|
accommodation_booking
|
||||||
|
ADD
|
||||||
|
access_link_issued_at DATETIME DEFAULT NULL COMMENT '(DC2Type:datetime_immutable)'
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE accommodation_booking DROP access_link_issued_at');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260710130000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Store an authoritative price snapshot on accommodation bookings';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE accommodation_booking ADD price_breakdown JSON DEFAULT NULL, ADD total_price INT DEFAULT NULL, ADD pricing_currency VARCHAR(3) DEFAULT NULL, ADD pricing_version SMALLINT DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE accommodation_booking DROP price_breakdown, DROP total_price, DROP pricing_currency, DROP pricing_version');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260716101949 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Split the single accommodation booking discount into per-component discounts';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE accommodation_booking CHANGE discount accommodation_discount INT DEFAULT NULL, ADD board_service_discount INT DEFAULT NULL, ADD additional_services_discount INT DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE accommodation_booking CHANGE accommodation_discount discount INT DEFAULT NULL, DROP board_service_discount, DROP additional_services_discount');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260717103431 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE accommodation_booking ADD managed_by_id INT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE accommodation_booking ADD CONSTRAINT FK_AC389924873649CA FOREIGN KEY (managed_by_id) REFERENCES user (id) ON DELETE SET NULL');
|
||||||
|
$this->addSql('CREATE INDEX IDX_AC389924873649CA ON accommodation_booking (managed_by_id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE accommodation_booking DROP FOREIGN KEY FK_AC389924873649CA');
|
||||||
|
$this->addSql('DROP INDEX IDX_AC389924873649CA ON accommodation_booking');
|
||||||
|
$this->addSql('ALTER TABLE accommodation_booking DROP managed_by_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+55
-1
@@ -6,6 +6,13 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "myep",
|
"name": "myep",
|
||||||
"license": "WTFPL",
|
"license": "WTFPL",
|
||||||
|
"dependencies": {
|
||||||
|
"@trevoreyre/autocomplete-js": "^3.0.3",
|
||||||
|
"glightbox": "^3.3.1",
|
||||||
|
"sortablejs": "^1.15.7",
|
||||||
|
"stimulus-use": "^0.52.3",
|
||||||
|
"vanilla-calendar-pro": "^3.1.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.17.0",
|
"@babel/core": "^7.17.0",
|
||||||
"@babel/preset-env": "^7.16.0",
|
"@babel/preset-env": "^7.16.0",
|
||||||
@@ -1645,7 +1652,6 @@
|
|||||||
"version": "3.2.2",
|
"version": "3.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@hotwired/stimulus/-/stimulus-3.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@hotwired/stimulus/-/stimulus-3.2.2.tgz",
|
||||||
"integrity": "sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A==",
|
"integrity": "sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@hotwired/stimulus-webpack-helpers": {
|
"node_modules/@hotwired/stimulus-webpack-helpers": {
|
||||||
@@ -2045,6 +2051,12 @@
|
|||||||
"tailwindcss": ">=3.0.0 || >=4.0.0 || insiders"
|
"tailwindcss": ">=3.0.0 || >=4.0.0 || insiders"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@trevoreyre/autocomplete-js": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@trevoreyre/autocomplete-js/-/autocomplete-js-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-Z0KhA4EtMOixzYOFR+OiMc60IgNy9HkPnVH0kfxFkbP1I4my//sB/MRoPkqKmKgR+qHMujXhjdflMk5TpwVN2Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.9",
|
"version": "1.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||||
@@ -4552,6 +4564,12 @@
|
|||||||
"assert-plus": "^1.0.0"
|
"assert-plus": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/glightbox": {
|
||||||
|
"version": "3.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/glightbox/-/glightbox-3.3.1.tgz",
|
||||||
|
"integrity": "sha512-nXoKfJRnQTDaAFAw5799hjpfAAHx5aLvOLG0SIGudeMCwtHgO3P2/avNYapJ+SL4UEZxO2YNBPtq6pzMhSx42g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/glob-parent": {
|
"node_modules/glob-parent": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||||
@@ -4670,6 +4688,16 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hotkeys-js": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-hseNiqaskxSnujuGp8aRMLJfcjaFiTSS0I2GQhqru82N/sx6CGyUf6pvU5X1iycvw2EqmvILkFIb5OzYFXY+9A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"funding": {
|
||||||
|
"url": "https://jaywcjlove.github.io/#/sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/htmlparser2": {
|
"node_modules/htmlparser2": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
|
||||||
@@ -7588,6 +7616,12 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sortablejs": {
|
||||||
|
"version": "1.15.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.7.tgz",
|
||||||
|
"integrity": "sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/source-list-map": {
|
"node_modules/source-list-map": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
|
||||||
@@ -7659,6 +7693,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/stimulus-use": {
|
||||||
|
"version": "0.52.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/stimulus-use/-/stimulus-use-0.52.3.tgz",
|
||||||
|
"integrity": "sha512-stZ5dID6FUrGCR/ChWUa0FT5Z8iqkzT6lputOAb50eF+Ayg7RzJj4U/HoRlp2NV333QfvoRidru9HLbom4hZVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@hotwired/stimulus": ">= 3",
|
||||||
|
"hotkeys-js": ">= 3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string-width": {
|
"node_modules/string-width": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
@@ -8520,6 +8564,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/vanilla-calendar-pro": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/vanilla-calendar-pro/-/vanilla-calendar-pro-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-yXDtCaedcKz6i5OOdWGwui0C8MAmjXjj7JzKZyjDlkczSRqnhI8BDGFygqT2K+qL1uY7R2fLYlTlxA6oyFs2yg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://buymeacoffee.com/uvarov"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/verror": {
|
"node_modules/verror": {
|
||||||
"version": "1.10.0",
|
"version": "1.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
|
||||||
|
|||||||
@@ -30,5 +30,12 @@
|
|||||||
"watch": "encore dev --watch",
|
"watch": "encore dev --watch",
|
||||||
"build": "encore production --progress",
|
"build": "encore production --progress",
|
||||||
"cy:open": "cypress open"
|
"cy:open": "cypress open"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@trevoreyre/autocomplete-js": "^3.0.3",
|
||||||
|
"glightbox": "^3.3.1",
|
||||||
|
"sortablejs": "^1.15.7",
|
||||||
|
"stimulus-use": "^0.52.3",
|
||||||
|
"vanilla-calendar-pro": "^3.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\BpnConnect;
|
||||||
|
|
||||||
|
use App\BpnConnect\Exception\BpnConnectException;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
|
||||||
|
abstract class AbstractApiClient
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected readonly HttpClientInterface $httpClient,
|
||||||
|
protected readonly LoggerInterface $logger,
|
||||||
|
protected readonly ?string $baseUrl = null,
|
||||||
|
protected readonly ?string $apiKey = null,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $query
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
protected function request(string $path, array $query = []): array
|
||||||
|
{
|
||||||
|
$this->assertConfigured();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = $this->httpClient->request('GET', $this->baseUrl.$path, [
|
||||||
|
'headers' => [
|
||||||
|
'X-API-KEY' => $this->apiKey,
|
||||||
|
],
|
||||||
|
'query' => $query,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $response->toArray();
|
||||||
|
} catch (ExceptionInterface $e) {
|
||||||
|
$this->logger->error('BpnConnect request failed', [
|
||||||
|
'path' => $path,
|
||||||
|
'query' => $query,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
throw new BpnConnectException('BpnConnect request failed: '.$e->getMessage(), 0, $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function assertConfigured(): void
|
||||||
|
{
|
||||||
|
if (null === $this->baseUrl || null === $this->apiKey) {
|
||||||
|
throw new BpnConnectException('BpnConnect client is not configured (missing BPN_CONNECT_BASE_URL or BPN_CONNECT_API_KEY).');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\BpnConnect;
|
||||||
|
|
||||||
|
use App\BpnConnect\Model\ContingentCalendarEntry;
|
||||||
|
use App\BpnConnect\Model\ContingentCalendarMeta;
|
||||||
|
use App\BpnConnect\Model\ContingentCalendarResponse;
|
||||||
|
use App\BpnConnect\Model\ContingentMode;
|
||||||
|
use App\BpnConnect\Model\ContingentRangeEntry;
|
||||||
|
use App\BpnConnect\Model\ContingentRangeResponse;
|
||||||
|
use App\BpnConnect\Model\ContingentStatus;
|
||||||
|
|
||||||
|
class ContingentsClient extends AbstractApiClient
|
||||||
|
{
|
||||||
|
private const string CALENDAR_PATH = '/api/v1/contingents/calendar';
|
||||||
|
|
||||||
|
public function getContingentCalendar(
|
||||||
|
string $hotelCode,
|
||||||
|
string $dateFrom,
|
||||||
|
string $dateTo,
|
||||||
|
): ContingentCalendarResponse {
|
||||||
|
$payload = $this->request(self::CALENDAR_PATH, [
|
||||||
|
'hotelCode' => $hotelCode,
|
||||||
|
'dateFrom' => $dateFrom,
|
||||||
|
'dateTo' => $dateTo,
|
||||||
|
'mode' => ContingentMode::Days->value,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$entries = array_map(
|
||||||
|
static fn (array $item) => new ContingentCalendarEntry(
|
||||||
|
date: $item['date'],
|
||||||
|
status: ContingentStatus::from($item['status']),
|
||||||
|
),
|
||||||
|
$payload['data'] ?? [],
|
||||||
|
);
|
||||||
|
|
||||||
|
return new ContingentCalendarResponse($this->buildMeta($payload), $entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContingentRanges(
|
||||||
|
string $hotelCode,
|
||||||
|
string $dateFrom,
|
||||||
|
string $dateTo,
|
||||||
|
): ContingentRangeResponse {
|
||||||
|
$payload = $this->request(self::CALENDAR_PATH, [
|
||||||
|
'hotelCode' => $hotelCode,
|
||||||
|
'dateFrom' => $dateFrom,
|
||||||
|
'dateTo' => $dateTo,
|
||||||
|
'mode' => ContingentMode::Ranges->value,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$entries = array_map(
|
||||||
|
static fn (array $item) => new ContingentRangeEntry(
|
||||||
|
dateFrom: $item['date_from'],
|
||||||
|
dateTo: $item['date_to'],
|
||||||
|
status: ContingentStatus::from($item['status']),
|
||||||
|
),
|
||||||
|
$payload['data'] ?? [],
|
||||||
|
);
|
||||||
|
|
||||||
|
return new ContingentRangeResponse($this->buildMeta($payload), $entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $payload
|
||||||
|
*/
|
||||||
|
private function buildMeta(array $payload): ContingentCalendarMeta
|
||||||
|
{
|
||||||
|
$raw = $payload['meta'] ?? [];
|
||||||
|
|
||||||
|
return new ContingentCalendarMeta(
|
||||||
|
dateFrom: $raw['date_from'] ?? '',
|
||||||
|
dateTo: $raw['date_to'] ?? '',
|
||||||
|
mode: $raw['mode'] ?? '',
|
||||||
|
hotelCode: $raw['hotel_code'] ?? '',
|
||||||
|
hotelId: (int) ($raw['hotel_id'] ?? 0),
|
||||||
|
rowCount: (int) ($raw['row_count'] ?? 0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\BpnConnect\Exception;
|
||||||
|
|
||||||
|
class BpnConnectException extends \RuntimeException
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\BpnConnect\Model;
|
||||||
|
|
||||||
|
readonly class ContingentCalendarEntry
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $date,
|
||||||
|
public ContingentStatus $status,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\BpnConnect\Model;
|
||||||
|
|
||||||
|
readonly class ContingentCalendarMeta
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $dateFrom,
|
||||||
|
public string $dateTo,
|
||||||
|
public string $mode,
|
||||||
|
public string $hotelCode,
|
||||||
|
public int $hotelId,
|
||||||
|
public int $rowCount,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\BpnConnect\Model;
|
||||||
|
|
||||||
|
readonly class ContingentCalendarResponse
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param ContingentCalendarEntry[] $data
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public ContingentCalendarMeta $meta,
|
||||||
|
public array $data,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\BpnConnect\Model;
|
||||||
|
|
||||||
|
enum ContingentMode: string
|
||||||
|
{
|
||||||
|
case Days = 'days';
|
||||||
|
case Ranges = 'ranges';
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\BpnConnect\Model;
|
||||||
|
|
||||||
|
readonly class ContingentRangeEntry
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $dateFrom,
|
||||||
|
public string $dateTo,
|
||||||
|
public ContingentStatus $status,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\BpnConnect\Model;
|
||||||
|
|
||||||
|
readonly class ContingentRangeResponse
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param ContingentRangeEntry[] $data
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public ContingentCalendarMeta $meta,
|
||||||
|
public array $data,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\BpnConnect\Model;
|
||||||
|
|
||||||
|
enum ContingentStatus: string
|
||||||
|
{
|
||||||
|
case Ok = 'OK';
|
||||||
|
case Blocked = 'BLOCKED';
|
||||||
|
case OnRequest = 'ON_REQUEST';
|
||||||
|
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
return 'enum.contingent_status.'.strtolower($this->value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,8 @@ class CrmAttributesResponseParser
|
|||||||
private const BPN_CRM_ID_ADMIN = 1292;
|
private const BPN_CRM_ID_ADMIN = 1292;
|
||||||
private const BPN_CRM_ID_MANAGER = 1293;
|
private const BPN_CRM_ID_MANAGER = 1293;
|
||||||
private const BPN_CRM_ID_TEAMER = 1070;
|
private const BPN_CRM_ID_TEAMER = 1070;
|
||||||
|
private const BPN_CRM_ID_GROUPS_MANAGER = 1477;
|
||||||
|
private const BPN_CRM_ID_GROUPS_ADMIN = 1478;
|
||||||
private const BPN_DEFAULT_HOTEL_CODE = 'SSL';
|
private const BPN_DEFAULT_HOTEL_CODE = 'SSL';
|
||||||
|
|
||||||
public function parse(Crawler $result): CrmAttributes
|
public function parse(Crawler $result): CrmAttributes
|
||||||
@@ -57,6 +59,12 @@ class CrmAttributesResponseParser
|
|||||||
if (self::BPN_CRM_ID_TEAMER === $attribute->id && true === $attribute->selected) {
|
if (self::BPN_CRM_ID_TEAMER === $attribute->id && true === $attribute->selected) {
|
||||||
$roles[] = 'ROLE_TEAMER';
|
$roles[] = 'ROLE_TEAMER';
|
||||||
}
|
}
|
||||||
|
if (self::BPN_CRM_ID_GROUPS_MANAGER === $attribute->id && true === $attribute->selected) {
|
||||||
|
$roles[] = 'ROLE_GROUPS_MANAGER';
|
||||||
|
}
|
||||||
|
if (self::BPN_CRM_ID_GROUPS_ADMIN === $attribute->id && true === $attribute->selected) {
|
||||||
|
$roles[] = 'ROLE_GROUPS_ADMIN';
|
||||||
|
}
|
||||||
|
|
||||||
$attributes[] = $attribute;
|
$attributes[] = $attribute;
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,582 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Command;
|
||||||
|
|
||||||
|
use App\Entity\Groups\Accommodation;
|
||||||
|
use App\Entity\Groups\AccommodationPrice;
|
||||||
|
use App\Entity\Groups\AdditionalService;
|
||||||
|
use App\Entity\Groups\BoardService;
|
||||||
|
use App\Enum\Groups\AdditionalServiceType;
|
||||||
|
use App\Enum\Groups\Season;
|
||||||
|
use App\Repository\Groups\AccommodationRepository;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
|
||||||
|
#[AsCommand(
|
||||||
|
name: 'app:groups:import-legacy-data',
|
||||||
|
description: 'Imports Accommodation/AccommodationPrice/BoardService/AdditionalService data from the legacy TYPO3 CSV exports (hotels.csv, groupspriceconfig.csv, groupspriceboard.csv, groupspriceoption.csv, seasons.csv)',
|
||||||
|
)]
|
||||||
|
class ImportGroupsLegacyDataCommand extends Command
|
||||||
|
{
|
||||||
|
private const int DEFAULT_MIN_NIGHTS = 1;
|
||||||
|
private const Season DEFAULT_SEASON = Season::SECONDARY;
|
||||||
|
|
||||||
|
/** @var array<int, AdditionalServiceType> maps groupspriceoption "type" column to AdditionalServiceType */
|
||||||
|
private const array OPTION_TYPE_MAP = [
|
||||||
|
1 => AdditionalServiceType::Flat,
|
||||||
|
2 => AdditionalServiceType::PerPerson,
|
||||||
|
3 => AdditionalServiceType::PerNight,
|
||||||
|
4 => AdditionalServiceType::PerPersonPerNight,
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @var array<string, Season> maps seasons.csv "token" column to Season, per translations/messages.de.yaml */
|
||||||
|
private const array SEASON_TOKEN_MAP = [
|
||||||
|
'HS' => Season::PEAK,
|
||||||
|
'NS' => Season::SECONDARY,
|
||||||
|
'VNS' => Season::ADV_SECONDARY,
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly AccommodationRepository $accommodationRepository,
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
) {
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->addArgument('path', InputArgument::OPTIONAL, 'Directory containing hotels.csv, groupspriceconfig.csv, groupspriceboard.csv, groupspriceoption.csv, seasons.csv', 'temp')
|
||||||
|
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Parse and report without persisting anything')
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
$dryRun = (bool) $input->getOption('dry-run');
|
||||||
|
$path = rtrim((string) $input->getArgument('path'), '/');
|
||||||
|
|
||||||
|
foreach (['hotels.csv', 'groupspriceconfig.csv', 'groupspriceboard.csv', 'groupspriceoption.csv', 'seasons.csv'] as $file) {
|
||||||
|
if (!is_readable($path.'/'.$file)) {
|
||||||
|
$io->error(sprintf('Cannot read %s/%s', $path, $file));
|
||||||
|
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$hotels = $this->readCsv($path.'/hotels.csv');
|
||||||
|
$configRows = $this->readCsv($path.'/groupspriceconfig.csv');
|
||||||
|
$boardRows = $this->readCsv($path.'/groupspriceboard.csv');
|
||||||
|
$optionRows = $this->readCsv($path.'/groupspriceoption.csv');
|
||||||
|
$seasonRows = $this->readCsv($path.'/seasons.csv', ',');
|
||||||
|
|
||||||
|
$currencyByHotelUid = $this->deriveCurrencies($configRows);
|
||||||
|
$seasonRanges = $this->parseSeasonRanges($seasonRows);
|
||||||
|
|
||||||
|
$created = [];
|
||||||
|
$updated = [];
|
||||||
|
/** @var array<int, Accommodation> $accommodationByUid */
|
||||||
|
$accommodationByUid = [];
|
||||||
|
|
||||||
|
foreach ($hotels as $row) {
|
||||||
|
$uid = (int) $row['uid'];
|
||||||
|
$code = trim($row['code']);
|
||||||
|
$name = trim($row['name']);
|
||||||
|
$currency = $currencyByHotelUid[$uid] ?? 'EUR';
|
||||||
|
|
||||||
|
$existing = $this->accommodationRepository->findOneByCmsCode($code)
|
||||||
|
?? $this->accommodationRepository->findOneByCalendarCode($code);
|
||||||
|
|
||||||
|
if (null !== $existing) {
|
||||||
|
$existing->setName($name);
|
||||||
|
$existing->setCurrency($currency);
|
||||||
|
|
||||||
|
foreach ($existing->getAccommodationPrices()->toArray() as $price) {
|
||||||
|
$existing->removeAccommodationPrice($price);
|
||||||
|
if (!$dryRun) {
|
||||||
|
$this->entityManager->remove($price);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($existing->getBoardServices()->toArray() as $board) {
|
||||||
|
$existing->removeBoardService($board);
|
||||||
|
if (!$dryRun) {
|
||||||
|
$this->entityManager->remove($board);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($existing->getAdditionalServices()->toArray() as $service) {
|
||||||
|
$existing->removeAdditionalService($service);
|
||||||
|
if (!$dryRun) {
|
||||||
|
$this->entityManager->remove($service);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$accommodationByUid[$uid] = $existing;
|
||||||
|
$updated[] = sprintf('%s (%s) — replacing existing prices/board/services', $name, $code);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$existingByCalendarCode = null;
|
||||||
|
foreach ($accommodationByUid as $a) {
|
||||||
|
// guard against double-import: a prior run may already have created this one
|
||||||
|
if ($a->getCalendarCode() === $code) {
|
||||||
|
$existingByCalendarCode = $a;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $existingByCalendarCode) {
|
||||||
|
$accommodationByUid[$uid] = $existingByCalendarCode;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$accommodation = new Accommodation();
|
||||||
|
$accommodation->setCalendarCode($code);
|
||||||
|
$accommodation->setName($name);
|
||||||
|
$accommodation->setCurrency($currency);
|
||||||
|
$accommodation->setMaxAdolescentAge(0);
|
||||||
|
|
||||||
|
if (!$dryRun) {
|
||||||
|
$this->entityManager->persist($accommodation);
|
||||||
|
}
|
||||||
|
|
||||||
|
$accommodationByUid[$uid] = $accommodation;
|
||||||
|
$created[] = sprintf('%s (%s, %s)', $name, $code, $currency);
|
||||||
|
}
|
||||||
|
|
||||||
|
$knownUids = array_keys($accommodationByUid);
|
||||||
|
|
||||||
|
[$priceCount, $skippedPriceUids, $unmatchedSeasonCount] = $this->importPrices($configRows, $accommodationByUid, $knownUids, $seasonRanges, $dryRun);
|
||||||
|
[$boardCount, $skippedBoardUids] = $this->importBoardServices($boardRows, $accommodationByUid, $knownUids, $dryRun);
|
||||||
|
[$optionCount, $skippedOptionUids, $optionsNeedingDefaultRange] = $this->importAdditionalServices($optionRows, $accommodationByUid, $knownUids, $dryRun);
|
||||||
|
|
||||||
|
// BoardService/AdditionalService need a date range; derive it from each
|
||||||
|
// accommodation's freshly-imported AccommodationPrice rows now that they exist.
|
||||||
|
// AdditionalServices whose title carried its own year keep the scoped range set above.
|
||||||
|
$this->applyDateRangeToServices($accommodationByUid, $optionsNeedingDefaultRange);
|
||||||
|
|
||||||
|
if (!$dryRun) {
|
||||||
|
$this->entityManager->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
$io->section($dryRun ? 'Dry run summary' : 'Import summary');
|
||||||
|
$io->writeln(sprintf('Accommodations created: %d', \count($created)));
|
||||||
|
foreach ($created as $line) {
|
||||||
|
$io->writeln(' + '.$line);
|
||||||
|
}
|
||||||
|
$io->writeln(sprintf('Accommodations updated: %d', \count($updated)));
|
||||||
|
foreach ($updated as $line) {
|
||||||
|
$io->writeln(' ~ '.$line);
|
||||||
|
}
|
||||||
|
$io->writeln(sprintf('AccommodationPrice rows imported: %d', $priceCount));
|
||||||
|
if ($unmatchedSeasonCount > 0) {
|
||||||
|
$io->writeln(sprintf(' of which %d fell outside all seasons.csv ranges and defaulted to %s', $unmatchedSeasonCount, self::DEFAULT_SEASON->value));
|
||||||
|
}
|
||||||
|
$io->writeln(sprintf('BoardService rows imported: %d', $boardCount));
|
||||||
|
$io->writeln(sprintf('AdditionalService rows imported: %d', $optionCount));
|
||||||
|
|
||||||
|
$skippedUids = array_unique(array_merge($skippedPriceUids, $skippedBoardUids, $skippedOptionUids));
|
||||||
|
sort($skippedUids);
|
||||||
|
if ([] !== $skippedUids) {
|
||||||
|
$io->warning(sprintf(
|
||||||
|
'Skipped rows referencing hotel uid(s) not present in hotels.csv: %s',
|
||||||
|
implode(', ', $skippedUids),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$io->note('Dropped legacy columns with no entity equivalent (verified all-zero or unused): bonus_card_included, price_additional_person_group_1/2/3[_chf], ignore_with_board.');
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$io->note('Dry run — nothing was persisted.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$io->success('Done.');
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array<string, string>>
|
||||||
|
*/
|
||||||
|
private function readCsv(string $file, string $separator = ';'): array
|
||||||
|
{
|
||||||
|
$handle = fopen($file, 'r');
|
||||||
|
if (false === $handle) {
|
||||||
|
throw new \RuntimeException(sprintf('Unable to open %s', $file));
|
||||||
|
}
|
||||||
|
|
||||||
|
$header = fgetcsv($handle, escape: '\\', separator: $separator, enclosure: '"');
|
||||||
|
if (false === $header) {
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
while (false !== ($row = fgetcsv($handle, escape: '\\', separator: $separator, enclosure: '"'))) {
|
||||||
|
$rows[] = array_combine($header, $row);
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
return $rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derives EUR/CHF per hotel uid from groupspriceconfig.csv: if any row for that hotel
|
||||||
|
* has a non-zero price_chf, the hotel is priced in CHF, otherwise EUR.
|
||||||
|
*
|
||||||
|
* @param list<array<string, string>> $configRows
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function deriveCurrencies(array $configRows): array
|
||||||
|
{
|
||||||
|
$currencies = [];
|
||||||
|
foreach ($configRows as $row) {
|
||||||
|
$uid = (int) $row['hotel'];
|
||||||
|
if ($this->parseDecimal($row['price_chf']) > 0.0) {
|
||||||
|
$currencies[$uid] = 'CHF';
|
||||||
|
} elseif (!isset($currencies[$uid])) {
|
||||||
|
$currencies[$uid] = 'EUR';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $currencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseDecimal(string $value): float
|
||||||
|
{
|
||||||
|
$value = trim($value);
|
||||||
|
if ('' === $value) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (float) str_replace(',', '.', $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function toCents(string $value): int
|
||||||
|
{
|
||||||
|
return (int) round($this->parseDecimal($value) * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function priceColumn(string $currency): string
|
||||||
|
{
|
||||||
|
return 'CHF' === $currency ? 'price_chf' : 'price';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses seasons.csv (token,date_from,date_to; date_to exclusive, same convention as
|
||||||
|
* groupspriceconfig.csv) into Season-tagged ranges, skipping unrecognized tokens.
|
||||||
|
*
|
||||||
|
* @param list<array<string, string>> $rows
|
||||||
|
*
|
||||||
|
* @return list<array{season: Season, dateFrom: \DateTimeImmutable, dateTo: \DateTimeImmutable}>
|
||||||
|
*/
|
||||||
|
private function parseSeasonRanges(array $rows): array
|
||||||
|
{
|
||||||
|
$ranges = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$season = self::SEASON_TOKEN_MAP[trim($row['token'])] ?? null;
|
||||||
|
if (null === $season) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ranges[] = [
|
||||||
|
'season' => $season,
|
||||||
|
'dateFrom' => new \DateTimeImmutable(trim($row['date_from'])),
|
||||||
|
'dateTo' => new \DateTimeImmutable(trim($row['date_to'])),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ranges;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the season with the largest night-count overlap with [$dateFrom, $dateToExclusive)
|
||||||
|
* among $seasonRanges. Falls back to DEFAULT_SEASON (with matched=false) when the price
|
||||||
|
* period doesn't overlap any season range at all (e.g. a gap in seasons.csv).
|
||||||
|
*
|
||||||
|
* @param list<array{season: Season, dateFrom: \DateTimeImmutable, dateTo: \DateTimeImmutable}> $seasonRanges
|
||||||
|
*
|
||||||
|
* @return array{0: Season, 1: bool}
|
||||||
|
*/
|
||||||
|
private function resolveSeason(\DateTimeImmutable $dateFrom, \DateTimeImmutable $dateToExclusive, array $seasonRanges): array
|
||||||
|
{
|
||||||
|
$bestSeason = null;
|
||||||
|
$bestOverlapDays = 0;
|
||||||
|
|
||||||
|
foreach ($seasonRanges as $range) {
|
||||||
|
$overlapStart = max($dateFrom, $range['dateFrom']);
|
||||||
|
$overlapEnd = min($dateToExclusive, $range['dateTo']);
|
||||||
|
|
||||||
|
if ($overlapStart >= $overlapEnd) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$overlapDays = $overlapStart->diff($overlapEnd)->days;
|
||||||
|
if ($overlapDays > $bestOverlapDays) {
|
||||||
|
$bestOverlapDays = $overlapDays;
|
||||||
|
$bestSeason = $range['season'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$bestSeason ?? self::DEFAULT_SEASON, null !== $bestSeason];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array<string, string>> $rows
|
||||||
|
* @param array<int, Accommodation> $accommodationByUid
|
||||||
|
* @param list<int> $knownUids
|
||||||
|
* @param list<array{season: Season, dateFrom: \DateTimeImmutable, dateTo: \DateTimeImmutable}> $seasonRanges
|
||||||
|
*
|
||||||
|
* @return array{0: int, 1: list<int>, 2: int}
|
||||||
|
*/
|
||||||
|
private function importPrices(array $rows, array $accommodationByUid, array $knownUids, array $seasonRanges, bool $dryRun): array
|
||||||
|
{
|
||||||
|
$count = 0;
|
||||||
|
$skipped = [];
|
||||||
|
$unmatchedSeasonCount = 0;
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$uid = (int) $row['hotel'];
|
||||||
|
if (!\in_array($uid, $knownUids, true)) {
|
||||||
|
$skipped[] = $uid;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$accommodation = $accommodationByUid[$uid];
|
||||||
|
$column = $this->priceColumn($accommodation->getCurrency());
|
||||||
|
$additionalColumn = 'CHF' === $accommodation->getCurrency()
|
||||||
|
? 'price_additional_person_chf'
|
||||||
|
: 'price_additional_person';
|
||||||
|
|
||||||
|
$rawDateFrom = (new \DateTimeImmutable('@'.$row['date_from']))->setTime(0, 0);
|
||||||
|
// Source date_to is exclusive; AccommodationPrice::dateTo is the last included night.
|
||||||
|
$rawDateToExclusive = (new \DateTimeImmutable('@'.$row['date_to']))->setTime(0, 0);
|
||||||
|
|
||||||
|
[$season, $matched] = $this->resolveSeason($rawDateFrom, $rawDateToExclusive, $seasonRanges);
|
||||||
|
if (!$matched) {
|
||||||
|
++$unmatchedSeasonCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
$price = new AccommodationPrice();
|
||||||
|
$price->setDateFrom($rawDateFrom);
|
||||||
|
$price->setDateTo($rawDateToExclusive->modify('-1 day'));
|
||||||
|
$price->setSeason($season);
|
||||||
|
$price->setIncludedPax((int) $row['persons_included']);
|
||||||
|
$price->setPricePerNight($this->toCents($row[$column]));
|
||||||
|
$price->setPriceAdditionalPerson($this->toCents($row[$additionalColumn]));
|
||||||
|
$price->setMinNights(self::DEFAULT_MIN_NIGHTS);
|
||||||
|
$price->setType(null);
|
||||||
|
$price->setAcceptUndersubscription(false);
|
||||||
|
$price->setAcceptShortTerm(false);
|
||||||
|
|
||||||
|
$accommodation->addAccommodationPrice($price);
|
||||||
|
if (!$dryRun) {
|
||||||
|
$this->entityManager->persist($price);
|
||||||
|
}
|
||||||
|
++$count;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$count, $skipped, $unmatchedSeasonCount];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array<string, string>> $rows
|
||||||
|
* @param array<int, Accommodation> $accommodationByUid
|
||||||
|
* @param list<int> $knownUids
|
||||||
|
*
|
||||||
|
* @return array{0: int, 1: list<int>}
|
||||||
|
*/
|
||||||
|
private function importBoardServices(array $rows, array $accommodationByUid, array $knownUids, bool $dryRun): array
|
||||||
|
{
|
||||||
|
$count = 0;
|
||||||
|
$skipped = [];
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$uid = (int) $row['hotel'];
|
||||||
|
if (!\in_array($uid, $knownUids, true)) {
|
||||||
|
$skipped[] = $uid;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$accommodation = $accommodationByUid[$uid];
|
||||||
|
$column = $this->priceColumn($accommodation->getCurrency());
|
||||||
|
|
||||||
|
$board = new BoardService();
|
||||||
|
$board->setLabel(trim($row['title']));
|
||||||
|
$board->setPrice($this->toCents($row[$column]));
|
||||||
|
// dateFrom/dateTo filled in applyDateRangeToServices() once prices are known.
|
||||||
|
$board->setDateFrom(new \DateTimeImmutable('today'));
|
||||||
|
$board->setDateTo(new \DateTimeImmutable('today'));
|
||||||
|
|
||||||
|
$accommodation->addBoardService($board);
|
||||||
|
if (!$dryRun) {
|
||||||
|
$this->entityManager->persist($board);
|
||||||
|
}
|
||||||
|
++$count;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$count, $skipped];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array<string, string>> $rows
|
||||||
|
* @param array<int, Accommodation> $accommodationByUid
|
||||||
|
* @param list<int> $knownUids
|
||||||
|
*
|
||||||
|
* @return array{0: int, 1: list<int>, 2: list<AdditionalService>}
|
||||||
|
*/
|
||||||
|
private function importAdditionalServices(array $rows, array $accommodationByUid, array $knownUids, bool $dryRun): array
|
||||||
|
{
|
||||||
|
$count = 0;
|
||||||
|
$skipped = [];
|
||||||
|
$needsDefaultRange = [];
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$uid = (int) $row['hotel'];
|
||||||
|
if (!\in_array($uid, $knownUids, true)) {
|
||||||
|
$skipped[] = $uid;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$accommodation = $accommodationByUid[$uid];
|
||||||
|
$column = $this->priceColumn($accommodation->getCurrency());
|
||||||
|
$type = self::OPTION_TYPE_MAP[(int) $row['type']] ?? AdditionalServiceType::Flat;
|
||||||
|
[$year, $label] = $this->stripYearPrefix(trim($row['title']));
|
||||||
|
|
||||||
|
$service = new AdditionalService();
|
||||||
|
$service->setLabel($label);
|
||||||
|
$service->setPrice($this->toCents($row[$column]));
|
||||||
|
$service->setType($type);
|
||||||
|
|
||||||
|
if (null !== $year) {
|
||||||
|
// Title carried its own year (e.g. "2026 Endreinigung ..."); scope validity to
|
||||||
|
// that accommodation's own price rows within that year, not the calendar year.
|
||||||
|
[$yearDateFrom, $yearDateTo] = $this->findYearPriceRange($accommodation, $year);
|
||||||
|
$service->setDateFrom($yearDateFrom ?? new \DateTimeImmutable($year.'-01-01'));
|
||||||
|
$service->setDateTo($yearDateTo ?? new \DateTimeImmutable($year.'-12-31'));
|
||||||
|
} else {
|
||||||
|
// dateFrom/dateTo filled in applyDateRangeToServices() once prices are known.
|
||||||
|
$service->setDateFrom(new \DateTimeImmutable('today'));
|
||||||
|
$service->setDateTo(new \DateTimeImmutable('today'));
|
||||||
|
$needsDefaultRange[] = $service;
|
||||||
|
}
|
||||||
|
|
||||||
|
$accommodation->addAdditionalService($service);
|
||||||
|
if (!$dryRun) {
|
||||||
|
$this->entityManager->persist($service);
|
||||||
|
}
|
||||||
|
++$count;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$count, $skipped, $needsDefaultRange];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits a leading "YYYY " prefix off a title, e.g. "2026 Endreinigung Küche" →
|
||||||
|
* [2026, "Endreinigung Küche"]. Returns [null, $label unchanged] when no such prefix
|
||||||
|
* is present.
|
||||||
|
*
|
||||||
|
* @return array{0: ?int, 1: string}
|
||||||
|
*/
|
||||||
|
private function stripYearPrefix(string $label): array
|
||||||
|
{
|
||||||
|
if (preg_match('/^(\d{4})\s+(.+)$/', $label, $matches)) {
|
||||||
|
return [(int) $matches[1], $matches[2]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [null, $label];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the earliest dateFrom / latest dateTo among an accommodation's already-imported
|
||||||
|
* AccommodationPrice rows that fall within the given calendar year.
|
||||||
|
*
|
||||||
|
* @return array{0: ?\DateTimeImmutable, 1: ?\DateTimeImmutable}
|
||||||
|
*/
|
||||||
|
private function findYearPriceRange(Accommodation $accommodation, int $year): array
|
||||||
|
{
|
||||||
|
$dateFrom = null;
|
||||||
|
$dateTo = null;
|
||||||
|
|
||||||
|
foreach ($accommodation->getAccommodationPrices() as $price) {
|
||||||
|
if ((int) $price->getDateFrom()->format('Y') !== $year) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (null === $dateFrom || $price->getDateFrom() < $dateFrom) {
|
||||||
|
$dateFrom = $price->getDateFrom();
|
||||||
|
}
|
||||||
|
if (null === $dateTo || $price->getDateTo() > $dateTo) {
|
||||||
|
$dateTo = $price->getDateTo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$dateFrom, $dateTo];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, Accommodation> $accommodationByUid
|
||||||
|
* @param list<AdditionalService> $additionalServicesNeedingDefaultRange only these
|
||||||
|
* (titles without a leading year) get the accommodation-
|
||||||
|
* wide fallback range; year-prefixed ones already have
|
||||||
|
* their scoped range set in importAdditionalServices().
|
||||||
|
*/
|
||||||
|
private function applyDateRangeToServices(array $accommodationByUid, array $additionalServicesNeedingDefaultRange): void
|
||||||
|
{
|
||||||
|
$needsDefaultRange = new \SplObjectStorage();
|
||||||
|
foreach ($additionalServicesNeedingDefaultRange as $service) {
|
||||||
|
$needsDefaultRange->attach($service);
|
||||||
|
}
|
||||||
|
|
||||||
|
$seen = [];
|
||||||
|
foreach ($accommodationByUid as $accommodation) {
|
||||||
|
$id = spl_object_id($accommodation);
|
||||||
|
if (isset($seen[$id])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$seen[$id] = true;
|
||||||
|
|
||||||
|
$prices = $accommodation->getAccommodationPrices();
|
||||||
|
if ($prices->isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dateFrom = null;
|
||||||
|
$dateTo = null;
|
||||||
|
foreach ($prices as $price) {
|
||||||
|
if (null === $dateFrom || $price->getDateFrom() < $dateFrom) {
|
||||||
|
$dateFrom = $price->getDateFrom();
|
||||||
|
}
|
||||||
|
if (null === $dateTo || $price->getDateTo() > $dateTo) {
|
||||||
|
$dateTo = $price->getDateTo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $dateFrom || null === $dateTo) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($accommodation->getBoardServices() as $board) {
|
||||||
|
$board->setDateFrom($dateFrom);
|
||||||
|
$board->setDateTo($dateTo);
|
||||||
|
}
|
||||||
|
foreach ($accommodation->getAdditionalServices() as $service) {
|
||||||
|
if ($needsDefaultRange->contains($service)) {
|
||||||
|
$service->setDateFrom($dateFrom);
|
||||||
|
$service->setDateTo($dateTo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Admin\Accommodation;
|
||||||
|
|
||||||
|
use App\Entity\Groups\Accommodation;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[IsGranted('ROLE_GROUPS_ADMIN')]
|
||||||
|
class CalendarController extends AbstractController
|
||||||
|
{
|
||||||
|
#[Route('/admin/accommodation/{id}/calendar', name: 'app_admin_accommodation_calendar', methods: ['GET'])]
|
||||||
|
public function index(Accommodation $accommodation, Request $request): Response
|
||||||
|
{
|
||||||
|
return $this->render('admin/accommodation/_calendar_section.html.twig', [
|
||||||
|
'accommodation' => $accommodation,
|
||||||
|
'startMonth' => $request->query->get('month'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Admin\Accommodation;
|
||||||
|
|
||||||
|
use App\Entity\Groups\Accommodation;
|
||||||
|
use App\Form\Admin\Groups\AccommodationType;
|
||||||
|
use App\Htmx\HxRedirectResponse;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[IsGranted('ROLE_GROUPS_ADMIN')]
|
||||||
|
class CreateController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/admin/accommodation/create', name: 'app_admin_accommodation_create')]
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$accommodation = new Accommodation();
|
||||||
|
|
||||||
|
$form = $this->createForm(AccommodationType::class, $accommodation, ['hx_post' => $request->getRequestUri()]);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$this->entityManager->persist($accommodation);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$this->addFlash('success', 'Das Gruppenhaus wurde angelegt');
|
||||||
|
|
||||||
|
$this->logger->info('Created accommodation', [
|
||||||
|
'name' => $accommodation->getName(),
|
||||||
|
'code' => $accommodation->getCalendarCode(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return new HxRedirectResponse($this->generateUrl('app_admin_accommodation'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('admin/accommodation/modal_create.html.twig', [
|
||||||
|
'accommodation' => $accommodation,
|
||||||
|
'form' => $form,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Admin\Accommodation;
|
||||||
|
|
||||||
|
use App\Entity\Groups\Accommodation;
|
||||||
|
use App\Htmx\HxRedirectResponse;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[IsGranted('ROLE_GROUPS_ADMIN')]
|
||||||
|
class DeleteController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/admin/accommodation/{id}/delete', name: 'app_admin_accommodation_delete')]
|
||||||
|
public function index(Accommodation $accommodation, Request $request): Response
|
||||||
|
{
|
||||||
|
if (true === $request->isMethod(Request::METHOD_POST)) {
|
||||||
|
if (!$this->isCsrfTokenValid('delete_accommodation_'.$accommodation->getId(), $request->request->getString('_token'))) {
|
||||||
|
throw $this->createAccessDeniedException('Invalid CSRF token.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->entityManager->remove($accommodation);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$this->addFlash('success', 'Das Gruppenhaus wurde gelöscht');
|
||||||
|
|
||||||
|
$this->logger->info('Deleted accommodation', [
|
||||||
|
'name' => $accommodation->getName(),
|
||||||
|
'code' => $accommodation->getCalendarCode(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return new HxRedirectResponse($this->generateUrl('app_admin_accommodation'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('admin/accommodation/modal_delete.html.twig', [
|
||||||
|
'accommodation' => $accommodation,
|
||||||
|
'csrf_token_id' => 'delete_accommodation_'.$accommodation->getId(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Admin\Accommodation;
|
||||||
|
|
||||||
|
use App\Entity\Groups\Accommodation;
|
||||||
|
use App\Form\Admin\Groups\AccommodationType;
|
||||||
|
use App\Service\CmsDataProvider;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[IsGranted('ROLE_GROUPS_ADMIN')]
|
||||||
|
class EditController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly CmsDataProvider $cmsDataProvider,
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/admin/accommodation/{id}/edit', name: 'app_admin_accommodation_edit')]
|
||||||
|
public function index(Accommodation $accommodation, Request $request): Response
|
||||||
|
{
|
||||||
|
// fetch additional data from cms if available
|
||||||
|
$cmsData = null;
|
||||||
|
|
||||||
|
if (null !== $accommodation->getCalendarCode()) {
|
||||||
|
$cmsData = $this->cmsDataProvider->getHotelDetails($accommodation->getEffectiveCmsCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = $this->createForm(AccommodationType::class, $accommodation);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$this->entityManager->persist($accommodation);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$this->addFlash('success', 'Das Gruppenhaus wurde aktualisiert');
|
||||||
|
|
||||||
|
$this->logger->info('Updated accommodation', [
|
||||||
|
'name' => $accommodation->getName(),
|
||||||
|
'code' => $accommodation->getCalendarCode(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->redirectToRoute('app_admin_accommodation_edit', ['id' => $accommodation->getId()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('admin/accommodation/edit.html.twig', [
|
||||||
|
'accommodation' => $accommodation,
|
||||||
|
'form' => $form,
|
||||||
|
'cmsData' => $cmsData,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Admin\Accommodation;
|
||||||
|
|
||||||
|
use App\Repository\Groups\AccommodationRepository;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[IsGranted('ROLE_GROUPS_ADMIN')]
|
||||||
|
class IndexController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly AccommodationRepository $accommodationRepository,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/admin/accommodation', name: 'app_admin_accommodation')]
|
||||||
|
public function index(): Response
|
||||||
|
{
|
||||||
|
$accommodations = $this->accommodationRepository->findBy([], ['calendarCode' => 'ASC']);
|
||||||
|
|
||||||
|
return $this->render('admin/accommodation/index.html.twig', [
|
||||||
|
'accommodations' => $accommodations,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Admin\AccommodationBooking;
|
||||||
|
|
||||||
|
use App\Entity\Groups\AccommodationBooking;
|
||||||
|
use App\Form\Admin\Groups\AccommodationBookingType;
|
||||||
|
use App\Service\AccommodationBookingService;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[IsGranted('ROLE_GROUPS_MANAGER')]
|
||||||
|
class CreateController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
|
private readonly AccommodationBookingService $bookingService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/admin/accommodation-booking/create', name: 'app_admin_accommodationbooking_create')]
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$booking = new AccommodationBooking();
|
||||||
|
|
||||||
|
$form = $this->createForm(AccommodationBookingType::class, $booking, [
|
||||||
|
'with_accommodation' => true,
|
||||||
|
]);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$this->bookingService->refreshPriceSnapshot($booking);
|
||||||
|
$this->entityManager->persist($booking);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$this->bookingService->issueAccessLinkForDirectBooking($booking);
|
||||||
|
$this->bookingService->sendCustomerConfirmationEmail($booking);
|
||||||
|
|
||||||
|
$this->addFlash('success', 'Die Buchung wurde erstellt');
|
||||||
|
|
||||||
|
$this->logger->info('Created accommodation booking', [
|
||||||
|
'id' => $booking->getId(),
|
||||||
|
'groupName' => $booking->getGroupName(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->redirectToRoute('app_admin_accommodationbooking_edit', ['id' => $booking->getId()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('admin/accommodation_booking/create.html.twig', [
|
||||||
|
'form' => $form,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Admin\AccommodationBooking;
|
||||||
|
|
||||||
|
use App\Entity\Groups\AccommodationBooking;
|
||||||
|
use App\Entity\Groups\AdditionalService;
|
||||||
|
use App\Entity\Groups\BoardService;
|
||||||
|
use App\Form\Admin\Groups\AccommodationBookingType;
|
||||||
|
use App\Repository\Groups\AdditionalServiceRepository;
|
||||||
|
use App\Repository\Groups\BoardServiceRepository;
|
||||||
|
use App\Service\AccommodationBookingService;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[IsGranted('ROLE_GROUPS_MANAGER')]
|
||||||
|
class EditController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
|
private readonly BoardServiceRepository $boardServiceRepo,
|
||||||
|
private readonly AdditionalServiceRepository $additionalServiceRepo,
|
||||||
|
private readonly AccommodationBookingService $bookingService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/admin/accommodation-booking/{id}/edit', name: 'app_admin_accommodationbooking_edit')]
|
||||||
|
public function index(AccommodationBooking $booking, Request $request): Response
|
||||||
|
{
|
||||||
|
$accommodation = $booking->getAccommodation();
|
||||||
|
$boardServices = [];
|
||||||
|
$additionalServices = [];
|
||||||
|
$currentBoardService = null;
|
||||||
|
$currentAdditionalServices = [];
|
||||||
|
|
||||||
|
if (null !== $accommodation && null !== $booking->getDateFrom() && null !== $booking->getDateTo()) {
|
||||||
|
$boardServices = $this->boardServiceRepo->findByAccommodationAndDateRange(
|
||||||
|
$accommodation,
|
||||||
|
$booking->getDateFrom(),
|
||||||
|
$booking->getDateTo(),
|
||||||
|
);
|
||||||
|
$additionalServices = $this->additionalServiceRepo->findByAccommodationAndDateRange(
|
||||||
|
$accommodation,
|
||||||
|
$booking->getDateFrom(),
|
||||||
|
$booking->getDateTo(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pre-select current board service if it is still in the available choices
|
||||||
|
foreach ($boardServices as $bs) {
|
||||||
|
if ($bs->getId() === $booking->getBoardServiceOriginalId()) {
|
||||||
|
$currentBoardService = $bs;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-select current additional services that are still in the available choices
|
||||||
|
$currentIds = array_column($booking->getAdditionalServices(), 'originalServiceId');
|
||||||
|
$currentAdditionalServices = array_values(array_filter(
|
||||||
|
$additionalServices,
|
||||||
|
fn (AdditionalService $s) => in_array($s->getId(), $currentIds, true),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = $this->createForm(AccommodationBookingType::class, $booking, [
|
||||||
|
'max_adolescent_age' => $accommodation?->getMaxAdolescentAge() ?? 0,
|
||||||
|
'board_services' => $boardServices,
|
||||||
|
'additional_services' => $additionalServices,
|
||||||
|
'current_board_service' => $currentBoardService,
|
||||||
|
'current_additional_services' => $currentAdditionalServices,
|
||||||
|
]);
|
||||||
|
$wasInquiry = $booking->isInquiry();
|
||||||
|
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
if ($form->has('boardService')) {
|
||||||
|
$selectedBoardService = $form->get('boardService')->getData();
|
||||||
|
if ($selectedBoardService instanceof BoardService) {
|
||||||
|
$booking->setBoardServiceLabel($selectedBoardService->getLabel());
|
||||||
|
$booking->setBoardServicePrice($selectedBoardService->getPrice());
|
||||||
|
$booking->setBoardServiceOriginalId($selectedBoardService->getId());
|
||||||
|
} else {
|
||||||
|
$booking->setBoardServiceLabel(null);
|
||||||
|
$booking->setBoardServicePrice(null);
|
||||||
|
$booking->setBoardServiceOriginalId(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($form->has('selectedAdditionalServices')) {
|
||||||
|
$booking->setAdditionalServices([]);
|
||||||
|
foreach ($form->get('selectedAdditionalServices')->getData() as $service) {
|
||||||
|
$booking->addAdditionalServiceSnapshot(
|
||||||
|
$service->getLabel() ?? '',
|
||||||
|
$service->getPrice() ?? 0,
|
||||||
|
$service->getType(),
|
||||||
|
$service->getId(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->bookingService->refreshPriceSnapshot($booking);
|
||||||
|
|
||||||
|
$this->entityManager->persist($booking);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
if ($wasInquiry && !$booking->isInquiry()) {
|
||||||
|
$this->bookingService->issueAccessLinkForDirectBooking($booking);
|
||||||
|
$this->bookingService->sendCustomerConfirmationEmail($booking);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->addFlash('success', 'Die Buchung wurde aktualisiert');
|
||||||
|
|
||||||
|
$this->logger->info('Updated accommodation booking', [
|
||||||
|
'id' => $booking->getId(),
|
||||||
|
'groupName' => $booking->getGroupName(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->redirectToRoute('app_admin_accommodationbooking_edit', ['id' => $booking->getId()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('admin/accommodation_booking/edit.html.twig', [
|
||||||
|
'booking' => $booking,
|
||||||
|
'form' => $form,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Admin\AccommodationBooking;
|
||||||
|
|
||||||
|
use App\Entity\Groups\AccommodationBooking;
|
||||||
|
use App\Htmx\HxRedirectResponse;
|
||||||
|
use App\Service\AccommodationBookingService;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[IsGranted('ROLE_GROUPS_MANAGER')]
|
||||||
|
class GenerateAccessLinkController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly AccommodationBookingService $bookingService,
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/admin/accommodation-booking/{id}/generate-access-link', name: 'app_admin_accommodationbooking_generate_access_link')]
|
||||||
|
public function index(AccommodationBooking $booking, Request $request): Response
|
||||||
|
{
|
||||||
|
if ($request->isMethod(Request::METHOD_POST)) {
|
||||||
|
if (!$this->isCsrfTokenValid('generate_accommodation_booking_access_link_'.$booking->getId(), $request->request->getString('_token'))) {
|
||||||
|
throw $this->createAccessDeniedException('Invalid CSRF token.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->bookingService->regenerateAccessLink($booking);
|
||||||
|
|
||||||
|
$this->addFlash('success', 'Der Zugangslink wurde neu generiert.');
|
||||||
|
|
||||||
|
$this->logger->info('Generated accommodation booking access link', [
|
||||||
|
'id' => $booking->getId(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return new HxRedirectResponse($this->generateUrl('app_admin_accommodationbooking_show', ['id' => $booking->getId()]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('admin/accommodation_booking/modal_generate_access_link.html.twig', [
|
||||||
|
'booking' => $booking,
|
||||||
|
'csrf_token_id' => 'generate_accommodation_booking_access_link_'.$booking->getId(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Admin\AccommodationBooking;
|
||||||
|
|
||||||
|
use App\Repository\Groups\AccommodationBookingRepository;
|
||||||
|
use Knp\Component\Pager\PaginatorInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[IsGranted('ROLE_GROUPS_MANAGER')]
|
||||||
|
class IndexController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly AccommodationBookingRepository $bookingRepository,
|
||||||
|
private readonly PaginatorInterface $paginator,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/admin/accommodation-booking', name: 'app_admin_accommodationbooking')]
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$today = new \DateTimeImmutable('today');
|
||||||
|
|
||||||
|
$qb = $this
|
||||||
|
->bookingRepository
|
||||||
|
->createQueryBuilder('booking')
|
||||||
|
->leftJoin('booking.accommodation', 'accommodation')
|
||||||
|
->addSelect('accommodation')
|
||||||
|
->where('booking.dateTo >= :today')
|
||||||
|
->setParameter('today', $today)
|
||||||
|
;
|
||||||
|
|
||||||
|
$pagination = $this->paginator->paginate(
|
||||||
|
$qb,
|
||||||
|
$request->query->getInt('page', 1),
|
||||||
|
$request->query->getInt('limit', 20),
|
||||||
|
[
|
||||||
|
'defaultSortFieldName' => 'booking.createdAt',
|
||||||
|
'defaultSortDirection' => 'DESC',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->render('admin/accommodation_booking/index.html.twig', [
|
||||||
|
'pagination' => $pagination,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Admin\AccommodationBooking;
|
||||||
|
|
||||||
|
use App\Entity\Groups\AccommodationBooking;
|
||||||
|
use App\Htmx\HxRedirectResponse;
|
||||||
|
use App\Service\AccommodationBookingService;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[IsGranted('ROLE_GROUPS_MANAGER')]
|
||||||
|
class SendAccessLinkController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly AccommodationBookingService $bookingService,
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/admin/accommodation-booking/{id}/send-access-link', name: 'app_admin_accommodationbooking_send_access_link')]
|
||||||
|
public function index(AccommodationBooking $booking, Request $request): Response
|
||||||
|
{
|
||||||
|
if (null === $booking->getAccessLinkIssuedAt()) {
|
||||||
|
return $this->redirectToRoute('app_admin_accommodationbooking_show', ['id' => $booking->getId()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->isMethod(Request::METHOD_POST)) {
|
||||||
|
if (!$this->isCsrfTokenValid('send_accommodation_booking_access_link_'.$booking->getId(), $request->request->getString('_token'))) {
|
||||||
|
throw $this->createAccessDeniedException('Invalid CSRF token.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->bookingService->sendCustomerConfirmationEmail($booking);
|
||||||
|
|
||||||
|
$this->addFlash('success', 'Der Zugangslink wurde dem Kunden per E-Mail zugestellt.');
|
||||||
|
|
||||||
|
$this->logger->info('Sent accommodation booking access link', [
|
||||||
|
'id' => $booking->getId(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return new HxRedirectResponse($this->generateUrl('app_admin_accommodationbooking_show', ['id' => $booking->getId()]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('admin/accommodation_booking/modal_send_access_link.html.twig', [
|
||||||
|
'booking' => $booking,
|
||||||
|
'csrf_token_id' => 'send_accommodation_booking_access_link_'.$booking->getId(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Admin\AccommodationBooking;
|
||||||
|
|
||||||
|
use App\Controller\Traits\ReturnUrlTrait;
|
||||||
|
use App\Entity\Groups\AccommodationBooking;
|
||||||
|
use App\Service\AccommodationBookingBreakdownCalculator;
|
||||||
|
use App\Service\AccommodationBookingLinkSigner;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[IsGranted('ROLE_GROUPS_MANAGER')]
|
||||||
|
class ShowController extends AbstractController
|
||||||
|
{
|
||||||
|
use ReturnUrlTrait;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly AccommodationBookingBreakdownCalculator $breakdownCalculator,
|
||||||
|
private readonly AccommodationBookingLinkSigner $linkSigner,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/admin/accommodation-booking/{id}', name: 'app_admin_accommodationbooking_show')]
|
||||||
|
public function index(AccommodationBooking $booking, Request $request): Response
|
||||||
|
{
|
||||||
|
$hasAccessLink = null !== $booking->getAccessLinkIssuedAt();
|
||||||
|
|
||||||
|
return $this->render('admin/accommodation_booking/show.html.twig', [
|
||||||
|
'booking' => $booking,
|
||||||
|
'priceBreakdown' => $this->breakdownCalculator->compute($booking),
|
||||||
|
'returnUrl' => $this->getReturnUrl($request, 'app_admin_accommodationbooking'),
|
||||||
|
'accessLink' => $hasAccessLink ? $this->linkSigner->sign($booking) : null,
|
||||||
|
'accessLinkExpiresAt' => $hasAccessLink ? $this->linkSigner->expiresAt($booking) : null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Admin\AccommodationPrice;
|
||||||
|
|
||||||
|
use App\Entity\Groups\Accommodation;
|
||||||
|
use App\Entity\Groups\AccommodationPrice;
|
||||||
|
use App\Form\Admin\Groups\AccommodationPriceType;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[IsGranted('ROLE_GROUPS_ADMIN')]
|
||||||
|
class CreateController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/admin/accommodation-price/{id}/create', name: 'app_admin_accommodationprice_create')]
|
||||||
|
public function index(Accommodation $accommodation, Request $request): Response
|
||||||
|
{
|
||||||
|
$accommodationPrice = new AccommodationPrice();
|
||||||
|
|
||||||
|
$form = $this->createForm(AccommodationPriceType::class, $accommodationPrice, ['currency' => $accommodation->getCurrency()]);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$accommodation->addAccommodationPrice($accommodationPrice);
|
||||||
|
|
||||||
|
$this->entityManager->persist($accommodationPrice);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$this->addFlash('success', 'Der Preis wurde angelegt');
|
||||||
|
|
||||||
|
$this->logger->info('Created accommodation price', [
|
||||||
|
'name' => $accommodation->getName(),
|
||||||
|
'code' => $accommodation->getCalendarCode(),
|
||||||
|
'date_range' => [
|
||||||
|
'from' => $accommodationPrice->getDateFrom()->format('Y-m-d'),
|
||||||
|
'to' => $accommodationPrice->getDateTo()->format('Y-m-d'),
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$returnUrl = $this->generateUrl('app_admin_accommodation_edit', [
|
||||||
|
'id' => $accommodation->getId(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->redirect($returnUrl.'#prices');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('admin/accommodation_price/create.html.twig', [
|
||||||
|
'accommodation' => $accommodation,
|
||||||
|
'form' => $form,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Admin\AccommodationPrice;
|
||||||
|
|
||||||
|
use App\Entity\Groups\AccommodationPrice;
|
||||||
|
use App\Htmx\HxRedirectResponse;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[IsGranted('ROLE_GROUPS_ADMIN')]
|
||||||
|
class DeleteController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/admin/accommodation-price/{id}/delete', name: 'app_admin_accommodationprice_delete')]
|
||||||
|
public function index(AccommodationPrice $accommodationPrice, Request $request): Response
|
||||||
|
{
|
||||||
|
$accommodation = $accommodationPrice->getAccommodation();
|
||||||
|
|
||||||
|
if (true === $request->isMethod(Request::METHOD_POST)) {
|
||||||
|
if (!$this->isCsrfTokenValid('delete_accommodation_price_'.$accommodationPrice->getId(), $request->request->getString('_token'))) {
|
||||||
|
throw $this->createAccessDeniedException('Invalid CSRF token.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->entityManager->remove($accommodationPrice);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$this->addFlash('success', 'Der Preis wurde gelöscht');
|
||||||
|
|
||||||
|
$this->logger->info('Deleted accommodation price', [
|
||||||
|
'name' => $accommodation->getName(),
|
||||||
|
'code' => $accommodation->getCalendarCode(),
|
||||||
|
'date_range' => [
|
||||||
|
'from' => $accommodationPrice->getDateFrom()->format('Y-m-d'),
|
||||||
|
'to' => $accommodationPrice->getDateTo()->format('Y-m-d'),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$returnUrl = $this->generateUrl('app_admin_accommodation_edit', [
|
||||||
|
'id' => $accommodation->getId(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return new HxRedirectResponse($returnUrl.'#prices');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('admin/accommodation_price/modal_delete.html.twig', [
|
||||||
|
'accommodationPrice' => $accommodationPrice,
|
||||||
|
'accommodation' => $accommodation,
|
||||||
|
'csrf_token_id' => 'delete_accommodation_price_'.$accommodationPrice->getId(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Admin\AccommodationPrice;
|
||||||
|
|
||||||
|
use App\Entity\Groups\AccommodationPrice;
|
||||||
|
use App\Form\Admin\Groups\AccommodationPriceType;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[IsGranted('ROLE_GROUPS_ADMIN')]
|
||||||
|
class EditController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/admin/accommodation-price/{id}/edit', name: 'app_admin_accommodationprice_edit', defaults: ['duplicate' => false])]
|
||||||
|
#[Route('/admin/accommodation-price/{id}/duplicate', name: 'app_admin_accommodationprice_duplicate', defaults: ['duplicate' => true])]
|
||||||
|
public function index(AccommodationPrice $accommodationPrice, bool $duplicate, Request $request): Response
|
||||||
|
{
|
||||||
|
$accommodation = $accommodationPrice->getAccommodation();
|
||||||
|
|
||||||
|
if (true === $duplicate) {
|
||||||
|
$accommodationPrice = clone $accommodationPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = $this->createForm(AccommodationPriceType::class, $accommodationPrice, ['currency' => $accommodation->getCurrency()]);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
if (true === $duplicate) {
|
||||||
|
$accommodation->addAccommodationPrice($accommodationPrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->entityManager->persist($accommodationPrice);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$this->addFlash('success', 'Der Preis wurde aktualisiert');
|
||||||
|
|
||||||
|
$this->logger->info('Edited accommodation price', [
|
||||||
|
'name' => $accommodation->getName(),
|
||||||
|
'code' => $accommodation->getCalendarCode(),
|
||||||
|
'date_range' => [
|
||||||
|
'from' => $accommodationPrice->getDateFrom()->format('Y-m-d'),
|
||||||
|
'to' => $accommodationPrice->getDateTo()->format('Y-m-d'),
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$returnUrl = $this->generateUrl('app_admin_accommodation_edit', [
|
||||||
|
'id' => $accommodation->getId(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->redirect($returnUrl.'#prices');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('admin/accommodation_price/edit.html.twig', [
|
||||||
|
'accommodation' => $accommodation,
|
||||||
|
'accommodationPrice' => $accommodationPrice,
|
||||||
|
'form' => $form,
|
||||||
|
'duplicate' => $duplicate,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Admin\AdditionalService;
|
||||||
|
|
||||||
|
use App\Entity\Groups\Accommodation;
|
||||||
|
use App\Entity\Groups\AdditionalService;
|
||||||
|
use App\Form\Admin\Groups\AdditionalServiceType;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[IsGranted('ROLE_GROUPS_ADMIN')]
|
||||||
|
class CreateController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/admin/additional-service/{id}/create', name: 'app_admin_additionalservice_create')]
|
||||||
|
public function index(Accommodation $accommodation, Request $request): Response
|
||||||
|
{
|
||||||
|
$additionalService = new AdditionalService();
|
||||||
|
|
||||||
|
$form = $this->createForm(AdditionalServiceType::class, $additionalService, ['currency' => $accommodation->getCurrency()]);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$accommodation->addAdditionalService($additionalService);
|
||||||
|
|
||||||
|
$this->entityManager->persist($additionalService);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$this->addFlash('success', 'Die Zusatzleistung wurde angelegt');
|
||||||
|
|
||||||
|
$this->logger->info('Created additional service', [
|
||||||
|
'name' => $accommodation->getName(),
|
||||||
|
'code' => $accommodation->getCalendarCode(),
|
||||||
|
'label' => $additionalService->getLabel(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$returnUrl = $this->generateUrl('app_admin_accommodation_edit', [
|
||||||
|
'id' => $accommodation->getId(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->redirect($returnUrl.'#additional-services');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('admin/additional_service/create.html.twig', [
|
||||||
|
'accommodation' => $accommodation,
|
||||||
|
'form' => $form,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Admin\AdditionalService;
|
||||||
|
|
||||||
|
use App\Entity\Groups\AdditionalService;
|
||||||
|
use App\Htmx\HxRedirectResponse;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[IsGranted('ROLE_GROUPS_ADMIN')]
|
||||||
|
class DeleteController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/admin/additional-service/{id}/delete', name: 'app_admin_additionalservice_delete')]
|
||||||
|
public function index(AdditionalService $additionalService, Request $request): Response
|
||||||
|
{
|
||||||
|
$accommodation = $additionalService->getAccommodation();
|
||||||
|
|
||||||
|
if (true === $request->isMethod(Request::METHOD_POST)) {
|
||||||
|
if (!$this->isCsrfTokenValid('delete_additional_service_'.$additionalService->getId(), $request->request->getString('_token'))) {
|
||||||
|
throw $this->createAccessDeniedException('Invalid CSRF token.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->entityManager->remove($additionalService);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$this->addFlash('success', 'Die Zusatzleistung wurde gelöscht');
|
||||||
|
|
||||||
|
$this->logger->info('Deleted additional service', [
|
||||||
|
'name' => $accommodation->getName(),
|
||||||
|
'code' => $accommodation->getCalendarCode(),
|
||||||
|
'label' => $additionalService->getLabel(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$returnUrl = $this->generateUrl('app_admin_accommodation_edit', [
|
||||||
|
'id' => $accommodation->getId(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return new HxRedirectResponse($returnUrl.'#additional-services');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('admin/additional_service/modal_delete.html.twig', [
|
||||||
|
'additionalService' => $additionalService,
|
||||||
|
'accommodation' => $accommodation,
|
||||||
|
'csrf_token_id' => 'delete_additional_service_'.$additionalService->getId(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Admin\AdditionalService;
|
||||||
|
|
||||||
|
use App\Entity\Groups\Accommodation;
|
||||||
|
use App\Entity\Groups\AdditionalService;
|
||||||
|
use App\Form\Admin\Groups\AdditionalServiceType;
|
||||||
|
use App\Htmx\HxRedirectResponse;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\Form\FormInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[IsGranted('ROLE_GROUPS_ADMIN')]
|
||||||
|
class EditController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/admin/additional-service/{id}/edit', name: 'app_admin_additionalservice_edit', defaults: ['duplicate' => false])]
|
||||||
|
#[Route('/admin/additional-service/{id}/duplicate', name: 'app_admin_additionalservice_duplicate', defaults: ['duplicate' => true])]
|
||||||
|
public function index(AdditionalService $additionalService, bool $duplicate, Request $request): Response
|
||||||
|
{
|
||||||
|
$accommodation = $additionalService->getAccommodation();
|
||||||
|
|
||||||
|
if (true === $duplicate) {
|
||||||
|
$additionalService = clone $additionalService;
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = $this->createForm(AdditionalServiceType::class, $additionalService, ['currency' => $accommodation->getCurrency()]);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
if (true === $duplicate) {
|
||||||
|
$accommodation->addAdditionalService($additionalService);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->entityManager->persist($additionalService);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$this->addFlash('success', 'Die Zusatzleistung wurde aktualisiert');
|
||||||
|
|
||||||
|
$this->logger->info('Edited additional service', [
|
||||||
|
'name' => $accommodation->getName(),
|
||||||
|
'code' => $accommodation->getCalendarCode(),
|
||||||
|
'label' => $additionalService->getLabel(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$returnUrl = $this->generateUrl('app_admin_accommodation_edit', [
|
||||||
|
'id' => $accommodation->getId(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->redirect($returnUrl.'#additional-services');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('admin/additional_service/edit.html.twig', [
|
||||||
|
'accommodation' => $accommodation,
|
||||||
|
'additionalService' => $additionalService,
|
||||||
|
'form' => $form,
|
||||||
|
'duplicate' => $duplicate,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Admin\BoardService;
|
||||||
|
|
||||||
|
use App\Entity\Groups\Accommodation;
|
||||||
|
use App\Entity\Groups\BoardService;
|
||||||
|
use App\Form\Admin\Groups\BoardServiceType;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[IsGranted('ROLE_GROUPS_ADMIN')]
|
||||||
|
class CreateController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/admin/board-service/{id}/create', name: 'app_admin_boardservice_create')]
|
||||||
|
public function index(Accommodation $accommodation, Request $request): Response
|
||||||
|
{
|
||||||
|
$boardService = new BoardService();
|
||||||
|
|
||||||
|
$form = $this->createForm(BoardServiceType::class, $boardService, ['currency' => $accommodation->getCurrency()]);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$accommodation->addBoardService($boardService);
|
||||||
|
|
||||||
|
$this->entityManager->persist($boardService);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$this->addFlash('success', 'Die Verpflegungsleistung wurde angelegt');
|
||||||
|
|
||||||
|
$this->logger->info('Created board service', [
|
||||||
|
'name' => $accommodation->getName(),
|
||||||
|
'code' => $accommodation->getCalendarCode(),
|
||||||
|
'label' => $boardService->getLabel(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$returnUrl = $this->redirectToRoute('app_admin_accommodation_edit', [
|
||||||
|
'id' => $accommodation->getId(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->redirect($returnUrl.'#board-services');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('admin/board_service/create.html.twig', [
|
||||||
|
'accommodation' => $accommodation,
|
||||||
|
'form' => $form,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Admin\BoardService;
|
||||||
|
|
||||||
|
use App\Entity\Groups\BoardService;
|
||||||
|
use App\Htmx\HxRedirectResponse;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[IsGranted('ROLE_GROUPS_ADMIN')]
|
||||||
|
class DeleteController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/admin/board-service/{id}/delete', name: 'app_admin_boardservice_delete')]
|
||||||
|
public function index(BoardService $boardService, Request $request): Response
|
||||||
|
{
|
||||||
|
$accommodation = $boardService->getAccommodation();
|
||||||
|
|
||||||
|
if (true === $request->isMethod(Request::METHOD_POST)) {
|
||||||
|
if (!$this->isCsrfTokenValid('delete_board_service_'.$boardService->getId(), $request->request->getString('_token'))) {
|
||||||
|
throw $this->createAccessDeniedException('Invalid CSRF token.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->entityManager->remove($boardService);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$this->addFlash('success', 'Die Verpflegungsleistung wurde gelöscht');
|
||||||
|
|
||||||
|
$this->logger->info('Deleted board service', [
|
||||||
|
'name' => $accommodation->getName(),
|
||||||
|
'code' => $accommodation->getCalendarCode(),
|
||||||
|
'label' => $boardService->getLabel(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$returnUrl = $this->generateUrl('app_admin_accommodation_edit', [
|
||||||
|
'id' => $accommodation->getId(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return new HxRedirectResponse($returnUrl.'#board-services');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('admin/board_service/modal_delete.html.twig', [
|
||||||
|
'boardService' => $boardService,
|
||||||
|
'accommodation' => $accommodation,
|
||||||
|
'csrf_token_id' => 'delete_board_service_'.$boardService->getId(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Admin\BoardService;
|
||||||
|
|
||||||
|
use App\Entity\Groups\BoardService;
|
||||||
|
use App\Form\Admin\Groups\BoardServiceType;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[IsGranted('ROLE_GROUPS_ADMIN')]
|
||||||
|
class EditController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/admin/board-service/{id}/edit', name: 'app_admin_boardservice_edit', defaults: ['duplicate' => false])]
|
||||||
|
#[Route('/admin/board-service/{id}/duplicate', name: 'app_admin_boardservice_duplicate', defaults: ['duplicate' => true])]
|
||||||
|
public function index(BoardService $boardService, bool $duplicate, Request $request): Response
|
||||||
|
{
|
||||||
|
$accommodation = $boardService->getAccommodation();
|
||||||
|
|
||||||
|
if (true === $duplicate) {
|
||||||
|
$boardService = clone $boardService;
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = $this->createForm(BoardServiceType::class, $boardService, ['currency' => $accommodation->getCurrency()]);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
if (true === $duplicate) {
|
||||||
|
$accommodation->addBoardService($boardService);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->entityManager->persist($boardService);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$this->addFlash('success', 'Die Verpflegungsleistung wurde aktualisiert');
|
||||||
|
|
||||||
|
$this->logger->info('Edited board service', [
|
||||||
|
'name' => $accommodation->getName(),
|
||||||
|
'code' => $accommodation->getCalendarCode(),
|
||||||
|
'label' => $boardService->getLabel(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$returnUrl = $this->redirectToRoute('app_admin_accommodation_edit', [
|
||||||
|
'id' => $accommodation->getId(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->redirect($returnUrl.'#board-services');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('admin/board_service/edit.html.twig', [
|
||||||
|
'accommodation' => $accommodation,
|
||||||
|
'boardService' => $boardService,
|
||||||
|
'form' => $form,
|
||||||
|
'duplicate' => $duplicate,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Admin\BookingEditDraft;
|
||||||
|
|
||||||
|
use App\Controller\Traits\ReturnUrlTrait;
|
||||||
|
use App\Entity\BookingEditDraft;
|
||||||
|
use App\Htmx\HxRedirectResponse;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[IsGranted('ROLE_GROUPS_ADMIN')]
|
||||||
|
class DeleteController extends AbstractController
|
||||||
|
{
|
||||||
|
use ReturnUrlTrait;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly LoggerInterface $adminLogger,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/admin/booking-edit-draft/{id}/delete', name: 'app_admin_bookingeditdraft_delete')]
|
||||||
|
public function index(BookingEditDraft $draft, Request $request): Response
|
||||||
|
{
|
||||||
|
if (true === $request->isMethod(Request::METHOD_POST)) {
|
||||||
|
if (!$this->isCsrfTokenValid('delete_booking_edit_draft_'.$draft->getId(), $request->request->getString('_token'))) {
|
||||||
|
throw $this->createAccessDeniedException('Invalid CSRF token.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->entityManager->remove($draft);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$this->adminLogger->info('Delete booking edit draft', [
|
||||||
|
'booking_number' => $draft->getBookingNumber(),
|
||||||
|
]);
|
||||||
|
$this->addFlash('success', 'Der Buchungsentwurf wurde gelöscht');
|
||||||
|
|
||||||
|
$returnUrl = $this->getReturnUrl($request, 'app_admin_bookingeditdraft');
|
||||||
|
|
||||||
|
return new HxRedirectResponse($returnUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('admin/booking_edit_draft/modal_delete.html.twig', [
|
||||||
|
'draft' => $draft,
|
||||||
|
'csrf_token_id' => 'delete_booking_edit_draft_'.$draft->getId(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Admin\BookingEditDraft;
|
||||||
|
|
||||||
|
use App\Controller\Traits\ReturnUrlTrait;
|
||||||
|
use App\Entity\BookingEditDraft;
|
||||||
|
use App\Service\BookingExporter;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[IsGranted('ROLE_GROUPS_ADMIN')]
|
||||||
|
class ExportController extends AbstractController
|
||||||
|
{
|
||||||
|
use ReturnUrlTrait;
|
||||||
|
|
||||||
|
public function __construct(private readonly BookingExporter $bookingExporter)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/admin/booking-edit-draft/{id}/export', name: 'app_admin_bookingeditdraft_export')]
|
||||||
|
public function index(BookingEditDraft $draft): Response
|
||||||
|
{
|
||||||
|
if (false === $draft->hasExportData()) {
|
||||||
|
$this->addFlash('warning', 'Der Export ist fehlgeschlagen');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('app_admin_bookingeditdraft');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return $this->bookingExporter->createExportResponse($draft);
|
||||||
|
} catch (\RuntimeException $e) {
|
||||||
|
$this->addFlash('warning', 'Der Export ist fehlgeschlagen: '.$e->getMessage());
|
||||||
|
|
||||||
|
return $this->redirectToRoute('app_admin_bookingeditdraft');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Admin\BookingEditDraft;
|
||||||
|
|
||||||
|
use App\Controller\Traits\ReturnUrlTrait;
|
||||||
|
use App\Repository\BookingEditDraftRepository;
|
||||||
|
use Knp\Component\Pager\PaginatorInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[IsGranted('ROLE_GROUPS_ADMIN')]
|
||||||
|
class IndexController extends AbstractController
|
||||||
|
{
|
||||||
|
use ReturnUrlTrait;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly BookingEditDraftRepository $bookingEditDraftRepository,
|
||||||
|
private readonly PaginatorInterface $paginator,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/admin/booking-edit-draft', name: 'app_admin_bookingeditdraft')]
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$qb = $this
|
||||||
|
->bookingEditDraftRepository
|
||||||
|
->createQueryBuilder('booking_edit_draft')
|
||||||
|
->leftJoin('booking_edit_draft.user', 'user')
|
||||||
|
;
|
||||||
|
|
||||||
|
$pagination = $this->paginator->paginate(
|
||||||
|
$qb,
|
||||||
|
$request->query->getInt('page', 1),
|
||||||
|
$request->query->getInt('limit', 20),
|
||||||
|
[
|
||||||
|
'defaultSortFieldName' => 'booking_edit_draft.createdAt',
|
||||||
|
'defaultSortDirection' => 'DESC',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->render('admin/booking_edit_draft/index.html.twig', [
|
||||||
|
'pagination' => $pagination,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Admin\BookingEditDraft;
|
||||||
|
|
||||||
|
use App\Controller\Traits\ReturnUrlTrait;
|
||||||
|
use App\Entity\BookingEditDraft;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[IsGranted('ROLE_GROUPS_ADMIN')]
|
||||||
|
class ShowController extends AbstractController
|
||||||
|
{
|
||||||
|
use ReturnUrlTrait;
|
||||||
|
|
||||||
|
#[Route('/admin/booking-edit-draft/{id}/show', name: 'app_admin_bookingeditdraft_show')]
|
||||||
|
public function index(BookingEditDraft $draft, Request $request): Response
|
||||||
|
{
|
||||||
|
$returnUrl = $this->getReturnUrl($request, 'app_admin_bookingeditdraft');
|
||||||
|
|
||||||
|
return $this->render('admin/booking_edit_draft/show.html.twig', [
|
||||||
|
'draft' => $draft,
|
||||||
|
'returnUrl' => $returnUrl,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Controller\Admin;
|
|
||||||
|
|
||||||
use App\Controller\Traits\ReturnUrlTrait;
|
|
||||||
use App\Entity\BookingEditDraft;
|
|
||||||
use App\Htmx\HxRedirectResponse;
|
|
||||||
use App\Repository\BookingEditDraftRepository;
|
|
||||||
use App\Service\BookingExporter;
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
|
||||||
use Knp\Component\Pager\PaginatorInterface;
|
|
||||||
use Psr\Log\LoggerInterface;
|
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
|
||||||
|
|
||||||
class BookingEditDraftController extends AbstractController
|
|
||||||
{
|
|
||||||
use ReturnUrlTrait;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
private readonly EntityManagerInterface $entityManager,
|
|
||||||
private readonly BookingEditDraftRepository $bookingEditDraftRepository,
|
|
||||||
private readonly PaginatorInterface $paginator,
|
|
||||||
private readonly BookingExporter $bookingExporter,
|
|
||||||
private readonly LoggerInterface $adminLogger,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('/admin/booking-edit-draft', name: 'app_admin_bookingeditdraft')]
|
|
||||||
public function index(Request $request): Response
|
|
||||||
{
|
|
||||||
$qb = $this
|
|
||||||
->bookingEditDraftRepository
|
|
||||||
->createQueryBuilder('booking_edit_draft')
|
|
||||||
->leftJoin('booking_edit_draft.user', 'user')
|
|
||||||
;
|
|
||||||
|
|
||||||
$pagination = $this->paginator->paginate(
|
|
||||||
$qb,
|
|
||||||
$request->query->getInt('page', 1),
|
|
||||||
$request->query->getInt('limit', 20),
|
|
||||||
[
|
|
||||||
'defaultSortFieldName' => 'booking_edit_draft.createdAt',
|
|
||||||
'defaultSortDirection' => 'DESC',
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
return $this->render('admin/booking_edit_draft/index.html.twig', [
|
|
||||||
'pagination' => $pagination,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('/admin/booking-edit-draft/{id}/show', name: 'app_admin_bookingeditdraft_show')]
|
|
||||||
public function show(BookingEditDraft $draft, Request $request): Response
|
|
||||||
{
|
|
||||||
$returnUrl = $this->getReturnUrl($request, 'app_admin_bookingeditdraft');
|
|
||||||
|
|
||||||
return $this->render('admin/booking_edit_draft/show.html.twig', [
|
|
||||||
'draft' => $draft,
|
|
||||||
'returnUrl' => $returnUrl,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('/admin/booking-edit-draft/{id}/delete', name: 'app_admin_bookingeditdraft_delete')]
|
|
||||||
public function delete(BookingEditDraft $draft, Request $request): Response
|
|
||||||
{
|
|
||||||
if (true === $request->isMethod(Request::METHOD_POST)) {
|
|
||||||
$this->entityManager->remove($draft);
|
|
||||||
$this->entityManager->flush();
|
|
||||||
|
|
||||||
$this->adminLogger->info('Delete booking edit draft', [
|
|
||||||
'booking_number' => $draft->getBookingNumber(),
|
|
||||||
]);
|
|
||||||
$this->addFlash('success', 'Der Buchungsentwurf wurde gelöscht');
|
|
||||||
|
|
||||||
$returnUrl = $this->getReturnUrl($request, 'app_admin_bookingeditdraft');
|
|
||||||
|
|
||||||
return new HxRedirectResponse($returnUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->render('admin/booking_edit_draft/modal_delete.html.twig', [
|
|
||||||
'draft' => $draft,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('/admin/booking-edit-draft/{id}/export', name: 'app_admin_bookingeditdraft_export')]
|
|
||||||
public function export(BookingEditDraft $draft): Response
|
|
||||||
{
|
|
||||||
if (false === $draft->hasExportData()) {
|
|
||||||
$this->addFlash('warning', 'Der Export ist fehlgeschlagen');
|
|
||||||
|
|
||||||
return $this->redirectToRoute('app_admin_bookingeditdraft');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return $this->bookingExporter->createExportResponse($draft);
|
|
||||||
} catch (\RuntimeException $e) {
|
|
||||||
$this->addFlash('warning', 'Der Export ist fehlgeschlagen: '.$e->getMessage());
|
|
||||||
|
|
||||||
return $this->redirectToRoute('app_admin_bookingeditdraft');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,10 +4,13 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Controller\Admin;
|
namespace App\Controller\Admin;
|
||||||
|
|
||||||
|
use App\Security\Voter\AdministrativeAccessVoter;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[IsGranted(AdministrativeAccessVoter::ADMINISTRATIVE_ACCESS)]
|
||||||
class DashboardController extends AbstractController
|
class DashboardController extends AbstractController
|
||||||
{
|
{
|
||||||
#[Route('/admin/dashboard', name: 'app_admin_dashboard')]
|
#[Route('/admin/dashboard', name: 'app_admin_dashboard')]
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Admin\Log;
|
||||||
|
|
||||||
|
use App\Service\XmlDumpReader;
|
||||||
|
use League\Flysystem\FilesystemException;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[IsGranted('ROLE_ADMIN')]
|
||||||
|
class DownloadController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(private readonly XmlDumpReader $xmlDumpReader)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/admin/log/download/{filename}', name: 'app_admin_log_xmldump_download', requirements: ['filename' => '.+'])]
|
||||||
|
public function index(string $filename): Response
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (false === $this->xmlDumpReader->fileExists($filename)) {
|
||||||
|
throw $this->createNotFoundException('Dump file not found. It may have been cleaned up.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = $this->xmlDumpReader->getContent($filename);
|
||||||
|
} catch (FilesystemException $e) {
|
||||||
|
throw $this->createNotFoundException('Failed to read dump file: '.$e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = new Response($content);
|
||||||
|
$response->headers->set('Content-Type', 'application/xml');
|
||||||
|
|
||||||
|
$disposition = $response->headers->makeDisposition(
|
||||||
|
ResponseHeaderBag::DISPOSITION_ATTACHMENT,
|
||||||
|
basename($filename)
|
||||||
|
);
|
||||||
|
$response->headers->set('Content-Disposition', $disposition);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Admin\Log;
|
||||||
|
|
||||||
|
use App\Repository\LogEntryRepository;
|
||||||
|
use Knp\Component\Pager\PaginatorInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[IsGranted('ROLE_ADMIN')]
|
||||||
|
class IndexController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly LogEntryRepository $logEntryRepository,
|
||||||
|
private readonly PaginatorInterface $paginator,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/admin/log', name: 'app_admin_log')]
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$qb = $this
|
||||||
|
->logEntryRepository
|
||||||
|
->createQueryBuilder('log_entry')
|
||||||
|
;
|
||||||
|
|
||||||
|
$pagination = $this->paginator->paginate(
|
||||||
|
$qb,
|
||||||
|
$request->query->getInt('page', 1),
|
||||||
|
$request->query->getInt('limit', 50),
|
||||||
|
[
|
||||||
|
'defaultSortFieldName' => 'log_entry.createdAt',
|
||||||
|
'defaultSortDirection' => 'DESC',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->render('admin/log/index.html.twig', [
|
||||||
|
'pagination' => $pagination,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Admin\Log;
|
||||||
|
|
||||||
|
use App\Entity\LogEntry;
|
||||||
|
use App\Service\XmlDumpReader;
|
||||||
|
use League\Flysystem\FilesystemException;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[IsGranted('ROLE_ADMIN')]
|
||||||
|
class XmlDumpController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(private readonly XmlDumpReader $xmlDumpReader)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/admin/log/{id}/xml-dumps', name: 'app_admin_log_xmldumps')]
|
||||||
|
public function index(LogEntry $logEntry): Response
|
||||||
|
{
|
||||||
|
$dumps = [];
|
||||||
|
try {
|
||||||
|
$dumps = $this->xmlDumpReader->findDumpsForRequestId($logEntry->getRequestId());
|
||||||
|
} catch (FilesystemException) {
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('admin/log/xml_dumps.html.twig', [
|
||||||
|
'logEntry' => $logEntry,
|
||||||
|
'dumps' => $dumps,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Controller\Admin;
|
|
||||||
|
|
||||||
use App\Entity\Log;
|
|
||||||
use App\Entity\LogEntry;
|
|
||||||
use App\Repository\LogEntryRepository;
|
|
||||||
use App\Service\XmlDumpReader;
|
|
||||||
use Knp\Component\Pager\PaginatorInterface;
|
|
||||||
use League\Flysystem\FilesystemException;
|
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
|
||||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
|
||||||
|
|
||||||
class LogController extends AbstractController
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly LogEntryRepository $logEntryRepository,
|
|
||||||
private readonly PaginatorInterface $paginator,
|
|
||||||
private readonly XmlDumpReader $xmlDumpReader,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('/admin/log', name: 'app_admin_log')]
|
|
||||||
public function index(Request $request): Response
|
|
||||||
{
|
|
||||||
$qb = $this
|
|
||||||
->logEntryRepository
|
|
||||||
->createQueryBuilder('log_entry')
|
|
||||||
;
|
|
||||||
|
|
||||||
$pagination = $this->paginator->paginate(
|
|
||||||
$qb,
|
|
||||||
$request->query->getInt('page', 1),
|
|
||||||
$request->query->getInt('limit', 50),
|
|
||||||
[
|
|
||||||
'defaultSortFieldName' => 'log_entry.createdAt',
|
|
||||||
'defaultSortDirection' => 'DESC',
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
return $this->render('admin/log/index.html.twig', [
|
|
||||||
'pagination' => $pagination,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('/admin/log/{id}/xml-dumps', name: 'app_admin_log_xmldumps')]
|
|
||||||
public function xmlDumps(LogEntry $logEntry): Response
|
|
||||||
{
|
|
||||||
$dumps = [];
|
|
||||||
try {
|
|
||||||
$dumps = $this->xmlDumpReader->findDumpsForRequestId($logEntry->getRequestId());
|
|
||||||
} catch (FilesystemException) {
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->render('admin/log/xml_dumps.html.twig', [
|
|
||||||
'logEntry' => $logEntry,
|
|
||||||
'dumps' => $dumps,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('/admin/log/download/{filename}', name: 'app_admin_log_xmldump_download', requirements: ['filename' => '.+'])]
|
|
||||||
public function downloadXmlDump(string $filename): Response
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
if (false === $this->xmlDumpReader->fileExists($filename)) {
|
|
||||||
throw $this->createNotFoundException('Dump file not found. It may have been cleaned up.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$content = $this->xmlDumpReader->getContent($filename);
|
|
||||||
} catch (FilesystemException $e) {
|
|
||||||
throw $this->createNotFoundException('Failed to read dump file: '.$e->getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
$response = new Response($content);
|
|
||||||
$response->headers->set('Content-Type', 'application/xml');
|
|
||||||
|
|
||||||
$disposition = $response->headers->makeDisposition(
|
|
||||||
ResponseHeaderBag::DISPOSITION_ATTACHMENT,
|
|
||||||
basename($filename)
|
|
||||||
);
|
|
||||||
$response->headers->set('Content-Disposition', $disposition);
|
|
||||||
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+4
-2
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Controller\Admin;
|
namespace App\Controller\Admin\User;
|
||||||
|
|
||||||
use App\Repository\UserRepository;
|
use App\Repository\UserRepository;
|
||||||
use Knp\Component\Pager\PaginatorInterface;
|
use Knp\Component\Pager\PaginatorInterface;
|
||||||
@@ -10,8 +10,10 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
class UserController extends AbstractController
|
#[IsGranted('ROLE_ADMIN')]
|
||||||
|
class IndexController extends AbstractController
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly UserRepository $userRepository,
|
private readonly UserRepository $userRepository,
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller\Api;
|
||||||
|
|
||||||
|
use App\Entity\Groups\AccommodationBooking;
|
||||||
|
use App\Model\AccommodationBookingApiResponse;
|
||||||
|
use App\Repository\Groups\AccommodationBookingRepository;
|
||||||
|
use App\Service\AccommodationBookingBreakdownCalculator;
|
||||||
|
use App\Service\AccommodationBookingService;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
use Symfony\Component\Serializer\SerializerInterface;
|
||||||
|
|
||||||
|
#[Route('/api')]
|
||||||
|
#[IsGranted('ROLE_OAUTH2_API')]
|
||||||
|
class AccommodationBookingController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly AccommodationBookingRepository $bookingRepository,
|
||||||
|
private readonly AccommodationBookingBreakdownCalculator $breakdownCalculator,
|
||||||
|
private readonly SerializerInterface $serializer,
|
||||||
|
private readonly AccommodationBookingService $bookingService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/accommodation-bookings/{uuid}', name: 'api_accommodation_bookings_single', methods: ['GET'])]
|
||||||
|
public function single(string $uuid): JsonResponse
|
||||||
|
{
|
||||||
|
$booking = $this->bookingRepository->findOneBy(['uuid' => $uuid]);
|
||||||
|
|
||||||
|
if (null === $booking) {
|
||||||
|
return new JsonResponse(['message' => 'Not found'], Response::HTTP_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->bookingResponse($booking);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/accommodation-bookings/{uuid}/accept', name: 'api_accommodation_bookings_accept', methods: ['POST'])]
|
||||||
|
public function accept(string $uuid): JsonResponse
|
||||||
|
{
|
||||||
|
$booking = $this->bookingRepository->findOneBy(['uuid' => $uuid]);
|
||||||
|
|
||||||
|
if (null === $booking) {
|
||||||
|
return new JsonResponse(['message' => 'Not found'], Response::HTTP_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->bookingService->acceptBooking($booking);
|
||||||
|
|
||||||
|
return $this->bookingResponse($booking);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function bookingResponse(AccommodationBooking $booking): JsonResponse
|
||||||
|
{
|
||||||
|
$response = new AccommodationBookingApiResponse($booking, $this->breakdownCalculator->compute($booking));
|
||||||
|
$json = $this->serializer->serialize($response, 'json', ['groups' => ['api:single']]);
|
||||||
|
|
||||||
|
return new JsonResponse($json, Response::HTTP_OK, [], true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Api;
|
||||||
|
|
||||||
|
use App\BpnConnect\ContingentsClient;
|
||||||
|
use App\BpnConnect\Exception\BpnConnectException;
|
||||||
|
use App\Entity\Groups\AccommodationPrice;
|
||||||
|
use App\Enum\Groups\PriceType;
|
||||||
|
use App\Model\ContingentCalendarQuery;
|
||||||
|
use App\Model\ContingentPricesQuery;
|
||||||
|
use App\Repository\Groups\AccommodationPriceRepository;
|
||||||
|
use App\Repository\Groups\AccommodationRepository;
|
||||||
|
use App\Service\PriceTimelineBuilder;
|
||||||
|
use Psr\Cache\InvalidArgumentException;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\Attribute\MapQueryString;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
use Symfony\Contracts\Cache\CacheInterface;
|
||||||
|
use Symfony\Contracts\Cache\ItemInterface;
|
||||||
|
|
||||||
|
#[Route('/api')]
|
||||||
|
#[IsGranted('ROLE_OAUTH2_API')]
|
||||||
|
class ContingentController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ContingentsClient $contingentsClient,
|
||||||
|
private readonly AccommodationRepository $accommodationRepository,
|
||||||
|
private readonly AccommodationPriceRepository $priceRepository,
|
||||||
|
private readonly CacheInterface $cache,
|
||||||
|
private readonly PriceTimelineBuilder $priceTimelineBuilder,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/contingents/prices', name: 'api_contingents_prices', methods: ['GET'])]
|
||||||
|
public function prices(
|
||||||
|
#[MapQueryString(validationFailedStatusCode: Response::HTTP_BAD_REQUEST)]
|
||||||
|
ContingentPricesQuery $query,
|
||||||
|
): JsonResponse {
|
||||||
|
$accommodation = $this->accommodationRepository->findOneBy(['calendarCode' => $query->hotelCode]);
|
||||||
|
|
||||||
|
if (null === $accommodation) {
|
||||||
|
return $this->json(['error' => 'hotel not found for hotelCode.'], Response::HTTP_BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
$yearStart = new \DateTimeImmutable("{$query->year}-01-01");
|
||||||
|
$yearEnd = new \DateTimeImmutable("{$query->year}-12-31");
|
||||||
|
|
||||||
|
$prices = $this->priceRepository->findByHotelCodeAndDateRange($query->hotelCode, $yearStart, $yearEnd);
|
||||||
|
|
||||||
|
$currency = $accommodation->getCurrency();
|
||||||
|
|
||||||
|
return $this->json($this->priceTimelineBuilder->buildTimeline($prices, $yearStart, $yearEnd, $currency));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/contingents/calendar', name: 'api_contingents_calendar', methods: ['GET'])]
|
||||||
|
public function calendar(
|
||||||
|
#[MapQueryString(validationFailedStatusCode: Response::HTTP_BAD_REQUEST)]
|
||||||
|
ContingentCalendarQuery $query,
|
||||||
|
): JsonResponse {
|
||||||
|
$dateFrom = $query->dateFromDate();
|
||||||
|
$dateTo = $query->dateToDate();
|
||||||
|
|
||||||
|
$accommodation = $this->accommodationRepository->findOneBy(['calendarCode' => $query->hotelCode]);
|
||||||
|
|
||||||
|
if (null === $accommodation) {
|
||||||
|
return $this->json(['error' => 'hotel not found for hotelCode.'], Response::HTTP_BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$cacheKey = sprintf('contingents_calendar_%s_%s_%s', $query->hotelCode, $query->dateFrom, $query->dateTo);
|
||||||
|
$calendar = $this->cache->get($cacheKey, function (ItemInterface $item) use ($query) {
|
||||||
|
$item->expiresAfter(3600);
|
||||||
|
|
||||||
|
return $this->contingentsClient->getContingentCalendar($query->hotelCode, $query->dateFrom, $query->dateTo);
|
||||||
|
});
|
||||||
|
} catch (BpnConnectException|InvalidArgumentException $e) {
|
||||||
|
return $this->json(['error' => 'Failed to fetch contingent data.'], Response::HTTP_BAD_GATEWAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
$prices = $this->priceRepository->findByHotelCodeAndDateRange($query->hotelCode, $dateFrom, $dateTo);
|
||||||
|
|
||||||
|
$currency = $accommodation->getCurrency();
|
||||||
|
|
||||||
|
$data = array_map(
|
||||||
|
fn ($entry) => $this->enrichEntry($entry->date, $entry->status->value, $prices, $currency),
|
||||||
|
$calendar->data,
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->json($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param AccommodationPrice[] $prices
|
||||||
|
*
|
||||||
|
* @return array{date: string, status: string, type: string|null, pricePerNight: float|null, defaultPricePerNight: float|null, priceAdditionalPerson: float|null, defaultPriceAdditionalPerson: float|null, includedPax: int|null, minNights: int|null}
|
||||||
|
*/
|
||||||
|
private function enrichEntry(string $date, string $status, array $prices, string $currency): array
|
||||||
|
{
|
||||||
|
$day = (new \DateTimeImmutable($date))->setTime(0, 0);
|
||||||
|
|
||||||
|
// dateFrom and dateTo are both inclusive (last night, not checkout day)
|
||||||
|
$candidates = array_filter(
|
||||||
|
$prices,
|
||||||
|
fn ($p) => $p->getDateFrom() <= $day && $p->getDateTo() >= $day,
|
||||||
|
);
|
||||||
|
|
||||||
|
$winner = $this->priceTimelineBuilder->resolveWinner($candidates);
|
||||||
|
|
||||||
|
$defaultPrice = null;
|
||||||
|
if (PriceType::DISCOUNT === $winner?->getType()) {
|
||||||
|
$defaults = array_filter($candidates, fn ($p) => null === $p->getType());
|
||||||
|
$defaultPrice = $this->priceTimelineBuilder->resolveWinner($defaults);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'date' => $date,
|
||||||
|
'status' => $status,
|
||||||
|
'type' => $winner?->getType()?->value,
|
||||||
|
'pricePerNight' => null !== $winner ? round($winner->getPricePerNight() / 100, 2) : null,
|
||||||
|
'defaultPricePerNight' => null !== $defaultPrice ? round($defaultPrice->getPricePerNight() / 100, 2) : null,
|
||||||
|
'priceAdditionalPerson' => null !== $winner ? round($winner->getPriceAdditionalPerson() / 100, 2) : null,
|
||||||
|
'defaultPriceAdditionalPerson' => null !== $defaultPrice ? round($defaultPrice->getPriceAdditionalPerson() / 100, 2) : null,
|
||||||
|
'currency' => $currency,
|
||||||
|
'includedPax' => $winner?->getIncludedPax(),
|
||||||
|
'minNights' => $winner?->getMinNights(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,7 +49,7 @@ class IndexController extends AbstractController
|
|||||||
path: '/bookings/create',
|
path: '/bookings/create',
|
||||||
name: 'app_booking_create',
|
name: 'app_booking_create',
|
||||||
)]
|
)]
|
||||||
public function index(Request $request, #[MapQueryString] ?BookingQueryParams $params): Response
|
public function index(#[MapQueryString] ?BookingQueryParams $params): Response
|
||||||
{
|
{
|
||||||
if (null === $params) {
|
if (null === $params) {
|
||||||
throw $this->createNotFoundException('Invalid booking parameters provided');
|
throw $this->createNotFoundException('Invalid booking parameters provided');
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Groups;
|
||||||
|
|
||||||
|
use App\Exception\AccommodationSessionNotFoundException;
|
||||||
|
use App\Form\Model\AccommodationBookingDto;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
abstract class AbstractAccommodationController extends AbstractController
|
||||||
|
{
|
||||||
|
protected function validateStepAccess(AccommodationBookingDto $dto, int $expectedStep): ?RedirectResponse
|
||||||
|
{
|
||||||
|
if ($expectedStep > $dto->currentStep) {
|
||||||
|
$this->addFlash('error', 'Bitte erst die vorherigen Schritte abschließen.');
|
||||||
|
|
||||||
|
return $this->redirectToCurrentStep($dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createFailureResponse(\Throwable $exception, bool $htmx): Response
|
||||||
|
{
|
||||||
|
if ($htmx) {
|
||||||
|
return new Response('', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$message = $exception instanceof AccommodationSessionNotFoundException
|
||||||
|
? 'Deine Sitzung ist abgelaufen. Bitte starte die Anfrage erneut.'
|
||||||
|
: 'Die Anfrage konnte nicht geladen werden.';
|
||||||
|
|
||||||
|
$this->addFlash('error', $message);
|
||||||
|
|
||||||
|
return $this->redirectToRoute('app_groups_booking_error');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function redirectToCurrentStep(AccommodationBookingDto $dto): RedirectResponse
|
||||||
|
{
|
||||||
|
$route = match ($dto->currentStep) {
|
||||||
|
2 => 'app_groups_booking_step_2',
|
||||||
|
3 => 'app_groups_booking_step_3',
|
||||||
|
4 => 'app_groups_booking_step_4',
|
||||||
|
default => 'app_groups_booking_step_1',
|
||||||
|
};
|
||||||
|
|
||||||
|
return $this->redirectToRoute($route);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user