Merge branch 'PreorderIFrame/add-new-capabilities' into 'master'
added new capabilities in iframe See merge request fronk/thetool!1540
This commit is contained in:
@@ -9,8 +9,6 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Bestellformular</title>
|
||||
|
||||
<!-- <script src="https://cdn.jsdelivr.net/npm/vue@3.4.27/dist/vue.global.prod.js"></script>-->
|
||||
<!-- use non production vue for testing-->
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@3.4.27/dist/vue.global.js"></script>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios@1.7.2/dist/axios.min.js"></script>
|
||||
@@ -18,40 +16,22 @@
|
||||
window.mfNotify = <?=isset($mfNotify) ? json_encode($mfNotify) : "null"; ?>;
|
||||
window.TT_CONFIG = {};
|
||||
<?php
|
||||
// var_dump($JSGlobals);exit;
|
||||
?>
|
||||
|
||||
<?php
|
||||
if(isset($JSGlobals) && is_array($JSGlobals) && count($JSGlobals)):
|
||||
foreach($JSGlobals as $key => $value): ?>
|
||||
window.TT_CONFIG.<?=$key?> = <?=is_array($value) ? json_encode($value) : "'$value'"; ?>;
|
||||
window.TT_CONFIG.<?=$key?> = <?=is_array($value) || is_object($value) ? json_encode($value) : "'$value'"; ?>;
|
||||
<?php endforeach; endif;?>
|
||||
</script>
|
||||
|
||||
<style id="theme-style">
|
||||
/* Color Theme CSS Variables will be populated by Vue */
|
||||
:root {}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
.slide-fade-enter-active, .slide-fade-leave-active {
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
.slide-fade-enter-from, .slide-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(15px);
|
||||
}
|
||||
/* Default border for inputs for better visibility */
|
||||
.form-input, .form-textarea, .form-select {
|
||||
border-width: 1px;
|
||||
}
|
||||
.custom-select-button {
|
||||
border-width: 1px;
|
||||
}
|
||||
body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
|
||||
.slide-fade-enter-active, .slide-fade-leave-active { transition: opacity 0.3s ease, transform 0.3s ease; }
|
||||
.slide-fade-enter-from, .slide-fade-leave-to { opacity: 0; transform: translateX(15px); }
|
||||
.form-input, .form-textarea, .form-select { border-width: 1px; }
|
||||
.custom-select-button { border-width: 1px; }
|
||||
.form-input:focus, .form-select:focus, .custom-select-button:focus-within, .form-textarea:focus {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
@@ -59,10 +39,7 @@
|
||||
box-shadow: 0 0 0 2px var(--tw-ring-color);
|
||||
border-color: var(--color-primary-500);
|
||||
}
|
||||
.wizard-step-active {
|
||||
border-color: var(--color-primary-500);
|
||||
box-shadow: 0 0 0 1px var(--color-primary-500);
|
||||
}
|
||||
.wizard-step-active { border-color: var(--color-primary-500); box-shadow: 0 0 0 1px var(--color-primary-500); }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-slate-100 flex items-start justify-center min-h-full p-4 font-sans">
|
||||
@@ -71,157 +48,79 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Injected configuration from the PHP Controller
|
||||
window.VUE_APP_CONFIG = window.TT_CONFIG
|
||||
window.VUE_APP_CONFIG = window.TT_CONFIG;
|
||||
|
||||
const { createApp, ref, reactive, onMounted, onBeforeUnmount, watch, computed, nextTick } = Vue;
|
||||
|
||||
const SearchableSelect = {
|
||||
props: {
|
||||
options: { type: Array, required: true },
|
||||
modelValue: { type: [String, Number], default: '' },
|
||||
placeholder: { type: String, default: 'Bitte wählen...' },
|
||||
disabled: { type: Boolean, default: false },
|
||||
getOptionLabel: { type: Function, default: (opt) => opt },
|
||||
getOptionKey: { type: Function, default: (opt) => opt }
|
||||
options: { type: Array, required: true }, modelValue: { type: [String, Number], default: '' },
|
||||
placeholder: { type: String, default: 'Bitte wählen...' }, disabled: { type: Boolean, default: false },
|
||||
getOptionLabel: { type: Function, default: (opt) => opt }, getOptionKey: { type: Function, default: (opt) => opt }
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup(props, { emit }) {
|
||||
const isOpen = ref(false);
|
||||
const searchTerm = ref('');
|
||||
const highlightedIndex = ref(-1);
|
||||
const searchInput = ref(null);
|
||||
const rootEl = ref(null);
|
||||
|
||||
const isOpen = ref(false); const searchTerm = ref(''); const highlightedIndex = ref(-1);
|
||||
const searchInput = ref(null); const rootEl = ref(null);
|
||||
const filteredOptions = computed(() => {
|
||||
if (!searchTerm.value) return props.options;
|
||||
return props.options.filter(option =>
|
||||
props.getOptionLabel(option).toLowerCase().includes(searchTerm.value.toLowerCase())
|
||||
);
|
||||
return props.options.filter(option => props.getOptionLabel(option).toLowerCase().includes(searchTerm.value.toLowerCase()));
|
||||
});
|
||||
|
||||
function closeDropdown() {
|
||||
isOpen.value = false;
|
||||
searchTerm.value = '';
|
||||
highlightedIndex.value = -1;
|
||||
}
|
||||
|
||||
const handleClickOutside = (event) => {
|
||||
if (rootEl.value && !rootEl.value.contains(event.target)) {
|
||||
closeDropdown();
|
||||
}
|
||||
};
|
||||
|
||||
function closeDropdown() { isOpen.value = false; searchTerm.value = ''; highlightedIndex.value = -1; }
|
||||
const handleClickOutside = (event) => { if (rootEl.value && !rootEl.value.contains(event.target)) closeDropdown(); };
|
||||
watch(isOpen, (val) => {
|
||||
if (val) {
|
||||
nextTick(() => {
|
||||
searchInput.value?.focus();
|
||||
document.addEventListener('click', handleClickOutside, true);
|
||||
if (val) { nextTick(() => { searchInput.value?.focus(); document.addEventListener('click', handleClickOutside, true); }); }
|
||||
else { document.removeEventListener('click', handleClickOutside, true); }
|
||||
});
|
||||
} else {
|
||||
document.removeEventListener('click', handleClickOutside, true);
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', handleClickOutside, true);
|
||||
});
|
||||
|
||||
function selectOption(option) {
|
||||
emit('update:modelValue', props.getOptionKey(option));
|
||||
closeDropdown();
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => { document.removeEventListener('click', handleClickOutside, true); });
|
||||
function selectOption(option) { emit('update:modelValue', props.getOptionKey(option)); closeDropdown(); }
|
||||
function onKeyDown(event) {
|
||||
const optionsLength = filteredOptions.value.length;
|
||||
if (!optionsLength) return;
|
||||
const optionsLength = filteredOptions.value.length; if (!optionsLength) return;
|
||||
switch (event.key) {
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
highlightedIndex.value = (highlightedIndex.value + 1) % optionsLength;
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
event.preventDefault();
|
||||
highlightedIndex.value = (highlightedIndex.value - 1 + optionsLength) % optionsLength;
|
||||
break;
|
||||
case 'Enter':
|
||||
event.preventDefault();
|
||||
if (highlightedIndex.value >= 0) {
|
||||
selectOption(filteredOptions.value[highlightedIndex.value]);
|
||||
}
|
||||
break;
|
||||
case 'Escape':
|
||||
closeDropdown();
|
||||
break;
|
||||
case 'ArrowDown': event.preventDefault(); highlightedIndex.value = (highlightedIndex.value + 1) % optionsLength; break;
|
||||
case 'ArrowUp': event.preventDefault(); highlightedIndex.value = (highlightedIndex.value - 1 + optionsLength) % optionsLength; break;
|
||||
case 'Enter': event.preventDefault(); if (highlightedIndex.value >= 0) { selectOption(filteredOptions.value[highlightedIndex.value]); } break;
|
||||
case 'Escape': closeDropdown(); break;
|
||||
}
|
||||
}
|
||||
|
||||
const displayValue = computed(() => {
|
||||
const selected = props.options.find(o => props.getOptionKey(o) === props.modelValue);
|
||||
return selected ? props.getOptionLabel(selected) : '';
|
||||
});
|
||||
|
||||
return {
|
||||
isOpen, searchTerm, highlightedIndex, searchInput, filteredOptions,
|
||||
rootEl, closeDropdown, selectOption, onKeyDown, displayValue
|
||||
};
|
||||
const displayValue = computed(() => { const selected = props.options.find(o => props.getOptionKey(o) === props.modelValue); return selected ? props.getOptionLabel(selected) : ''; });
|
||||
return { isOpen, searchTerm, highlightedIndex, searchInput, filteredOptions, rootEl, closeDropdown, selectOption, onKeyDown, displayValue };
|
||||
},
|
||||
template: `
|
||||
<div class="relative" ref="rootEl">
|
||||
<button type="button" @click="isOpen = !isOpen" :disabled="disabled" class="custom-select-button relative w-full cursor-default rounded-md border border-slate-300 bg-white py-2.5 pl-3 pr-10 text-left shadow-sm focus:outline-none sm:text-sm disabled:bg-slate-100 disabled:cursor-not-allowed">
|
||||
<span class="block truncate" :class="{'text-slate-400': !displayValue}">{{ displayValue || placeholder }}</span>
|
||||
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"><svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 3a.75.75 0 01.53.22l3.5 3.5a.75.75 0 01-1.06 1.06L10 4.81 7.03 7.78a.75.75 0 01-1.06-1.06l3.5-3.5A.75.75 0 0110 3zm-3.72 9.53a.75.75 0 011.06 0L10 15.19l2.97-2.97a.75.75 0 111.06 1.06l-3.5 3.5a.75.75 0 01-1.06 0l-3.5-3.5a.75.75 0 010-1.06z" clip-rule="evenodd" /></svg></span>
|
||||
</button>
|
||||
<transition leave-active-class="transition ease-in duration-100" leave-from-class="opacity-100" leave-to-class="opacity-0">
|
||||
<div v-if="isOpen" class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
|
||||
<div class="p-2">
|
||||
<input ref="searchInput" type="text" v-model="searchTerm" @keydown="onKeyDown" placeholder="Suchen..." class="form-input w-full px-2 py-1.5 border border-slate-200 rounded-md"/>
|
||||
</div>
|
||||
<ul>
|
||||
<li v-for="(option, index) in filteredOptions" :key="getOptionKey(option)" @click="selectOption(option)" :class="{'bg-[var(--color-primary-100)] text-[var(--color-primary-800)]': index === highlightedIndex, 'text-gray-900': index !== highlightedIndex}" class="relative cursor-default select-none py-2 pl-3 pr-9 hover:bg-slate-100">
|
||||
<span class="block truncate">{{ getOptionLabel(option) }}</span>
|
||||
</li>
|
||||
<li v-if="!filteredOptions.length" class="relative cursor-default select-none py-2 px-4 text-gray-500">Keine Ergebnisse</li>
|
||||
</ul>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
`
|
||||
template: `<div class="relative" ref="rootEl"><button type="button" @click="isOpen = !isOpen" :disabled="disabled" class="custom-select-button relative w-full cursor-default rounded-md border border-slate-300 bg-white py-2.5 pl-3 pr-10 text-left shadow-sm focus:outline-none sm:text-sm disabled:bg-slate-100 disabled:cursor-not-allowed"><span class="block truncate" :class="{'text-slate-400': !displayValue}">{{ displayValue || placeholder }}</span><span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"><svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 3a.75.75 0 01.53.22l3.5 3.5a.75.75 0 01-1.06 1.06L10 4.81 7.03 7.78a.75.75 0 01-1.06-1.06l3.5-3.5A.75.75 0 0110 3zm-3.72 9.53a.75.75 0 011.06 0L10 15.19l2.97-2.97a.75.75 0 111.06 1.06l-3.5 3.5a.75.75 0 01-1.06 0l-3.5-3.5a.75.75 0 010-1.06z" clip-rule="evenodd" /></svg></span></button><transition leave-active-class="transition ease-in duration-100" leave-from-class="opacity-100" leave-to-class="opacity-0"><div v-if="isOpen" class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"><div class="p-2"><input ref="searchInput" type="text" v-model="searchTerm" @keydown="onKeyDown" placeholder="Suchen..." class="form-input w-full px-2 py-1.5 border border-slate-200 rounded-md"/></div><ul><li v-for="(option, index) in filteredOptions" :key="getOptionKey(option)" @click="selectOption(option)" :class="{'bg-[var(--color-primary-100)] text-[var(--color-primary-800)]': index === highlightedIndex, 'text-gray-900': index !== highlightedIndex}" class="relative cursor-default select-none py-2 pl-3 pr-9 hover:bg-slate-100"><span class="block truncate">{{ getOptionLabel(option) }}</span></li><li v-if="!filteredOptions.length" class="relative cursor-default select-none py-2 px-4 text-gray-500">Keine Ergebnisse</li></ul></div></transition></div>`
|
||||
};
|
||||
|
||||
const app = createApp({
|
||||
setup() {
|
||||
// --- CONFIGURATION & STATE ---
|
||||
const config = window.VUE_APP_CONFIG;
|
||||
const API_BASE_URL = config.baseUrl;
|
||||
|
||||
const initialArea = reactive({ clusterId: config.clusterId || null, gemeindeId: config.gemeindeId || null });
|
||||
const currentStep = ref('loading');
|
||||
const orderType = ref(config.orderType || 'order');
|
||||
const clusterId = ref(config.clusterId || null);
|
||||
const gemeindeId = ref(config.gemeindeId || null);
|
||||
const clusters = ref([]);
|
||||
const addressStep = ref('zip');
|
||||
const addressForm = reactive({ zip: '', city: '', street: '', housenumber: '', wohneinheit_id: '' });
|
||||
const cities = ref([]);
|
||||
const streets = ref([]);
|
||||
const units = ref([]);
|
||||
const cities = ref([]); const streets = ref([]); const units = ref([]);
|
||||
const selectedAddress = ref(null);
|
||||
const isLoading = ref(false);
|
||||
const errorMessage = ref('');
|
||||
const orderResponse = ref(null);
|
||||
const isLoading = ref(false); const errorMessage = ref(''); const orderResponse = ref(null);
|
||||
const iframeConsents = reactive({});
|
||||
const form = reactive({
|
||||
customerType: 'Privatkunde', title: '', birthDate: '', firstName: '', lastName: '', phone: '', email: '', isOwner: 'Ja', notes: '',
|
||||
billingAddressChoice: 'Anschlussadresse',
|
||||
connectionType: null,
|
||||
billingAddressChoice: 'Anschlussadresse', connectionType: null,
|
||||
billing: { name: '', street: '', housenumber: '', zip: '', city: '' },
|
||||
acceptAgb: false, acceptMarketing: false, acceptWithdrawal: false, acceptDsgvo: false,
|
||||
});
|
||||
// add header where the iframe is embedded
|
||||
|
||||
const referrer = document.referrer.split('?')[0];
|
||||
const api = axios.create({ baseURL: API_BASE_URL, headers: { 'X-Requested-With': 'XMLHttpRequest', 'X-Frame-Options': 'SAMEORIGIN', 'X-Frame-Referrer': referrer } });
|
||||
config.color = new URLSearchParams(window.location.search).get('color') || config.color || 'blue';
|
||||
|
||||
// --- THEME ---
|
||||
const areaIdentifierParams = computed(() => {
|
||||
return gemeindeId.value ? { gemeinde_id: gemeindeId.value } : { cluster_id: clusterId.value };
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
const themes = {
|
||||
blue: { 50: '#eff6ff', 100: '#dbeafe', 200: '#bfdbfe', 500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8', 800: '#1e40af', text: '#ffffff' },
|
||||
@@ -229,19 +128,15 @@
|
||||
lime: { 50: '#f7fee7', 100: '#ecfccb', 200: '#d9f99d', 500: '#84cc16', 600: '#65a30d', 700: '#4d7c0f', 800: '#3f6212', text: '#ffffff' },
|
||||
};
|
||||
const theme = themes[config.color] || themes.blue;
|
||||
const styleSheet = document.getElementById('theme-style');
|
||||
console.log(theme);
|
||||
styleSheet.innerHTML = `:root { --color-primary-50: ${theme[50]}; --color-primary-100: ${theme[100]}; --color-primary-200: ${theme[200]}; --color-primary-500: ${theme[500]}; --color-primary-600: ${theme[600]}; --color-primary-700: ${theme[700]}; --color-primary-800: ${theme[800]}; --color-text-on-primary: ${theme.text}; }`;
|
||||
console.log(styleSheet.innerHTML);
|
||||
document.getElementById('theme-style').innerHTML = `:root { --color-primary-50: ${theme[50]}; --color-primary-100: ${theme[100]}; --color-primary-200: ${theme[200]}; --color-primary-500: ${theme[500]}; --color-primary-600: ${theme[600]}; --color-primary-700: ${theme[700]}; --color-primary-800: ${theme[800]}; --color-text-on-primary: ${theme.text}; }`;
|
||||
|
||||
if (clusterId.value) {
|
||||
if (clusterId.value || gemeindeId.value) {
|
||||
currentStep.value = 'addressSearch';
|
||||
} else {
|
||||
fetchClusters();
|
||||
}
|
||||
});
|
||||
|
||||
// --- WIZARD LOGIC ---
|
||||
function clearSubsequentSteps(fromStep) {
|
||||
if (fromStep === 'zip') { addressForm.city = ''; cities.value = []; }
|
||||
if (fromStep === 'zip' || fromStep === 'city') { addressForm.street = ''; streets.value = []; }
|
||||
@@ -258,12 +153,9 @@
|
||||
if (newStep === 'addressSearch' && clusterId.value) {
|
||||
const response = await api.get(`/getClusterInfo`, { params: { cluster_id: clusterId.value } });
|
||||
Object.assign(iframeConsents, response.data?.iframe_consents || {});
|
||||
console.log(response.data.iframe_consents);
|
||||
console.log('Iframe consents:', iframeConsents);
|
||||
}
|
||||
});
|
||||
|
||||
// --- API CALLS & METHODS ---
|
||||
function handleError(error, message) {
|
||||
console.error(error);
|
||||
errorMessage.value = error.response?.data?.result?.error || message;
|
||||
@@ -275,7 +167,6 @@
|
||||
try {
|
||||
const response = await api.get('/getClusters');
|
||||
clusters.value = response.data?.clusters || [];
|
||||
|
||||
if (clusters.value.length === 1) {
|
||||
clusterId.value = clusters.value[0].id;
|
||||
currentStep.value = 'addressSearch';
|
||||
@@ -287,7 +178,7 @@
|
||||
async function fetchCities(zip) {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const response = await api.get('/findCity', { params: { zip, cluster_id: clusterId.value } });
|
||||
const response = await api.get('/findCity', { params: { zip, ...areaIdentifierParams.value } });
|
||||
cities.value = response.data?.cities || [];
|
||||
} catch (e) { handleError(e, 'Orte für diese PLZ konnten nicht geladen werden.'); }
|
||||
finally { isLoading.value = false; }
|
||||
@@ -296,7 +187,7 @@
|
||||
async function fetchStreets(zip, city) {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const response = await api.get('/findStreet', { params: { zip, city, cluster_id: clusterId.value } });
|
||||
const response = await api.get('/findStreet', { params: { zip, city, ...areaIdentifierParams.value } });
|
||||
streets.value = response.data?.streets || [];
|
||||
} catch (e) { handleError(e, 'Straßen für diesen Ort konnten nicht geladen werden.'); }
|
||||
finally { isLoading.value = false; }
|
||||
@@ -306,9 +197,10 @@
|
||||
if (!addressForm.housenumber) return;
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const params = { ...addressForm, cluster_id: clusterId.value, format: 'flat' };
|
||||
const params = { ...addressForm, ...areaIdentifierParams.value, format: 'flat' };
|
||||
const response = await api.get('/findAddress', { params });
|
||||
const addresses = (response.data?.addresses || []).filter(a => a.preorderTypes?.includes('order'));
|
||||
const addresses = (response.data?.addresses || []).filter(a => a.preorderTypes?.includes(orderType.value));
|
||||
|
||||
if (addresses.length === 0) { currentStep.value = 'noOrderPossible'; }
|
||||
else if (addresses.length === 1) { selectAddress(addresses[0]); }
|
||||
else { units.value = addresses; addressStep.value = 'unit'; }
|
||||
@@ -323,22 +215,22 @@
|
||||
}
|
||||
|
||||
function startNewOrder() {
|
||||
clusterId.value = initialArea.clusterId;
|
||||
gemeindeId.value = initialArea.gemeindeId;
|
||||
Object.assign(form, { customerType: 'Privatkunde', title: '', birthDate: '', firstName: '', lastName: '', phone: '', email: '', isOwner: 'Ja', notes: '', billingAddressChoice: 'Anschlussadresse', billing: { name: '', street: '', housenumber: '', zip: '', city: '' }, acceptAgb: false, acceptMarketing: false, acceptWithdrawal: false, acceptDsgvo: false });
|
||||
Object.assign(addressForm, { zip: '', city: '', street: '', housenumber: '', wohneinheit_id: '' });
|
||||
selectedAddress.value = null; errorMessage.value = ''; addressStep.value = 'zip';
|
||||
currentStep.value = config.clusterId ? 'addressSearch' : 'clusterSelect';
|
||||
currentStep.value = (initialArea.clusterId || initialArea.gemeindeId) ? 'addressSearch' : 'clusterSelect';
|
||||
}
|
||||
|
||||
async function submitOrder() {
|
||||
if (isFormInvalid.value) return;
|
||||
isLoading.value = true; errorMessage.value = '';
|
||||
const preorderData = {
|
||||
preorderType: 'order',
|
||||
customerType: form.customerType,
|
||||
connectionType: form.connectionType,
|
||||
preorderType: orderType.value, customerType: form.customerType,
|
||||
connectionType: orderType.value === 'order' ? form.connectionType : null,
|
||||
acceptAgb: form.acceptAgb, acceptMarketing: form.acceptMarketing, acceptDsgvo: form.acceptDsgvo, acceptWithdrawal: form.acceptWithdrawal,
|
||||
address: selectedAddress.value,
|
||||
address_info: form.notes,
|
||||
address: selectedAddress.value, address_info: form.notes,
|
||||
customer: {
|
||||
type: form.isOwner === 'Ja' ? 'owner' : 'tenant',
|
||||
firstname: form.firstName, lastname: form.lastName, phone: form.phone, email: form.email,
|
||||
@@ -348,55 +240,43 @@
|
||||
zip: form.billingAddressChoice === 'Andere' ? form.billing.zip : selectedAddress.value.zip,
|
||||
city: form.billingAddressChoice === 'Andere' ? form.billing.city : selectedAddress.value.city,
|
||||
},
|
||||
additionalData: {
|
||||
birthDate: form.birthDate,
|
||||
title: form.title,
|
||||
clusterId: clusterId.value // CRUCIAL: Add clusterId for backend security check
|
||||
}
|
||||
additionalData: { birthDate: form.birthDate, title: form.title, clusterId: clusterId.value, gemeindeId: gemeindeId.value }
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await api.post('/submitOrder', preorderData);
|
||||
orderResponse.value = response.data;
|
||||
currentStep.value = 'confirmation';
|
||||
} catch (e) { handleError(e, 'Ihre Bestellung konnte nicht verarbeitet werden. Bitte überprüfen Sie Ihre Eingaben.'); }
|
||||
} catch (e) { handleError(e, 'Ihre Anfrage konnte nicht verarbeitet werden. Bitte überprüfen Sie Ihre Eingaben.'); }
|
||||
finally { isLoading.value = false; }
|
||||
}
|
||||
|
||||
const isFormInvalid = computed(() => {
|
||||
const { firstName, lastName, email, billingAddressChoice, billing } = form;
|
||||
|
||||
// Basic form field validation
|
||||
const { firstName, lastName, email, billingAddressChoice, billing, connectionType } = form;
|
||||
if (!firstName || !lastName || !email) return true;
|
||||
|
||||
// Billing address validation
|
||||
if (billingAddressChoice === 'Andere') {
|
||||
if (!billing.name || !billing.street || !billing.housenumber || !billing.zip || !billing.city) return true;
|
||||
}
|
||||
|
||||
// Dynamic validation for required consents
|
||||
for (const key in iframeConsents) {
|
||||
if (iframeConsents[key].required && !form[key]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (billingAddressChoice === 'Andere' && (!billing.name || !billing.street || !billing.housenumber || !billing.zip || !billing.city)) return true;
|
||||
if (orderType.value === 'order' && !connectionType) return true;
|
||||
for (const key in iframeConsents) { if (iframeConsents[key].required && !form[key]) return true; }
|
||||
return false;
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
currentStep, clusterId, clusters, addressStep, addressForm, goBackToStep,
|
||||
cities, streets, units, isLoading, selectedAddress, isFormInvalid, form,
|
||||
findAddress, submitOrder, startNewOrder, orderResponse, errorMessage, iframeConsents,
|
||||
currentStep, clusterId, gemeindeId, clusters, addressStep, addressForm, goBackToStep, orderType,
|
||||
cities, streets, units, isLoading, selectedAddress, isFormInvalid, form, findAddress, submitOrder,
|
||||
startNewOrder, orderResponse, errorMessage, iframeConsents,
|
||||
handleClusterSelection: () => { if (clusterId.value) currentStep.value = 'addressSearch'; },
|
||||
};
|
||||
},
|
||||
template: `
|
||||
<div class="bg-white p-6 sm:p-10 rounded-2xl shadow-xl transition-all duration-500 w-full">
|
||||
<header class="text-center mb-8">
|
||||
<h1 class="text-4xl font-extrabold text-slate-800 tracking-tight">Glasfaser Bestellung</h1>
|
||||
<p class="text-slate-500 mt-2 text-lg">In wenigen Schritten zu Ihrem Anschluss.</p>
|
||||
<h1 class="text-4xl font-extrabold text-slate-800 tracking-tight">
|
||||
<span v-if="orderType === 'interest'">Glasfaser Interesse</span>
|
||||
<span v-else>Glasfaser Bestellung</span>
|
||||
</h1>
|
||||
<p class="text-slate-500 mt-2 text-lg">
|
||||
<span v-if="orderType === 'interest'">Bekunden Sie unverbindlich Ihr Interesse.</span>
|
||||
<span v-else>In wenigen Schritten zu Ihrem Anschluss.</span>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
@@ -407,14 +287,8 @@
|
||||
|
||||
<div v-else-if="currentStep === 'clusterSelect'" class="space-y-4 max-w-lg mx-auto">
|
||||
<label class="block text-xl font-medium text-slate-700 text-center">Bitte wählen Sie Ihr Ausbaugebiet:</label>
|
||||
<SearchableSelect
|
||||
v-model="clusterId"
|
||||
:options="clusters"
|
||||
:get-option-key="(c) => c.id"
|
||||
:get-option-label="(c) => c.name"
|
||||
placeholder="Gebiet auswählen..."
|
||||
/>
|
||||
<button @click="handleClusterSelection" :disabled="!clusterId" class="w-full bg-[var(--color-primary-600)] text-[var(--color-text-on-primary)] font-bold py-3 px-4 rounded-md hover:bg-[var(--color-primary-700)] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[var(--color-primary-500)] disabled:bg-slate-400 disabled:cursor-not-allowed transition-colors">Weiter</button>
|
||||
<SearchableSelect v-model="clusterId" :options="clusters" :get-option-key="(c) => c.id" :get-option-label="(c) => c.name" placeholder="Gebiet auswählen..." />
|
||||
<button @click="handleClusterSelection" :disabled="!clusterId" class="w-full bg-[var(--color-primary-600)] text-[var(--color-text-on-primary)] font-bold py-3 px-4 rounded-md hover:bg-[var(--color-primary-700)] disabled:bg-slate-400">Weiter</button>
|
||||
</div>
|
||||
|
||||
<div v-else-if="currentStep === 'addressSearch'" class="max-w-xl mx-auto">
|
||||
@@ -426,7 +300,7 @@
|
||||
<div v-if="addressStep === 'housenumber' || addressStep === 'unit'" class="p-4 rounded-lg border border-slate-300 transition-all" :class="{'wizard-step-active': true}"><p class="font-semibold text-slate-700">Hausnummer & Einheit</p>
|
||||
<div v-if="addressStep === 'housenumber'" class="mt-2">
|
||||
<label for="housenumber" class="block text-sm text-slate-500 mb-1">Hausnummer eingeben und prüfen</label>
|
||||
<div class="flex items-center space-x-2"><input id="housenumber" v-model.trim="addressForm.housenumber" @keyup.enter="findAddress" type="text" placeholder="z.B. 12A" class="form-input w-full rounded-md border-slate-300 shadow-sm py-2.5 px-3"><button @click="findAddress" :disabled="!addressForm.housenumber || isLoading" class="py-2.5 px-4 bg-[var(--color-primary-600)] text-[var(--color-text-on-primary)] rounded-md hover:bg-[var(--color-primary-700)] disabled:bg-slate-400 transition-colors flex-shrink-0"><svg v-if="isLoading" class="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg><span v-else>Prüfen</span></button></div>
|
||||
<div class="flex items-center space-x-2"><input id="housenumber" v-model.trim="addressForm.housenumber" @keyup.enter="findAddress" type="text" placeholder="z.B. 12A" class="form-input w-full rounded-md border-slate-300 shadow-sm py-2.5 px-3"><button @click="findAddress" :disabled="!addressForm.housenumber || isLoading" class="py-2.5 px-4 bg-[var(--color-primary-600)] text-[var(--color-text-on-primary)] rounded-md hover:bg-[var(--color-primary-700)] disabled:bg-slate-400 flex-shrink-0"><svg v-if="isLoading" class="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg><span v-else>Prüfen</span></button></div>
|
||||
</div>
|
||||
<div v-if="addressStep === 'unit'" class="mt-2"><label class="block text-sm text-slate-500 mb-1">Mehrere Einheiten gefunden, bitte wählen</label><SearchableSelect v-model="addressForm.wohneinheit_id" :options="units" :get-option-key="(u) => u.wohneinheit_id" :get-option-label="(u) => u.showText" placeholder="Einheit auswählen..." /></div>
|
||||
</div>
|
||||
@@ -439,198 +313,42 @@
|
||||
<p class="text-[var(--color-primary-700)]">{{ selectedAddress.street }} {{ selectedAddress.housenumber }}, {{ selectedAddress.zip }} {{ selectedAddress.city }}</p>
|
||||
<button @click="startNewOrder" class="text-sm text-[var(--color-primary-600)] hover:underline mt-1 font-semibold">Neue Adresse suchen</button>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-slate-800 text-center">2. Bestelldaten eingeben</h3>
|
||||
<h3 class="text-xl font-semibold text-slate-800 text-center">2. Daten eingeben</h3>
|
||||
<form @submit.prevent="submitOrder" class="space-y-6">
|
||||
<fieldset>
|
||||
<legend class="text-lg font-semibold text-slate-800 mb-2">Kunde</legend>
|
||||
<div class="flex items-center space-x-6">
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<input type="radio" v-model="form.customerType" value="Privatkunde" class="form-radio h-5 w-5 text-[var(--color-primary-600)]">
|
||||
<span class="text-slate-700">Privatkunde</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<input type="radio" v-model="form.customerType" value="Businesskunde" class="form-radio h-5 w-5 text-[var(--color-primary-600)]">
|
||||
<span class="text-slate-700">Businesskunde</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-6"><label class="flex items-center space-x-2 cursor-pointer"><input type="radio" v-model="form.customerType" value="Privatkunde" class="form-radio h-5 w-5 text-[var(--color-primary-600)]"><span class="text-slate-700">Privatkunde</span></label><label class="flex items-center space-x-2 cursor-pointer"><input type="radio" v-model="form.customerType" value="Businesskunde" class="form-radio h-5 w-5 text-[var(--color-primary-600)]"><span class="text-slate-700">Businesskunde</span></label></div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<fieldset v-if="orderType === 'order'">
|
||||
<legend class="text-lg font-semibold text-slate-800 mb-2">Anschlussart</legend>
|
||||
<div>
|
||||
<label for="connectionType" class="block text-sm font-medium text-slate-600 mb-2">Anschlussart *</label>
|
||||
|
||||
<!-- For building_type = 1 (single option) -->
|
||||
<div v-if="selectedAddress.building_type === 2" class="space-y-2">
|
||||
<div class="p-4 border-2 border-[var(--color-primary-600)] rounded-md bg-[var(--color-primary-50)]">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-medium text-slate-800">Wohnungs-Anschluss</span>
|
||||
<span class="font-bold text-[var(--color-primary-600)]" v-text="form.customerType === 'Privatkunde' ? '150€' : '125€'"/>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" v-model="form.connectionType" value="wohnung">
|
||||
</div>
|
||||
|
||||
<!-- For building_type = 2 (multiple options) -->
|
||||
<div v-else-if="selectedAddress.building_type === 1" class="space-y-3">
|
||||
<label class="flex items-center p-4 border-2 rounded-md cursor-pointer hover:bg-slate-50 transition-colors"
|
||||
:class="form.connectionType === 'vorsorge' ? 'border-[var(--color-primary-600)] bg-[var(--color-primary-50)]' : 'border-slate-300'">
|
||||
<input type="radio" v-model="form.connectionType" value="vorsorge" class="form-radio h-5 w-5 text-[var(--color-primary-600)] mr-3">
|
||||
<div class="flex-1 flex items-center justify-between">
|
||||
<span class="font-medium text-slate-800">Vorsorgeanschluss</span>
|
||||
<span class="font-bold text-[var(--color-primary-600)]" v-text="form.customerType !== 'Privatkunde' ? '500€' : '600€'"/>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="flex items-center p-4 border-2 rounded-md cursor-pointer hover:bg-slate-50 transition-colors"
|
||||
:class="form.connectionType === 'voll' ? 'border-[var(--color-primary-600)] bg-[var(--color-primary-50)]' : 'border-slate-300'">
|
||||
<input type="radio" v-model="form.connectionType" value="voll" class="form-radio h-5 w-5 text-[var(--color-primary-600)] mr-3">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="font-medium text-slate-800">Vollanschluss</span>
|
||||
<span class="font-bold text-[var(--color-primary-600)]" v-text="form.customerType !== 'Privatkunde' ? '250€' : '300€'"/>
|
||||
</div>
|
||||
<p class="text-xs text-slate-600 mt-1">
|
||||
Vollanschluss-Preis nur bei Aktivierung innerhalb von 8 Wochen nach Fertigstellung
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Fallback if building_type is not 1 or 2 -->
|
||||
<div v-else class="p-4 border border-slate-300 rounded-md bg-slate-50">
|
||||
<p class="text-slate-600">Bitte wählen Sie zuerst eine gültige Adresse aus.</p>
|
||||
</div>
|
||||
<div v-if="selectedAddress.building_type === 2" class="space-y-2"><div class="p-4 border-2 border-[var(--color-primary-600)] rounded-md bg-[var(--color-primary-50)]"><div class="flex items-center justify-between"><span class="font-medium text-slate-800">Wohnungs-Anschluss</span><span class="font-bold text-[var(--color-primary-600)]" v-text="form.customerType === 'Privatkunde' ? '150€' : '125€'"/></div></div><input type="hidden" v-model="form.connectionType" value="wohnung"></div>
|
||||
<div v-else-if="selectedAddress.building_type === 1" class="space-y-3"><label class="flex items-center p-4 border-2 rounded-md cursor-pointer" :class="form.connectionType === 'vorsorge' ? 'border-[var(--color-primary-600)] bg-[var(--color-primary-50)]' : 'border-slate-300'"><input type="radio" v-model="form.connectionType" value="vorsorge" class="form-radio h-5 w-5 text-[var(--color-primary-600)] mr-3"><div class="flex-1 flex justify-between"><span class="font-medium text-slate-800">Vorsorgeanschluss</span><span class="font-bold text-[var(--color-primary-600)]" v-text="form.customerType !== 'Privatkunde' ? '500€' : '600€'"/></div></label><label class="flex items-center p-4 border-2 rounded-md cursor-pointer" :class="form.connectionType === 'voll' ? 'border-[var(--color-primary-600)] bg-[var(--color-primary-50)]' : 'border-slate-300'"><input type="radio" v-model="form.connectionType" value="voll" class="form-radio h-5 w-5 text-[var(--color-primary-600)] mr-3"><div class="flex-1"><div class="flex justify-between mb-1"><span class="font-medium text-slate-800">Vollanschluss</span><span class="font-bold text-[var(--color-primary-600)]" v-text="form.customerType !== 'Privatkunde' ? '250€' : '300€'"/></div><p class="text-xs text-slate-600 mt-1">Vollanschluss-Preis nur bei Aktivierung innerhalb von 8 Wochen nach Fertigstellung</p></div></label></div>
|
||||
<div v-else class="p-4 border border-slate-300 rounded-md bg-slate-50"><p class="text-slate-600">Keine Anschlussart für diesen Gebäudetyp verfügbar.</p></div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend class="text-lg font-semibold text-slate-800 mb-2">Persönliche Daten</legend>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="title" class="block text-sm font-medium text-slate-600">Titel</label>
|
||||
<input id="title" v-model="form.title" type="text" placeholder="z.B. Dr." class="form-input mt-1 block w-full rounded-md border-slate-300 shadow-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label for="birthDate" class="block text-sm font-medium text-slate-600">Geburtsdatum</label>
|
||||
<input id="birthDate" v-model="form.birthDate" type="date" class="form-input mt-1 block w-full rounded-md border-slate-300 shadow-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label for="firstName" class="block text-sm font-medium text-slate-600">Vorname *</label>
|
||||
<input id="firstName" v-model="form.firstName" required type="text" placeholder="Max" class="form-input mt-1 block w-full rounded-md border-slate-300 shadow-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label for="lastName" class="block text-sm font-medium text-slate-600">Nachname *</label>
|
||||
<input id="lastName" v-model="form.lastName" required type="text" placeholder="Mustermann" class="form-input mt-1 block w-full rounded-md border-slate-300 shadow-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label for="phone" class="block text-sm font-medium text-slate-600">Telefon</label>
|
||||
<input id="phone" v-model="form.phone" type="tel" placeholder="+43 664 1234567" class="form-input mt-1 block w-full rounded-md border-slate-300 shadow-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-slate-600">E-Mail *</label>
|
||||
<input id="email" v-model="form.email" required type="email" placeholder="max.mustermann@email.com" class="form-input mt-1 block w-full rounded-md border-slate-300 shadow-sm">
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<p class="block text-sm font-medium text-slate-600">Ich bin Eigentümer der Liegenschaft *</p>
|
||||
<div class="flex items-center space-x-4 mt-1">
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<input type="radio" v-model="form.isOwner" value="Ja" class="form-radio h-4 w-4 text-[var(--color-primary-600)]">
|
||||
<span class="text-slate-700">Ja</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<input type="radio" v-model="form.isOwner" value="Nein" class="form-radio h-4 w-4 text-[var(--color-primary-600)]">
|
||||
<span class="text-slate-700">Nein</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend class="text-lg font-semibold text-slate-800 mb-2">Anmerkungen</legend>
|
||||
<div>
|
||||
<label for="notes" class="block text-sm font-medium text-slate-600">Ihre Anmerkungen zur Anschlussadresse</label>
|
||||
<textarea id="notes" v-model="form.notes" rows="3" placeholder="z.B. Hinterhaus, bei Firma XY läuten" class="form-textarea mt-1 block w-full rounded-md border-slate-300 shadow-sm"></textarea>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend class="text-lg font-semibold text-slate-800 mb-2">Adresse zur Rechnungszusendung</legend>
|
||||
<div class="flex items-center space-x-6 mb-4">
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<input type="radio" v-model="form.billingAddressChoice" value="Anschlussadresse" class="form-radio h-5 w-5 text-[var(--color-primary-600)]">
|
||||
<span class="text-slate-700">Anschlussadresse</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<input type="radio" v-model="form.billingAddressChoice" value="Andere" class="form-radio h-5 w-5 text-[var(--color-primary-600)]">
|
||||
<span class="text-slate-700">Andere Adresse</span>
|
||||
</label>
|
||||
</div>
|
||||
<transition name="slide-fade">
|
||||
<div v-if="form.billingAddressChoice === 'Andere'" class="grid grid-cols-1 sm:grid-cols-2 gap-4 p-4 border rounded-md bg-slate-50">
|
||||
<div class="sm:col-span-2">
|
||||
<label for="billingName" class="block text-sm font-medium text-slate-600">Name/Firma *</label>
|
||||
<input id="billingName" v-model="form.billing.name" required type="text" placeholder="Maxi Mustermann GmbH" class="form-input mt-1 block w-full rounded-md border-slate-300 shadow-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label for="billingStreet" class="block text-sm font-medium text-slate-600">Straße *</label>
|
||||
<input id="billingStreet" v-model="form.billing.street" required type="text" placeholder="Musterstraße" class="form-input mt-1 block w-full rounded-md border-slate-300 shadow-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label for="billingHousenumber" class="block text-sm font-medium text-slate-600">Hausnr. *</label>
|
||||
<input id="billingHousenumber" v-model="form.billing.housenumber" required type="text" placeholder="1" class="form-input mt-1 block w-full rounded-md border-slate-300 shadow-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label for="billingZip" class="block text-sm font-medium text-slate-600">PLZ *</label>
|
||||
<input id="billingZip" v-model="form.billing.zip" required type="text" placeholder="8010" class="form-input mt-1 block w-full rounded-md border-slate-300 shadow-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label for="billingCity" class="block text-sm font-medium text-slate-600">Ort *</label>
|
||||
<input id="billingCity" v-model="form.billing.city" required type="text" placeholder="Graz" class="form-input mt-1 block w-full rounded-md border-slate-300 shadow-sm">
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend class="text-lg font-semibold text-slate-800 mb-2">Zustimmungen</legend>
|
||||
<div class="space-y-4">
|
||||
<template v-for="(consent, key) in iframeConsents" :key="key">
|
||||
<label v-if="consent.activated" class="flex items-start space-x-3 cursor-pointer">
|
||||
<input type="checkbox" v-model="form[key]" class="form-checkbox h-5 w-5 text-[var(--color-primary-600)] mt-0.5 flex-shrink-0">
|
||||
<span class="text-slate-600 text-sm">
|
||||
<template v-if="consent.replace && consent.url">
|
||||
{{ consent.text.split(consent.replace)[0] }}
|
||||
<a :href="consent.url" target="_blank" class="text-[var(--color-primary-600)] hover:underline">{{ consent.replace }}</a>
|
||||
{{ consent.text.split(consent.replace)[1] }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ consent.text }}
|
||||
</template>
|
||||
<span v-if="consent.required" class="text-red-500">*</span>
|
||||
</span>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset><legend class="text-lg font-semibold text-slate-800 mb-2">Persönliche Daten</legend><div class="grid grid-cols-1 sm:grid-cols-2 gap-4"><div><label for="title" class="block text-sm font-medium text-slate-600">Titel</label><input id="title" v-model="form.title" type="text" placeholder="z.B. Dr." class="form-input mt-1 block w-full rounded-md border-slate-300"></div><div><label for="birthDate" class="block text-sm font-medium text-slate-600">Geburtsdatum</label><input id="birthDate" v-model="form.birthDate" type="date" class="form-input mt-1 block w-full rounded-md border-slate-300"></div><div><label for="firstName" class="block text-sm font-medium text-slate-600">Vorname *</label><input id="firstName" v-model="form.firstName" required type="text" placeholder="Max" class="form-input mt-1 block w-full rounded-md border-slate-300"></div><div><label for="lastName" class="block text-sm font-medium text-slate-600">Nachname *</label><input id="lastName" v-model="form.lastName" required type="text" placeholder="Mustermann" class="form-input mt-1 block w-full rounded-md border-slate-300"></div><div><label for="phone" class="block text-sm font-medium text-slate-600">Telefon</label><input id="phone" v-model="form.phone" type="tel" placeholder="+43 664 1234567" class="form-input mt-1 block w-full rounded-md border-slate-300"></div><div><label for="email" class="block text-sm font-medium text-slate-600">E-Mail *</label><input id="email" v-model="form.email" required type="email" placeholder="max.mustermann@email.com" class="form-input mt-1 block w-full rounded-md border-slate-300"></div><div class="sm:col-span-2"><p class="block text-sm font-medium text-slate-600">Ich bin Eigentümer der Liegenschaft *</p><div class="flex items-center space-x-4 mt-1"><label class="flex items-center space-x-2 cursor-pointer"><input type="radio" v-model="form.isOwner" value="Ja" class="form-radio h-4 w-4 text-[var(--color-primary-600)]"><span class="text-slate-700">Ja</span></label><label class="flex items-center space-x-2 cursor-pointer"><input type="radio" v-model="form.isOwner" value="Nein" class="form-radio h-4 w-4 text-[var(--color-primary-600)]"><span class="text-slate-700">Nein</span></label></div></div></div></fieldset>
|
||||
<fieldset><legend class="text-lg font-semibold text-slate-800 mb-2">Anmerkungen</legend><div><label for="notes" class="block text-sm font-medium text-slate-600">Ihre Anmerkungen zur Anschlussadresse</label><textarea id="notes" v-model="form.notes" rows="3" placeholder="z.B. Hinterhaus, bei Firma XY läuten" class="form-textarea mt-1 block w-full rounded-md border-slate-300"></textarea></div></fieldset>
|
||||
<fieldset><legend class="text-lg font-semibold text-slate-800 mb-2">Adresse zur Rechnungszusendung</legend><div class="flex items-center space-x-6 mb-4"><label class="flex items-center space-x-2 cursor-pointer"><input type="radio" v-model="form.billingAddressChoice" value="Anschlussadresse" class="form-radio h-5 w-5 text-[var(--color-primary-600)]"><span class="text-slate-700">Anschlussadresse</span></label><label class="flex items-center space-x-2 cursor-pointer"><input type="radio" v-model="form.billingAddressChoice" value="Andere" class="form-radio h-5 w-5 text-[var(--color-primary-600)]"><span class="text-slate-700">Andere Adresse</span></label></div><transition name="slide-fade"><div v-if="form.billingAddressChoice === 'Andere'" class="grid grid-cols-1 sm:grid-cols-2 gap-4 p-4 border rounded-md bg-slate-50"><div class="sm:col-span-2"><label for="billingName" class="block text-sm font-medium text-slate-600">Name/Firma *</label><input id="billingName" v-model="form.billing.name" required type="text" placeholder="Maxi Mustermann GmbH" class="form-input mt-1 block w-full rounded-md border-slate-300"></div><div><label for="billingStreet" class="block text-sm font-medium text-slate-600">Straße *</label><input id="billingStreet" v-model="form.billing.street" required type="text" placeholder="Musterstraße" class="form-input mt-1 block w-full rounded-md border-slate-300"></div><div><label for="billingHousenumber" class="block text-sm font-medium text-slate-600">Hausnr. *</label><input id="billingHousenumber" v-model="form.billing.housenumber" required type="text" placeholder="1" class="form-input mt-1 block w-full rounded-md border-slate-300"></div><div><label for="billingZip" class="block text-sm font-medium text-slate-600">PLZ *</label><input id="billingZip" v-model="form.billing.zip" required type="text" placeholder="8010" class="form-input mt-1 block w-full rounded-md border-slate-300"></div><div><label for="billingCity" class="block text-sm font-medium text-slate-600">Ort *</label><input id="billingCity" v-model="form.billing.city" required type="text" placeholder="Graz" class="form-input mt-1 block w-full rounded-md border-slate-300"></div></div></transition></fieldset>
|
||||
<fieldset><legend class="text-lg font-semibold text-slate-800 mb-2">Zustimmungen</legend><div class="space-y-4"><template v-for="(consent, key) in iframeConsents" :key="key"><label v-if="consent.activated" class="flex items-start space-x-3 cursor-pointer"><input type="checkbox" v-model="form[key]" class="form-checkbox h-5 w-5 text-[var(--color-primary-600)] mt-0.5 flex-shrink-0"><span class="text-slate-600 text-sm"><template v-if="consent.replace && consent.url">{{ consent.text.split(consent.replace)[0] }}<a :href="consent.url" target="_blank" class="text-[var(--color-primary-600)] hover:underline">{{ consent.replace }}</a>{{ consent.text.split(consent.replace)[1] }}</template><template v-else>{{ consent.text }}</template><span v-if="consent.required" class="text-red-500">*</span></span></label></template></div></fieldset>
|
||||
|
||||
<div class="pt-4 border-t">
|
||||
<p class="text-xs text-slate-500 mb-4">* Pflichtfelder</p>
|
||||
<button type="submit" :disabled="isFormInvalid || isLoading" class="w-full bg-[var(--color-primary-600)] text-[var(--color-text-on-primary)] font-bold py-4 px-4 rounded-md hover:bg-[var(--color-primary-700)] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[var(--color-primary-500)] disabled:bg-slate-400 disabled:cursor-not-allowed flex items-center justify-center text-lg transition-colors">
|
||||
<svg v-if="isLoading" class="animate-spin -ml-1 mr-3 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<button type="submit" :disabled="isFormInvalid || isLoading" class="w-full bg-[var(--color-primary-600)] text-[var(--color-text-on-primary)] font-bold py-4 px-4 rounded-md hover:bg-[var(--color-primary-700)] flex items-center justify-center text-lg">
|
||||
<svg v-if="isLoading" class="animate-spin -ml-1 mr-3 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
|
||||
<span v-if="orderType === 'interest'">Interesse bekunden</span>
|
||||
<span v-else>Kostenpflichtig bestellen</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div v-else-if="currentStep === 'noOrderPossible'" class="text-center p-8 bg-amber-50 rounded-lg"><svg class="mx-auto h-12 w-12 text-amber-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg><h2 class="mt-4 text-2xl font-bold text-slate-800">Bestellung nicht möglich</h2><p class="mt-2 text-slate-600">Leider ist an der von Ihnen gewählten Adresse derzeit kein Glasfaseranschluss bestellbar.</p><button @click="startNewOrder" class="mt-6 bg-[var(--color-primary-600)] text-[var(--color-text-on-primary)] font-bold py-2 px-6 rounded-md hover:bg-[var(--color-primary-700)]">Andere Adresse suchen</button></div>
|
||||
<div v-else-if="currentStep === 'confirmation' && orderResponse" class="text-center p-8 bg-green-50 rounded-lg"><svg class="mx-auto h-16 w-16 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg><h2 class="mt-4 text-2xl font-bold text-slate-800">Vielen Dank!</h2><p class="mt-2 text-slate-600">Ihre Bestellung wurde erfolgreich übermittelt.</p><div class="mt-6 p-4 bg-white border border-green-200 rounded-md inline-block"><p class="text-slate-600">Ihre Bestellnummer:</p><p class="text-2xl font-mono font-bold text-green-700 tracking-wider">{{ orderResponse.orderCode }}</p></div><button @click="startNewOrder" class="mt-8 block w-full text-center bg-[var(--color-primary-600)] text-[var(--color-text-on-primary)] font-bold py-3 px-4 rounded-md hover:bg-[var(--color-primary-700)]">Neue Bestellung</button></div>
|
||||
<div v-else-if="currentStep === 'error'" class="text-center p-8 bg-red-50 rounded-lg"><svg class="mx-auto h-12 w-12 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg><h2 class="mt-4 text-2xl font-bold text-slate-800">Ein Fehler ist aufgetreten</h2><p class="mt-2 text-red-700 bg-red-100 p-3 rounded-md">{{ errorMessage }}</p><button @click="startNewOrder" class="mt-6 bg-[var(--color-primary-600)] text-[var(--color-text-on-primary)] font-bold py-2 px-6 rounded-md hover:bg-[var(--color-primary-700)]">Erneut versuchen</button></div>
|
||||
<div v-else-if="currentStep === 'noOrderPossible'" class="text-center p-8 bg-amber-50 rounded-lg"><svg class="mx-auto h-12 w-12 text-amber-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg><h2 class="mt-4 text-2xl font-bold text-slate-800">Keine Verfügbarkeit</h2><p class="mt-2 text-slate-600">Leider ist an der von Ihnen gewählten Adresse derzeit kein Glasfaseranschluss verfügbar.</p><button @click="startNewOrder" class="mt-6 bg-[var(--color-primary-600)] text-[var(--color-text-on-primary)] font-bold py-2 px-6 rounded-md hover:bg-[var(--color-primary-700)]">Andere Adresse suchen</button></div>
|
||||
<div v-else-if="currentStep === 'confirmation' && orderResponse" class="text-center p-8 bg-green-50 rounded-lg"><svg class="mx-auto h-16 w-16 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg><h2 class="mt-4 text-2xl font-bold text-slate-800">Vielen Dank!</h2><p class="mt-2 text-slate-600">Ihre Anfrage wurde erfolgreich übermittelt.</p><div class="mt-6 p-4 bg-white border border-green-200 rounded-md inline-block"><p class="text-slate-600">Ihre Referenznummer:</p><p class="text-2xl font-mono font-bold text-green-700 tracking-wider">{{ orderResponse.orderCode }}</p></div><button @click="startNewOrder" class="mt-8 block w-full text-center bg-[var(--color-primary-600)] text-[var(--color-text-on-primary)] font-bold py-3 px-4 rounded-md">Neue Anfrage</button></div>
|
||||
<div v-else-if="currentStep === 'error'" class="text-center p-8 bg-red-50 rounded-lg"><svg class="mx-auto h-12 w-12 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg><h2 class="mt-4 text-2xl font-bold text-slate-800">Ein Fehler ist aufgetreten</h2><p class="mt-2 text-red-700 bg-red-100 p-3 rounded-md">{{ errorMessage }}</p><button @click="startNewOrder" class="mt-6 bg-[var(--color-primary-600)] text-[var(--color-text-on-primary)] font-bold py-2 px-6 rounded-md">Erneut versuchen</button></div>
|
||||
</transition>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,31 +1,22 @@
|
||||
<?php
|
||||
// in /controllers/PreorderIFrameController.php
|
||||
|
||||
class PreorderIFrameController extends mfBaseController
|
||||
{
|
||||
|
||||
private PreorderIFrameModel $preorderIFrameModel;
|
||||
|
||||
public function init()
|
||||
{
|
||||
// The model is autoloaded or included elsewhere
|
||||
// 'X-Requested-With': 'XMLHttpRequest', 'X-Frame-Options': 'SAMEORIGIN', 'X-Frame-Referrer': document.referrer
|
||||
$this->preorderIFrameModel = new PreorderIFrameModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Serves the main order form HTML.
|
||||
* This action injects the necessary configuration into the Vue app.
|
||||
*/
|
||||
public function indexAction()
|
||||
{
|
||||
$clusterId = $this->request->get('clusterId', 'NULL');
|
||||
$color = $this->request->get('color', 'blue');
|
||||
|
||||
$vue_config = [
|
||||
'baseUrl' => '/PreorderIFrame', // URL to this controller
|
||||
'clusterId' => $clusterId !== NULL ? intval($clusterId) : null,
|
||||
'color' => htmlspecialchars($color),
|
||||
'baseUrl' => '/PreorderIFrame',
|
||||
'clusterId' => $this->request->get('clusterId') ? intval($this->request->get('clusterId')) : null,
|
||||
'gemeindeId' => $this->request->get('gemeindeId') ? intval($this->request->get('gemeindeId')) : null,
|
||||
'color' => htmlspecialchars($this->request->get('color', 'blue')),
|
||||
'orderType' => htmlspecialchars($this->request->get('orderType', 'order')), // 'order' or 'interest'
|
||||
];
|
||||
|
||||
$this->layout()->set("JSGlobals", $vue_config);
|
||||
@@ -34,7 +25,8 @@ class PreorderIFrameController extends mfBaseController
|
||||
|
||||
// --- API ENDPOINTS ---
|
||||
|
||||
public function getClustersAction() {
|
||||
public function getClustersAction()
|
||||
{
|
||||
self::returnJson(['clusters' => $this->preorderIFrameModel->getClusters($_SERVER['HTTP_X_FRAME_REFERRER'])]);
|
||||
}
|
||||
|
||||
@@ -44,8 +36,6 @@ class PreorderIFrameController extends mfBaseController
|
||||
if (!$clusterId) self::sendError("Cluster ID is required.");
|
||||
|
||||
$allClusters = $this->preorderIFrameModel->getClusters($_SERVER['HTTP_X_FRAME_REFERRER']);
|
||||
if (!$allClusters) self::sendError("No cluster found for the given ID.");
|
||||
|
||||
$clusterInfo = null;
|
||||
foreach ($allClusters as $cluster) {
|
||||
if ($cluster['id'] == $clusterId) {
|
||||
@@ -54,49 +44,59 @@ class PreorderIFrameController extends mfBaseController
|
||||
}
|
||||
}
|
||||
|
||||
$preorderCampaign = new Preordercampaign($clusterInfo['campaign_id']);
|
||||
if (!$clusterInfo) self::sendError("No cluster found for the given ID or origin.");
|
||||
|
||||
$preorderCampaign = new Preordercampaign($clusterInfo['campaign_id']);
|
||||
self::returnJson(['iframe_consents' => json_decode($preorderCampaign->iframe_consents ?? '[]')]);
|
||||
}
|
||||
|
||||
public function findCityAction()
|
||||
{
|
||||
$allowedClusters = $this->preorderIFrameModel->getClusters($_SERVER['HTTP_X_FRAME_REFERRER']);
|
||||
|
||||
$zip = $this->request->get('zip');
|
||||
$clusterId = $this->request->get('cluster_id');
|
||||
$cities = $this->preorderIFrameModel->findCities($zip, $clusterId);
|
||||
self::returnJson(['cities' => $cities]);
|
||||
$params = [
|
||||
'zip' => $this->request->get('zip'),
|
||||
'clusterId' => $this->request->get('cluster_id'),
|
||||
'gemeindeId' => $this->request->get('gemeinde_id'),
|
||||
];
|
||||
self::returnJson(['cities' => $this->preorderIFrameModel->findCities($params)]);
|
||||
}
|
||||
|
||||
public function findStreetAction()
|
||||
{
|
||||
// $this->checkOriginAndGetCampaign(); // Security check
|
||||
$zip = $this->request->get('zip');
|
||||
$city = $this->request->get('city');
|
||||
$clusterId = $this->request->get('cluster_id');
|
||||
$streets = $this->preorderIFrameModel->findStreets($zip, $city, $clusterId);
|
||||
self::returnJson(['streets' => $streets]);
|
||||
$params = [
|
||||
'zip' => $this->request->get('zip'),
|
||||
'city' => $this->request->get('city'),
|
||||
'clusterId' => $this->request->get('cluster_id'),
|
||||
'gemeindeId' => $this->request->get('gemeinde_id'),
|
||||
];
|
||||
self::returnJson(['streets' => $this->preorderIFrameModel->findStreets($params)]);
|
||||
}
|
||||
|
||||
public function findAddressAction()
|
||||
{
|
||||
$addresses = $this->preorderIFrameModel->findAddresses($_GET);
|
||||
self::returnJson(['addresses' => $addresses]);
|
||||
self::returnJson(['addresses' => $this->preorderIFrameModel->findAddresses($_GET)]);
|
||||
}
|
||||
|
||||
public function submitOrderAction()
|
||||
{
|
||||
$requestBody = file_get_contents('php://input');
|
||||
$preorderData = json_decode($requestBody, true);
|
||||
|
||||
$preorderData = json_decode(file_get_contents('php://input'), true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) self::sendError("Invalid JSON data.");
|
||||
|
||||
// Determine network/campaign from clusterId or gemeindeId
|
||||
$networkId = null;
|
||||
if (!empty($preorderData['additionalData']['clusterId'])) {
|
||||
$tt_network = NetworkModel::getFirst(['adb_network_id' => $preorderData['additionalData']['clusterId']]);
|
||||
if (!$tt_network) self::sendError("No network found for the given cluster ID.");
|
||||
if ($tt_network) $networkId = $tt_network->id;
|
||||
} elseif (!empty($preorderData['additionalData']['gemeindeId'])) {
|
||||
$gn = GemeindeNetzgebietModel::getFirst(['gemeinde_id' => $preorderData['additionalData']['gemeindeId']]);
|
||||
if ($gn) {
|
||||
$tt_network = NetworkModel::getFirst(['adb_netzgebiet_id' => $gn->netzgebiet_id]);
|
||||
if ($tt_network) $networkId = $tt_network->id;
|
||||
}
|
||||
}
|
||||
|
||||
$campaign = PreordercampaignModel::getFirst(['network_id' => $tt_network->id]);
|
||||
if (!$campaign) self::sendError("No campaign found for the given cluster ID.");
|
||||
if (!$networkId) self::sendError("No network/campaign found for the given area.");
|
||||
$campaign = PreordercampaignModel::getFirst(['network_id' => $networkId]);
|
||||
if (!$campaign) self::sendError("No campaign found for the given area.");
|
||||
|
||||
$h = new ADBHausnummer($preorderData['address']['hausnummer_id']);
|
||||
if (!$h->id) self::sendError("Invalid house number ID provided.");
|
||||
@@ -109,7 +109,6 @@ class PreorderIFrameController extends mfBaseController
|
||||
$data['adb_hausnummer_id'] = $preorderData['address']['hausnummer_id'];
|
||||
$data['adb_wohneinheit_id'] = $preorderData['address']['wohneinheit_id'];
|
||||
|
||||
|
||||
$new_status = null;
|
||||
if ($data['adb_wohneinheit_id'] && $w->id) {
|
||||
$status_code = max($w->status->code, $w->hausnummer->status->code);
|
||||
@@ -119,7 +118,7 @@ class PreorderIFrameController extends mfBaseController
|
||||
}
|
||||
$data["status_id"] = $new_status ? $new_status->id : 1;
|
||||
|
||||
$data['type'] = $preorderData['connectionType'] === 'vorsorge' ? 'provision' : 'order';
|
||||
$data['type'] = $preorderData['preorderType'] ?? 'order'; // 'order', 'interest', 'provision'
|
||||
$data['connection_type'] = $preorderData['customerType'] === 'business' ? 'business' : 'single-dwelling';
|
||||
|
||||
$data['accept_agb'] = $preorderData['acceptAgb'] ? 1 : 0;
|
||||
@@ -137,17 +136,12 @@ class PreorderIFrameController extends mfBaseController
|
||||
$data['city'] = (trim($preorderData['customer']['city'])) ?: null;
|
||||
$data['phone'] = (trim($preorderData['customer']['phone'])) ?: null;
|
||||
$data['email'] = (trim($preorderData['customer']['email'])) ?: null;
|
||||
|
||||
$data['edit_by'] = 1;
|
||||
$data['create_by'] = 1;
|
||||
|
||||
$preorder = PreorderModel::create($data);
|
||||
$preorder->createUcode();
|
||||
$new_id = $preorder->save();
|
||||
|
||||
if (!$new_id) {
|
||||
self::sendError("Failed to create preorder record.");
|
||||
}
|
||||
if (!$preorder->save()) self::sendError("Failed to create preorder record.");
|
||||
|
||||
self::returnJson(['orderCode' => $preorder->ucode, 'status' => 'success']);
|
||||
}
|
||||
|
||||
@@ -2,46 +2,9 @@
|
||||
|
||||
class PreorderIFrameModel extends mfBaseModel
|
||||
{
|
||||
// The constructor will use the default FronkDB connection
|
||||
// No need to override __construct if mfBaseModel handles it
|
||||
|
||||
public function init() {
|
||||
// Overwrite the table name if it doesn't match the class name
|
||||
$this->table = 'Preorder'; // Set a primary table if needed, though most methods define their own
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an origin has rights for a given cluster and returns the campaign.
|
||||
* @param int $clusterId The 'adb_netzgebiet_id' from the thetool.Network table.
|
||||
* @param string|null $origin The requesting origin (e.g., https://www.example.com).
|
||||
* @return array|null The campaign data if valid, otherwise null.
|
||||
*/
|
||||
public function getCampaignByClusterIdAndOrigin(int $clusterId, ?string $origin): ?array
|
||||
public function init()
|
||||
{
|
||||
// A null origin is not allowed for security reasons
|
||||
if (!$origin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$query = "
|
||||
SELECT pc.*
|
||||
FROM thetool.Preordercampaign pc
|
||||
JOIN thetool.Network n ON pc.Network_id = n.id
|
||||
WHERE n.adb_netzgebiet_id = ?
|
||||
";
|
||||
$res = $this->db->query($query, [$clusterId]);
|
||||
$campaign = $this->db->fetch_assoc($res);
|
||||
|
||||
if (!$campaign || empty($campaign['iframe_origins'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$allowedOrigins = json_decode($campaign['iframe_origins'], true);
|
||||
if (is_array($allowedOrigins) && in_array($origin, $allowedOrigins)) {
|
||||
return $campaign;
|
||||
}
|
||||
|
||||
return null;
|
||||
$this->table = 'Preorder';
|
||||
}
|
||||
|
||||
public function getClusters($frame_referrer): array
|
||||
@@ -59,63 +22,87 @@ class PreorderIFrameModel extends mfBaseModel
|
||||
$res = $this->db->query($query);
|
||||
$clusters = $this->db->fetch_all_assoc($res);
|
||||
|
||||
if (empty($clusters) || count($clusters) === 0) {
|
||||
return [
|
||||
['id' => 98, 'name' => 'Leibnitz', 'campaign_id' => 99, 'campaign_name' => 'X - Leibnitz']
|
||||
];
|
||||
if (empty($clusters)) {
|
||||
// Fallback for development or specific cases
|
||||
return [['id' => 98, 'name' => 'Leibnitz', 'campaign_id' => 99, 'campaign_name' => 'X - Leibnitz']];
|
||||
}
|
||||
|
||||
return $clusters;
|
||||
}
|
||||
|
||||
public function findCities(string $zip, int $adb_network_id): array
|
||||
public function findCities(array $params): array
|
||||
{
|
||||
$whereClause = "p.plzstring = " . $this->db->escape($params['zip']);
|
||||
if (!empty($params['gemeindeId'])) {
|
||||
$whereClause .= " AND g.id = " . intval($params['gemeindeId']);
|
||||
} elseif (!empty($params['clusterId'])) {
|
||||
$whereClause .= " AND gn.netzgebiet_id = " . intval($params['clusterId']);
|
||||
} else {
|
||||
return []; // No identifier provided
|
||||
}
|
||||
|
||||
$query = "
|
||||
SELECT DISTINCT o.name
|
||||
FROM addressdb.Plz p
|
||||
JOIN addressdb.Ortschaft o ON p.gemeinde_id = o.gemeinde_id
|
||||
JOIN addressdb.Gemeinde g ON o.gemeinde_id = g.id
|
||||
JOIN addressdb.GemeindeNetzgebiet gn ON g.id = gn.gemeinde_id
|
||||
WHERE p.plzstring = " . $this->db->escape($zip) . " AND gn.netzgebiet_id = " . intval($adb_network_id) . "
|
||||
LEFT JOIN addressdb.GemeindeNetzgebiet gn ON g.id = gn.gemeinde_id
|
||||
WHERE $whereClause
|
||||
ORDER BY o.name ASC
|
||||
";
|
||||
|
||||
$res = $this->db->query($query);
|
||||
$cities = $this->db->fetch_all_assoc($res);
|
||||
return array_column($cities, 'name');
|
||||
return array_column($this->db->fetch_all_assoc($res), 'name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds streets for a given ZIP, city, and cluster.
|
||||
* @param string $zip
|
||||
* @param string $city
|
||||
* @param int $clusterId
|
||||
* @return array
|
||||
*/
|
||||
public function findStreets(string $zip, string $city, int $clusterId): array
|
||||
public function findStreets(array $params): array
|
||||
{
|
||||
$whereClauses = [];
|
||||
if (!empty($params['gemeindeId'])) {
|
||||
$whereClauses[] = "g.id = " . intval($params['gemeindeId']);
|
||||
} elseif (!empty($params['clusterId'])) {
|
||||
$whereClauses[] = "gn.netzgebiet_id = " . intval($params['clusterId']);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
|
||||
$whereClauses[] = "o.name = '" . $this->db->escape($params['city']) . "'";
|
||||
$whereClauses[] = "EXISTS (SELECT 1 FROM addressdb.Plz p WHERE p.plzstring = " . $this->db->escape($params['zip']) . " AND p.gemeinde_id = o.gemeinde_id)";
|
||||
$whereString = implode(" AND ", $whereClauses);
|
||||
|
||||
$query = "
|
||||
SELECT DISTINCT s.name
|
||||
FROM addressdb.Strasse s
|
||||
JOIN addressdb.Gemeinde g ON s.gemeinde_id = g.id
|
||||
JOIN addressdb.Ortschaft o ON o.gemeinde_id = g.id AND o.name = '" . $this->db->escape($city) . "'
|
||||
JOIN addressdb.GemeindeNetzgebiet gn ON g.id = gn.gemeinde_id
|
||||
WHERE gn.netzgebiet_id = " . intval($clusterId) . "
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM addressdb.Plz p
|
||||
WHERE p.plzstring = " . $this->db->escape($zip) . "
|
||||
AND p.gemeinde_id = o.gemeinde_id
|
||||
)
|
||||
JOIN addressdb.Ortschaft o ON o.gemeinde_id = g.id
|
||||
LEFT JOIN addressdb.GemeindeNetzgebiet gn ON g.id = gn.gemeinde_id
|
||||
WHERE $whereString
|
||||
ORDER BY s.name ASC
|
||||
";
|
||||
|
||||
$res = $this->db->query($query);
|
||||
$streets = $this->db->fetch_all_assoc($res);
|
||||
return array_column($streets, 'name');
|
||||
return array_column($this->db->fetch_all_assoc($res), 'name');
|
||||
}
|
||||
|
||||
public function findAddresses(array $params): array
|
||||
{
|
||||
$whereClauses = [
|
||||
"p.plzstring = " . $this->db->escape($params['zip']),
|
||||
"o.name = '" . $this->db->escape($params['city']) . "'",
|
||||
"s.name = '" . $this->db->escape($params['street']) . "'",
|
||||
"h.hausnummer = '" . $this->db->escape($params['housenumber']) . "'",
|
||||
];
|
||||
|
||||
if (!empty($params['gemeinde_id'])) {
|
||||
$whereClauses[] = "h.gemeinde_id = " . intval($params['gemeinde_id']);
|
||||
} elseif (!empty($params['cluster_id'])) {
|
||||
$whereClauses[] = "h.netzgebiet_id = " . intval($params['cluster_id']);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
|
||||
$whereString = implode(" AND ", $whereClauses);
|
||||
|
||||
$query = "
|
||||
SELECT h.oaid, h.hausnummer, s.name as street, p.plzstring as zip, o.name as city, h.id as hausnummer_id, w.id as wohneinheit_id,
|
||||
h.stiege, h.unit_count as building_unit_count, h.tool_building_type as building_type,
|
||||
@@ -125,11 +112,7 @@ class PreorderIFrameModel extends mfBaseModel
|
||||
JOIN addressdb.Plz p ON h.plz_id = p.id
|
||||
JOIN addressdb.Ortschaft o ON s.gemeinde_id = o.gemeinde_id
|
||||
LEFT JOIN addressdb.Wohneinheit w ON w.hausnummer_id = h.id
|
||||
WHERE h.netzgebiet_id = " . intval($params['cluster_id']) . "
|
||||
AND p.plzstring = " . $this->db->escape($params['zip']) . "
|
||||
AND o.name = '" . $this->db->escape($params['city']) . "'
|
||||
AND s.name = '" . $this->db->escape($params['street']) . "'
|
||||
AND h.hausnummer = '" . $this->db->escape($params['housenumber']) . "'
|
||||
WHERE $whereString
|
||||
";
|
||||
|
||||
$results = $this->db->fetch_all_assoc($this->db->query($query));
|
||||
@@ -138,31 +121,23 @@ class PreorderIFrameModel extends mfBaseModel
|
||||
$addresses = [];
|
||||
$topCounter = 1;
|
||||
|
||||
if (count($results) > 1) {
|
||||
if (count($results) > 1 && $results[0]['wohneinheit_id'] !== null) {
|
||||
foreach ($results as $row) {
|
||||
$showText = $this->buildShowText($row, $topCounter++);
|
||||
$addresses[] = [
|
||||
'oaid' => $row['unit_oaid'],
|
||||
'street' => $row['street'],
|
||||
'housenumber' => $row['hausnummer'],
|
||||
'hausnummer_id' => $row['hausnummer_id'],
|
||||
'wohneinheit_id' => $row['wohneinheit_id'],
|
||||
'building_type' => intval($row['building_type']),
|
||||
'zip' => $row['zip'],
|
||||
'city' => $row['city'],
|
||||
'stiege' => $row['stiege'],
|
||||
'stock' => $row['stock'],
|
||||
'tuer' => $row['tuer'],
|
||||
'zusatz' => $row['zusatz'],
|
||||
'building_unit_count' => $row['building_unit_count'],
|
||||
'showText' => $showText,
|
||||
'preorderTypes' => ['order']
|
||||
];
|
||||
$addresses[] = array_merge($this->formatAddressRow($row), [
|
||||
'showText' => $this->buildShowText($row, $topCounter++)
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
$row = $results[0];
|
||||
$addresses[] = [
|
||||
'oaid' => $row['oaid'],
|
||||
$addresses[] = $this->formatAddressRow($results[0]);
|
||||
}
|
||||
|
||||
return $addresses;
|
||||
}
|
||||
|
||||
private function formatAddressRow(array $row): array
|
||||
{
|
||||
return [
|
||||
'oaid' => $row['unit_oaid'] ?? $row['oaid'],
|
||||
'street' => $row['street'],
|
||||
'housenumber' => $row['hausnummer'],
|
||||
'hausnummer_id' => $row['hausnummer_id'],
|
||||
@@ -176,13 +151,10 @@ class PreorderIFrameModel extends mfBaseModel
|
||||
'zusatz' => $row['zusatz'],
|
||||
'building_unit_count' => $row['building_unit_count'],
|
||||
'showText' => $this->buildShowText($row, 1),
|
||||
'preorderTypes' => ['order']
|
||||
'preorderTypes' => ['order', 'interest'] // Can be used for both
|
||||
];
|
||||
}
|
||||
|
||||
return $addresses;
|
||||
}
|
||||
|
||||
private function buildShowText(array $row, int $counter): string
|
||||
{
|
||||
$parts = array_filter([
|
||||
@@ -194,54 +166,4 @@ class PreorderIFrameModel extends mfBaseModel
|
||||
|
||||
return $parts ? implode(', ', $parts) : "Top {$counter}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new preorder record in the database.
|
||||
* @param array $data The validated preorder data from the form.
|
||||
* @param int $campaignId The ID of the associated preorder campaign.
|
||||
* @return array The result containing the new order code.
|
||||
*/
|
||||
public function createPreorder(array $data, int $campaignId): array
|
||||
{
|
||||
$customer = $data['customer'];
|
||||
$address = $data['address'];
|
||||
|
||||
// Generate a unique code for the preorder
|
||||
$ucode = strtoupper(substr(md5(uniqid(rand(), true)), 0, 10));
|
||||
|
||||
$preorderData = [
|
||||
'ucode' => $ucode,
|
||||
'thetool.Preordercampaign_id' => $campaignId,
|
||||
'oaid' => $address['oaid'],
|
||||
'type' => $data['preorderType'],
|
||||
'connection_type' => $data['connectionType'],
|
||||
'contact_type' => $customer['type'],
|
||||
'firstname' => $customer['firstname'],
|
||||
'lastname' => $customer['lastname'],
|
||||
'company' => $customer['company'] ?? null,
|
||||
'street' => $customer['street'],
|
||||
'housenumber' => $customer['housenumber'],
|
||||
'zip' => $customer['zip'],
|
||||
'city' => $customer['city'],
|
||||
'phone' => $customer['phone'],
|
||||
'email' => $customer['email'],
|
||||
'address_info' => $data['address_info'],
|
||||
'accept_agb' => $data['acceptAgb'] ? 1 : 0,
|
||||
'accept_dsgvo' => $data['acceptDsgvo'] ? 1 : 0,
|
||||
'accept_marketing' => $data['acceptMarketing'] ? 1 : 0,
|
||||
'accept_withdrawal' => $data['acceptWithdrawal'] ? 1 : 0,
|
||||
'addon_data' => json_encode($data['additionalData']),
|
||||
'submit_type' => 'api',
|
||||
'submit_request' => json_encode($data),
|
||||
'order_date' => time(),
|
||||
'create' => time(),
|
||||
'edit' => time(),
|
||||
'create_by' => 0, // System user
|
||||
'edit_by' => 0, // System user
|
||||
];
|
||||
|
||||
$this->db->insert('Preorder', $preorderData);
|
||||
|
||||
return ['code' => $ucode];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user