| 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 : |
<?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="…">, ä) 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' );