Files
on-motorrad-buchungs-plugin/includes/class-on-booking-ajax.php

724 lines
26 KiB
PHP

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