feat: Implement the motorcycle booking plugin including frontend booking flow and admin management.
This commit is contained in:
BIN
assets/.DS_Store
vendored
Normal file
BIN
assets/.DS_Store
vendored
Normal file
Binary file not shown.
346
assets/css/admin-style.css
Normal file
346
assets/css/admin-style.css
Normal 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
392
assets/css/style.css
Normal 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
287
assets/js/admin-calendar.js
Normal 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
237
assets/js/admin-kanban.js
Normal 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
360
assets/js/script.js
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
808
includes/class-on-booking-admin.php
Normal file
808
includes/class-on-booking-admin.php
Normal 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 – 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 – 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ä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">×</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">×</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
|
||||||
|
}
|
||||||
|
}
|
||||||
723
includes/class-on-booking-ajax.php
Normal file
723
includes/class-on-booking-ajax.php
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
402
includes/class-on-email-manager.php
Normal file
402
includes/class-on-email-manager.php
Normal 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">
|
||||||
|
©
|
||||||
|
<?php echo date('Y'); ?>
|
||||||
|
<?php echo get_bloginfo('name'); ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
<?php
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
}
|
||||||
153
includes/class-on-service-manager.php
Normal file
153
includes/class-on-service-manager.php
Normal 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
364
on-motorrad-buchung.php
Normal 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">×</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">×</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">×</button>
|
||||||
|
|
||||||
|
<h3><?php echo $simple_mode ? 'Anfrage <span>Senden</span>' : 'Termin <span>Wä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ählen</label>
|
||||||
|
<select class="on-input" name="service_id" id="serviceSelect" required>
|
||||||
|
<option value="" disabled selected>Bitte Service wä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ählen</label>
|
||||||
|
<div class="on-calendar-header">
|
||||||
|
<button type="button" class="on-cal-btn" id="prevMonth"><</button>
|
||||||
|
<span class="on-month-label" id="monthYear"></span>
|
||||||
|
<button type="button" class="on-cal-btn" id="nextMonth">></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ä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ü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ür Rü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();
|
||||||
Reference in New Issue
Block a user