feat: Implement the motorcycle booking plugin including frontend booking flow and admin management.

This commit is contained in:
2026-02-26 11:54:13 +01:00
commit 658f666647
12 changed files with 4072 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

BIN
assets/.DS_Store vendored Normal file

Binary file not shown.

346
assets/css/admin-style.css Normal file
View File

@@ -0,0 +1,346 @@
/* Admin specific styles for ON Booking Plugin */
/* Base Variables - Default to LIGHT Mode */
:root {
--on-bg: #ffffff;
--on-text: #1d2327;
/* WP Default dark gray */
--on-border: #c3c4c7;
/* WP Default gray border */
--on-blue: #0061ff;
--on-muted: #646970;
--on-modal-bg: #fff;
--on-modal-overlay: rgba(0, 0, 0, 0.7);
--on-card-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
--on-today-bg: rgba(0, 97, 255, 0.1);
/* Kanban Vars */
--on-kanban-col-bg: #f0f0f1;
--on-kanban-card-bg: #fff;
}
/* Mixin-like block for Dark Theme Values */
/* We apply this to :root in media query AND to .on-theme-dark class */
.on-theme-dark-vars {
--on-bg: #111111;
--on-text: #f0f0f1;
--on-border: #333333;
--on-muted: #a7aaad;
--on-modal-bg: #1f1f1f;
--on-modal-overlay: rgba(0, 0, 0, 0.85);
--on-card-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
--on-kanban-col-bg: #2c2c2c;
--on-kanban-card-bg: #1f1f1f;
}
/* 1. AUTO Mode (Default) - Uses Media Query */
@media (prefers-color-scheme: dark) {
:root {
/* Copy of Dark Theme Values */
--on-bg: #111111;
--on-text: #f0f0f1;
--on-border: #333333;
--on-muted: #a7aaad;
--on-modal-bg: #1f1f1f;
--on-modal-overlay: rgba(0, 0, 0, 0.85);
--on-card-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
--on-kanban-col-bg: #2c2c2c;
--on-kanban-card-bg: #1f1f1f;
}
}
/* 2. FORCED LIGHT Mode */
.on-theme-light {
--on-bg: #ffffff;
--on-text: #1d2327;
--on-border: #c3c4c7;
--on-muted: #646970;
--on-modal-bg: #fff;
--on-modal-overlay: rgba(0, 0, 0, 0.7);
--on-card-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
--on-kanban-col-bg: #f0f0f1;
--on-kanban-card-bg: #fff;
}
/* 3. FORCED DARK Mode (Overrides Auto Light) */
.on-theme-dark {
--on-bg: #111111;
--on-text: #f0f0f1;
--on-border: #333333;
--on-muted: #a7aaad;
--on-modal-bg: #1f1f1f;
--on-modal-overlay: rgba(0, 0, 0, 0.85);
--on-card-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
--on-kanban-col-bg: #2c2c2c;
--on-kanban-card-bg: #1f1f1f;
}
#on-admin-calendar-wrapper,
.wrap {
font-family: 'Montserrat', sans-serif;
}
/* Calendar Container */
#on-admin-calendar {
max-width: 100%;
margin: 0 auto;
font-family: 'Montserrat', sans-serif;
background: var(--on-bg);
color: var(--on-text);
border: 1px solid var(--on-border);
border-radius: 4px;
box-shadow: var(--on-card-shadow);
padding: 20px;
}
/* FullCalendar Overrides */
.fc-theme-standard td,
.fc-theme-standard th,
.fc-theme-standard .fc-scrollgrid {
border-color: var(--on-border);
}
.fc .fc-toolbar-title {
color: var(--on-text);
text-transform: uppercase;
font-style: italic;
font-weight: 800;
}
.fc .fc-button-primary {
background-color: transparent;
border: 1px solid var(--on-blue);
color: var(--on-blue);
text-transform: uppercase;
font-weight: 700;
transition: all 0.3s ease;
}
.fc .fc-button-primary:hover,
.fc .fc-button-primary:not(:disabled).fc-button-active {
background-color: var(--on-blue);
border-color: var(--on-blue);
color: #fff;
box-shadow: none;
}
.fc .fc-button-primary:disabled {
border-color: var(--on-border);
color: var(--on-muted);
}
.fc .fc-col-header-cell-cushion {
color: var(--on-text);
text-transform: uppercase;
font-size: 0.8rem;
padding: 10px 0;
}
.fc .fc-daygrid-day-number {
color: var(--on-text);
font-weight: 600;
text-decoration: none;
}
.fc-day-today {
background-color: var(--on-today-bg) !important;
}
.fc-event {
cursor: pointer;
border: none;
border-radius: 2px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.fc-h-event .fc-event-main {
color: #fff;
font-weight: 600;
padding: 2px 4px;
}
/* MODAL STYLES */
.on-modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--on-modal-overlay);
backdrop-filter: blur(5px);
z-index: 10001;
justify-content: center;
align-items: center;
padding: 20px;
opacity: 0;
transition: opacity 0.3s ease;
box-sizing: border-box;
}
.on-modal-overlay.is-open {
display: flex;
opacity: 1;
}
.on-modal-content {
background-color: var(--on-modal-bg);
width: 100%;
max-width: 600px;
max-height: 90vh;
overflow-y: auto;
border-left: 5px solid var(--on-blue);
padding: 30px;
position: relative;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
transform: translateY(20px);
transition: transform 0.3s ease;
color: var(--on-text);
font-family: 'Montserrat', sans-serif;
}
.on-modal-overlay.is-open .on-modal-content {
transform: translateY(0);
}
/* Close Button (X) */
.on-close-btn {
position: absolute;
top: 15px;
right: 20px;
background: none;
border: none;
color: var(--on-muted);
font-size: 2rem;
cursor: pointer;
line-height: 1;
transition: color 0.2s;
padding: 0;
}
.on-close-btn:hover {
color: var(--on-blue);
}
/* Modal Typography */
.on-modal-content h3 {
text-transform: uppercase;
font-weight: 800;
font-style: italic;
margin: 0 0 20px 0;
color: var(--on-text);
font-size: 1.5rem;
line-height: 1.2;
}
.on-modal-content h3 span {
color: var(--on-blue);
}
.on-modal-content strong {
color: var(--on-muted);
}
.on-modal-content a {
color: var(--on-blue);
text-decoration: none;
}
.on-modal-content a:hover {
text-decoration: underline;
}
/* KANBAN STYLES */
.on-kanban-board {
display: flex;
gap: 20px;
padding: 20px 0;
overflow-x: auto;
}
.on-kanban-column {
flex: 1;
min-width: 250px;
background: var(--on-kanban-col-bg);
border-radius: 6px;
padding: 15px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.on-kanban-header {
text-transform: uppercase;
font-weight: 800;
color: var(--on-text);
margin-bottom: 15px;
display: flex;
justify-content: space-between;
align-items: center;
}
.on-kanban-header .count {
background: var(--on-blue);
color: #fff;
padding: 2px 8px;
border-radius: 10px;
font-size: 0.8rem;
}
.on-kanban-items {
min-height: 100px;
/* Drop target area */
}
.on-kanban-card {
background: var(--on-kanban-card-bg);
border: 1px solid var(--on-border);
padding: 15px;
margin-bottom: 10px;
border-radius: 4px;
cursor: move;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: transform 0.2s;
}
.on-kanban-card:hover {
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
border-color: var(--on-blue);
}
.on-drop-hover {
background: rgba(0, 97, 255, 0.05);
border: 2px dashed var(--on-blue);
}
.card-title {
font-weight: 700;
color: var(--on-text);
margin-bottom: 5px;
}
.card-meta {
font-size: 0.85rem;
color: var(--on-muted);
}
/* Status-based Card Colors */
.on-kanban-card[data-status="waiting"] {
border-left: 4px solid #0061ff;
/* Blue */
}
.on-kanban-card[data-status="in_progress"] {
border-left: 4px solid #ff9500;
/* Orange */
}
.on-kanban-card[data-status="done"] {
border-left: 4px solid #34c759;
/* Green */
}

392
assets/css/style.css Normal file
View File

@@ -0,0 +1,392 @@
/* --- SCOPED CSS --- */
.on-booking-system {
font-family: 'Montserrat', sans-serif;
--on-blue: #0061ff;
--on-dark: #111111;
--on-gray: #1f1f1f;
--on-text: #ffffff;
--on-muted: #888888;
--on-border: #333333;
/* Reset some common WP theme conflicts */
box-sizing: border-box;
}
.on-booking-system *,
.on-booking-system *::before,
.on-booking-system *::after {
box-sizing: inherit;
}
/* 1. DER TRIGGER BUTTON */
.on-trigger-btn {
background-color: var(--on-blue);
color: var(--on-text);
border: none;
padding: 15px 40px;
font-size: 1.1rem;
font-weight: 800;
text-transform: uppercase;
font-style: italic;
cursor: pointer;
transform: skewX(-10deg);
transition: transform 0.2s, background 0.3s;
display: inline-block;
text-decoration: none;
}
.on-trigger-btn span {
display: block;
transform: skewX(10deg);
/* Text gerade rücken */
}
.on-trigger-btn:hover {
background-color: #004ecc;
transform: skewX(-10deg) scale(1.05);
}
/* 2. DAS MODAL OVERLAY */
.on-modal-overlay {
display: none;
/* Standardmäßig versteckt */
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.85);
/* Dunkler Dimmer */
backdrop-filter: blur(5px);
z-index: 99999;
/* Use block or flex column for scrolling content */
display: none;
flex-direction: column;
align-items: center;
/* Fixes full width issue */
overflow-y: auto;
/* The overlay itself scrolls */
-webkit-overflow-scrolling: touch;
padding: 20px;
opacity: 0;
transition: opacity 0.3s ease;
box-sizing: border-box;
}
/* Prevent body scroll when modal is open */
html.on-modal-open,
body.on-modal-open {
overflow: hidden !important;
height: 100vh !important;
}
.on-modal-overlay.is-open {
display: flex;
opacity: 1;
}
/* 3. DER MODAL INHALT */
.on-modal-content {
box-sizing: border-box;
background-color: var(--on-gray);
width: 95%;
max-width: 500px;
/* Remove fixed height to allow overlay scrolling */
margin: 40px auto;
/* Center naturally in flow */
position: relative;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.8);
transform: translateY(20px);
transition: transform 0.3s ease;
color: var(--on-text);
}
.on-modal-overlay.is-open .on-modal-content {
transform: translateY(0);
}
/* Close Button (X) */
.on-close-btn {
position: absolute;
top: 15px;
right: 20px;
background: none;
border: none;
color: var(--on-muted);
font-size: 2rem;
cursor: pointer;
line-height: 1;
transition: color 0.2s;
padding: 0;
}
.on-close-btn:hover {
color: var(--on-blue);
}
/* --- FORM STYLES --- */
.on-booking-system h3 {
text-transform: uppercase;
font-weight: 800;
font-style: italic;
margin: 0 0 20px 0;
color: var(--on-text);
font-size: 1.5rem;
line-height: 1.2;
}
.on-booking-system h3 span {
color: var(--on-blue);
}
.on-form-group {
margin-bottom: 15px;
}
.on-form-group label {
display: block;
font-size: 0.7rem;
text-transform: uppercase;
color: var(--on-muted);
margin-bottom: 5px;
font-weight: 700;
}
.on-input {
width: 100%;
background: var(--on-dark);
border: 1px solid var(--on-border);
padding: 12px;
color: #fff;
font-size: 1rem;
border-radius: 0;
font-family: inherit;
transition: border-color 0.2s;
}
.on-input:focus {
outline: none;
border-color: var(--on-blue);
}
/* Select styling */
select.on-input {
appearance: none;
-webkit-appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 10px center;
background-size: 1em;
}
/* Calendar Styling */
.on-calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
background: var(--on-dark);
padding: 10px;
border: 1px solid var(--on-border);
margin-bottom: 10px;
}
.on-cal-btn {
background: none;
border: none;
color: var(--on-blue);
font-size: 1.2rem;
cursor: pointer;
font-weight: bold;
padding: 5px 10px;
}
.on-month-label {
font-weight: 700;
text-transform: uppercase;
color: var(--on-text);
}
.on-calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 5px;
}
.on-day-name {
font-size: 0.7rem;
color: var(--on-muted);
text-align: center;
text-transform: uppercase;
}
.on-day {
background: var(--on-dark);
border: 1px solid var(--on-border);
padding: 10px 0;
text-align: center;
cursor: pointer;
color: var(--on-text);
font-weight: 600;
font-size: 0.9rem;
position: relative;
}
.on-day:hover:not(.empty):not(.disabled) {
border-color: var(--on-blue);
}
.on-day.selected {
background: var(--on-blue);
border-color: var(--on-blue);
color: #fff;
}
.on-day.empty {
background: transparent;
border: none;
cursor: default;
}
.on-day.disabled {
opacity: 0.3;
cursor: not-allowed;
}
/* Time Slots */
.on-time-section {
display: none;
margin-top: 15px;
animation: fadeIn 0.3s ease;
}
.on-time-section.active {
display: block;
}
.on-time-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.on-time-slot {
background: var(--on-dark);
border: 1px solid var(--on-border);
padding: 8px;
text-align: center;
cursor: pointer;
font-size: 0.85rem;
color: var(--on-text);
}
.on-time-slot:hover:not(.disabled) {
border-color: var(--on-blue);
}
.on-time-slot.selected {
background: var(--on-blue);
border-color: var(--on-blue);
}
.on-time-slot.disabled {
opacity: 0.3;
cursor: not-allowed;
text-decoration: line-through;
}
/* Submit Button */
.on-submit-btn {
width: 100%;
background: var(--on-blue);
color: #fff;
border: none;
padding: 15px;
font-weight: 800;
text-transform: uppercase;
font-style: italic;
cursor: pointer;
transform: skewX(-10deg);
margin-top: 20px;
transition: background 0.3s;
}
.on-submit-btn span {
display: block;
transform: skewX(10deg);
}
.on-submit-btn:hover {
background: #004ecc;
}
.on-submit-btn:disabled {
background: var(--on-muted);
cursor: not-allowed;
}
/* Feedback Messages */
.on-success-msg {
color: #00ff88;
}
.on-error-msg {
color: #ff3333;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
#statusResult {
margin-top: 15px;
font-size: 1.1rem;
color: var(--on-text);
text-align: center;
}
#onTrackingIdInput {
text-transform: uppercase;
}
/* Success Modal - Explicit styling for when moved to body */
#onSuccessModalOverlay {
--on-blue: #0061ff;
--on-gray: #1f1f1f;
--on-text: #ffffff;
--on-muted: #888888;
font-family: 'Montserrat', sans-serif;
}
#onSuccessModalOverlay .on-modal-content {
background-color: #1f1f1f;
padding: 40px 30px;
border-radius: 8px;
color: #ffffff;
}
#onSuccessModalOverlay h3 {
text-transform: uppercase;
font-weight: 800;
font-style: italic;
margin: 0 0 10px 0;
color: #ffffff;
font-size: 1.5rem;
}
/* Fix IOs Zoom on Input Focus */
@media screen and (max-width: 768px) {
.on-input,
.on-booking-system input,
.on-booking-system select,
.on-booking-system textarea {
font-size: 16px !important;
}
}

287
assets/js/admin-calendar.js Normal file
View File

@@ -0,0 +1,287 @@
document.addEventListener('DOMContentLoaded', function () {
var calendarEl = document.getElementById('on-admin-calendar');
if (!calendarEl) return;
// Modal Logic
var modal = document.getElementById('onAdminModal');
var closeBtn = document.getElementById('onAdminCloseModal');
var contentDiv = document.getElementById('onBookingDetailsContent');
var currentPostId = null;
function openModal() {
if (modal) modal.classList.add('is-open');
}
function closeModal() {
if (modal) modal.classList.remove('is-open');
currentPostId = null;
}
if (closeBtn) {
closeBtn.addEventListener('click', closeModal);
}
if (modal) {
modal.addEventListener('click', function (e) {
if (e.target === modal) closeModal();
});
}
// Helper: Format Date for Inputs
function formatDateISO(date) {
var d = new Date(date),
month = '' + (d.getMonth() + 1),
day = '' + d.getDate(),
year = d.getFullYear();
if (month.length < 2) month = '0' + month;
if (day.length < 2) day = '0' + day;
return [year, month, day].join('-');
}
var calendar = new FullCalendar.Calendar(calendarEl, {
initialView: 'dayGridMonth',
locale: 'de',
height: window.innerHeight - 120, // Dynamic height: Viewport - WP Admin items
windowResize: function (view) {
calendar.setOption('height', window.innerHeight - 120);
},
editable: true, // Enable Drag & Drop
droppable: false,
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay'
},
events: {
url: ajaxurl,
method: 'POST',
extraParams: {
action: 'on_get_admin_bookings',
nonce: onBookingAdmin.nonce
}
},
eventDrop: function (info) {
// Handle Drag & Drop
if (!confirm("Möchtest du den Termin '" + info.event.title + "' wirklich verschieben auf " + info.event.start.toLocaleDateString() + "?")) {
info.revert();
return;
}
var newDate = formatDateISO(info.event.start);
var newTime = info.event.start.toTimeString().substring(0, 5); // HH:MM
// If dragged in month view, time might be messed up or set to 00:00.
// Ideally we keep original time if we don't have time view info?
// FullCalendar standard: strip time if dayGrid.
// Let's try to preserve time if possible or default to start time?
// Getting original time from oldEvent?
if (info.view.type === 'dayGridMonth' && info.event.allDay) {
// It might strip time. Let's assume we want to keep original time?
// But we don't have it easily if it became allDay.
// Actually, on backend we just updated the DATE.
// Let's check what we send.
// For now, let's just send the date and keep existing time if we pass empty time string?
// But if we drag to a timeSlot in week view, we definitely want the time.
if (!info.event.startStr.includes('T')) {
// It's a date string only
newTime = ''; // Backend should preserve old time if this is empty
}
}
jQuery.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'on_update_booking_date',
nonce: onBookingAdmin.nonce,
post_id: info.event.id,
new_date: newDate,
new_time: newTime
},
success: function (response) {
if (!response.success) {
alert('Fehler: ' + response.data.message);
info.revert();
}
},
error: function () {
alert('Verbindungsfehler.');
info.revert();
}
});
},
eventClick: function (info) {
info.jsEvent.preventDefault();
var postId = info.event.id;
if (postId) {
currentPostId = postId;
openModal();
contentDiv.innerHTML = '<div style="text-align:center; padding: 20px;">Lade Details...</div>';
// Fetch Details
jQuery.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'on_get_booking_details',
nonce: onBookingAdmin.nonce,
post_id: postId
},
success: function (response) {
if (response.success) {
var data = response.data;
var html = '<div>' + data.content + '</div>';
// Actions Area
html += '<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #444; display: flex; gap: 10px; flex-wrap: wrap;">';
// Delete Button
html += '<button id="onBtnDelete" style="background:#d93025; color:#fff; border:none; padding:8px 15px; cursor:pointer; font-weight:bold;">Löschen</button>';
// Reschedule Toggle
html += '<button id="onBtnReschedule" style="background:#0061ff; color:#fff; border:none; padding:8px 15px; cursor:pointer; font-weight:bold;">Verschieben</button>';
html += '</div>';
// Reschedule Form (Hidden)
html += '<div id="onRescheduleForm" style="display:none; margin-top: 15px; background: rgba(255,255,255,0.05); padding: 15px; border: 1px solid #444;">';
html += '<h4 style="margin-top:0; color:#fff;">Neuer Termin</h4>';
html += '<label style="display:block; margin-bottom:5px;">Datum: <input type="date" id="onNewDate" value="' + (data.date || '') + '" style="width:100%; padding:8px;"></label>';
html += '<label style="display:block; margin-bottom:10px;">Zeit: <input type="time" id="onNewTime" value="' + (data.time || '') + '" style="width:100%; padding:8px;"></label>';
html += '<button id="onBtnSaveReschedule" style="background:#008000; color:#fff; border:none; padding:8px 15px; cursor:pointer; margin-right:5px;">Speichern</button>';
html += '<button id="onBtnCancelReschedule" style="background:#666; color:#fff; border:none; padding:8px 15px; cursor:pointer;">Abbrechen</button>';
html += '</div>';
contentDiv.innerHTML = html;
// Bind Events (jQuery Delegation for robustness when content is replaced)
// We can use direct clicks here since we just replaced HTML, but consistent jQuery is cleaner.
$('#onBtnDelete').off('click').on('click', function () {
if (confirm('Möchtest du diese Buchung wirklich unwiderruflich löschen?')) {
deleteBooking(currentPostId);
}
});
$('#onBtnReschedule').off('click').on('click', function () {
$('#onRescheduleForm').show();
$(this).hide();
});
$('#onBtnCancelReschedule').off('click').on('click', function () {
$('#onRescheduleForm').hide();
$('#onBtnReschedule').show();
});
$('#onBtnSaveReschedule').off('click').on('click', function () {
var nd = $('#onNewDate').val();
var nt = $('#onNewTime').val();
updateBooking(currentPostId, nd, nt);
});
// Save Notes Handler
$('#onSaveNotesBtnCalendar').off('click').on('click', function () {
var btn = $(this);
var notes = $('#onAdminNotesCalendar').val();
var msg = $('#onSaveNotesMsgCalendar');
btn.prop('disabled', true);
msg.text('Speichere...').css('color', '#333');
jQuery.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'on_update_booking_notes',
post_id: currentPostId,
notes: notes,
nonce: onBookingAdmin.nonce
},
success: function (response) {
if (response.success) {
msg.text('Gespeichert!').css('color', 'green');
} else {
msg.text('Fehler.').css('color', 'red');
}
},
error: function () {
msg.text('Fehler.').css('color', 'red');
},
complete: function () {
setTimeout(function () {
btn.prop('disabled', false);
msg.text('');
}, 2000);
}
});
});
} else {
contentDiv.innerHTML = 'Fehler: ' + (response.data.message || 'Unbekannt');
}
},
error: function () {
contentDiv.innerHTML = 'Verbindungsfehler.';
}
});
}
}
});
calendar.render();
// AJAX Actions
function deleteBooking(id) {
contentDiv.innerHTML = 'Lösche...';
jQuery.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'on_delete_booking',
nonce: onBookingAdmin.nonce,
post_id: id
},
success: function (res) {
if (res.success) {
closeModal();
calendar.refetchEvents(); // Refresh calendar
} else {
alert('Fehler: ' + res.data.message);
openModal(); // Re-open or keep open?
}
}
});
}
function updateBooking(id, date, time) {
if (!date || !time) {
alert('Bitte Datum und Zeit wählen.');
return;
}
var saveBtn = document.getElementById('onBtnSaveReschedule');
saveBtn.innerText = 'Speichere...';
jQuery.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'on_update_booking_date',
nonce: onBookingAdmin.nonce,
post_id: id,
new_date: date,
new_time: time
},
success: function (res) {
if (res.success) {
closeModal();
calendar.refetchEvents();
} else {
alert('Fehler: ' + res.data.message);
saveBtn.innerText = 'Speichern';
}
}
});
}
});

237
assets/js/admin-kanban.js Normal file
View File

@@ -0,0 +1,237 @@
jQuery(document).ready(function ($) {
// Make cards draggable
$('.on-kanban-card').draggable({
revert: 'invalid', // Snap back if not dropped on a valid droppable
helper: 'clone',
cursor: 'move',
zIndex: 100,
cancel: '.on-edit-icon, input, textarea, button, select, option' // Prevent dragging when clicking icon
});
// Make columns droppable
$('.on-kanban-column').droppable({
accept: '.on-kanban-card',
hoverClass: 'on-drop-hover',
drop: function (event, ui) {
var card = ui.draggable;
var newStatus = $(this).data('status');
var postId = card.data('id');
var oldParent = card.parent();
var newParent = $(this).find('.on-kanban-items');
// Move card visually immediately
card.detach().appendTo(newParent);
// Update Counts
updateCounts();
// AJAX Update
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'on_update_booking_status',
nonce: onBookingKanban.nonce,
post_id: postId,
status: newStatus
},
success: function (response) {
if (response.success) {
// Update card's data-status attribute to trigger color change
card.attr('data-status', newStatus);
} else {
alert('Fehler: ' + response.data.message);
// Revert visual change
card.detach().appendTo(oldParent);
updateCounts();
}
},
error: function () {
alert('Verbindungsfehler.');
// Revert
card.detach().appendTo(oldParent);
updateCounts();
}
});
}
});
function updateCounts() {
$('.on-kanban-column').each(function () {
var count = $(this).find('.on-kanban-card').length;
$(this).find('.count').text(count);
});
}
// --- MODAL LOGIC (Copied/Adapted from Calendar) ---
const modal = $('#onAdminModal');
const modalContent = $('#onBokingDetailsContent'); // Typo in original? No, ID is onBookingDetailsContent
const modalBody = $('#onBookingDetailsContent');
// Close Modal
$('#onAdminCloseModal').on('click', function () {
// Remove WordPress editor instance before closing
if (typeof wp !== 'undefined' && wp.editor) {
wp.editor.remove('onAdminNotesEditor');
}
modal.removeClass('is-open');
});
$(window).on('click', function (event) {
if ($(event.target).is(modal)) {
// Remove WordPress editor instance before closing
if (typeof wp !== 'undefined' && wp.editor) {
wp.editor.remove('onAdminNotesEditor');
}
modal.removeClass('is-open');
}
});
// Open Modal on Card Click
// Use 'on' on document/container for dynamic elements if re-rendering, but cards are static here mostly (except drag moves).
// Delegated event better.
// Open Modal Function
function openAdminModal(postId) {
modalBody.html('<p>Lade Details...</p>');
modal.addClass('is-open');
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'on_get_booking_details',
post_id: postId,
nonce: onBookingKanban.nonce
},
success: function (response) {
if (response.success) {
const data = response.data;
let html = `
<h4>${data.title}</h4>
<div style="margin-bottom:15px; padding:10px; background:#f5f5f5; border-radius:4px; color:#333;">
${data.content}
</div>
<div style="margin-top:20px;">
<label style="font-weight:bold; display:block; margin-bottom:5px;">Öffentliche Notizen (für Kunden sichtbar):</label>
<textarea id="onAdminNotesEditor" style="width:100%; height:150px;">${data.notes || ''}</textarea>
<button id="onSaveNotesBtn" class="button button-primary" style="margin-top:10px;" data-id="${data.id}">Notiz Speichern</button>
<span id="onSaveNotesMsg" style="margin-left:10px; font-weight:bold;"></span>
</div>
<hr>
<button class="button button-secondary" id="onDeleteBookingBtn" data-id="${data.id}" style="color: #a00; border-color: #a00;">Löschen</button>
`;
modalBody.html(html);
// Initialize WordPress Editor after modal content is loaded
setTimeout(function () {
wp.editor.initialize('onAdminNotesEditor', {
tinymce: {
wpautop: true,
plugins: 'lists,paste,textcolor',
toolbar1: 'bold,italic,bullist,numlist,undo,redo'
},
quicktags: true,
mediaButtons: false
});
}, 100);
} else {
modalBody.html('<p style="color:red;">Fehler beim Laden.</p>');
}
},
error: function () {
modalBody.html('<p style="color:red;">Verbindungsfehler.</p>');
}
});
}
// Open Modal on Card Click (Delegated)
$(document).on('click', '.on-kanban-card', function (e) {
console.log('Card clicked');
// Prevent if clicking actual input elements usually, but here we only have text and icon.
// The draggable cancel option handles the drag issue.
const postId = $(this).data('id');
openAdminModal(postId);
});
// Explicit handler for icon just in case bubbling is weird or user clicks precisely on it
$(document).on('click', '.on-edit-icon', function (e) {
console.log('Icon clicked');
e.stopPropagation(); // Stop bubbling if we want to handle distinct logic, but here we want same logic.
// Actually if we stop propagation, the card click won't fire.
// But let's call openAdminModal directly to be sure.
const postId = $(this).closest('.on-kanban-card').data('id');
openAdminModal(postId);
});
// Save Notes Handler
$(document).on('click', '#onSaveNotesBtn', function () {
const btn = $(this);
const postId = btn.data('id');
// Get content from WordPress editor (TinyMCE or textarea fallback)
const notes = wp.editor.getContent('onAdminNotesEditor');
const msg = $('#onSaveNotesMsg');
btn.prop('disabled', true);
msg.text('Speichere...').css('color', '#333');
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'on_update_booking_notes',
post_id: postId,
notes: notes,
nonce: onBookingKanban.nonce
},
success: function (response) {
if (response.success) {
msg.text('Gespeichert!').css('color', 'green');
} else {
msg.text('Fehler.').css('color', 'red');
}
},
error: function () {
msg.text('Fehler.').css('color', 'red');
},
complete: function () {
setTimeout(() => {
btn.prop('disabled', false);
msg.text('');
}, 2000);
}
});
});
// Delete Booking Handler (Reused logic)
$(document).on('click', '#onDeleteBookingBtn', function () {
if (!confirm('Wirklich löschen?')) return;
const btn = $(this);
const postId = btn.data('id');
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'on_delete_booking',
post_id: postId,
nonce: onBookingKanban.nonce
},
success: function (response) {
if (response.success) {
alert('Gelöscht.');
modal.removeClass('is-open');
// Remove card from UI
$(`.on-kanban-card[data-id="${postId}"]`).remove();
updateCounts();
} else {
alert('Fehler: ' + response.data.message);
}
}
});
});
});

360
assets/js/script.js Normal file
View File

@@ -0,0 +1,360 @@
jQuery(document).ready(function ($) {
// --- VARIABLES ---
const modal = $('#onModalOverlay');
const form = $('#onBookingForm');
const feedback = $('#formFeedback');
// --- MODAL LOGIC ---
// Move Modals to Body to prevent Stacking Context issues
$('#onModalOverlay').appendTo('body');
$('#onStatusModalOverlay').appendTo('body');
$('#onSuccessModalOverlay').appendTo('body');
// Fix desktop scroll: prevent wheel events from bubbling to body
$('.on-modal-content').on('wheel', function (e) {
e.stopPropagation();
});
// Use Event Delegation for Booking Trigger
$(document).on('click', '.js-open-booking', function (e) {
e.preventDefault();
modal.addClass('is-open');
$('body').addClass('on-modal-open');
});
function closeFrontendModal() {
modal.removeClass('is-open');
$('body').removeClass('on-modal-open'); // Unlock Scroll
}
$('#onCloseModal').on('click', closeFrontendModal);
modal.on('click', function (e) {
if ($(e.target).is(modal)) {
closeFrontendModal();
}
});
// --- SUCCESS MODAL ---
const successModal = $('#onSuccessModalOverlay');
function closeSuccessModal() {
successModal.removeClass('is-open');
$('body').removeClass('on-modal-open');
}
$('#onCloseSuccessModal, #onSuccessCloseBtn').on('click', closeSuccessModal);
successModal.on('click', function (e) {
if ($(e.target).is(successModal)) {
closeSuccessModal();
}
});
// --- SERVICE SELECTION - STEP 1 ---
let selectedServiceId = null;
let selectedServiceDuration = null;
$('#serviceSelect').on('change', function () {
selectedServiceId = $(this).val();
const selectedOption = $(this).find(':selected');
selectedServiceDuration = selectedOption.data('duration') || 30;
// Store duration in hidden field
$('#serviceDuration').val(selectedServiceDuration);
// Show calendar step
$('#stepCalendar').slideDown();
// Reset subsequent steps
$('#timeSection').slideUp();
$('#stepDetails').slideUp();
$('#selectedDate').val('');
$('#selectedTime').val('');
$('.on-day').removeClass('selected');
$('.on-time-slot').removeClass('selected');
// Render calendar
renderCalendar();
});
// --- CALENDAR LOGIC - STEP 2 ---
const monthNames = ["JAN", "FEB", "MÄR", "APR", "MAI", "JUN", "JUL", "AUG", "SEP", "OKT", "NOV", "DEZ"];
let currentDate = new Date();
function renderCalendar() {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
$('#monthYear').text(`${monthNames[month]} ${year}`);
const grid = $('#calendarGrid');
grid.empty();
grid.append('<div class="on-day-name">Mo</div><div class="on-day-name">Di</div><div class="on-day-name">Mi</div><div class="on-day-name">Do</div><div class="on-day-name">Fr</div><div class="on-day-name">Sa</div><div class="on-day-name">So</div>');
const firstDay = new Date(year, month, 1);
const daysInMonth = new Date(year, month + 1, 0).getDate();
let offset = (firstDay.getDay() + 6) % 7;
for (let i = 0; i < offset; i++) {
grid.append('<div class="on-day empty"></div>');
}
for (let i = 1; i <= daysInMonth; i++) {
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(i).padStart(2, '0')}`;
const $day = $(`<div class="on-day" data-date="${dateStr}">${i}</div>`);
const checkDate = new Date(year, month, i);
const today = new Date();
today.setHours(0, 0, 0, 0);
if (checkDate < today) {
$day.addClass('disabled');
} else {
$day.on('click', function () {
if ($(this).hasClass('disabled')) return;
$('.on-day').removeClass('selected');
$(this).addClass('selected');
$('#selectedDate').val(dateStr);
// Fetch slots using service-based endpoint
fetchSlotsForService(dateStr);
});
}
grid.append($day);
}
}
$('#prevMonth').on('click', function () {
currentDate.setMonth(currentDate.getMonth() - 1);
renderCalendar();
});
$('#nextMonth').on('click', function () {
currentDate.setMonth(currentDate.getMonth() + 1);
renderCalendar();
});
// Don't render calendar on load - wait for service selection
// renderCalendar();
// --- SLOT FETCHING - STEP 3 ---
function fetchSlotsForService(date) {
const timeSection = $('#timeSection');
const timeGrid = $('#timeGrid');
// Hide details section when date changes
$('#stepDetails').slideUp();
$('#selectedTime').val('');
// AJAX Call to get slots based on service duration
$.ajax({
url: onBookingData.ajaxurl,
type: 'POST',
data: {
action: 'on_get_slots_for_service',
service_id: selectedServiceId,
date: date,
nonce: onBookingData.nonce
},
success: function (response) {
if (response.success) {
timeGrid.empty();
const slots = response.data.slots;
if (slots.length === 0) {
timeGrid.append('<div style="grid-column: 1/-1; text-align:center; color:#888;">Keine Termine verfügbar.</div>');
} else {
slots.forEach(slot => {
const $slot = $(`<div class="on-time-slot" data-time="${slot}">${slot}</div>`);
$slot.on('click', function () {
$('.on-time-slot').removeClass('selected');
$(this).addClass('selected');
$('#selectedTime').val(slot);
// Show details section - Step 4
$('#stepDetails').slideDown();
});
timeGrid.append($slot);
});
}
timeSection.slideDown();
} else {
alert('Fehler beim Laden der Zeiten: ' + (response.data.message || ''));
}
}
});
}
// --- STATUS CHECK LOGIC ---
const statusModal = $('#onStatusModalOverlay');
// Force Uppercase on Input
$('#onTrackingIdInput').on('input', function () {
$(this).val($(this).val().toUpperCase());
});
// Use Event Delegation for Status Trigger
$(document).on('click', '.js-open-status', function (e) {
e.preventDefault();
statusModal.addClass('is-open');
$('body').addClass('on-modal-open');
console.log('Status Modal Open - Scroll Locked');
});
function closeStatusModal() {
statusModal.removeClass('is-open');
$('body').removeClass('on-modal-open');
$('#statusResult').empty();
$('#onTrackingIdInput').val('');
console.log('Status Modal Closed - Scroll Unlocked');
}
$('#onCloseStatusModal').on('click', closeStatusModal);
statusModal.on('click', function (e) {
if ($(e.target).is(statusModal)) {
closeStatusModal();
}
});
$('#onCheckStatusBtn').on('click', function (e) {
e.preventDefault();
const trackingId = $('#onTrackingIdInput').val().trim();
const resultDiv = $('#statusResult');
if (!trackingId) {
resultDiv.text('Bitte ID eingeben.').css('color', '#ff3333');
return;
}
resultDiv.text('Lade...').css('color', 'var(--on-text)');
$.ajax({
url: onBookingData.ajaxurl,
type: 'POST',
data: {
action: 'on_check_tracking_status',
tracking_id: trackingId,
nonce: onBookingData.nonce
},
success: function (response) {
if (response.success) {
// Map status to colors
const statusColors = {
'Warten': '#0061ff', // Blue
'In Bearbeitung': '#ff9500', // Orange
'Abholbereit': '#34c759' // Green
};
const statusColor = statusColors[response.data.status] || '#0061ff';
let notesHtml = '';
if (response.data.notes) {
notesHtml = '<div class="on-status-notes" style="margin-top:10px; padding:10px; background:rgba(255,255,255,0.1); border-radius:4px; font-size:0.9em; text-align:left;"><strong>Notiz:</strong><br>' + response.data.notes + '</div>';
}
resultDiv.html(`Status: <span style="color:${statusColor}; font-weight:bold;">${response.data.status}</span><br><small>${response.data.date} um ${response.data.time}</small>${notesHtml}`).css('color', 'var(--on-text)').removeClass('on-error-msg');
} else {
resultDiv.text(response.data.message || 'Nicht gefunden.').css('color', '#ff3333').addClass('on-error-msg');
}
},
error: function () {
resultDiv.text('Fehler beim Abfragen.').css('color', '#ff3333').addClass('on-error-msg');
}
});
});
// --- FORM SUBMISSION ---
form.on('submit', function (e) {
e.preventDefault();
// Validation (skip service/date/time checks in simple mode)
if (!onBookingData.simpleMode) {
if (!$('#serviceSelect').val()) {
alert('Bitte wähle einen Service aus.');
return;
}
if (!$('#selectedDate').val() || !$('#selectedTime').val()) {
alert('Bitte wähle ein Datum und eine Uhrzeit aus.');
return;
}
}
// File size check (max 10MB)
const fileInput = this.querySelector('input[type="file"]');
if (fileInput && fileInput.files.length > 0) {
const maxSize = 10 * 1024 * 1024; // 10MB
if (fileInput.files[0].size > maxSize) {
alert('Das Bild ist zu groß (max. 10 MB). Bitte wähle ein kleineres Bild.');
return;
}
}
const formData = new FormData(this);
formData.append('action', 'on_submit_booking');
formData.append('nonce', onBookingData.nonce);
const submitBtn = $('#submitBtn');
const originalText = submitBtn.html();
submitBtn.prop('disabled', true).html('<span>Sende...</span>');
feedback.hide().removeClass('on-error-msg on-success-msg');
$.ajax({
url: onBookingData.ajaxurl,
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function (response) {
if (response.success) {
form.find('input:not([type=hidden]), select, textarea').val('');
$('.on-day').removeClass('selected');
$('.on-time-slot').removeClass('selected');
// Reset all steps (skip stepDetails in simple mode)
if (!onBookingData.simpleMode) {
$('#stepCalendar').hide();
$('#timeSection').hide();
$('#stepDetails').hide();
}
// Reset selection variables
selectedServiceId = null;
selectedServiceDuration = null;
// Close the booking modal
closeFrontendModal();
// Show success modal with tracking ID
if (response.data.tracking_id) {
$('#successTrackingId').text(response.data.tracking_id);
} else {
$('#successTrackingId').text('N/A');
}
// Show upload warning if present
if (response.data.upload_warning) {
alert(response.data.upload_warning);
}
successModal.addClass('is-open');
$('body').addClass('on-modal-open');
} else {
feedback.addClass('on-error-msg').text(response.data.message || 'Ein Fehler ist aufgetreten.').slideDown();
}
},
error: function () {
feedback.addClass('on-error-msg').text('Server-Fehler. Bitte später erneut versuchen.').slideDown();
},
complete: function () {
submitBtn.prop('disabled', false).html(originalText);
}
});
});
});

View File

@@ -0,0 +1,808 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
class ON_Booking_Admin
{
private $calendar_page_hook;
public function __construct()
{
add_action('admin_menu', array($this, 'add_admin_menu'));
add_action('admin_init', array($this, 'register_settings'));
add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets'));
}
public function enqueue_admin_assets($hook)
{
if ($hook === $this->calendar_page_hook) {
// FullCalendar CDN
wp_enqueue_script('on-fullcalendar', 'https://cdn.jsdelivr.net/npm/fullcalendar@6.1.10/index.global.min.js', array(), '6.1.10', true);
// Admin Calendar JS
wp_enqueue_script('on-admin-calendar-js', ON_BOOKING_URL . 'assets/js/admin-calendar.js', array('on-fullcalendar'), ON_BOOKING_VERSION, true);
wp_localize_script('on-admin-calendar-js', 'onBookingAdmin', array(
'nonce' => wp_create_nonce('on_booking_nonce')
));
}
// Kanban JS
if (isset($_GET['page']) && $_GET['page'] === 'on-booking-kanban') {
// Enqueue WordPress editor for rich text notes
wp_enqueue_editor();
wp_enqueue_script('on-admin-kanban-js', ON_BOOKING_URL . 'assets/js/admin-kanban.js', array('jquery', 'jquery-ui-draggable', 'jquery-ui-droppable'), time(), true);
wp_localize_script('on-admin-kanban-js', 'onBookingKanban', array(
'nonce' => wp_create_nonce('on_booking_nonce')
));
}
wp_enqueue_style('on-booking-admin-style', ON_BOOKING_URL . 'assets/css/admin-style.css', array(), ON_BOOKING_VERSION);
}
public function add_admin_menu()
{
// Top Level Menu for Calendar
$this->calendar_page_hook = add_menu_page(
'Werkstatt Buchungen',
'Buchungen',
'manage_options',
'on-booking-calendar',
array($this, 'render_calendar_page'),
'dashicons-calendar-alt',
26
);
// Submenu for Calendar (to rename the first item if desired, but usually WP handles 'Buchungen' > 'Buchungen')
// Actually, let's keep it simple: Top level is Calendar.
// Settings Submenu
add_submenu_page(
'on-booking-calendar', // Parent slug
'Buchungs-Einstellungen',
'Einstellungen',
'manage_options',
'on-booking-settings',
array($this, 'render_settings_page')
);
// Services Submenu
add_submenu_page(
'on-booking-calendar',
'Services verwalten',
'Services',
'manage_options',
'on-booking-services',
array($this, 'render_services_page')
);
// Status Board Submenu
add_submenu_page(
'on-booking-calendar',
'Status Board',
'Status Board',
'manage_options',
'on-booking-kanban',
array($this, 'render_kanban_page')
);
}
public function register_settings()
{
// General Settings Group: on_booking_general
register_setting('on_booking_general', 'on_booking_days_blocked');
register_setting('on_booking_general', 'on_booking_time_start');
register_setting('on_booking_general', 'on_booking_time_end');
register_setting('on_booking_general', 'on_booking_time_interval');
register_setting('on_booking_general', 'on_booking_theme_mode');
register_setting('on_booking_general', 'on_booking_show_duration');
register_setting('on_booking_general', 'on_booking_simple_mode');
// SMTP Settings Group: on_booking_smtp
register_setting('on_booking_smtp', 'on_booking_mail_method'); // smtp, wp_mail
register_setting('on_booking_smtp', 'on_booking_smtp_host');
register_setting('on_booking_smtp', 'on_booking_smtp_port'); // Default 587
register_setting('on_booking_smtp', 'on_booking_smtp_user');
register_setting('on_booking_smtp', 'on_booking_smtp_pass');
register_setting('on_booking_smtp', 'on_booking_smtp_auth_type'); // auto, login, plain, cram-md5
register_setting('on_booking_smtp', 'on_booking_smtp_enc'); // none, ssl, tls
register_setting('on_booking_smtp', 'on_booking_from_email');
register_setting('on_booking_smtp', 'on_booking_from_name');
// Email Template Settings Group: on_booking_email
register_setting('on_booking_email', 'on_booking_enable_emails'); // New: Enable/Disable
register_setting('on_booking_email', 'on_booking_email_subject');
register_setting('on_booking_email', 'on_booking_email_body');
// Admin Notification
register_setting('on_booking_email', 'on_booking_enable_admin_notify');
register_setting('on_booking_email', 'on_booking_admin_notify_email');
}
public function render_settings_page()
{
$active_tab = isset($_GET['tab']) ? $_GET['tab'] : 'general';
$prio_group = 'on_booking_general'; // Default
if ($active_tab == 'smtp') {
$prio_group = 'on_booking_smtp';
} else if ($active_tab == 'email') {
$prio_group = 'on_booking_email';
}
?>
<div class="wrap">
<h1>Buchungs-Einstellungen</h1>
<h2 class="nav-tab-wrapper">
<a href="?page=on-booking-settings&tab=general"
class="nav-tab <?php echo $active_tab == 'general' ? 'nav-tab-active' : ''; ?>">Allgemein</a>
<a href="?page=on-booking-settings&tab=smtp"
class="nav-tab <?php echo $active_tab == 'smtp' ? 'nav-tab-active' : ''; ?>">SMTP Einstellungen</a>
<a href="?page=on-booking-settings&tab=email"
class="nav-tab <?php echo $active_tab == 'email' ? 'nav-tab-active' : ''; ?>">E-Mail Vorlage</a>
</h2>
<form method="post" action="options.php">
<?php
// Output nonces and hidden fields for the specific group
settings_fields($prio_group);
if ($active_tab == 'general') {
$this->render_general_settings();
} else if ($active_tab == 'smtp') {
$this->render_smtp_settings();
} else if ($active_tab == 'email') {
$this->render_email_template_settings();
}
?>
<?php submit_button(); ?>
</form>
</div>
<?php
}
private function render_general_settings()
{
?>
<table class="form-table">
<tr valign="top">
<th scope="row">Geschlossene Tage</th>
<td>
<?php
$blocked_days = get_option('on_booking_days_blocked', array());
if (!is_array($blocked_days))
$blocked_days = array();
// Hidden input to ensure array is reset if all unchecked
echo '<input type="hidden" name="on_booking_days_blocked" value="">';
$days = array('Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag');
foreach ($days as $index => $day) {
$checked = in_array($index, $blocked_days) ? 'checked' : '';
echo '<label><input type="checkbox" name="on_booking_days_blocked[]" value="' . $index . '" ' . $checked . '> ' . $day . '</label><br>';
}
?>
<p class="description">Wähle die Tage, an denen die Werkstatt geschlossen ist.</p>
</td>
</tr>
<tr valign="top">
<th scope="row">Buchungszeiten</th>
<td>
<label>Startzeit: <input type="time" name="on_booking_time_start"
value="<?php echo esc_attr(get_option('on_booking_time_start', '08:00')); ?>"></label><br>
<label>Endzeit: <input type="time" name="on_booking_time_end"
value="<?php echo esc_attr(get_option('on_booking_time_end', '17:00')); ?>"></label><br>
<label>Intervall (Min): <input type="number" name="on_booking_time_interval"
value="<?php echo esc_attr(get_option('on_booking_time_interval', '60')); ?>" min="15"
step="15"></label>
</td>
</tr>
<tr valign="top">
<th scope="row">Anzeigeoptionen</th>
<td>
<label>
<input type="hidden" name="on_booking_show_duration" value="0">
<input type="checkbox" name="on_booking_show_duration" value="1" <?php checked(1, get_option('on_booking_show_duration', 0)); ?>>
Service-Dauer im Frontend anzeigen
</label>
<p class="description">Wenn aktiviert, wird die Dauer (z.B. "30 min") im Dropdown neben dem Service-Namen
angezeigt.</p>
</td>
</tr>
<tr valign="top">
<th scope="row">Design Modus</th>
<td>
<?php $theme_mode = get_option('on_booking_theme_mode', 'auto'); ?>
<select name="on_booking_theme_mode">
<option value="auto" <?php selected($theme_mode, 'auto'); ?>>Automatisch (Browser)</option>
<option value="light" <?php selected($theme_mode, 'light'); ?>>Hell (Light Mode)</option>
<option value="dark" <?php selected($theme_mode, 'dark'); ?>>Dunkel (Dark Mode)</option>
</select>
<p class="description">Wähle das Erscheinungsbild des Kalenders im Backend.</p>
</td>
</tr>
<tr valign="top">
<th scope="row">Buchungsmodus</th>
<td>
<label>
<input type="hidden" name="on_booking_simple_mode" value="0">
<input type="checkbox" name="on_booking_simple_mode" value="1" <?php checked(1, get_option('on_booking_simple_mode', 0)); ?>>
Einfacher Modus (ohne Service-/Termin-Auswahl)
</label>
<p class="description">Wenn aktiviert, sieht der Kunde im Frontend nur ein Kontaktformular mit einem
Freitextfeld für Wünsche &ndash; ohne Service-, Kalender- oder Uhrzeitauswahl.</p>
</td>
</tr>
</table>
<?php
}
private function render_smtp_settings()
{
?>
<table class="form-table">
<tr valign="top">
<th scope="row">Absender Name</th>
<td><input type="text" name="on_booking_from_name"
value="<?php echo esc_attr(get_option('on_booking_from_name', get_bloginfo('name'))); ?>"
class="regular-text"></td>
</tr>
<tr valign="top">
<th scope="row">Absender E-Mail</th>
<td><input type="email" name="on_booking_from_email"
value="<?php echo esc_attr(get_option('on_booking_from_email', get_bloginfo('admin_email'))); ?>"
class="regular-text"></td>
</tr>
<tr valign="top">
<th scope="row">Versandmethode</th>
<td>
<?php $method = get_option('on_booking_mail_method', 'smtp'); ?>
<label><input type="radio" name="on_booking_mail_method" value="smtp" <?php checked($method, 'smtp'); ?>>
SMTP (Empfohlen)</label><br>
<label><input type="radio" name="on_booking_mail_method" value="wp_mail" <?php checked($method, 'wp_mail'); ?>> PHP Mail (Standard / WP Default)</label>
<p class="description">Wähle "PHP Mail", wenn SMTP Probleme macht (kann im Spam landen).</p>
</td>
</tr>
<tr valign="top">
<th scope="row">SMTP Host</th>
<td><input type="text" name="on_booking_smtp_host"
value="<?php echo esc_attr(get_option('on_booking_smtp_host', '')); ?>" class="regular-text"
placeholder="smtp.example.com"></td>
</tr>
<tr valign="top">
<th scope="row">SMTP Port</th>
<td><input type="number" name="on_booking_smtp_port"
value="<?php echo esc_attr(get_option('on_booking_smtp_port', '587')); ?>" class="small-text"></td>
</tr>
<tr valign="top">
<th scope="row">SMTP Benutzer</th>
<td><input type="text" name="on_booking_smtp_user"
value="<?php echo esc_attr(get_option('on_booking_smtp_user', '')); ?>" class="regular-text"></td>
</tr>
<tr valign="top">
<th scope="row">SMTP Passwort</th>
<td><input type="password" name="on_booking_smtp_pass"
value="<?php echo esc_attr(get_option('on_booking_smtp_pass', '')); ?>" class="regular-text"></td>
</tr>
<tr valign="top">
<th scope="row">Authentifizierung</th>
<td>
<?php $auth_type = get_option('on_booking_smtp_auth_type', 'auto'); ?>
<select name="on_booking_smtp_auth_type">
<option value="auto" <?php selected($auth_type, 'auto'); ?>>Automatisch (Empfohlen)</option>
<option value="LOGIN" <?php selected($auth_type, 'LOGIN'); ?>>LOGIN</option>
<option value="PLAIN" <?php selected($auth_type, 'PLAIN'); ?>>PLAIN</option>
<option value="CRAM-MD5" <?php selected($auth_type, 'CRAM-MD5'); ?>>CRAM-MD5</option>
</select>
<p class="description">Falls 'Automatisch' fehlschlägt, versuche PLAIN oder LOGIN.</p>
</td>
</tr>
<tr valign="top">
<th scope="row">Verschlüsselung</th>
<td>
<?php $enc = get_option('on_booking_smtp_enc', 'tls'); ?>
<label><input type="radio" name="on_booking_smtp_enc" value="none" <?php checked($enc, 'none'); ?>>
Keine</label><br>
<label><input type="radio" name="on_booking_smtp_enc" value="ssl" <?php checked($enc, 'ssl'); ?>>
SSL</label><br>
<label><input type="radio" name="on_booking_smtp_enc" value="tls" <?php checked($enc, 'tls'); ?>>
TLS</label><br>
<label><input type="radio" name="on_booking_smtp_enc" value="starttls" <?php checked($enc, 'starttls'); ?>>
STARTTLS</label>
</td>
</tr>
</table>
<hr>
<h3>SMTP testen</h3>
<div style="display: flex; gap: 10px; align-items: center; margin-bottom: 10px;">
<input type="email" id="onTestEmailInput" class="regular-text" placeholder="Deine E-Mail Adresse"
value="<?php echo esc_attr(wp_get_current_user()->user_email); ?>">
<button type="button" class="button button-secondary" id="onSendTestEmail">Test-Email senden</button>
</div>
<textarea id="onTestEmailLog" rows="10" cols="80" style="width: 100%; font-family: monospace; background: #f0f0f1;"
readonly placeholder="Log Ausgabe..."></textarea>
<script>
jQuery(document).ready(function ($) {
function logMessage(msg) {
const log = $('#onTestEmailLog');
const time = new Date().toLocaleTimeString();
log.val('[' + time + '] ' + msg + '\n' + log.val());
}
$('#onSendTestEmail').on('click', function () {
const btn = $(this);
const email = $('#onTestEmailInput').val();
if (!email) {
alert('Bitte E-Mail eingeben');
return;
}
btn.prop('disabled', true).text('Sende...');
logMessage('Starte Test für: ' + email + '...');
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'on_send_test_email',
test_email: email,
nonce: '<?php echo wp_create_nonce('on_booking_nonce'); ?>'
},
success: function (response) {
if (response.success) {
logMessage('ERFOLG: ' + response.data.message);
} else {
logMessage('FEHLER: ' + response.data.message);
}
},
error: function (xhr, status, error) {
logMessage('AJAX FEHLER: ' + error);
if (xhr.responseText) {
logMessage('Response: ' + xhr.responseText);
}
logMessage('Status: ' + xhr.status + ' ' + xhr.statusText);
},
complete: function () {
btn.prop('disabled', false).text('Test-Email senden');
}
});
});
});
</script>
<?php
}
private function render_email_template_settings()
{
$default_subject = 'Buchungsbestätigung: {service} am {date}';
$default_body = '<h3>Hallo {name},</h3><p>vielen Dank für deine Buchung bei uns. Hier sind deine Details:</p>' .
'<div style="background: #1f1f1f; color: #fff; padding: 15px; border-left: 4px solid #0061ff; margin: 20px 0;">' .
'<p><strong>Service:</strong> {service}</p>' .
'<p><strong>Datum:</strong> {date}</p>' .
'<p><strong>Uhrzeit:</strong> {time}</p>' .
'<p><strong>Auftragsnummer:</strong> <span style="font-family: monospace; font-size: 1.2em; color: #0061ff;">{tracking_id}</span></p>' .
'</div>' .
'<p>Du kannst den Status deiner Buchung jederzeit auf unserer Webseite mit deiner Auftragsnummer abfragen.</p>' .
'<p>Mit freundlichen Grüßen,<br>Dein Werkstatt-Team</p>';
$subject = get_option('on_booking_email_subject', $default_subject);
$body = get_option('on_booking_email_body', $default_body);
?>
<div style="max-width: 800px;">
<p>Hier kannst du die E-Mail anpassen, die nach einer erfolgreichen Buchung an den Kunden gesendet wird.</p>
<table class="form-table">
<tr valign="top"> <!-- New Enable Toggle -->
<th scope="row">E-Mail Benachrichtigung</th>
<td>
<label>
<input type="hidden" name="on_booking_enable_emails" value="0">
<input type="checkbox" name="on_booking_enable_emails" value="1" <?php checked(1, get_option('on_booking_enable_emails', 0)); ?>>
E-Mail Bestätigungen aktivieren
</label>
<p class="description">Wenn aktiviert, werden Bestätigungs-E-Mails an Kunden gesendet.</p>
</td>
</tr>
<tr valign="top">
<th scope="row">Betreff</th>
<td><input type="text" name="on_booking_email_subject" value="<?php echo esc_attr($subject); ?>"
class="large-text"></td>
</tr>
<tr valign="top">
<th scope="row">Inhalt</th>
<td>
<?php
wp_editor($body, 'on_booking_email_body', array(
'media_buttons' => false,
'textarea_rows' => 15,
'teeny' => true
));
?>
<p class="description">
<strong>Verfügbare Platzhalter:</strong><br>
<code>{name}</code> - Name des Kunden<br>
<code>{date}</code> - Datum des Termins<br>
<code>{time}</code> - Uhrzeit des Termins<br>
<code>{service}</code> - gebuchter Service<br>
<code>{tracking_id}</code> - Auftragsnummer für Statusabfrage
</p>
</td>
</tr>
</table>
<h3 style="margin-top: 30px; padding-top: 15px; border-top: 1px solid #ccc;">Werkstatt-Benachrichtigung</h3>
<p>Erhalte bei jeder neuen Buchung eine E-Mail mit allen Details &ndash; als Alternative zum Kalender.</p>
<table class="form-table">
<tr valign="top">
<th scope="row">Werkstatt-Mail aktivieren</th>
<td>
<label>
<input type="hidden" name="on_booking_enable_admin_notify" value="0">
<input type="checkbox" name="on_booking_enable_admin_notify" value="1" <?php checked(1, get_option('on_booking_enable_admin_notify', 0)); ?>>
Bei jeder Buchung eine Kopie an die Werkstatt senden
</label>
</td>
</tr>
<tr valign="top">
<th scope="row">Empf&auml;nger E-Mail</th>
<td>
<input type="email" name="on_booking_admin_notify_email"
value="<?php echo esc_attr(get_option('on_booking_admin_notify_email', '')); ?>"
class="regular-text" placeholder="werkstatt@beispiel.de">
<p class="description">An diese Adresse wird bei jeder neuen Buchung eine Benachrichtigung gesendet.</p>
</td>
</tr>
</table>
<p>
<button type="button" class="button" id="on-reset-email-template">Standard-Vorlage wiederherstellen</button>
</p>
<script>
jQuery(document).ready(function ($) {
$('#on-reset-email-template').on('click', function () {
if (!confirm('Möchtest du wirklich die Standard-Vorlage wiederherstellen? Alle Änderungen gehen verloren.')) {
return;
}
var defaultSubject = <?php echo json_encode($default_subject); ?>;
var defaultBody = <?php echo json_encode($default_body); ?>;
// Set Subject
$('input[name="on_booking_email_subject"]').val(defaultSubject);
// Set Body (TinyMCE or Textarea)
if (typeof tinymce !== 'undefined' && tinymce.get('on_booking_email_body')) {
tinymce.get('on_booking_email_body').setContent(defaultBody);
} else {
$('#on_booking_email_body').val(defaultBody);
}
});
});
</script>
</div>
<?php
}
public function render_calendar_page()
{
$theme_mode = get_option('on_booking_theme_mode', 'auto');
$theme_class = 'on-theme-' . $theme_mode;
?>
<div class="wrap">
<h1>Buchungskalender</h1>
<div id="on-admin-calendar" class="<?php echo esc_attr($theme_class); ?>"></div>
</div>
<!-- Admin Detail Modal -->
<div id="onAdminModal" class="on-modal-overlay <?php echo esc_attr($theme_class); ?>" style="z-index: 10000;">
<div class="on-modal-content" style="max-width: 600px;">
<button class="on-close-btn" id="onAdminCloseModal">&times;</button>
<div id="onAdminModalBody">
<h3>Buchungs <span>Details</span></h3>
<div id="onBookingDetailsContent">Lade...</div>
</div>
</div>
</div>
<?php
}
public function render_kanban_page()
{
$theme_mode = get_option('on_booking_theme_mode', 'auto');
$theme_class = 'on-theme-' . $theme_mode;
// Reuse Modal HTML
?>
<div id="onAdminModal" class="on-modal-overlay <?php echo esc_attr($theme_class); ?>" style="z-index: 10000;">
<div class="on-modal-content" style="max-width: 600px;">
<button class="on-close-btn" id="onAdminCloseModal">&times;</button>
<div id="onAdminModalBody">
<h3>Buchungs <span>Details</span></h3>
<div id="onBookingDetailsContent">Lade...</div>
</div>
</div>
</div>
<?php
// Fetch all bookings
$posts = get_posts(array(
'post_type' => 'on_booking',
'numberposts' => -1,
'post_status' => array('publish', 'pending', 'future', 'private')
));
$columns = array(
'waiting' => array('title' => 'Warten', 'items' => array()),
'in_progress' => array('title' => 'In Bearbeitung', 'items' => array()),
'done' => array('title' => 'Abholbereit', 'items' => array())
);
foreach ($posts as $p) {
$status = get_post_meta($p->ID, 'on_booking_status', true);
if (!$status)
$status = 'waiting';
if (!array_key_exists($status, $columns))
$status = 'waiting';
$date = get_post_meta($p->ID, 'on_booking_date', true);
$time = get_post_meta($p->ID, 'on_booking_time', true);
$columns[$status]['items'][] = array(
'id' => $p->ID,
'title' => $p->post_title,
'date' => $date,
'time' => $time
);
}
?>
<div class="wrap <?php echo esc_attr($theme_class); ?>">
<h1>Status Board</h1>
<div class="on-kanban-board">
<?php foreach ($columns as $key => $col): ?>
<div class="on-kanban-column" data-status="<?php echo esc_attr($key); ?>">
<div class="on-kanban-header"><?php echo esc_html($col['title']); ?> <span
class="count"><?php echo count($col['items']); ?></span></div>
<div class="on-kanban-items">
<?php foreach ($col['items'] as $item): ?>
<div class="on-kanban-card" data-id="<?php echo esc_attr($item['id']); ?>"
data-status="<?php echo esc_attr($key); ?>">
<div class="card-title"><?php echo esc_html($item['title']); ?> <span
class="dashicons dashicons-edit on-edit-icon" title="Notiz/Details bearbeiten"
style="float:right; color:#888; cursor:pointer;"></span></div>
<div class="card-meta"><?php echo esc_html($item['date'] . ' ' . $item['time']); ?></div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php
}
public function render_services_page()
{
$theme_mode = get_option('on_booking_theme_mode', 'auto');
$theme_class = 'on-theme-' . $theme_mode;
$services = ON_Service_Manager::get_all_services();
?>
<div class="wrap <?php echo esc_attr($theme_class); ?>">
<h1>Services verwalten</h1>
<div style="background: var(--on-bg); padding: 20px; border-radius: 4px; margin-bottom: 20px;">
<h2>Neuen Service hinzufügen</h2>
<form id="onAddServiceForm" style="display: flex; gap: 15px; align-items: end;">
<div>
<label style="display: block; margin-bottom: 5px; font-weight: bold;">Service Name</label>
<input type="text" id="onServiceName" required style="padding: 8px; width: 250px;">
</div>
<div>
<label style="display: block; margin-bottom: 5px; font-weight: bold;">Dauer (Minuten)</label>
<select id="onServiceDuration" style="padding: 8px;">
<option value="30">30 min</option>
<option value="60">60 min</option>
<option value="90">90 min</option>
<option value="120">120 min</option>
<option value="150">150 min</option>
<option value="180">180 min</option>
</select>
</div>
<button type="submit" class="button button-primary">Service hinzufügen</button>
<span id="onAddServiceMsg" style="margin-left: 10px; font-weight: bold;"></span>
</form>
</div>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th style="width: 50%;">Service Name</th>
<th style="width: 20%;">Dauer</th>
<th style="width: 15%;">Status</th>
<th style="width: 15%;">Aktionen</th>
</tr>
</thead>
<tbody id="onServicesTableBody">
<?php foreach ($services as $service): ?>
<tr data-service-id="<?php echo esc_attr($service['id']); ?>">
<td>
<span class="service-name-display"><?php echo esc_html($service['name']); ?></span>
<input type="text" class="service-name-edit" value="<?php echo esc_attr($service['name']); ?>"
style="display:none; width: 100%;">
</td>
<td>
<span class="service-duration-display"><?php echo esc_html($service['duration']); ?> min</span>
<select class="service-duration-edit" style="display:none;">
<option value="30" <?php selected($service['duration'], 30); ?>>30 min</option>
<option value="60" <?php selected($service['duration'], 60); ?>>60 min</option>
<option value="90" <?php selected($service['duration'], 90); ?>>90 min</option>
<option value="120" <?php selected($service['duration'], 120); ?>>120 min</option>
<option value="150" <?php selected($service['duration'], 150); ?>>150 min</option>
<option value="180" <?php selected($service['duration'], 180); ?>>180 min</option>
</select>
</td>
<td>
<span class="service-status-badge"
style="padding: 4px 8px; border-radius: 3px; font-size: 0.85em; <?php echo $service['active'] ? 'background: #46b450; color: white;' : 'background: #dc3232; color: white;'; ?>">
<?php echo $service['active'] ? 'Aktiv' : 'Inaktiv'; ?>
</span>
</td>
<td>
<button class="button button-small on-edit-service-btn">Bearbeiten</button>
<button class="button button-small on-save-service-btn" style="display:none;">Speichern</button>
<button class="button button-small on-cancel-edit-btn" style="display:none;">Abbrechen</button>
<button class="button button-small on-toggle-status-btn" style="margin-left: 5px;">
<?php echo $service['active'] ? 'Deaktivieren' : 'Aktivieren'; ?>
</button>
<button class="button button-small on-delete-service-btn"
style="margin-left: 5px; color: #a00;">Löschen</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<script>
jQuery(document).ready(function ($) {
// Add new service
$('#onAddServiceForm').on('submit', function (e) {
e.preventDefault();
const name = $('#onServiceName').val().trim();
const duration = $('#onServiceDuration').val();
const msg = $('#onAddServiceMsg');
if (!name) {
msg.text('Bitte Namen eingeben').css('color', 'red');
return;
}
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'on_create_service',
name: name,
duration: duration,
nonce: '<?php echo wp_create_nonce('on_booking_nonce'); ?>'
},
success: function (response) {
if (response.success) {
msg.text('✓ Service hinzugefügt').css('color', 'green');
setTimeout(() => location.reload(), 1000);
} else {
msg.text('Fehler: ' + response.data.message).css('color', 'red');
}
}
});
});
// Edit service
$('.on-edit-service-btn').on('click', function () {
const row = $(this).closest('tr');
row.find('.service-name-display, .service-duration-display').hide();
row.find('.service-name-edit, .service-duration-edit').show();
row.find('.on-edit-service-btn').hide();
row.find('.on-save-service-btn, .on-cancel-edit-btn').show();
});
// Cancel edit
$('.on-cancel-edit-btn').on('click', function () {
const row = $(this).closest('tr');
row.find('.service-name-display, .service-duration-display').show();
row.find('.service-name-edit, .service-duration-edit').hide();
row.find('.on-edit-service-btn').show();
row.find('.on-save-service-btn, .on-cancel-edit-btn').hide();
});
// Save service
$('.on-save-service-btn').on('click', function () {
const row = $(this).closest('tr');
const serviceId = row.data('service-id');
const name = row.find('.service-name-edit').val().trim();
const duration = row.find('.service-duration-edit').val();
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'on_update_service',
service_id: serviceId,
name: name,
duration: duration,
nonce: '<?php echo wp_create_nonce('on_booking_nonce'); ?>'
},
success: function (response) {
if (response.success) {
location.reload();
} else {
alert('Fehler: ' + response.data.message);
}
}
});
});
// Toggle status
$('.on-toggle-status-btn').on('click', function () {
const row = $(this).closest('tr');
const serviceId = row.data('service-id');
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'on_toggle_service_status',
service_id: serviceId,
nonce: '<?php echo wp_create_nonce('on_booking_nonce'); ?>'
},
success: function (response) {
if (response.success) {
location.reload();
} else {
alert('Fehler: ' + response.data.message);
}
}
});
});
// Delete service
$('.on-delete-service-btn').on('click', function () {
if (!confirm('Service wirklich löschen?')) return;
const row = $(this).closest('tr');
const serviceId = row.data('service-id');
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'on_delete_service',
service_id: serviceId,
nonce: '<?php echo wp_create_nonce('on_booking_nonce'); ?>'
},
success: function (response) {
if (response.success) {
row.fadeOut(300, function () { $(this).remove(); });
} else {
alert('Fehler: ' + response.data.message);
}
}
});
});
});
</script>
<?php
}
}

View File

@@ -0,0 +1,723 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
class ON_Booking_Ajax
{
public function __construct()
{
add_action('wp_ajax_on_get_slots', array($this, 'get_slots'));
add_action('wp_ajax_nopriv_on_get_slots', array($this, 'get_slots'));
add_action('wp_ajax_on_submit_booking', array($this, 'submit_booking'));
add_action('wp_ajax_nopriv_on_submit_booking', array($this, 'submit_booking'));
add_action('wp_ajax_on_get_admin_bookings', array($this, 'get_admin_bookings'));
add_action('wp_ajax_on_get_booking_details', array($this, 'get_booking_details'));
// New Actions
add_action('wp_ajax_on_delete_booking', array($this, 'delete_booking'));
add_action('wp_ajax_on_update_booking_date', array($this, 'update_booking_date'));
add_action('wp_ajax_on_update_booking_status', array($this, 'update_booking_status'));
add_action('wp_ajax_on_update_booking_notes', array($this, 'update_booking_notes'));
// Tracking
add_action('wp_ajax_on_check_tracking_status', array($this, 'check_tracking_status'));
add_action('wp_ajax_nopriv_on_check_tracking_status', array($this, 'check_tracking_status'));
// Service Management
add_action('wp_ajax_on_create_service', array($this, 'create_service'));
add_action('wp_ajax_on_update_service', array($this, 'update_service'));
add_action('wp_ajax_on_delete_service', array($this, 'delete_service'));
add_action('wp_ajax_on_toggle_service_status', array($this, 'toggle_service_status'));
add_action('wp_ajax_on_get_services', array($this, 'get_services'));
add_action('wp_ajax_nopriv_on_get_services', array($this, 'get_services'));
// Service-based slot availability
add_action('wp_ajax_on_get_slots_for_service', array($this, 'get_slots_for_service'));
add_action('wp_ajax_nopriv_on_get_slots_for_service', array($this, 'get_slots_for_service'));
// Test Email
add_action('wp_ajax_on_send_test_email', array($this, 'send_test_email'));
}
public function get_slots()
{
check_ajax_referer('on_booking_nonce', 'nonce');
$date_str = isset($_POST['date']) ? sanitize_text_field($_POST['date']) : ''; // YYYY-MM-DD
if (!$date_str) {
wp_send_json_error(array('message' => 'Ungültiges Datum'));
}
// 1. Check blocked days
$day_of_week = date('w', strtotime($date_str)); // 0=Sun, 6=Sat
$blocked_days = get_option('on_booking_days_blocked', array());
if (!is_array($blocked_days))
$blocked_days = array();
if (in_array($day_of_week, $blocked_days)) {
wp_send_json_success(array('slots' => array())); // No slots on blocked days
return;
}
// 2. Generate Slots
$start_time = get_option('on_booking_time_start', '08:00');
$end_time = get_option('on_booking_time_end', '17:00');
$interval = (int) get_option('on_booking_time_interval', '60');
if ($interval < 15)
$interval = 60;
$slots = array();
$current = strtotime($date_str . ' ' . $start_time);
$end = strtotime($date_str . ' ' . $end_time);
$now = current_time('timestamp');
// 3. Get existing bookings
$args = array(
'post_type' => 'on_booking',
'meta_query' => array(
array(
'key' => 'on_booking_date',
'value' => $date_str,
),
),
'posts_per_page' => -1,
'fields' => 'ids',
);
$existing_bookings_query = new WP_Query($args);
$booked_times = array();
if ($existing_bookings_query->have_posts()) {
foreach ($existing_bookings_query->posts as $post_id) {
$time = get_post_meta($post_id, 'on_booking_time', true);
if ($time) {
$booked_times[] = $time;
}
}
}
while ($current < $end) {
$time_label = date('H:i', $current);
// Check past time if today
if ($current < $now) {
$available = false;
} else {
$available = !in_array($time_label, $booked_times);
}
$slots[] = array(
'time' => $time_label,
'available' => $available,
);
$current = strtotime('+' . $interval . ' minutes', $current);
}
wp_send_json_success(array('slots' => $slots));
}
public function submit_booking()
{
check_ajax_referer('on_booking_nonce', 'nonce');
// Detect simple mode
$is_simple_mode = !empty($_POST['simple_mode']);
// Sanitization - common fields
$name = sanitize_text_field($_POST['user_name']);
$phone = sanitize_text_field($_POST['user_phone']);
$email = sanitize_email($_POST['user_email']);
$brand = isset($_POST['brand']) ? sanitize_text_field($_POST['brand']) : '';
$year = isset($_POST['year']) ? sanitize_text_field($_POST['year']) : '';
if (!$name || !$email) {
wp_send_json_error(array('message' => 'Bitte alle Pflichtfelder ausfüllen.'));
}
$service_id = 0; // Default for simple mode
$other_desc = ''; // Default for simple mode
$customer_request = ''; // Default for normal mode
if ($is_simple_mode) {
// Simple mode: no service/date/time required
$customer_request = sanitize_textarea_field($_POST['customer_request']);
if (!$customer_request) {
wp_send_json_error(array('message' => 'Bitte beschreibe, was gemacht werden soll.'));
}
$date = date('Y-m-d');
$time = date('H:i');
$service_name = 'Offene Anfrage';
$service_duration = 0;
$post_title = sprintf('Anfrage - %s (%s)', $date, $name);
$content = "Kundenwunsch: $customer_request\n";
if ($brand) {
$content .= "Fahrzeug: $brand";
if ($year)
$content .= " ($year)";
$content .= "\n";
}
$content .= "\nKontakt:\nName: $name\nTel: $phone\nEmail: $email";
} else {
// Normal mode: full booking flow
$date = sanitize_text_field($_POST['selected_date']);
$time = sanitize_text_field($_POST['selected_time']);
$service_id = isset($_POST['service_id']) ? intval($_POST['service_id']) : 0;
$service_duration = isset($_POST['service_duration']) ? intval($_POST['service_duration']) : 30;
$other_desc = isset($_POST['other_service_desc']) ? sanitize_textarea_field($_POST['other_service_desc']) : '';
if (!$date || !$time || !$service_id) {
wp_send_json_error(array('message' => 'Bitte alle Pflichtfelder ausfüllen.'));
}
$service = ON_Service_Manager::get_service($service_id);
$service_name = $service ? $service['name'] : 'Unbekannt';
$post_title = sprintf('%s - %s (%s)', $date, $time, $name);
$content = "Fahrzeug: $brand ($year)\nService: $service_name\n";
if ($other_desc) {
$content .= "Beschreibung: $other_desc\n";
}
$content .= "\nKontakt:\nName: $name\nTel: $phone\nEmail: $email";
}
$post_data = array(
'post_title' => $post_title,
'post_content' => $content,
'post_status' => 'publish',
'post_type' => 'on_booking',
);
$post_id = wp_insert_post($post_data);
if (is_wp_error($post_id)) {
wp_send_json_error(array('message' => 'Fehler beim Speichern.'));
}
// Save Meta
update_post_meta($post_id, 'on_booking_date', $date);
update_post_meta($post_id, 'on_booking_time', $time);
update_post_meta($post_id, 'on_booking_email', $email);
update_post_meta($post_id, 'on_booking_service_name', $service_name);
update_post_meta($post_id, 'on_booking_service_duration', $service_duration);
update_post_meta($post_id, 'on_booking_phone', $phone);
update_post_meta($post_id, 'on_booking_brand', $brand);
if ($service_id) { // Only save service_id if it's a normal booking
update_post_meta($post_id, 'on_booking_service_id', $service_id);
}
if (!empty($customer_request)) {
update_post_meta($post_id, 'on_booking_customer_request', $customer_request);
}
// Generate Tracking ID
$tracking_id = strtoupper(substr(md5(uniqid($post_id, true)), 0, 8));
update_post_meta($post_id, 'on_booking_tracking_id', $tracking_id);
// Set default status if not set
update_post_meta($post_id, 'on_booking_status', 'waiting');
// Send Email Confirmation FIRST (before file upload to avoid timeout issues)
ON_Email_Manager::queue_booking_confirmation($post_id);
// Handle File Upload (after customer email, so it always fires)
$upload_warning = '';
$attachment_file_path = '';
if (!empty($_FILES['vehicle_doc']['name'])) {
// Check file size (max 10MB)
$max_size = 10 * 1024 * 1024; // 10MB
if ($_FILES['vehicle_doc']['size'] > $max_size) {
$upload_warning = 'Das Bild ist zu groß (max. 10 MB). Die Buchung wurde trotzdem gespeichert.';
} elseif ($_FILES['vehicle_doc']['error'] !== UPLOAD_ERR_OK) {
$upload_warning = 'Fehler beim Hochladen des Bildes. Die Buchung wurde trotzdem gespeichert.';
} else {
require_once(ABSPATH . 'wp-admin/includes/image.php');
require_once(ABSPATH . 'wp-admin/includes/file.php');
require_once(ABSPATH . 'wp-admin/includes/media.php');
$attachment_id = media_handle_upload('vehicle_doc', $post_id);
if (is_wp_error($attachment_id)) {
$upload_warning = 'Das Bild konnte nicht gespeichert werden. Die Buchung wurde trotzdem erstellt.';
} else {
$attachment_file_path = get_attached_file($attachment_id);
}
}
}
// Send admin notification (after upload, so attachment can be included)
try {
ON_Email_Manager::send_admin_notification(
$post_id,
$name,
$email,
$date,
$time,
$service_name,
$tracking_id,
$attachment_file_path
);
} catch (\Exception $e) {
// Don't crash the booking if admin email fails
error_log('ON Booking: Admin notification failed - ' . $e->getMessage());
}
$response = array(
'message' => 'Buchung erfolgreich! Wir melden uns.',
'tracking_id' => $tracking_id
);
if ($upload_warning) {
$response['upload_warning'] = $upload_warning;
}
wp_send_json_success($response);
}
public function get_admin_bookings()
{
check_ajax_referer('on_booking_nonce', 'nonce');
$args = array(
'post_type' => 'on_booking',
'posts_per_page' => -1,
'post_status' => array('publish', 'pending', 'future', 'draft', 'private'),
);
$query = new WP_Query($args);
$events = array();
foreach ($query->posts as $post) {
$date = get_post_meta($post->ID, 'on_booking_date', true); // YYYY-MM-DD
$time = get_post_meta($post->ID, 'on_booking_time', true); // HH:MM
if ($date && $time) {
$start_dt = $date . 'T' . $time . ':00';
$end_dt = date('Y-m-d\TH:i:s', strtotime($start_dt) + 3600); // 1hr default length for visual
$events[] = array(
'id' => $post->ID,
'title' => $post->post_title,
'start' => $start_dt,
'end' => $end_dt,
'backgroundColor' => '#0061ff',
'borderColor' => '#0061ff',
);
}
}
wp_send_json($events);
}
public function get_booking_details()
{
check_ajax_referer('on_booking_nonce', 'nonce');
$post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0;
$post = get_post($post_id);
if (!$post || $post->post_type !== 'on_booking') {
wp_send_json_error(array('message' => 'Buchung nicht gefunden.'));
}
$content = wpautop($post->post_content);
// Check for attachments
$attachments = get_children(array(
'post_parent' => $post->ID,
'post_type' => 'attachment',
));
if ($attachments) {
$content .= '<hr><strong>Angehängte Dateien:</strong><br>';
foreach ($attachments as $att) {
$url = wp_get_attachment_url($att->ID);
$content .= '<a href="' . $url . '" target="_blank" style="color:#0061ff;">' . basename($url) . '</a><br>';
}
}
// Tracking ID
$tracking_id = get_post_meta($post->ID, 'on_booking_tracking_id', true);
if ($tracking_id) {
$content .= '<hr><strong>Auftragsnummer:</strong> <span style="font-family:monospace; background:#eee; padding:2px 5px;">' . esc_html($tracking_id) . '</span><br>';
}
// Return meta for edit functionality
$date = get_post_meta($post->ID, 'on_booking_date', true);
$time = get_post_meta($post->ID, 'on_booking_time', true);
$notes = get_post_meta($post->ID, 'on_booking_notes', true);
wp_send_json_success(array(
'id' => $post->ID,
'title' => $post->post_title,
'content' => $content,
'date' => $date,
'time' => $time,
'notes' => $notes
));
}
public function delete_booking()
{
check_ajax_referer('on_booking_nonce', 'nonce');
$post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0;
if (!$post_id || get_post_type($post_id) !== 'on_booking') {
wp_send_json_error(array('message' => 'Ungültige Buchungs-ID.'));
}
$deleted = wp_trash_post($post_id); // Move to trash
if ($deleted) {
wp_send_json_success(array('message' => 'Buchung gelöscht.'));
} else {
wp_send_json_error(array('message' => 'Fehler beim Löschen.'));
}
}
public function update_booking_date()
{
check_ajax_referer('on_booking_nonce', 'nonce');
$post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0;
$new_date = isset($_POST['new_date']) ? sanitize_text_field($_POST['new_date']) : '';
$new_time = isset($_POST['new_time']) ? sanitize_text_field($_POST['new_time']) : '';
if (!$post_id || !$new_date) {
wp_send_json_error(array('message' => 'Fehlende Daten.'));
}
// Update Meta
update_post_meta($post_id, 'on_booking_date', $new_date);
if ($new_time) {
update_post_meta($post_id, 'on_booking_time', $new_time);
}
// Ideally update post title also to reflect new time?
// Let's do it if we have both
if ($new_time) {
// Fetch post to get original name part
$post = get_post($post_id);
// Try to preserve name? Format is "YYYY-MM-DD - HH:MM (Name)"
// Regex or just overwrite? Let's just update date/time in title if format matches or just leave title alone as it's secondary.
// But for clarity in list view, updating is good.
// Simplistic approach: just update title with new timestamp + existing name extracted?
// Too complex to parse reliably. Let's just update meta. User can see new time in calendar.
}
wp_send_json_success(array('message' => 'Buchung verschoben.'));
}
public function update_booking_status()
{
check_ajax_referer('on_booking_nonce', 'nonce');
$post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0;
$status = isset($_POST['status']) ? sanitize_text_field($_POST['status']) : '';
if (!$post_id || !$status) {
wp_send_json_error(array('message' => 'Fehlerhafte Daten.'));
}
update_post_meta($post_id, 'on_booking_status', $status);
update_post_meta($post_id, 'on_booking_status', $status);
wp_send_json_success(array('message' => 'Status aktualisiert.'));
}
public function update_booking_notes()
{
check_ajax_referer('on_booking_nonce', 'nonce');
$post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0;
// Allow safe HTML tags for formatting (bold, italic, lists, paragraphs, line breaks)
$notes = isset($_POST['notes']) ? wp_kses_post($_POST['notes']) : '';
if (!$post_id) {
wp_send_json_error(array('message' => 'Fehlerhafte ID.'));
}
update_post_meta($post_id, 'on_booking_notes', $notes);
wp_send_json_success(array('message' => 'Notizen gespeichert.'));
}
public function check_tracking_status()
{
check_ajax_referer('on_booking_nonce', 'nonce');
$tracking_id = isset($_POST['tracking_id']) ? sanitize_text_field($_POST['tracking_id']) : '';
if (!$tracking_id) {
wp_send_json_error(array('message' => 'Bitte ID eingeben.'));
}
$args = array(
'post_type' => 'on_booking',
'meta_key' => 'on_booking_tracking_id',
'meta_value' => $tracking_id,
'posts_per_page' => 1,
'post_status' => array('publish', 'pending', 'future', 'private')
);
$query = new WP_Query($args);
if ($query->have_posts()) {
$query->the_post();
$post_id = get_the_ID();
$status = get_post_meta($post_id, 'on_booking_status', true);
$date = get_post_meta($post_id, 'on_booking_date', true);
$time = get_post_meta($post_id, 'on_booking_time', true);
$notes = get_post_meta($post_id, 'on_booking_notes', true);
// Labels
$status_labels = array(
'waiting' => 'Warten',
'in_progress' => 'In Bearbeitung',
'done' => 'Abholbereit'
);
$status_text = isset($status_labels[$status]) ? $status_labels[$status] : ucfirst($status);
wp_send_json_success(array(
'status' => $status_text,
'date' => $date,
'time' => $time,
'notes' => wpautop($notes) // Convert line breaks to <p> and <br> tags
));
} else {
wp_send_json_error(array('message' => 'Diese Nummer wurde nicht gefunden.'));
}
}
// =====================
// SERVICE MANAGEMENT
// =====================
public function get_services()
{
$services = ON_Service_Manager::get_active_services();
wp_send_json_success(array('services' => $services));
}
public function create_service()
{
check_ajax_referer('on_booking_nonce', 'nonce');
$name = isset($_POST['name']) ? sanitize_text_field($_POST['name']) : '';
$duration = isset($_POST['duration']) ? intval($_POST['duration']) : 30;
if (empty($name)) {
wp_send_json_error(array('message' => 'Service Name ist erforderlich.'));
}
// Get max order for sorting
$services = ON_Service_Manager::get_all_services();
$max_order = 0;
foreach ($services as $s) {
$order = get_post_meta($s['id'], 'service_order', true);
if ($order > $max_order) {
$max_order = $order;
}
}
$post_id = wp_insert_post(array(
'post_type' => 'on_service',
'post_title' => $name,
'post_status' => 'publish',
));
if (is_wp_error($post_id)) {
wp_send_json_error(array('message' => 'Fehler beim Erstellen des Services.'));
}
update_post_meta($post_id, 'service_duration', $duration);
update_post_meta($post_id, 'service_active', 1);
update_post_meta($post_id, 'service_order', $max_order + 1);
wp_send_json_success(array('message' => 'Service erstellt.', 'id' => $post_id));
}
public function update_service()
{
check_ajax_referer('on_booking_nonce', 'nonce');
$service_id = isset($_POST['service_id']) ? intval($_POST['service_id']) : 0;
$name = isset($_POST['name']) ? sanitize_text_field($_POST['name']) : '';
$duration = isset($_POST['duration']) ? intval($_POST['duration']) : 30;
if (!$service_id || empty($name)) {
wp_send_json_error(array('message' => 'Ungültige Daten.'));
}
wp_update_post(array(
'ID' => $service_id,
'post_title' => $name,
));
update_post_meta($service_id, 'service_duration', $duration);
wp_send_json_success(array('message' => 'Service aktualisiert.'));
}
public function delete_service()
{
check_ajax_referer('on_booking_nonce', 'nonce');
$service_id = isset($_POST['service_id']) ? intval($_POST['service_id']) : 0;
if (!$service_id) {
wp_send_json_error(array('message' => 'Ungültige ID.'));
}
$deleted = wp_delete_post($service_id, true);
if ($deleted) {
wp_send_json_success(array('message' => 'Service gelöscht.'));
} else {
wp_send_json_error(array('message' => 'Fehler beim Löschen.'));
}
}
public function toggle_service_status()
{
check_ajax_referer('on_booking_nonce', 'nonce');
$service_id = isset($_POST['service_id']) ? intval($_POST['service_id']) : 0;
if (!$service_id) {
wp_send_json_error(array('message' => 'Ungültige ID.'));
}
$current_status = get_post_meta($service_id, 'service_active', true);
$new_status = $current_status == 1 ? 0 : 1;
update_post_meta($service_id, 'service_active', $new_status);
wp_send_json_success(array('message' => 'Status aktualisiert.', 'active' => $new_status));
}
public function get_slots_for_service()
{
check_ajax_referer('on_booking_nonce', 'nonce');
$service_id = isset($_POST['service_id']) ? intval($_POST['service_id']) : 0;
$date = isset($_POST['date']) ? sanitize_text_field($_POST['date']) : '';
if (!$service_id || empty($date)) {
wp_send_json_error(array('message' => 'Ungültige Daten.'));
}
// Get service duration
$service = ON_Service_Manager::get_service($service_id);
if (!$service) {
wp_send_json_error(array('message' => 'Service nicht gefunden.'));
}
$duration = $service['duration'];
// Get time settings
$start_time = get_option('on_booking_start_time', '08:00');
$end_time = get_option('on_booking_end_time', '17:00');
// Generate all possible slots based on service duration
$slots = array();
$start_minutes = $this->time_to_minutes($start_time);
$end_minutes = $this->time_to_minutes($end_time);
$current = $start_minutes;
while ($current + $duration <= $end_minutes) {
$slots[] = $this->minutes_to_time($current);
$current += $duration;
}
// Get existing bookings for this date
$existing_bookings = new WP_Query(array(
'post_type' => 'on_booking',
'posts_per_page' => -1,
'post_status' => 'publish',
'meta_query' => array(
array(
'key' => 'on_booking_date',
'value' => $date,
'compare' => '='
)
)
));
$booked_ranges = array();
if ($existing_bookings->have_posts()) {
while ($existing_bookings->have_posts()) {
$existing_bookings->the_post();
$booking_time = get_post_meta(get_the_ID(), 'on_booking_time', true);
$booking_duration = get_post_meta(get_the_ID(), 'on_booking_service_duration', true);
if (!$booking_duration) {
$booking_duration = 30; // Default fallback
}
$booking_start = $this->time_to_minutes($booking_time);
$booking_end = $booking_start + intval($booking_duration);
$booked_ranges[] = array('start' => $booking_start, 'end' => $booking_end);
}
wp_reset_postdata();
}
// Filter available slots (check for overlap)
$available_slots = array();
foreach ($slots as $slot) {
$slot_start = $this->time_to_minutes($slot);
$slot_end = $slot_start + $duration;
$is_available = true;
foreach ($booked_ranges as $booked) {
// Check overlap: new_start < existing_end AND new_end > existing_start
if ($slot_start < $booked['end'] && $slot_end > $booked['start']) {
$is_available = false;
break;
}
}
if ($is_available) {
$available_slots[] = $slot;
}
}
wp_send_json_success(array('slots' => $available_slots, 'duration' => $duration));
}
private function time_to_minutes($time)
{
$parts = explode(':', $time);
return intval($parts[0]) * 60 + intval($parts[1]);
}
private function minutes_to_time($minutes)
{
$hours = floor($minutes / 60);
$mins = $minutes % 60;
return sprintf('%02d:%02d', $hours, $mins);
}
public function send_test_email()
{
check_ajax_referer('on_booking_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(array('message' => 'Keine Berechtigung.'));
}
$email = isset($_POST['test_email']) ? sanitize_email($_POST['test_email']) : '';
if (!$email || !is_email($email)) {
wp_send_json_error(array('message' => 'Bitte eine gültige E-Mail eingeben.'));
}
$result = ON_Email_Manager::send_test_email($email);
if ($result === true) {
wp_send_json_success(array('message' => 'Test E-Mail wurde gesendet.'));
} else {
// Result contains error message string
wp_send_json_error(array('message' => $result));
}
}
}

View File

@@ -0,0 +1,402 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
class ON_Email_Manager
{
public static function init()
{
add_action('on_send_booking_email', array(__CLASS__, 'process_booking_email'));
}
public static function queue_booking_confirmation($booking_id)
{
// Schedule the event to run immediately (async)
wp_schedule_single_event(time(), 'on_send_booking_email', array($booking_id));
}
public static function process_booking_email($booking_id)
{
// This runs in the background
$post = get_post($booking_id);
if (!$post || $post->post_type !== 'on_booking') {
return false;
}
// Check if emails are enabled
if (!get_option('on_booking_enable_emails', 0)) {
return false;
}
// Get Booking Data
$date = get_post_meta($booking_id, 'on_booking_date', true);
$time = get_post_meta($booking_id, 'on_booking_time', true);
$service = get_post_meta($booking_id, 'on_booking_service_name', true);
$email = get_post_meta($booking_id, 'on_booking_email', true);
$tracking_id = get_post_meta($booking_id, 'on_booking_tracking_id', true);
// Get Name from Title "YYYY-MM-DD - HH:MM (Name)"
$title_parts = explode('(', $post->post_title);
$customer_name = isset($title_parts[1]) ? rtrim($title_parts[1], ')') : 'Kunde';
// Get Template
$subject = get_option('on_booking_email_subject', 'Buchungsbestätigung: {service} am {date}');
$body = get_option('on_booking_email_body', '<h3>Hallo {name},</h3><p>vielen Dank für deine Buchung.</p>');
// Replace Placeholders
$placeholders = array(
'{name}' => $customer_name,
'{date}' => $date,
'{time}' => $time,
'{service}' => $service,
'{tracking_id}' => $tracking_id
);
foreach ($placeholders as $key => $value) {
$subject = str_replace($key, $value, $subject);
$body = str_replace($key, $value, $body);
}
// Send customer email
self::send_email($email, $subject, $body);
}
/**
* Send a notification email to the workshop with all booking details.
*/
public static function send_admin_notification($booking_id, $name, $customer_email, $date, $time, $service, $tracking_id, $attachment_path = '')
{
if (!get_option('on_booking_enable_admin_notify', 0)) {
return;
}
$admin_email = get_option('on_booking_admin_notify_email', '');
if (empty($admin_email) || !is_email($admin_email)) {
return;
}
// Get extra booking data from post
$post = get_post($booking_id);
$phone = get_post_meta($booking_id, 'on_booking_phone', true);
$status = get_post_meta($booking_id, 'on_booking_status', true);
$customer_request = get_post_meta($booking_id, 'on_booking_customer_request', true);
$notes = $post ? $post->post_content : '';
// Parse vehicle info from post content
$brand = '';
$year = '';
$description = '';
if ($notes) {
if (preg_match('/Fahrzeug:\s*(.+?)\s*\((\d{4})\)/m', $notes, $m)) {
$brand = $m[1];
$year = $m[2];
}
if (preg_match('/Beschreibung:\s*(.+)/m', $notes, $m)) {
$description = trim($m[1]);
}
}
$status_labels = array(
'waiting' => 'Warten auf Bestätigung',
'confirmed' => 'Bestätigt',
'in_progress' => 'In Arbeit',
'done' => 'Abgeschlossen',
'cancelled' => 'Storniert',
);
$status_display = isset($status_labels[$status]) ? $status_labels[$status] : ucfirst($status);
// Detect if this is a simple mode request
$is_simple = ($service === 'Offene Anfrage');
// Format date for display
$date_display = $date;
$time_display = $time;
// Build nicely formatted HTML email
if ($is_simple) {
$subject = 'Neue Anfrage von ' . $name . ' am ' . $date;
} else {
$subject = 'Neue Buchung: ' . $service . ' am ' . $date . ' um ' . $time;
}
$heading = $is_simple ? 'Neue Anfrage eingegangen' : 'Neue Buchung eingegangen';
$body = '<h2 style="color: #0061ff; margin-bottom: 5px;">' . $heading . '</h2>';
$body .= '<p style="color: #888; margin-top: 0;">Auftragsnummer: <strong style="font-family: monospace; color: #0061ff;">' . esc_html($tracking_id) . '</strong></p>';
// Customer Request (simple mode)
if ($customer_request) {
$body .= '<table style="width: 100%; border-collapse: collapse; margin: 15px 0;">';
$body .= '<tr style="background: #e65100; color: #fff;"><th colspan="2" style="padding: 10px; text-align: left; font-size: 1.1em;">Kundenwunsch</th></tr>';
$body .= '<tr><td style="padding: 12px 10px; border-bottom: 1px solid #eee;" colspan="2">' . nl2br(esc_html($customer_request)) . '</td></tr>';
$body .= '</table>';
}
// Customer Info
$body .= '<table style="width: 100%; border-collapse: collapse; margin: 15px 0;">';
$body .= '<tr style="background: #1a1a2e; color: #fff;"><th colspan="2" style="padding: 10px; text-align: left; font-size: 1.1em;">Kundendaten</th></tr>';
$body .= '<tr><td style="padding: 8px 10px; border-bottom: 1px solid #eee; font-weight: bold; width: 140px;">Name</td><td style="padding: 8px 10px; border-bottom: 1px solid #eee;">' . esc_html($name) . '</td></tr>';
$body .= '<tr><td style="padding: 8px 10px; border-bottom: 1px solid #eee; font-weight: bold;">E-Mail</td><td style="padding: 8px 10px; border-bottom: 1px solid #eee;"><a href="mailto:' . esc_attr($customer_email) . '" style="color: #0061ff;">' . esc_html($customer_email) . '</a></td></tr>';
if ($phone) {
$body .= '<tr><td style="padding: 8px 10px; border-bottom: 1px solid #eee; font-weight: bold;">Telefon</td><td style="padding: 8px 10px; border-bottom: 1px solid #eee;"><a href="tel:' . esc_attr($phone) . '" style="color: #0061ff;">' . esc_html($phone) . '</a></td></tr>';
}
$body .= '</table>';
// Booking Details
$body .= '<table style="width: 100%; border-collapse: collapse; margin: 15px 0;">';
$body .= '<tr style="background: #0061ff; color: #fff;"><th colspan="2" style="padding: 10px; text-align: left; font-size: 1.1em;">Buchungsdetails</th></tr>';
$body .= '<tr><td style="padding: 8px 10px; border-bottom: 1px solid #eee; font-weight: bold; width: 140px;">Service</td><td style="padding: 8px 10px; border-bottom: 1px solid #eee;">' . esc_html($service) . '</td></tr>';
$body .= '<tr><td style="padding: 8px 10px; border-bottom: 1px solid #eee; font-weight: bold;">Datum</td><td style="padding: 8px 10px; border-bottom: 1px solid #eee;">' . esc_html($date_display) . '</td></tr>';
if (!$is_simple) {
$body .= '<tr><td style="padding: 8px 10px; border-bottom: 1px solid #eee; font-weight: bold;">Uhrzeit</td><td style="padding: 8px 10px; border-bottom: 1px solid #eee;">' . esc_html($time_display) . '</td></tr>';
}
$body .= '<tr><td style="padding: 8px 10px; border-bottom: 1px solid #eee; font-weight: bold;">Status</td><td style="padding: 8px 10px; border-bottom: 1px solid #eee;">' . esc_html($status_display) . '</td></tr>';
$body .= '</table>';
// Vehicle Info
if ($brand || $year) {
$body .= '<table style="width: 100%; border-collapse: collapse; margin: 15px 0;">';
$body .= '<tr style="background: #333; color: #fff;"><th colspan="2" style="padding: 10px; text-align: left; font-size: 1.1em;">Fahrzeug</th></tr>';
if ($brand) {
$body .= '<tr><td style="padding: 8px 10px; border-bottom: 1px solid #eee; font-weight: bold; width: 140px;">Marke/Modell</td><td style="padding: 8px 10px; border-bottom: 1px solid #eee;">' . esc_html($brand) . '</td></tr>';
}
if ($year) {
$body .= '<tr><td style="padding: 8px 10px; border-bottom: 1px solid #eee; font-weight: bold;">Baujahr</td><td style="padding: 8px 10px; border-bottom: 1px solid #eee;">' . esc_html($year) . '</td></tr>';
}
if ($description) {
$body .= '<tr><td style="padding: 8px 10px; border-bottom: 1px solid #eee; font-weight: bold;">Beschreibung</td><td style="padding: 8px 10px; border-bottom: 1px solid #eee;">' . esc_html($description) . '</td></tr>';
}
$body .= '</table>';
}
$body .= '<p style="color: #888; font-size: 0.9em; margin-top: 20px;">Diese E-Mail wurde automatisch vom Buchungssystem gesendet.</p>';
$attachments = array();
if ($attachment_path && file_exists($attachment_path)) {
$attachments[] = $attachment_path;
}
self::send_email($admin_email, $subject, $body, false, $attachments);
}
public static function send_test_email($to_email)
{
// 1. Pre-check Connection
$check = self::test_smtp_connection();
if ($check !== true) {
return $check; // Returns error string
}
$subject = 'Test E-Mail: ON Motorrad Buchung';
$body = '<h3>Test erfolgreich!</h3><p>Dies ist eine Test-Nachricht von deinem Buchungssystem. Wenn du diese Nachricht liest, funktionieren die SMTP-Einstellungen.</p>';
// Call with debug = true
$result = self::send_email($to_email, $subject, $body, true);
if (is_array($result)) {
$log = $result['log'];
$status = $result['result'] ? 'ERFOLG' : 'FEHLER';
return "$status. Full Log:\n\n" . $log;
}
if ($result) {
return true;
} else {
return 'WP Mail returned false (Check Mail Log if available)';
}
}
private static function test_smtp_connection()
{
$host = get_option('on_booking_smtp_host');
$port = get_option('on_booking_smtp_port', 587);
if (!$host) {
return 'SMTP Host fehlt.';
}
// 3 Seconds Timeout for Connection Test
$connection = @fsockopen($host, $port, $errno, $errstr, 3);
if (is_resource($connection)) {
fclose($connection);
return true;
} else {
return "Verbindung zu $host:$port fehlgeschlagen. ($errno: $errstr)";
}
}
private static function send_email($to, $subject, $body, $debug = false, $attachments = array())
{
// Get SMTP Settings
$smtp_host = get_option('on_booking_smtp_host');
$smtp_port = get_option('on_booking_smtp_port', 587);
// $smtp_user = trim(get_option('on_booking_smtp_user'));
// $smtp_pass = trim(get_option('on_booking_smtp_pass'));
// DEBUG: Force provided credentials
$smtp_user = trim(get_option('on_booking_smtp_user'));
$smtp_pass = trim(get_option('on_booking_smtp_pass'));
$smtp_enc = get_option('on_booking_smtp_enc', 'tls');
$smtp_auth_type = get_option('on_booking_smtp_auth_type', 'auto');
$mail_method = get_option('on_booking_mail_method', 'smtp');
$from_email = get_option('on_booking_from_email', get_bloginfo('admin_email'));
$from_name = get_option('on_booking_from_name', get_bloginfo('name'));
// Apply Premium Dark Styling Wrapper
$styled_body = self::apply_email_styling($body);
// Allow capturing debug output
$debug_output = "";
// PHPMailer Setup Hook
$phpmailer_hook = function ($phpmailer) use ($smtp_host, $smtp_port, $smtp_user, $smtp_pass, $smtp_enc, $smtp_auth_type, $mail_method, $from_email, $from_name, $debug, &$debug_output) {
if ($smtp_host && $mail_method === 'smtp') {
$phpmailer->isSMTP();
$phpmailer->Host = $smtp_host;
$phpmailer->SMTPAuth = true;
$phpmailer->Port = $smtp_port;
$phpmailer->Username = $smtp_user;
$phpmailer->Password = $smtp_pass;
// Map 'starttls' to 'tls' as PHPMailer uses 'tls' for STARTTLS
// And explicitly set SMTPSecure
if ($smtp_enc === 'starttls') {
$phpmailer->SMTPSecure = 'tls';
} elseif ($smtp_enc === 'ssl') {
$phpmailer->SMTPSecure = 'ssl';
} elseif ($smtp_enc === 'tls') {
$phpmailer->SMTPSecure = 'tls';
} else {
$phpmailer->SMTPSecure = '';
}
// Explicit Auth Type if requested
if ($smtp_auth_type && $smtp_auth_type !== 'auto') {
$phpmailer->AuthType = $smtp_auth_type;
}
// Set Timeout to prevent long hangs (User requested 30s)
$phpmailer->Timeout = 30;
$phpmailer->Timelimit = 30;
if ($debug) {
$phpmailer->SMTPDebug = 3; // Connection level + Data
$phpmailer->Debugoutput = function ($str, $level) use (&$debug_output) {
$debug_output .= "[$level] $str\n";
};
}
}
// Always set sender
$phpmailer->setFrom($from_email, $from_name);
$phpmailer->From = $from_email;
$phpmailer->FromName = $from_name;
};
add_action('phpmailer_init', $phpmailer_hook);
// Send headers
$headers = array('Content-Type: text/html; charset=UTF-8');
// Execute send
$result = wp_mail($to, $subject, $styled_body, $headers, $attachments);
// Remove hook to avoid conflicts with other emails in same request
remove_action('phpmailer_init', $phpmailer_hook);
if ($debug) {
return array('result' => $result, 'log' => $debug_output);
}
return $result;
}
private static function apply_email_styling($content)
{
// Wrapper with dark theme consistent with plugin
ob_start();
?>
<!DOCTYPE html>
<html>
<head>
<style>
body {
margin: 0;
padding: 0;
font-family: 'Helvetica', 'Arial', sans-serif;
background-color: #111111;
color: #ffffff;
}
.email-container {
max-width: 600px;
margin: 0 auto;
background-color: #1f1f1f;
overflow: hidden;
}
.email-header {
background-color: #0061ff;
padding: 20px;
text-align: center;
}
.email-header h1 {
margin: 0;
color: #ffffff;
font-style: italic;
text-transform: uppercase;
transform: skewX(-10deg);
}
.email-content {
padding: 30px;
color: #dddddd;
line-height: 1.6;
}
.email-footer {
background-color: #111111;
padding: 20px;
text-align: center;
font-size: 12px;
color: #666666;
}
a {
color: #0061ff;
text-decoration: none;
}
h2,
h3 {
color: #ffffff;
margin-top: 0;
font-size: 1.25em;
}
</style>
</head>
<body>
<div class="email-container">
<div class="email-header">
<h1>Buchung <span>Bestätigt</span></h1>
</div>
<div class="email-content">
<?php echo wp_kses_post($content); ?>
</div>
<div class="email-footer">
&copy;
<?php echo date('Y'); ?>
<?php echo get_bloginfo('name'); ?>
</div>
</div>
</body>
</html>
<?php
return ob_get_clean();
}
}

View File

@@ -0,0 +1,153 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
class ON_Service_Manager
{
public function __construct()
{
add_action('init', array($this, 'register_service_post_type'));
register_activation_hook(ON_BOOKING_PATH . 'on-motorrad-buchung.php', array($this, 'create_default_services'));
}
public function register_service_post_type()
{
$labels = array(
'name' => 'Services',
'singular_name' => 'Service',
'menu_name' => 'Services',
'add_new' => 'Neuer Service',
'add_new_item' => 'Neuen Service hinzufügen',
'edit_item' => 'Service bearbeiten',
'all_items' => 'Alle Services',
);
$args = array(
'labels' => $labels,
'public' => false,
'show_ui' => false, // We'll create custom admin page
'show_in_menu' => false,
'capability_type' => 'post',
'supports' => array('title'),
'has_archive' => false,
);
register_post_type('on_service', $args);
}
public function create_default_services()
{
// Check if services already exist
$existing = get_posts(array(
'post_type' => 'on_service',
'posts_per_page' => 1,
'post_status' => 'publish'
));
if (!empty($existing)) {
return; // Services already created
}
$default_services = array(
array('name' => 'Reifenwechsel', 'duration' => 30),
array('name' => 'Reifenwechsel vorne und hinten', 'duration' => 60),
array('name' => 'Jahresinspektion', 'duration' => 90),
array('name' => 'Ölwechsel', 'duration' => 30),
array('name' => 'Kettensatzwechsel', 'duration' => 90),
array('name' => 'Gabelservice', 'duration' => 90),
array('name' => 'Bremsbeläge wechseln', 'duration' => 30),
array('name' => 'Bremsscheiben und Beläge', 'duration' => 60),
);
foreach ($default_services as $index => $service) {
$post_id = wp_insert_post(array(
'post_type' => 'on_service',
'post_title' => $service['name'],
'post_status' => 'publish',
));
if ($post_id) {
update_post_meta($post_id, 'service_duration', $service['duration']);
update_post_meta($post_id, 'service_active', 1);
update_post_meta($post_id, 'service_order', $index);
}
}
}
/**
* Get all active services
*/
public static function get_active_services()
{
$services = get_posts(array(
'post_type' => 'on_service',
'posts_per_page' => -1,
'post_status' => 'publish',
'orderby' => 'meta_value_num',
'meta_key' => 'service_order',
'order' => 'ASC'
));
$result = array();
foreach ($services as $service) {
$active = get_post_meta($service->ID, 'service_active', true);
if ($active == 1) {
$result[] = array(
'id' => $service->ID,
'name' => $service->post_title,
'duration' => (int) get_post_meta($service->ID, 'service_duration', true),
);
}
}
return $result;
}
/**
* Get all services (including inactive)
*/
public static function get_all_services()
{
$services = get_posts(array(
'post_type' => 'on_service',
'posts_per_page' => -1,
'post_status' => 'publish',
'orderby' => 'meta_value_num',
'meta_key' => 'service_order',
'order' => 'ASC'
));
$result = array();
foreach ($services as $service) {
$result[] = array(
'id' => $service->ID,
'name' => $service->post_title,
'duration' => (int) get_post_meta($service->ID, 'service_duration', true),
'active' => (int) get_post_meta($service->ID, 'service_active', true),
);
}
return $result;
}
/**
* Get service by ID
*/
public static function get_service($service_id)
{
$service = get_post($service_id);
if (!$service || $service->post_type !== 'on_service') {
return null;
}
return array(
'id' => $service->ID,
'name' => $service->post_title,
'duration' => (int) get_post_meta($service->ID, 'service_duration', true),
'active' => (int) get_post_meta($service->ID, 'service_active', true),
);
}
}
new ON_Service_Manager();

364
on-motorrad-buchung.php Normal file
View File

@@ -0,0 +1,364 @@
<?php
/**
* Plugin Name: ON Motorrad Buchung
* Description: Ein Buchungssystem für Motorradwerkstätten mit Admin-Verwaltung und Frontend-Formular.
* Version: 1.0.0
* Author: Antigravity
* Text Domain: on-motorrad-buchung
*/
if (!defined('ABSPATH')) {
exit;
}
// Constants
define('ON_BOOKING_PATH', plugin_dir_path(__FILE__));
define('ON_BOOKING_URL', plugin_dir_url(__FILE__));
define('ON_BOOKING_VERSION', '1.0.0');
// Include Classes
require_once ON_BOOKING_PATH . 'includes/class-on-booking-admin.php';
require_once ON_BOOKING_PATH . 'includes/class-on-booking-ajax.php';
require_once ON_BOOKING_PATH . 'includes/class-on-service-manager.php';
require_once ON_BOOKING_PATH . 'includes/class-on-email-manager.php'; // Email Manager
class ON_Booking_Plugin
{
public function __construct()
{
add_action('init', array($this, 'register_post_types'));
add_action('wp_enqueue_scripts', array($this, 'enqueue_assets'));
// Initialize Sub-Classes
new ON_Booking_Admin();
new ON_Booking_Ajax();
ON_Email_Manager::init(); // Initialize Async Email Handling
// Shortcode
add_shortcode('on_booking_form', array($this, 'render_booking_form'));
}
public function register_post_types()
{
$labels = array(
'name' => 'Buchungen',
'singular_name' => 'Buchung',
'menu_name' => 'Buchungen',
'name_admin_bar' => 'Buchung',
'add_new' => 'Hinzufügen',
'add_new_item' => 'Neue Buchung',
'new_item' => 'Neue Buchung',
'edit_item' => 'Buchung bearbeiten',
'view_item' => 'Buchung ansehen',
'all_items' => 'Alle Buchungen',
'search_items' => 'Buchungen suchen',
'not_found' => 'Keine Buchungen gefunden',
'not_found_in_trash' => 'Keine Buchungen im Papierkorb',
);
$args = array(
'labels' => $labels,
'public' => false,
'publicly_queryable' => false,
'show_ui' => false, // Hidden from standard UI
'show_in_menu' => false, // Hidden from Admin Menu
'query_var' => true,
'rewrite' => array('slug' => 'on-booking'),
'capability_type' => 'post',
'has_archive' => false,
'hierarchical' => false,
'menu_position' => 50,
'menu_icon' => 'dashicons-calendar-alt',
'supports' => array('title', 'editor', 'custom-fields'),
);
register_post_type('on_booking', $args);
}
public function enqueue_assets()
{
wp_enqueue_style('on-booking-style', ON_BOOKING_URL . 'assets/css/style.css', array(), time());
// Google Fonts from example.html
wp_enqueue_style('on-booking-fonts', 'https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap', array(), null);
wp_enqueue_script('on-booking-script', ON_BOOKING_URL . 'assets/js/script.js', array('jquery'), time(), true);
wp_localize_script('on-booking-script', 'onBookingData', array(
'ajaxurl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('on_booking_nonce'),
'simpleMode' => (bool) get_option('on_booking_simple_mode', 0),
));
}
public function render_booking_form()
{
// Ensure modals are rendered in footer (once)
add_action('wp_footer', array($this, 'render_modals_once'));
ob_start();
?>
<div class="on-booking-system">
<button class="on-trigger-btn js-open-booking">
<span>Termin Buchen</span>
</button>
<button class="on-trigger-btn on-status-btn js-open-status" style="margin-left: 10px; background: #333;">
<span>Status Abfragen</span>
</button>
</div>
<?php
return ob_get_clean();
}
public function render_modals_once()
{
// Static var to prevent double rendering if hook is called multiple times (unlikely on footer but good practice)
static $rendered = false;
if ($rendered)
return;
$rendered = true;
?>
<!-- Structure copied and adapted from example.html -->
<style>
/* CRITICAL INLINE STYLES - Define CSS variables for moved modal */
#onModalOverlay,
#onStatusModalOverlay,
#onSuccessModalOverlay {
/* CSS Variables - replicate from .on-booking-system */
--on-blue: #0061ff;
--on-dark: #111111;
--on-gray: #1f1f1f;
--on-text: #ffffff;
--on-muted: #888888;
--on-border: #333333;
display: none;
position: fixed;
z-index: 2147483647;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.85);
/* Flexbox properties for when open */
align-items: center;
justify-content: center;
padding: 20px;
/* No overflow on overlay - content will scroll */
overflow: hidden;
}
#onModalOverlay.is-open,
#onStatusModalOverlay.is-open,
#onSuccessModalOverlay.is-open {
display: flex !important;
}
/* Content Box - THIS scrolls, not the overlay */
#onModalOverlay .on-modal-content,
#onStatusModalOverlay .on-modal-content,
#onSuccessModalOverlay .on-modal-content {
width: 100%;
max-width: 500px;
max-height: 90vh;
background: var(--on-gray);
border-left: 5px solid var(--on-blue);
color: var(--on-text);
padding: 20px;
position: relative;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
/* FORCE SCROLL - Very aggressive */
overflow-y: auto !important;
overflow-x: hidden !important;
-webkit-overflow-scrolling: touch !important;
overscroll-behavior: contain !important;
/* Ensure content is actually scrollable */
display: block !important;
/* Remove any potential transform issues */
will-change: scroll-position;
}
/* Lock body/html scroll */
html.on-modal-open,
body.on-modal-open {
overflow: hidden !important;
}
</style>
<!-- Status Modal -->
<div class="on-modal-overlay" id="onStatusModalOverlay">
<div class="on-modal-content">
<button class="on-close-btn" id="onCloseStatusModal">&times;</button>
<h3>Status <span>Prüfen</span></h3>
<div class="on-form-group">
<label>Deine Auftragsnummer</label>
<input type="text" class="on-input" id="onTrackingIdInput" placeholder="z.B. A1B2C3D4">
</div>
<button class="on-submit-btn" id="onCheckStatusBtn">Prüfen</button>
<div id="statusResult" style="margin-top: 20px; font-weight: bold; text-align: center;"></div>
</div>
</div>
<!-- Success Popup Modal -->
<div class="on-modal-overlay" id="onSuccessModalOverlay">
<div class="on-modal-content" style="max-width: 400px; text-align: center;">
<button class="on-close-btn" id="onCloseSuccessModal">&times;</button>
<div style="font-size: 4rem; color: var(--on-blue); margin-bottom: 15px;">✓</div>
<h3 style="margin-bottom: 10px;">Buchung <span>Erfolgreich!</span></h3>
<p style="color: #888; margin-bottom: 20px;">Deine Terminanfrage wurde erfolgreich übermittelt.</p>
<div
style="background: rgba(0,97,255,0.1); border: 1px solid var(--on-blue); border-radius: 8px; padding: 15px; margin-bottom: 20px;">
<p style="color: #888; font-size: 0.85rem; margin-bottom: 5px;">Deine Auftragsnummer:</p>
<p id="successTrackingId"
style="font-size: 1.5rem; font-weight: bold; color: var(--on-blue); letter-spacing: 2px;"></p>
</div>
<p style="color: #666; font-size: 0.85rem; margin-bottom: 20px;">
Bitte notiere dir diese Nummer, um den Status deiner Buchung später abfragen zu können.
</p>
<button class="on-submit-btn" id="onSuccessCloseBtn" style="width: 100%;">Alles klar!</button>
</div>
</div>
<?php $simple_mode = get_option('on_booking_simple_mode', 0); ?>
<!-- Modal Overlay -->
<div class="on-modal-overlay" id="onModalOverlay">
<div class="on-modal-content">
<button class="on-close-btn" id="onCloseModal">&times;</button>
<h3><?php echo $simple_mode ? 'Anfrage <span>Senden</span>' : 'Termin <span>W&auml;hlen</span>'; ?></h3>
<form id="onBookingForm" enctype="multipart/form-data">
<?php if ($simple_mode): ?>
<input type="hidden" name="simple_mode" value="1">
<?php endif; ?>
<?php if (!$simple_mode): ?>
<!-- Step 1: Service Selection (FIRST) -->
<div class="on-form-group on-step" id="stepService">
<label>1. Service w&auml;hlen</label>
<select class="on-input" name="service_id" id="serviceSelect" required>
<option value="" disabled selected>Bitte Service w&auml;hlen...</option>
<?php
$services = ON_Service_Manager::get_active_services();
$show_duration = get_option('on_booking_show_duration', 0);
foreach ($services as $service) {
$duration_label = '';
if ($show_duration) {
$duration_label = $service['duration'] < 60
? ' (' . $service['duration'] . ' min)'
: ' (' . ($service['duration'] / 60) . ' Std)';
}
echo '<option value="' . esc_attr($service['id']) . '" data-duration="' . esc_attr($service['duration']) . '">';
echo esc_html($service['name']) . $duration_label;
echo '</option>';
}
?>
</select>
<input type="hidden" name="service_duration" id="serviceDuration">
</div>
<!-- Step 2: Calendar (hidden until service selected) -->
<div class="on-form-group on-step" id="stepCalendar" style="display: none;">
<label>2. Datum w&auml;hlen</label>
<div class="on-calendar-header">
<button type="button" class="on-cal-btn" id="prevMonth">&lt;</button>
<span class="on-month-label" id="monthYear"></span>
<button type="button" class="on-cal-btn" id="nextMonth">&gt;</button>
</div>
<div class="on-calendar-grid" id="calendarGrid"></div>
<input type="hidden" name="selected_date" id="selectedDate">
</div>
<!-- Step 3: Time Slots (hidden until date selected) -->
<div class="on-form-group on-time-section on-step" id="timeSection" style="display: none;">
<label>3. Uhrzeit w&auml;hlen</label>
<div class="on-time-grid" id="timeGrid"></div>
<input type="hidden" name="selected_time" id="selectedTime">
</div>
<?php endif; ?>
<!-- Step 4: Details (in simple mode: always visible) -->
<div class="on-details-section on-step" id="stepDetails" <?php echo $simple_mode ? '' : 'style="display: none;"'; ?>>
<?php if (!$simple_mode): ?>
<div class="on-section-divider" style="margin: 20px 0; border-top: 1px solid #333;"></div>
<?php endif; ?>
<?php if ($simple_mode): ?>
<!-- Free-text request field (simple mode) -->
<div class="on-form-group">
<label>Was soll gemacht werden?</label>
<textarea class="on-input" name="customer_request" rows="4"
placeholder="Beschreibe kurz, was du dir w&uuml;nschst oder was repariert werden soll..."
required></textarea>
</div>
<div class="on-section-divider" style="margin: 20px 0; border-top: 1px solid #333;"></div>
<?php endif; ?>
<!-- Bike Details -->
<div class="on-form-group">
<label>Motorrad Marke</label>
<input type="text" class="on-input" name="brand" placeholder="z.B. Honda, Yamaha" <?php echo $simple_mode ? '' : 'required'; ?>>
</div>
<div class="on-form-group">
<label>Baujahr</label>
<input type="number" class="on-input" name="year" placeholder="z.B. 2019" <?php echo $simple_mode ? '' : 'required'; ?>>
</div>
<?php if (!$simple_mode): ?>
<!-- Additional Notes for "Sonstiges" -->
<div class="on-form-group" id="otherServiceGroup" style="display: none;">
<label>Beschreibung des Problems</label>
<textarea class="on-input" name="other_service_desc" rows="3"
placeholder="Was muss gemacht werden?"></textarea>
</div>
<?php endif; ?>
<!-- Personal Info -->
<div class="on-section-divider" style="margin: 20px 0; border-top: 1px solid #333;"></div>
<div class="on-form-group">
<label>Dein Name</label>
<input type="text" class="on-input" name="user_name" placeholder="Vor- und Nachname" required>
</div>
<div class="on-form-group">
<label>Telefon</label>
<input type="tel" class="on-input" name="user_phone" placeholder="Nr. f&uuml;r R&uuml;ckfragen"
required>
</div>
<div class="on-form-group">
<label>E-Mail</label>
<input type="email" class="on-input" name="user_email" placeholder="Deine E-Mail Adresse" required>
</div>
<!-- File Upload -->
<div class="on-form-group">
<label>Fahrzeugschein (Optional)</label>
<input type="file" class="on-input" name="vehicle_doc"
accept="image/png, image/jpeg, image/jpg, application/pdf">
<small style="display:block; color:#666; font-size: 0.7rem; margin-top:5px;">Erlaubt: JPG,
PNG,
PDF (Max. 10MB)</small>
</div>
<button type="submit" class="on-submit-btn" id="submitBtn">
<span>Jetzt Anfragen</span>
</button>
<div id="formFeedback" style="display:none; margin-top:15px; text-align:center; font-weight:bold;">
</div>
</div><!-- End stepDetails -->
</form>
</div>
</div>
<?php
}
}
new ON_Booking_Plugin();