| 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/seaside.pacim.de/web/ |
Upload File : |
<?php
/**
* invgen-api.php – Backend für das InvGen-Tool (On-the-fly-SeaSide-Rechnungen). Nur Admins mit Tool-Recht.
* Rechnungen werden NIE gespeichert (nur temporär gerendert/versendet).
*
* action= customer_search | event_search | products | next_no | quote | preview | send
*/
require_once( __DIR__ . '/includes/config.inc.php' );
@ini_set( 'display_errors', '0' );
error_reporting( 0 );
function ig_json( $a ) { while ( ob_get_level() > 0 ) ob_end_clean(); header( 'Content-Type: application/json; charset=utf-8' ); header( 'Cache-Control: no-store' ); echo json_encode( $a ); exit(); }
function ig_fail( $m ) { ig_json( array( 'ok' => false, 'error' => $m ) ); }
if ( ! function_exists( 'is_admin' ) || ! is_admin() || ! pacim_user_has_tool( 'invgen' ) ) { http_response_code( 403 ); ig_fail( 'Nicht berechtigt.' ); }
$db = new MysqliDb( MYSQL_HOST, MYSQL_USER, MYSQL_PASS, MYSQL_DB );
$action = (string) ( $_POST['action'] ?? ( $_GET['action'] ?? '' ) );
/** Nächste mögliche PAS-Nummer NUR ermitteln (ohne den Pool zu verbrauchen – nichts wird gespeichert). */
function ig_next_no( $db ) {
$f = $db->rawQueryOne( "SELECT free_no FROM pacim_invoice_free_no ORDER BY free_no ASC LIMIT 1" );
if ( $f && (int) $f['free_no'] > 0 && ! $db->rawQueryOne( "SELECT invoice_id FROM pacim_invoice WHERE invoice_no = ? LIMIT 1", array( 'PAS' . (int) $f['free_no'] ) ) )
return 'PAS' . (int) $f['free_no'];
$r = $db->rawQueryOne( "SELECT CAST(REGEXP_REPLACE(invoice_no,'[a-zA-Z]+','') AS UNSIGNED)+1 AS nx FROM pacim_invoice WHERE invoice_no REGEXP '^PAS[0-9]+$' ORDER BY CAST(REGEXP_REPLACE(invoice_no,'[a-zA-Z]+','') AS UNSIGNED) DESC LIMIT 1" );
return 'PAS' . ( ( $r && $r['nx'] ) ? (int) $r['nx'] : 1 );
}
if ( $action === 'next_no' ) ig_json( array( 'ok' => true, 'no' => ig_next_no( $db ) ) );
/* ---------------------------------------------------- Kunden-Live-Suche */
if ( $action === 'customer_search' ) {
$q = trim( (string) ( $_GET['q'] ?? '' ) );
if ( mb_strlen( $q ) < 2 ) ig_json( array( 'ok' => true, 'results' => array() ) );
$like = '%' . $q . '%';
$rows = (array) $db->rawQuery(
"SELECT customer_id, customer_gender, customer_firstname, customer_lastname, customer_name,
customer_street, customer_zipcode, customer_city, customer_country, customer_email
FROM pacim_customer
WHERE customer_lastname LIKE ? OR customer_firstname LIKE ? OR customer_name LIKE ? OR customer_email LIKE ?
ORDER BY customer_lastname ASC, customer_firstname ASC LIMIT 15",
array( $like, $like, $like, $like ) );
$out = array();
foreach ( $rows as $r ) {
$nm = trim( (string) $r['customer_name'] ) !== '' ? $r['customer_name'] : trim( $r['customer_firstname'] . ' ' . $r['customer_lastname'] );
$out[] = array(
'id' => (int) $r['customer_id'], 'label' => $nm, 'email' => (string) $r['customer_email'],
'gender' => (int) $r['customer_gender'], 'firstname' => (string) $r['customer_firstname'], 'lastname' => (string) $r['customer_lastname'],
'company' => (string) $r['customer_name'], 'street' => (string) $r['customer_street'], 'zip' => (string) $r['customer_zipcode'],
'city' => (string) $r['customer_city'], 'country' => (string) ( $r['customer_country'] ?: 'Deutschland' ),
);
}
// Zusätzlich: STORNOS mit anzeigen – aber nur solche, die dem Kunden NOCH NICHT per Storno-Mail
// mitgeteilt wurden UND deren Anreise nicht älter als 6 Wochen ist. Beim Auswählen wird die
// stornierte Buchung im Formular reproduziert (Zeitraum/Stellplatz/Schiff/Zusatzleistungen) → neu berechnen/versenden.
$ctpl = $db->rawQueryOne( "SELECT template_id FROM pacim_templates WHERE template_key = 'cancellation' LIMIT 1" );
$cid = $ctpl ? (int) $ctpl['template_id'] : 5; // Storno-Mail-Template (Fallback 5)
$stRows = (array) $db->rawQuery(
"SELECT b.booking_id, b.booking_no, DATE(b.booking_begin) AS begin, DATE(b.booking_end) AS end,
b.booking_event, b.booking_type, b.booking_invoice,
c.customer_id, c.customer_gender, c.customer_firstname, c.customer_lastname, c.customer_name,
c.customer_street, c.customer_zipcode, c.customer_city, c.customer_country, c.customer_email,
(SELECT GROUP_CONCAT(ip.product_id) FROM pacim_invoice_pos ip
WHERE ip.invoice_id = b.booking_invoice AND ip.product_id > 2) AS extra_ids
FROM pacim_booking b
JOIN pacim_invoice i ON i.invoice_id = b.booking_invoice
LEFT JOIN pacim_customer c ON c.customer_id = b.booking_customer
WHERE i.invoice_status = 3
AND DATE(b.booking_begin) >= ( CURDATE() - INTERVAL 42 DAY )
AND NOT EXISTS ( SELECT 1 FROM pacim_messages m WHERE m.message_template = ? AND m.message_status <> 0
AND ( m.message_booking = b.booking_id OR m.message_invoice = b.booking_invoice ) )
AND ( c.customer_lastname LIKE ? OR c.customer_firstname LIKE ? OR c.customer_name LIKE ?
OR c.customer_email LIKE ? OR b.booking_no LIKE ? )
ORDER BY b.booking_begin DESC LIMIT 15",
array( $cid, $like, $like, $like, $like, $like ) );
$stOut = array();
foreach ( $stRows as $r ) {
$nm = trim( (string) $r['customer_name'] ) !== '' ? $r['customer_name'] : trim( $r['customer_firstname'] . ' ' . $r['customer_lastname'] );
$extras = array();
if ( trim( (string) $r['extra_ids'] ) !== '' )
foreach ( explode( ',', (string) $r['extra_ids'] ) as $x ) { $x = (int) $x; if ( $x > 0 ) $extras[] = $x; }
$stOut[] = array(
'id' => (int) $r['customer_id'], 'label' => $nm, 'email' => (string) $r['customer_email'],
'gender' => (int) $r['customer_gender'], 'firstname' => (string) $r['customer_firstname'], 'lastname' => (string) $r['customer_lastname'],
'company' => (string) $r['customer_name'], 'street' => (string) $r['customer_street'], 'zip' => (string) $r['customer_zipcode'],
'city' => (string) $r['customer_city'], 'country' => (string) ( $r['customer_country'] ?: 'Deutschland' ),
'storno' => true, 'booking_no' => (string) $r['booking_no'], 'begin' => (string) $r['begin'], 'end' => (string) $r['end'],
'ship' => (string) $r['booking_event'], 'type' => (int) $r['booking_type'], 'extras' => $extras,
);
}
// Stornos zuerst (Arbeitsliste), danach normale Kundentreffer.
ig_json( array( 'ok' => true, 'results' => array_merge( $stOut, $out ) ) );
}
/* ---------------------------------------------------- Event-Live-Suche */
if ( $action === 'event_search' ) {
$q = trim( (string) ( $_GET['q'] ?? '' ) );
if ( mb_strlen( $q ) < 2 ) ig_json( array( 'ok' => true, 'results' => array() ) );
$out = array();
$t = $db->rawQueryOne( "SHOW TABLES LIKE 'pacim_events'" );
if ( $t ) {
foreach ( (array) $db->rawQuery( "SELECT DISTINCT event_title FROM pacim_events WHERE event_title LIKE ? ORDER BY event_title ASC LIMIT 12", array( '%' . $q . '%' ) ) as $r )
if ( trim( (string) $r['event_title'] ) !== '' ) $out[] = (string) $r['event_title'];
}
// Fallback: bereits gebuchte Schiffe/Events
if ( ! $out )
foreach ( (array) $db->rawQuery( "SELECT DISTINCT booking_event FROM pacim_booking WHERE booking_event LIKE ? ORDER BY booking_event ASC LIMIT 12", array( '%' . $q . '%' ) ) as $r )
if ( trim( (string) $r['booking_event'] ) !== '' ) $out[] = (string) $r['booking_event'];
ig_json( array( 'ok' => true, 'results' => $out ) );
}
/* ---------------------------------------------------- Produkte (Zusatzleistungen) */
if ( $action === 'products' ) {
$rows = (array) $db->rawQuery( "SELECT product_id, product_title, product_price, product_tax FROM pacim_products WHERE product_price > 0 AND product_id > 2 ORDER BY product_title ASC" );
$out = array();
foreach ( $rows as $r ) $out[] = array( 'id' => (int) $r['product_id'], 'title' => (string) $r['product_title'], 'net' => (float) $r['product_price'], 'gross' => round( (float) $r['product_price'] * 1.19, 2 ) );
ig_json( array( 'ok' => true, 'results' => $out ) );
}
/* ---------------------------------------------------- Preisberechnung (quote) */
function ig_collect_positions( $in ) {
// Parkplatztyp + Zeitraum + Extras -> Positionen (Netto) via bookingservice::quote.
$type = ( (int) ( $in['type'] ?? 1 ) === 2 ) ? 2 : 1;
$begin = substr( (string) ( $in['begin'] ?? '' ), 0, 10 );
$end = substr( (string) ( $in['end'] ?? '' ), 0, 10 );
if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $begin ) ) $begin = date( 'Y-m-d' );
if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $end ) || $end < $begin ) $end = $begin;
$prods = array();
foreach ( (array) ( $in['products'] ?? array() ) as $pid ) { $pid = (int) $pid; if ( $pid > 2 ) $prods[] = $pid; }
$q = bookingservice::quote( array( 'type' => $type, 'begin' => $begin, 'end' => $end, 'products' => $prods, 'brokerId' => 0 ) );
return array( $q, $begin, $end, $type );
}
if ( $action === 'quote' ) {
list( $q ) = ig_collect_positions( $_POST );
ig_json( array( 'ok' => true, 'positions' => $q['positions'], 'net' => $q['net'], 'gross' => $q['gross'], 'duration' => $q['duration'] ) );
}
/* ---------------------------------------------------- Synthetisches (NICHT gespeichertes) Datenobjekt für createInvoice */
function ig_build_dataobj( $db, $in ) {
list( $q, $begin, $end ) = ig_collect_positions( $in );
// Anrede wird NICHT manuell gesetzt: Firma vorhanden -> gender 3 (Firmenname im Empfänger), sonst 0 (Vor-/Nachname).
$company = trim( (string) ( $in['company'] ?? '' ) );
$gender = ( $company !== '' ) ? 3 : 0;
$cust = array(
'customer_gender' => $gender,
'customer_firstname' => trim( (string) ( $in['firstname'] ?? '' ) ),
'customer_lastname' => trim( (string) ( $in['lastname'] ?? '' ) ),
'customer_name' => $company,
'customer_street' => trim( (string) ( $in['street'] ?? '' ) ),
'customer_zipcode' => trim( (string) ( $in['zip'] ?? '' ) ),
'customer_city' => trim( (string) ( $in['city'] ?? '' ) ),
'customer_country' => trim( (string) ( $in['country'] ?? 'Deutschland' ) ) ?: 'Deutschland',
'customer_email' => trim( (string) ( $in['email'] ?? '' ) ),
);
$no = trim( (string) ( $in['invoice_no'] ?? '' ) ); if ( $no === '' ) $no = ig_next_no( $db );
$date = substr( (string) ( $in['invoice_date'] ?? '' ), 0, 10 ); if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ) $date = $begin;
$pay = (int) ( $in['payment'] ?? 0 ); if ( ! in_array( $pay, array( 0, 1, 2, 3 ), true ) ) $pay = 0;
$coupon = (int) ( $in['coupon'] ?? 0 ); if ( ! in_array( $coupon, array( 0, 1, 2 ), true ) ) $coupon = 0;
$payLabel = array( 1 => 'Überweisung', 2 => 'EC-Karte', 3 => 'Bar' )[ $pay ] ?? '';
$inv = array(
'invoice_no' => $no, 'invoice_date' => $date, 'invoice_payment' => $pay, 'invoice_coupon' => $coupon,
'payment' => $payLabel, 'invoice_paydate' => $date, 'invgen' => 1, // InvGen-Variante: Titel „Rechnung <Nr.>", kein Bezahlt-Block
'booking_begin' => $begin, 'booking_end' => $end, 'booking_event' => trim( (string) ( $in['event'] ?? '' ) ),
);
$products = array();
foreach ( $q['positions'] as $p ) $products[] = array(
'product_id' => (int) $p['product_id'], 'invoice_pos_title' => (string) $p['title'],
'invoice_pos_description' => '', 'invoice_pos_price' => (float) $p['net'],
'invoice_pos_quantity' => (int) $p['qty'], 'invoice_pos_discount_type' => 0,
);
return (object) array( 'customer' => (object) $cust, 'invoice' => (object) $inv, 'products' => $products );
}
/* ---------------------------------------------------- Vorschau (PDF inline, nichts gespeichert) */
if ( $action === 'preview' ) {
$obj = ig_build_dataobj( $db, $_POST );
while ( ob_get_level() > 0 ) ob_end_clean();
$oPDF = new pdf();
$oPDF->createInvoice( $obj, FALSE, FALSE, FALSE, 'I' ); // create=FALSE -> nur Ausgabe, kein Speichern
exit();
}
/* ---------------------------------------------------- Versenden (PDF als Anhang; NICHTS gespeichert) */
if ( $action === 'send' ) {
if ( $_SERVER['REQUEST_METHOD'] !== 'POST' ) ig_fail( 'Methode nicht erlaubt.' );
$to = trim( (string) ( $_POST['to'] ?? '' ) );
if ( ! filter_var( $to, FILTER_VALIDATE_EMAIL ) ) ig_fail( 'Bitte eine gültige Empfängeradresse angeben.' );
$obj = ig_build_dataobj( $db, $_POST );
$no = (string) $obj->invoice->invoice_no;
// PDF temporär erzeugen (Output 'S' liefert keinen Rückgabewert in createInvoice -> via Output-Buffer abgreifen).
ob_start(); $oPDF = new pdf(); $oPDF->createInvoice( $obj, FALSE, FALSE, FALSE, 'I' ); $pdf = ob_get_clean();
if ( substr( (string) $pdf, 0, 4 ) !== '%PDF' ) ig_fail( 'PDF konnte nicht erzeugt werden.' );
$tmp = rtrim( PATH_DATAS, '/' ) . '/incoming/invgen_' . bin2hex( random_bytes( 8 ) ) . '.pdf';
@file_put_contents( $tmp, $pdf );
// E-Mail-Text aus der Vorlage „Rechnung [invoice_no]" (template_key inv_resend) rendern –
// mit der eingegebenen Rechnungsnummer und dem eingegebenen Namen.
$first = trim( (string) ( $_POST['firstname'] ?? '' ) );
$last = trim( (string) ( $_POST['lastname'] ?? '' ) );
$comp = trim( (string) ( $_POST['company'] ?? '' ) );
$ctx = array(
'invoice_no' => $no,
'customer_firstname' => $first,
'customer_lastname' => ( $last !== '' ? $last : ( $first === '' ? $comp : $last ) ),
'name' => ( $comp !== '' ? $comp : trim( $first . ' ' . $last ) ),
);
$plain = '';
$tplRow = $db->rawQueryOne( "SELECT * FROM pacim_templates WHERE template_key = 'inv_resend' AND template_active = 1" );
if ( $tplRow && class_exists( 'mailrenderer' ) ) {
$r = mailrenderer::render( (object) $tplRow, $ctx );
$subject = $r['subject']; $html = $r['html']; $plain = isset( $r['plain'] ) ? $r['plain'] : '';
} else {
$subject = 'Rechnung ' . $no;
$html = '<p>Guten Tag,</p><p>anbei erhalten Sie Ihre Rechnung ' . htmlspecialchars( $no, ENT_QUOTES, 'UTF-8' ) . ' als PDF.</p><p>Mit freundlichen Grüßen<br>Ihr SeaSide-Team · Parken-am-Schiff.de</p>';
}
$files = array( array( 'path' => $tmp, 'name' => $no . '.pdf' ) );
$res = mailservice::send( $to, '', $subject, $html, $files, array( 'plain' => $plain ) );
@unlink( $tmp );
if ( empty( $res['ok'] ) ) ig_fail( ( $res['error'] ?? 'Versand fehlgeschlagen.' ) );
ig_json( array( 'ok' => true, 'sent_to' => $to, 'no' => $no ) );
}
ig_fail( 'Unbekannte Aktion.' );