[ 'name' => 'Reservierungen', 'singular_name' => 'Reservierung', 'menu_name' => 'Reservierungen', 'add_new_item' => 'Neue Reservierung', 'edit_item' => 'Reservierung bearbeiten', 'view_item' => 'Reservierung ansehen', 'search_items' => 'Reservierungen suchen', ], 'public' => false, 'show_ui' => true, 'show_in_menu' => true, 'menu_icon' => 'dashicons-calendar-alt', 'supports' => ['title'], 'map_meta_cap' => true, ]); } public function register_shortcodes() { add_shortcode('nazar_reservation_form', [$this, 'reservation_form_shortcode']); } public function enqueue_assets() { $plugin_url = plugin_dir_url(__FILE__); $plugin_path = plugin_dir_path(__FILE__); wp_enqueue_style( 'nazar-kebap-mvp', $plugin_url . 'assets/site.css', [], filemtime($plugin_path . 'assets/site.css') ); wp_enqueue_script( 'nazar-kebap-reservations', $plugin_url . 'assets/reservation.js', [], filemtime($plugin_path . 'assets/reservation.js'), true ); wp_localize_script('nazar-kebap-reservations', 'nazarReservationData', [ 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce(self::AVAILABILITY_NONCE), 'messages' => [ 'chooseDateAndParty' => 'Bitte wählen Sie zuerst Datum und Personenzahl.', 'loading' => 'Verfügbare Uhrzeiten werden geladen …', 'empty' => 'Für diese Auswahl ist aktuell keine freie Uhrzeit verfügbar.', 'error' => 'Die verfügbaren Uhrzeiten konnten gerade nicht geladen werden. Bitte versuchen Sie es erneut.', ], ]); } public function body_classes($classes) { $classes[] = 'nazar-site'; return $classes; } public function reservation_form_shortcode() { $status = isset($_GET['reservation']) ? sanitize_key(wp_unslash($_GET['reservation'])) : ''; $messages = [ 'success' => 'Danke! Ihre Reservierungsanfrage wurde erfolgreich übermittelt. Sie erhalten in Kürze eine Bestätigung per E-Mail mit allen Buchungsdetails.', 'error' => 'Bitte prüfen Sie Ihre Eingaben. Alle Pflichtfelder müssen ausgefüllt sein.', 'conflict' => 'Diese Uhrzeit ist leider nicht mehr verfügbar. Bitte wählen Sie eine andere freie Uhrzeit.', ]; ob_start(); ?>

Bitte wählen Sie zuerst Datum und Personenzahl.

Reservierungen werden direkt gegen bestehende Buchungen geprüft. Gesperrte Zeiten richten sich nach der Personenzahl: 1 Person = 10 Min., 2 Personen = 15 Min., 3–4 Personen = 30 Min., ab 5 Personen = 60 Min.

sanitize_text_field(wp_unslash($_POST['guest_name'] ?? '')), 'guest_phone' => sanitize_text_field(wp_unslash($_POST['guest_phone'] ?? '')), 'guest_email' => sanitize_email(wp_unslash($_POST['guest_email'] ?? '')), 'party_size' => sanitize_text_field(wp_unslash($_POST['party_size'] ?? '')), 'reservation_date' => sanitize_text_field(wp_unslash($_POST['reservation_date'] ?? '')), 'reservation_time' => sanitize_text_field(wp_unslash($_POST['reservation_time'] ?? '')), 'reservation_notes' => sanitize_textarea_field(wp_unslash($_POST['reservation_notes'] ?? '')), ]; if (!$fields['guest_name'] || !$fields['guest_phone'] || !$fields['guest_email'] || !$fields['party_size'] || !$fields['reservation_date'] || !$fields['reservation_time']) { wp_safe_redirect(add_query_arg('reservation', 'error', $redirect_url)); exit; } $fields['reservation_time'] = $this->normalize_time($fields['reservation_time']); if (!$fields['reservation_time'] || !$this->is_time_available($fields['reservation_date'], $fields['reservation_time'], $fields['party_size'])) { wp_safe_redirect(add_query_arg('reservation', 'conflict', $redirect_url)); exit; } $title = sprintf( 'Reservierung – %s – %s %s', $fields['guest_name'], $fields['reservation_date'], $fields['reservation_time'] ); $post_id = wp_insert_post([ 'post_type' => self::POST_TYPE, 'post_status' => 'publish', 'post_title' => $title, 'meta_input' => [ '_guest_name' => $fields['guest_name'], '_guest_phone' => $fields['guest_phone'], '_guest_email' => $fields['guest_email'], '_party_size' => $fields['party_size'], '_reservation_date' => $fields['reservation_date'], '_reservation_time' => $fields['reservation_time'], '_reservation_notes' => $fields['reservation_notes'], '_reservation_duration' => $this->get_duration_minutes($fields['party_size']), self::STATUS_META => 'neu', ], ]); if (is_wp_error($post_id) || !$post_id) { wp_safe_redirect(add_query_arg('reservation', 'error', $redirect_url)); exit; } $notification_results = $this->send_new_reservation_notifications($post_id, $fields); update_post_meta($post_id, '_admin_notification_sent', $notification_results['admin'] ? 'yes' : 'no'); update_post_meta($post_id, '_customer_notification_sent', $notification_results['customer'] ? 'yes' : 'no'); update_post_meta($post_id, '_notification_sent_at', current_time('mysql')); wp_safe_redirect(add_query_arg('reservation', 'success', home_url('/reservierung/'))); exit; } public function ajax_get_available_times() { check_ajax_referer(self::AVAILABILITY_NONCE, 'nonce'); $date = sanitize_text_field(wp_unslash($_POST['reservation_date'] ?? '')); $party_size = sanitize_text_field(wp_unslash($_POST['party_size'] ?? '')); if (!$date || !$party_size) { wp_send_json_error([ 'message' => 'Datum und Personenzahl sind erforderlich.', ], 400); } $times = $this->get_available_time_slots($date, $party_size); $duration = $this->get_duration_minutes($party_size); wp_send_json_success([ 'times' => $times, 'duration' => $duration, 'message' => sprintf( 'Für %s blockiert eine Reservierung %d Minuten. Es werden nur freie Uhrzeiten angezeigt.', $party_size, $duration ), ]); } public function admin_columns($columns) { return [ 'cb' => $columns['cb'], 'title' => 'Anfrage', 'reservation_datetime' => 'Datum & Uhrzeit', 'party_size' => 'Personen', 'contact' => 'Kontakt', 'reservation_status' => 'Status', 'date' => 'Eingegangen', ]; } public function render_admin_columns($column, $post_id) { switch ($column) { case 'reservation_datetime': echo esc_html(get_post_meta($post_id, '_reservation_date', true)); echo '
'; echo esc_html(get_post_meta($post_id, '_reservation_time', true)); break; case 'party_size': echo esc_html(get_post_meta($post_id, '_party_size', true)); break; case 'contact': echo esc_html(get_post_meta($post_id, '_guest_name', true)); echo '
'; echo esc_html(get_post_meta($post_id, '_guest_phone', true)); $email = get_post_meta($post_id, '_guest_email', true); if ($email) { echo '
' . esc_html($email); } break; case 'reservation_status': echo esc_html(ucfirst(get_post_meta($post_id, self::STATUS_META, true) ?: 'neu')); break; } } public function add_meta_boxes() { add_meta_box( 'nazar_reservation_details', 'Reservierungsdetails', [$this, 'render_meta_box'], self::POST_TYPE, 'normal', 'high' ); } public function render_meta_box($post) { wp_nonce_field('nazar_save_reservation', 'nazar_save_reservation_nonce'); $status = get_post_meta($post->ID, self::STATUS_META, true) ?: 'neu'; ?>
NameID, '_guest_name', true)); ?>
TelefonID, '_guest_phone', true)); ?>
E-MailID, '_guest_email', true)); ?>
DatumID, '_reservation_date', true)); ?>
UhrzeitID, '_reservation_time', true)); ?>
PersonenID, '_party_size', true)); ?>
Blockiert bisget_blocked_until_label($post->ID)); ?>
Admin-E-Mailhumanize_notification_status(get_post_meta($post->ID, '_admin_notification_sent', true))); ?>
Kunden-E-Mailhumanize_notification_status(get_post_meta($post->ID, '_customer_notification_sent', true))); ?>
Status-E-Mailhumanize_notification_status(get_post_meta($post->ID, '_status_notification_sent', true))); ?>
HinweiseID, '_reservation_notes', true))); ?>
send_status_update_notification($post_id, $status, $previous_status); update_post_meta($post_id, '_status_notification_sent', $status_sent ? 'yes' : 'no'); update_post_meta($post_id, '_status_notification_sent_at', current_time('mysql')); } } public function get_available_time_slots($date, $party_size) { if (!$this->is_valid_reservation_date($date) || !$this->parse_party_size($party_size)) { return []; } $slots = []; $start = $this->date_time_to_timestamp($date, self::OPENING_TIME); $end = $this->date_time_to_timestamp($date, self::LAST_BOOKING_TIME); if (!$start || !$end || $start > $end) { return []; } for ($cursor = $start; $cursor <= $end; $cursor += self::TIME_STEP_MINUTES * MINUTE_IN_SECONDS) { $time = wp_date('H:i', $cursor); if ($this->is_time_available($date, $time, $party_size)) { $slots[] = [ 'value' => $time, 'label' => $time, ]; } } return $slots; } public function is_time_available($date, $time, $party_size, $exclude_post_id = 0) { if (!$this->is_valid_reservation_date($date)) { return false; } $time = $this->normalize_time($time); $party_count = $this->parse_party_size($party_size); if (!$time || !$party_count) { return false; } $requested_start = $this->date_time_to_timestamp($date, $time); $requested_end = $requested_start ? $requested_start + ($this->get_duration_minutes($party_size) * MINUTE_IN_SECONDS) : false; if (!$requested_start || !$requested_end) { return false; } foreach ($this->get_reservations_for_date($date, $exclude_post_id) as $reservation) { if (($reservation['status'] ?? '') === 'abgelehnt') { continue; } if (!$reservation['start'] || !$reservation['end']) { continue; } if ($requested_start < $reservation['end'] && $requested_end > $reservation['start']) { return false; } } return true; } private function send_new_reservation_notifications($post_id, $fields) { $admin_email = sanitize_email(get_option('admin_email')); $admin_sent = false; $customer_sent = false; $details = $this->get_reservation_email_details($post_id, $fields); if ($admin_email) { $subject = sprintf('Neue Reservierung: %s am %s um %s', $details['name'], $details['date_label'], $details['time_label']); $message = implode(" ", [ 'Eine neue Reservierungsanfrage wurde über die Website gesendet.', '', 'Name: ' . $details['name'], 'Telefon: ' . $details['phone'], 'E-Mail: ' . $details['email'], 'Personen: ' . $details['party_size'], 'Datum: ' . $details['date_label'], 'Uhrzeit: ' . $details['time_label'], 'Blockiert bis: ' . $details['blocked_until_label'], 'Hinweise: ' . ($details['notes'] ?: 'Keine'), '', 'Im Admin bearbeiten: ' . admin_url('post.php?post=' . $post_id . '&action=edit'), ]); $admin_sent = wp_mail($admin_email, $subject, $message, $this->get_email_headers()); } if ($details['email']) { $subject = sprintf('Ihre Reservierungsanfrage bei %s', get_bloginfo('name')); $message = implode(" ", [ 'Vielen Dank für Ihre Reservierungsanfrage bei ' . get_bloginfo('name') . '.', '', 'Ihre Buchungsdaten:', 'Name: ' . $details['name'], 'Telefon: ' . $details['phone'], 'E-Mail: ' . $details['email'], 'Personen: ' . $details['party_size'], 'Datum: ' . $details['date_label'], 'Uhrzeit: ' . $details['time_label'], 'Reservierungsdauer: ' . $details['duration_label'], 'Hinweise: ' . ($details['notes'] ?: 'Keine'), '', 'Falls wir Rückfragen haben, melden wir uns unter der angegebenen Telefonnummer oder E-Mail-Adresse.', 'Telefon Restaurant: +49 781 96643005', 'Adresse: Saarlandstraße 2, 77652 Offenburg', ]); $customer_sent = wp_mail($details['email'], $subject, $message, $this->get_email_headers()); } return [ 'admin' => (bool) $admin_sent, 'customer' => (bool) $customer_sent, ]; } private function send_status_update_notification($post_id, $new_status, $old_status) { $email = sanitize_email(get_post_meta($post_id, '_guest_email', true)); if (!$email) { return false; } $details = $this->get_reservation_email_details($post_id); $status_labels = [ 'neu' => 'neu', 'bestätigt' => 'bestätigt', 'abgelehnt' => 'abgelehnt', 'erledigt' => 'erledigt', ]; $new_label = $status_labels[$new_status] ?? $new_status; $old_label = $status_labels[$old_status] ?? $old_status; $subject = sprintf('Update zu Ihrer Reservierung bei %s', get_bloginfo('name')); $message = implode(" ", [ 'Es gibt ein Update zu Ihrer Reservierung bei ' . get_bloginfo('name') . '.', '', 'Bisheriger Status: ' . $old_label, 'Neuer Status: ' . $new_label, '', 'Name: ' . $details['name'], 'Personen: ' . $details['party_size'], 'Datum: ' . $details['date_label'], 'Uhrzeit: ' . $details['time_label'], '', 'Bei Fragen erreichen Sie uns unter +49 781 96643005.', ]); return (bool) wp_mail($email, $subject, $message, $this->get_email_headers()); } private function get_reservation_email_details($post_id = 0, $fields = []) { $name = $fields['guest_name'] ?? get_post_meta($post_id, '_guest_name', true); $phone = $fields['guest_phone'] ?? get_post_meta($post_id, '_guest_phone', true); $email = $fields['guest_email'] ?? get_post_meta($post_id, '_guest_email', true); $party_size = $fields['party_size'] ?? get_post_meta($post_id, '_party_size', true); $date = $fields['reservation_date'] ?? get_post_meta($post_id, '_reservation_date', true); $time = $fields['reservation_time'] ?? get_post_meta($post_id, '_reservation_time', true); $notes = $fields['reservation_notes'] ?? get_post_meta($post_id, '_reservation_notes', true); $date_label = $date ? wp_date('d.m.Y', strtotime($date)) : '–'; $time_label = $this->normalize_time($time) ?: '–'; $duration = $this->get_duration_minutes($party_size); $blocked_until_label = '–'; if ($date && $time_label) { $start = $this->date_time_to_timestamp($date, $time_label); if ($start) { $blocked_until_label = wp_date('d.m.Y H:i', $start + ($duration * MINUTE_IN_SECONDS)); } } return [ 'name' => $name ?: '–', 'phone' => $phone ?: '–', 'email' => $email ?: '', 'party_size' => $party_size ?: '–', 'date_label' => $date_label, 'time_label' => $time_label, 'duration_label' => $duration ? ($duration . ' Minuten') : '–', 'blocked_until_label' => $blocked_until_label, 'notes' => $notes ?: '', ]; } private function get_email_headers() { $blogname = wp_specialchars_decode(get_bloginfo('name'), ENT_QUOTES); $admin_email = sanitize_email(get_option('admin_email')); return [ 'Content-Type: text/plain; charset=UTF-8', sprintf('Reply-To: %s <%s>', $blogname, $admin_email), ]; } private function humanize_notification_status($status) { return $status === 'yes' ? 'Gesendet' : ($status === 'no' ? 'Fehlgeschlagen' : '–'); } private function get_reservations_for_date($date, $exclude_post_id = 0) { $query = new WP_Query([ 'post_type' => self::POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => -1, 'fields' => 'ids', 'meta_key' => '_reservation_date', 'meta_value' => $date, 'orderby' => 'meta_value', 'order' => 'ASC', 'post__not_in' => $exclude_post_id ? [(int) $exclude_post_id] : [], ]); $reservations = []; foreach ($query->posts as $post_id) { $time = $this->normalize_time(get_post_meta($post_id, '_reservation_time', true)); $party_size = get_post_meta($post_id, '_party_size', true); $start = $time ? $this->date_time_to_timestamp($date, $time) : false; $duration = $this->get_duration_minutes($party_size); $reservations[] = [ 'post_id' => $post_id, 'status' => get_post_meta($post_id, self::STATUS_META, true) ?: 'neu', 'time' => $time, 'party_size' => $party_size, 'duration' => $duration, 'start' => $start, 'end' => $start ? $start + ($duration * MINUTE_IN_SECONDS) : false, ]; } wp_reset_postdata(); return $reservations; } private function get_duration_minutes($party_size) { $count = $this->parse_party_size($party_size); if ($count <= 0) { return 0; } if ($count === 1) { return 10; } if ($count === 2) { return 15; } if ($count <= 4) { return 30; } return 60; } private function parse_party_size($party_size) { if (is_numeric($party_size)) { return max(0, (int) $party_size); } if (is_string($party_size) && preg_match('/^(\d+)\+?$/', $party_size, $matches)) { return max(0, (int) $matches[1]); } return 0; } private function normalize_time($time) { if (!is_string($time) || $time === '') { return ''; } $date_time = date_create_from_format('H:i', $time, wp_timezone()) ?: date_create_from_format('H:i:s', $time, wp_timezone()); if (!$date_time) { return ''; } return $date_time->format('H:i'); } private function is_valid_reservation_date($date) { if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', (string) $date)) { return false; } $candidate = date_create_immutable_from_format('Y-m-d', $date, wp_timezone()); return $candidate && $candidate->format('Y-m-d') === $date; } private function date_time_to_timestamp($date, $time) { $date_time = date_create_immutable_from_format('Y-m-d H:i', $date . ' ' . $time, wp_timezone()); return $date_time ? $date_time->getTimestamp() : false; } private function get_blocked_until_label($post_id) { $date = get_post_meta($post_id, '_reservation_date', true); $time = $this->normalize_time(get_post_meta($post_id, '_reservation_time', true)); $party_size = get_post_meta($post_id, '_party_size', true); $start = ($date && $time) ? $this->date_time_to_timestamp($date, $time) : false; if (!$start) { return '–'; } $end = $start + ($this->get_duration_minutes($party_size) * MINUTE_IN_SECONDS); return wp_date('Y-m-d H:i', $end); } } $GLOBALS['nazar_kebap_mvp'] = new Nazar_Kebap_MVP();