Files
thetool/Layout/default/VueViews/PreorderIFrame.php
2025-06-26 13:12:45 +02:00

644 lines
46 KiB
PHP

<?php
//var_dump($JSGlobals);exit;
?>
<!DOCTYPE html>
<html lang="de" class="h-full">
<head>
<meta charset="UTF-8">
<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>
<script type="text/javascript">
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'"; ?>;
<?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;
}
.form-input:focus, .form-select:focus, .custom-select-button:focus-within, .form-textarea:focus {
outline: 2px solid transparent;
outline-offset: 2px;
--tw-ring-color: var(--color-primary-500);
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);
}
</style>
</head>
<body class="bg-slate-100 flex items-start justify-center min-h-full p-4 font-sans">
<div id="app" class="w-full max-w-3xl">
</div>
<script>
// Injected configuration from the PHP Controller
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 }
},
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 filteredOptions = computed(() => {
if (!searchTerm.value) return props.options;
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();
}
};
watch(isOpen, (val) => {
if (val) {
nextTick(() => {
searchInput.value?.focus();
document.addEventListener('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();
}
function onKeyDown(event) {
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;
}
}
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>
`
};
const app = createApp({
setup() {
// --- CONFIGURATION & STATE ---
const config = window.VUE_APP_CONFIG;
const API_BASE_URL = config.baseUrl;
const currentStep = ref('loading');
const clusterId = ref(config.clusterId || 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 selectedAddress = 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,
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 ---
onMounted(() => {
const themes = {
blue: { 50: '#eff6ff', 100: '#dbeafe', 200: '#bfdbfe', 500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8', 800: '#1e40af', text: '#ffffff' },
green: { 50: '#f0fdf4', 100: '#dcfce7', 200: '#bbf7d0', 500: '#22c55e', 600: '#16a34a', 700: '#15803d', 800: '#166534', text: '#ffffff' },
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);
if (clusterId.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 = []; }
if (fromStep !== 'unit' && fromStep !== 'housenumber') { addressForm.housenumber = ''; }
if (fromStep !== 'unit') { addressForm.wohneinheit_id = ''; units.value = []; }
}
function goBackToStep(step) { addressStep.value = step; clearSubsequentSteps(step); }
watch(() => addressForm.zip, (n, o) => { if (n !== o && n?.length >= 4) { addressStep.value = 'city'; clearSubsequentSteps('zip'); fetchCities(n); } });
watch(() => addressForm.city, (n, o) => { if (n && n !== o) { addressStep.value = 'street'; clearSubsequentSteps('city'); fetchStreets(addressForm.zip, n); } });
watch(() => addressForm.street, (n, o) => { if (n && n !== o) { addressStep.value = 'housenumber'; clearSubsequentSteps('street');} });
watch(() => addressForm.wohneinheit_id, (n) => { if (n) { const unit = units.value.find(u => u.wohneinheit_id === n); if (unit) selectAddress(unit); } });
watch(currentStep, async (newStep) => {
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;
currentStep.value = 'error';
}
async function fetchClusters() {
isLoading.value = true;
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';
} else currentStep.value = 'clusterSelect';
} catch (e) { handleError(e, 'Cluster-Daten konnten nicht geladen werden.'); }
finally { isLoading.value = false; }
}
async function fetchCities(zip) {
isLoading.value = true;
try {
const response = await api.get('/findCity', { params: { zip, cluster_id: clusterId.value } });
cities.value = response.data?.cities || [];
} catch (e) { handleError(e, 'Orte für diese PLZ konnten nicht geladen werden.'); }
finally { isLoading.value = false; }
}
async function fetchStreets(zip, city) {
isLoading.value = true;
try {
const response = await api.get('/findStreet', { params: { zip, city, cluster_id: clusterId.value } });
streets.value = response.data?.streets || [];
} catch (e) { handleError(e, 'Straßen für diesen Ort konnten nicht geladen werden.'); }
finally { isLoading.value = false; }
}
async function findAddress() {
if (!addressForm.housenumber) return;
isLoading.value = true;
try {
const params = { ...addressForm, cluster_id: clusterId.value, format: 'flat' };
const response = await api.get('/findAddress', { params });
const addresses = (response.data?.addresses || []).filter(a => a.preorderTypes?.includes('order'));
if (addresses.length === 0) { currentStep.value = 'noOrderPossible'; }
else if (addresses.length === 1) { selectAddress(addresses[0]); }
else { units.value = addresses; addressStep.value = 'unit'; }
} catch (e) { handleError(e, 'Adresse konnte nicht verifiziert werden.'); }
finally { isLoading.value = false; }
}
function selectAddress(address) {
if (!address) { currentStep.value = 'noOrderPossible'; return; }
selectedAddress.value = address;
currentStep.value = 'orderForm';
}
function startNewOrder() {
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';
}
async function submitOrder() {
if (isFormInvalid.value) return;
isLoading.value = true; errorMessage.value = '';
const preorderData = {
preorderType: 'order',
customerType: form.customerType,
connectionType: form.connectionType,
acceptAgb: form.acceptAgb, acceptMarketing: form.acceptMarketing, acceptDsgvo: form.acceptDsgvo, acceptWithdrawal: form.acceptWithdrawal,
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,
company: form.customerType === 'Businesskunde' ? form.firstName + ' ' + form.lastName : '',
street: form.billingAddressChoice === 'Andere' ? form.billing.street : selectedAddress.value.street,
housenumber: form.billingAddressChoice === 'Andere' ? form.billing.housenumber : selectedAddress.value.housenumber,
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
}
};
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.'); }
finally { isLoading.value = false; }
}
const isFormInvalid = computed(() => {
const { firstName, lastName, email, billingAddressChoice, billing } = form;
// Basic form field validation
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;
}
}
return false;
});
return {
currentStep, clusterId, clusters, addressStep, addressForm, goBackToStep,
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>
</header>
<main>
<transition name="slide-fade" mode="out-in">
<div v-if="currentStep === 'loading'" class="text-center p-8">
<div class="flex justify-center items-center"><svg class="animate-spin -ml-1 mr-3 h-8 w-8 text-[var(--color-primary-600)]" 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 class="text-slate-600 text-lg">Lade...</span></div>
</div>
<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>
</div>
<div v-else-if="currentStep === 'addressSearch'" class="max-w-xl mx-auto">
<h3 class="text-xl font-semibold text-slate-800 text-center mb-6">1. Adresse prüfen</h3>
<div class="space-y-4">
<div class="p-4 rounded-lg border border-slate-300 transition-all" :class="{'wizard-step-active': addressStep === 'zip'}"><div class="flex justify-between items-center"><p class="font-semibold text-slate-700">Postleitzahl</p><button v-if="addressStep !== 'zip'" @click="goBackToStep('zip')" class="text-sm font-semibold text-[var(--color-primary-600)] hover:text-[var(--color-primary-800)]">Ändern</button></div><div v-if="addressStep === 'zip'" class="mt-2"><input v-model.trim="addressForm.zip" type="text" placeholder="PLZ eingeben, z.B. 8010" class="form-input w-full rounded-md border-slate-300 shadow-sm py-2.5 px-3"></div><p v-else class="text-slate-600 mt-1 font-medium">{{ addressForm.zip }}</p></div>
<div v-if="addressStep === 'city' || addressForm.city" class="p-4 rounded-lg border border-slate-300 transition-all" :class="{'wizard-step-active': addressStep === 'city'}"><div class="flex justify-between items-center"><p class="font-semibold text-slate-700">Ort</p><button v-if="addressStep !== 'zip' && addressStep !== 'city'" @click="goBackToStep('city')" class="text-sm font-semibold text-[var(--color-primary-600)] hover:text-[var(--color-primary-800)]">Ändern</button></div><div v-if="addressStep === 'city'" class="mt-2"><SearchableSelect v-model="addressForm.city" :options="cities" :disabled="isLoading" placeholder="Ort auswählen..." /></div><p v-else class="text-slate-600 mt-1 font-medium">{{ addressForm.city }}</p></div>
<div v-if="addressStep === 'street' || addressForm.street" class="p-4 rounded-lg border border-slate-300 transition-all" :class="{'wizard-step-active': addressStep === 'street'}"><div class="flex justify-between items-center"><p class="font-semibold text-slate-700">Straße</p><button v-if="addressStep !== 'zip' && addressStep !== 'city' && addressStep !== 'street'" @click="goBackToStep('street')" class="text-sm font-semibold text-[var(--color-primary-600)] hover:text-[var(--color-primary-800)]">Ändern</button></div><div v-if="addressStep === 'street'" class="mt-2"><SearchableSelect v-model="addressForm.street" :options="streets" :disabled="isLoading" placeholder="Straße auswählen..." /></div><p v-else class="text-slate-600 mt-1 font-medium">{{ addressForm.street }}</p></div>
<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>
<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>
</div>
</div>
<div v-else-if="currentStep === 'orderForm' && selectedAddress" class="space-y-8">
<div class="p-4 bg-[var(--color-primary-50)] border-l-4 border-[var(--color-primary-500)] rounded-r-lg">
<h3 class="font-bold text-[var(--color-primary-800)]">Anschlussadresse ausgewählt:</h3>
<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>
<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>
</fieldset>
<fieldset>
<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>
</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>
<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>
<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>
</transition>
</main>
</div>
`
});
app.component('SearchableSelect', SearchableSelect);
app.mount('#app');
</script>
</body>
</html>