[
'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();
?>
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';
?>
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();