From 658f666647c0a5340d5550321efeac3216e90775 Mon Sep 17 00:00:00 2001 From: Jonas Date: Thu, 26 Feb 2026 11:54:13 +0100 Subject: [PATCH] feat: Implement the motorcycle booking plugin including frontend booking flow and admin management. --- .DS_Store | Bin 0 -> 10244 bytes assets/.DS_Store | Bin 0 -> 10244 bytes assets/css/admin-style.css | 346 +++++++++++ assets/css/style.css | 392 +++++++++++++ assets/js/admin-calendar.js | 287 +++++++++ assets/js/admin-kanban.js | 237 ++++++++ assets/js/script.js | 360 ++++++++++++ includes/class-on-booking-admin.php | 808 ++++++++++++++++++++++++++ includes/class-on-booking-ajax.php | 723 +++++++++++++++++++++++ includes/class-on-email-manager.php | 402 +++++++++++++ includes/class-on-service-manager.php | 153 +++++ on-motorrad-buchung.php | 364 ++++++++++++ 12 files changed, 4072 insertions(+) create mode 100644 .DS_Store create mode 100644 assets/.DS_Store create mode 100644 assets/css/admin-style.css create mode 100644 assets/css/style.css create mode 100644 assets/js/admin-calendar.js create mode 100644 assets/js/admin-kanban.js create mode 100644 assets/js/script.js create mode 100644 includes/class-on-booking-admin.php create mode 100644 includes/class-on-booking-ajax.php create mode 100644 includes/class-on-email-manager.php create mode 100644 includes/class-on-service-manager.php create mode 100644 on-motorrad-buchung.php diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..c6c2031647001fcf5550e89987607cf1424c8baf GIT binary patch literal 10244 zcmeHMeQXp(6rZ;(WtJ{<3k8nP9yE$#EiI)51j4mF(DK#VYiTLS_3n0UH|*_}+r2AV z8yfw~XcaYn#Hi7~gsA*sL=74>5(G7YMC%8}Xks++BZ`UsW8xpanc0J0uO%TG1G;nB zc{6X`d$aTQ_xA0~5kjCbt zvU$O_${I+69u0YPSWq0n0zwt3Pz7&`0YV-5K~FB^(P2S_IstF<0p85u?NETvj`o9G zoIprWzxF-gdm!HfZ0}x3sz@&}h?~2AKiD*OENO?sanp_?j}E+>`MT=xsfOHf?ULjD zePhlBpa)e+5s488e>!oA6CZH;J^RPw{ifl#)zzPZR8TmjXeymbi|K8VK6@bI#@w{i z7)$MDy>{C+UEzRM&5ivkzDo54% zgM%xp>(^8UYinzVDuaVHyi-@ZdT2ji#+^FyN}NQ>HqBPsF_@WMU9Nt>Wn+0m zUEPkIa+|oZE(7fgDK+irZ8{n?GI|(II}AQ!3&=-j%w4$Tn&m52t=-VLdCQK`618;Z ztTILEMt@tbal4_XItC2Q?b{V|61t@sR&QsEeL0n{k^`I@TH$W|U=t>t!`Y+Z|k} zFj1*g$*Lo_??X_CscMv3s%mnF8H?+tVAU&YsJd0|%m`l#W|^{{%3XNImBht3g10fy zK-JT9FPnN>%yC=#bqjBaFJWU~6Q$iwUo54!3?L-4+-6Go!$Uq(*`yh+?F}vtY^QV= zGuEJK-qKoH1A(Zj8dk=@0Ki8l;z0d~Qn@1Q|BBFZ(X4oXdrKtxMT}>lXcNEyb{v z8*om>dyPm(Ckd#VZ)x7L_38Pm<}X;dNGb2YaL^A-7MPxlzF$V61(7a*50HjY9Yp=EHoD>Az4-l7i?@QFIQP){bLwEN4_E7 zlV8c7i18UP3z5AHs-Yg%!)DllxQ;+ObRepGAPxzRY#VW%0tfDbArIa6!3aDEhu|T2 z5}tymIl_;?3-BVm1h2wt@H!la6L1nv!D)COK7fzmOZX0cgx|&?d3zSag2M0Pkvulx zo_%)n`V$ZEgqwr*qK)1Rl5?Jb*cy6%`tOwZpA%d0Bs85Fl<*yEG_!az^{($ zNBDFCQ9*+?@Ic^!@g88|QbJP1C2n$H^#0v4U9YzGOOQ%RCrq3~C($x`bG+XfhI8le-g3g?okpq-y%*bN*aF^%Olg&it^Cx zP!(0HX-_U`?#r3pZJevj4@G442;HUQ^|v}YTT$m%SvWtW>}1@Qtbwgg#WKp`&^5BE z(M}f)6-(48OJ&uSJNF_G#HH$#daCMjm!%~QORyT1RaD(9cjtw#1+!9FOXVJXmT5G` z6}JggZo>M|21_NjL?+z^`yx0%@{TCMnWPDJ(6QR!9v}qqI_rO080d)GhUiTbBw!)+@$^ zR?@KNzceg2?;rX}hc6IHH{95|Y4ep6N++j^;F&RVR)j^=s&!3gp(l`90Ox4fbDWQf z4vuqr(gw0;CPzkFOFjT~F(PA7<#4w>FRkP=W zl$tO`))A^od~$7FSW!zb_GCV6sKFulgVon7idu?MC=1PbN>r9r!Y4Mjgu^O}secUJ zC&~BZNAer_6VW{trX!{oLoGDITG$BN5Ychygf7H$A0#2gG3_9tv*5xVFy!O8G~2tJ0-;am6teum$RFub*ZUP0l^ zISi+rSj<1Tri+qiPQCxY@utyI{UXv#|L7zWWsCcfk&kC>%dz($hsBXZZv79XSZ>YO z1bYwr0vG%sy$0zuNUwh@y%qu~NUr!z86?;9d^%q4$(18`bZR~JUe%{3L~8YA=MrnN zY;0P688eGWEk#L{@q==y9w-rt5qAE+;lls_FLn7qFYrL%fh)iRP#TNJn(;;p`~Bj3 zW@qgYtbJHnVe^Is literal 0 HcmV?d00001 diff --git a/assets/css/admin-style.css b/assets/css/admin-style.css new file mode 100644 index 0000000..b9c7b91 --- /dev/null +++ b/assets/css/admin-style.css @@ -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 */ +} \ No newline at end of file diff --git a/assets/css/style.css b/assets/css/style.css new file mode 100644 index 0000000..89bca1e --- /dev/null +++ b/assets/css/style.css @@ -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; + } +} \ No newline at end of file diff --git a/assets/js/admin-calendar.js b/assets/js/admin-calendar.js new file mode 100644 index 0000000..a51fa8f --- /dev/null +++ b/assets/js/admin-calendar.js @@ -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 = '
Lade Details...
'; + + // 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 = '
' + data.content + '
'; + + // Actions Area + html += '
'; + + // Delete Button + html += ''; + + // Reschedule Toggle + html += ''; + + html += '
'; + + // Reschedule Form (Hidden) + html += ''; + + 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'; + } + } + }); + } +}); diff --git a/assets/js/admin-kanban.js b/assets/js/admin-kanban.js new file mode 100644 index 0000000..7fd1faf --- /dev/null +++ b/assets/js/admin-kanban.js @@ -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('

Lade Details...

'); + 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 = ` +

${data.title}

+
+ ${data.content} +
+ +
+ + + + +
+ +
+ + `; + 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('

Fehler beim Laden.

'); + } + }, + error: function () { + modalBody.html('

Verbindungsfehler.

'); + } + }); + } + + // 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); + } + } + }); + }); + +}); diff --git a/assets/js/script.js b/assets/js/script.js new file mode 100644 index 0000000..81536b3 --- /dev/null +++ b/assets/js/script.js @@ -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('
Mo
Di
Mi
Do
Fr
Sa
So
'); + + 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('
'); + } + + for (let i = 1; i <= daysInMonth; i++) { + const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(i).padStart(2, '0')}`; + const $day = $(`
${i}
`); + + 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('
Keine Termine verfügbar.
'); + } else { + slots.forEach(slot => { + const $slot = $(`
${slot}
`); + $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 = '
Notiz:
' + response.data.notes + '
'; + } + resultDiv.html(`Status: ${response.data.status}
${response.data.date} um ${response.data.time}${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('Sende...'); + 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); + } + }); + }); + +}); diff --git a/includes/class-on-booking-admin.php b/includes/class-on-booking-admin.php new file mode 100644 index 0000000..9cfb56e --- /dev/null +++ b/includes/class-on-booking-admin.php @@ -0,0 +1,808 @@ +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'; + } + ?> +
+

Buchungs-Einstellungen

+ + + +
+ render_general_settings(); + } else if ($active_tab == 'smtp') { + $this->render_smtp_settings(); + } else if ($active_tab == 'email') { + $this->render_email_template_settings(); + } + ?> + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Geschlossene Tage + '; + + $days = array('Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'); + foreach ($days as $index => $day) { + $checked = in_array($index, $blocked_days) ? 'checked' : ''; + echo '
'; + } + ?> +

Wähle die Tage, an denen die Werkstatt geschlossen ist.

+
Buchungszeiten +
+
+ +
Anzeigeoptionen + +

Wenn aktiviert, wird die Dauer (z.B. "30 min") im Dropdown neben dem Service-Namen + angezeigt.

+
Design Modus + + +

Wähle das Erscheinungsbild des Kalenders im Backend.

+
Buchungsmodus + +

Wenn aktiviert, sieht der Kunde im Frontend nur ein Kontaktformular mit einem + Freitextfeld für Wünsche – ohne Service-, Kalender- oder Uhrzeitauswahl.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Absender Name
Absender E-Mail
Versandmethode + +
+ +

Wähle "PHP Mail", wenn SMTP Probleme macht (kann im Spam landen).

+
SMTP Host
SMTP Port
SMTP Benutzer
SMTP Passwort
Authentifizierung + + +

Falls 'Automatisch' fehlschlägt, versuche PLAIN oder LOGIN.

+
Verschlüsselung + +
+
+
+ +
+ +
+ +

SMTP testen

+
+ + +
+ + + + + Hallo {name},

vielen Dank für deine Buchung bei uns. Hier sind deine Details:

' . + '
' . + '

Service: {service}

' . + '

Datum: {date}

' . + '

Uhrzeit: {time}

' . + '

Auftragsnummer: {tracking_id}

' . + '
' . + '

Du kannst den Status deiner Buchung jederzeit auf unserer Webseite mit deiner Auftragsnummer abfragen.

' . + '

Mit freundlichen Grüßen,
Dein Werkstatt-Team

'; + + $subject = get_option('on_booking_email_subject', $default_subject); + $body = get_option('on_booking_email_body', $default_body); + ?> +
+

Hier kannst du die E-Mail anpassen, die nach einer erfolgreichen Buchung an den Kunden gesendet wird.

+ + + + + + + + + + + + + + +
E-Mail Benachrichtigung + +

Wenn aktiviert, werden Bestätigungs-E-Mails an Kunden gesendet.

+
Betreff
Inhalt + false, + 'textarea_rows' => 15, + 'teeny' => true + )); + ?> +

+ Verfügbare Platzhalter:
+ {name} - Name des Kunden
+ {date} - Datum des Termins
+ {time} - Uhrzeit des Termins
+ {service} - gebuchter Service
+ {tracking_id} - Auftragsnummer für Statusabfrage +

+
+ +

Werkstatt-Benachrichtigung

+

Erhalte bei jeder neuen Buchung eine E-Mail mit allen Details – als Alternative zum Kalender.

+ + + + + + + + + +
Werkstatt-Mail aktivieren + +
Empfänger E-Mail + +

An diese Adresse wird bei jeder neuen Buchung eine Benachrichtigung gesendet.

+
+ +

+ +

+ + +
+ +
+

Buchungskalender

+
+
+ + +
+
+ +
+

Buchungs Details

+
Lade...
+
+
+
+ +
+
+ +
+

Buchungs Details

+
Lade...
+
+
+
+ '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 + ); + } + + ?> +
+

Status Board

+
+ $col): ?> +
+
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+

Services verwalten

+ +
+

Neuen Service hinzufügen

+
+
+ + +
+
+ + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + +
Service NameDauerStatusAktionen
+ + + + min + + + + + + + + + + + +
+
+ + + '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 .= '
Angehängte Dateien:
'; + foreach ($attachments as $att) { + $url = wp_get_attachment_url($att->ID); + $content .= '' . basename($url) . '
'; + } + } + + // Tracking ID + $tracking_id = get_post_meta($post->ID, 'on_booking_tracking_id', true); + if ($tracking_id) { + $content .= '
Auftragsnummer: ' . esc_html($tracking_id) . '
'; + } + + // 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

and
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)); + } + } +} diff --git a/includes/class-on-email-manager.php b/includes/class-on-email-manager.php new file mode 100644 index 0000000..dcf0562 --- /dev/null +++ b/includes/class-on-email-manager.php @@ -0,0 +1,402 @@ +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', '

Hallo {name},

vielen Dank für deine Buchung.

'); + + // 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 = '

' . $heading . '

'; + $body .= '

Auftragsnummer: ' . esc_html($tracking_id) . '

'; + + // Customer Request (simple mode) + if ($customer_request) { + $body .= ''; + $body .= ''; + $body .= ''; + $body .= '
Kundenwunsch
' . nl2br(esc_html($customer_request)) . '
'; + } + + // Customer Info + $body .= ''; + $body .= ''; + $body .= ''; + $body .= ''; + if ($phone) { + $body .= ''; + } + $body .= '
Kundendaten
Name' . esc_html($name) . '
E-Mail' . esc_html($customer_email) . '
Telefon' . esc_html($phone) . '
'; + + // Booking Details + $body .= ''; + $body .= ''; + $body .= ''; + $body .= ''; + if (!$is_simple) { + $body .= ''; + } + $body .= ''; + $body .= '
Buchungsdetails
Service' . esc_html($service) . '
Datum' . esc_html($date_display) . '
Uhrzeit' . esc_html($time_display) . '
Status' . esc_html($status_display) . '
'; + + // Vehicle Info + if ($brand || $year) { + $body .= ''; + $body .= ''; + if ($brand) { + $body .= ''; + } + if ($year) { + $body .= ''; + } + if ($description) { + $body .= ''; + } + $body .= '
Fahrzeug
Marke/Modell' . esc_html($brand) . '
Baujahr' . esc_html($year) . '
Beschreibung' . esc_html($description) . '
'; + } + + $body .= '

Diese E-Mail wurde automatisch vom Buchungssystem gesendet.

'; + + $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 = '

Test erfolgreich!

Dies ist eine Test-Nachricht von deinem Buchungssystem. Wenn du diese Nachricht liest, funktionieren die SMTP-Einstellungen.

'; + + // 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(); + ?> + + + + + + + + + + + + + '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(); diff --git a/on-motorrad-buchung.php b/on-motorrad-buchung.php new file mode 100644 index 0000000..94aa99d --- /dev/null +++ b/on-motorrad-buchung.php @@ -0,0 +1,364 @@ + '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(); + ?> +
+ + +
+ + + + + +
+
+ +

Status Prüfen

+
+ + +
+ +
+
+
+ + +
+
+ +
+

Buchung Erfolgreich!

+

Deine Terminanfrage wurde erfolgreich übermittelt.

+
+

Deine Auftragsnummer:

+

+
+

+ Bitte notiere dir diese Nummer, um den Status deiner Buchung später abfragen zu können. +

+ +
+
+ + + + +
+
+ + +

Senden' : 'Termin Wählen'; ?>

+ +
+ + + + + + +
+ + + +
+ + + + + + + + + +
> + +
+ + + + +
+ + +
+
+ + + +
+ + > +
+
+ + > +
+ + + + + + + +
+ +
+ + +
+
+ + +
+
+ + +
+ + +
+ + + Erlaubt: JPG, + PNG, + PDF (Max. 10MB) +
+ + + +
+
+ +
+
+