| 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
/**
* checkin.php – Mobile Check-in & Zahlung erfassen zu einer Buchung (Phase 2, funktional).
*
* Aufruf: /checkin.php?b=<booking_id>&h=<hash> (Hash = pacim_sign('checkin:'.id), ohne Login).
* AJAX-Aktionen (POST, gleicher Hash):
* action=pay type(1|2|3), coupon(0|1|2) -> Zahlung erfassen/ändern (Betrag serverseitig)
* action=checkin -> Check-in (nur wenn bezahlt)
* Rabatte: coupon 1 = 5 € Bestandskundenrabatt, 2 = 10 % Frühbucherrabatt (auf Parkplatz, prod_id<=2).
*/
require_once( __DIR__ . '/includes/config.inc.php' );
require_once( __DIR__ . '/includes/day-arrivals.php' );
require_once( __DIR__ . '/includes/mobileaccess.php' );
require_once( __DIR__ . '/includes/booking-notes.php' );
require_once( __DIR__ . '/includes/plate-info.php' );
@ini_set( 'display_errors', '0' );
error_reporting( 0 );
while ( ob_get_level() > 0 ) ob_end_clean();
$bid = isset( $_GET['b'] ) ? (int) $_GET['b'] : (int) ( $_POST['b'] ?? 0 );
$h = trim( (string) ( $_GET['h'] ?? ( $_POST['h'] ?? '' ) ) );
$tok = trim( (string) ( $_GET['t'] ?? ( $_POST['t'] ?? '' ) ) ); // Mobile-Zugang-Token (alternativ zu Hash)
$embed = ! empty( $_GET['embed'] ); // im Overlay/iframe der Tagesliste eingebettet
$action = (string) ( $_POST['action'] ?? '' );
function ci_json( $a ) { while ( ob_get_level() > 0 ) ob_end_clean(); header( 'Content-Type: application/json; charset=utf-8' ); echo json_encode( $a ); exit(); }
function ci_fail( $m ) { ci_json( array( 'ok' => false, 'error' => $m ) ); }
$db = new MysqliDb( MYSQL_HOST, MYSQL_USER, MYSQL_PASS, MYSQL_DB );
// Zugriff: entweder Mobile-Zugang-Token (mit Rechten/Geltungsbereich) ODER Legacy-Hash (volle Rechte).
$access = null; $canAct = true;
if ( $tok !== '' ) {
$access = pacim_ma_load( $db, $tok );
$authOk = ( $access && $bid > 0 && pacim_ma_in_scope( $db, $access, $bid ) );
$canAct = ( $access && (int) $access['ma_perm_action'] === 1 );
// PIN-geschützter Zugang: Freischalt-Cookie der Tagesliste ODER gültige Terminal-Sitzung erforderlich.
if ( $access && (string) $access['ma_pin'] !== '' ) {
$ck = 'mau_' . $tok;
$okPin = ( isset( $_COOKIE[ $ck ] ) && hash_equals( pacim_ma_unlock_token( $tok ), (string) $_COOKIE[ $ck ] ) )
|| ( function_exists( 'pacim_terminal_grants' ) && pacim_terminal_grants( $db, $tok ) );
if ( ! $okPin ) $authOk = false;
}
} else {
$authOk = ( $bid > 0 && function_exists( 'pacim_sign_verify' ) && pacim_sign_verify( 'checkin:' . $bid, $h ) );
}
$readOnly = ( $tok !== '' && ! $canAct );
// Modus des Zugangs: Abreisen (Check-out) statt Anreisen (Check-in). Legacy-Hash = immer Anreisen.
$isDep = ( $access && function_exists( 'pacim_ma_mode' ) && pacim_ma_mode( $access ) === 'abreise' );
if ( ! $authOk ) {
if ( $action !== '' ) ci_fail( 'Ungültiger oder abgelaufener Link.' );
ci_error_page();
}
/* ----------------------------------------- Daten laden */
function ci_load( $db, $bid ) {
$r = $db->rawQueryOne(
"SELECT b.booking_id, b.booking_no, b.booking_event, b.booking_type, b.booking_checkin, b.booking_checkout,
b.booking_begin, b.booking_end, b.booking_valet, license_plate(b.booking_serial) AS plate,
b.booking_hint, b.booking_hint_reply,
DATE_FORMAT(b.booking_hint_reply_at, '%d.%m.%Y · %H:%i') AS hint_reply_at, b.booking_hint_reply_by,
DATE_FORMAT(b.booking_cruise_checkin, '%H:%i') AS boarding,
DATE_FORMAT(b.booking_checkin, '%H:%i') AS checkin_time,
DATE_FORMAT(b.booking_checkout, '%H:%i') AS checkout_time,
DATEDIFF(b.booking_end, b.booking_begin) AS trip_days,
d.invoice_id, d.invoice_no, d.invoice_payment, d.invoice_status, d.invoice_coupon,
c.customer_firstname, c.customer_lastname, c.customer_name, c.customer_source, c.customer_gender,
c.customer_street, c.customer_zipcode, c.customer_city, c.customer_country,
c.customer_phone, c.customer_email
FROM pacim_booking b
JOIN pacim_invoice d ON d.invoice_id = b.booking_invoice
LEFT JOIN pacim_customer c ON c.customer_id = b.booking_customer
WHERE b.booking_id = ?", array( $bid ) );
return $r ?: null;
}
function ci_positions( $db, $iid ) {
return (array) $db->rawQuery( "SELECT product_id, invoice_pos_price, invoice_pos_quantity, invoice_pos_discount_type FROM pacim_invoice_pos WHERE invoice_id = ?", array( $iid ) );
}
/** Brutto-Betrag für einen Coupon (0/1/2) – Logik wie invoice.class::setPayment (Parkplatz = prod_id<=2). */
function ci_amount( $pos, $coupon ) {
$sum = 0.0;
foreach ( $pos as $p ) {
$net = (float) $p['invoice_pos_price']; $qty = (float) $p['invoice_pos_quantity']; $pid = (int) $p['product_id'];
if ( $pid <= 2 && $coupon === 2 ) $line = $net * 0.9 * $qty * 1.19;
elseif ( $pid <= 2 && $coupon === 1 ) $line = ( $net * $qty * 1.19 ) - 5;
else $line = $net * $qty * 1.19;
$sum += $line;
}
return round( max( 0, $sum ), 2 );
}
function ci_paid_value( $db, $iid ) {
$r = $db->rawQueryOne( "SELECT COALESCE(SUM(payment_value),0) v FROM pacim_payments WHERE payment_invoice = ?", array( $iid ) );
return $r ? (float) $r['v'] : 0.0;
}
function ci_eur( $v ) { return number_format( (float) $v, 2, ',', '.' ) . ' €'; }
function ci_payname( $t ) { return array( 1 => 'Überweisung', 2 => 'EC-Karte', 3 => 'Bar' )[ (int) $t ] ?? '—'; }
/** Rechnungs-Posten (Titel + Brutto) für die POS-Ansicht. removable = Extra (kein Parkplatz 1/2). */
function ci_pos_items( $db, $iid ) {
$out = array();
foreach ( (array) $db->rawQuery(
"SELECT ip.invoice_pos_id AS id, ip.product_id, ip.invoice_pos_price AS net, ip.invoice_pos_quantity AS qty,
COALESCE(NULLIF(TRIM(ip.invoice_pos_title),''), p.product_title, CONCAT('Position ', ip.product_id)) AS title
FROM pacim_invoice_pos ip LEFT JOIN pacim_products p ON p.product_id = ip.product_id
WHERE ip.invoice_id = ? ORDER BY ip.invoice_pos_id", array( $iid ) ) as $pp ) {
$out[] = array(
'id' => (int) $pp['id'],
'title' => (string) $pp['title'],
'qty' => (float) $pp['qty'],
'gross' => round( (float) $pp['net'] * (float) $pp['qty'] * 1.19, 2 ),
'removable' => ( (int) $pp['product_id'] > 2 ), // Parkplatz (1/2) bleibt
);
}
return $out;
}
/** Aktive Zusatzprodukte (sichtbar, außer Parkplatz 1/2) – zum Hinzufügen. Preis brutto (Jahrespreis falls vorhanden). */
function ci_add_products( $db, $year ) {
$out = array();
foreach ( (array) $db->rawQuery(
"SELECT p.product_id AS id, p.product_title AS title,
COALESCE((SELECT pp.price FROM pacim_product_prices pp WHERE pp.product_id = p.product_id AND pp.year = ? LIMIT 1), p.product_price) AS net
FROM pacim_products p WHERE p.product_visible = 1 AND p.product_id NOT IN (1,2) ORDER BY p.product_title",
array( (int) $year ) ) as $p ) {
$out[] = array( 'id' => (int) $p['id'], 'title' => (string) $p['title'], 'gross' => round( (float) $p['net'] * 1.19, 2 ) );
}
return $out;
}
$r = ci_load( $db, $bid );
if ( ! $r ) { if ( $action !== '' ) ci_fail( 'Buchung nicht gefunden.' ); ci_error_page( 404, 'Buchung nicht gefunden.' ); }
$iid = (int) $r['invoice_id'];
$storno = ( (int) $r['invoice_status'] === 3 );
$isAida = ( strcasecmp( trim( (string) $r['customer_source'] ), 'AIDA' ) === 0 ); // AIDA = über myAIDA bezahlt
$pos = ci_positions( $db, $iid );
// Einzelposten (POS-Ansicht) + hinzufügbare Produkte (aktiv, außer Parkplatz 1/2).
$bkYear = (int) date( 'Y', strtotime( (string) $r['booking_begin'] ) ) ?: (int) date( 'Y' );
$posItems = ci_pos_items( $db, $iid );
$addProducts = ( $canAct && ! $isAida && ! $storno ) ? ci_add_products( $db, $bkYear ) : array();
/* ---- Notiz-Kontext (Autor/Zugang/Schreibrecht). Lesen immer erlaubt; Schreiben nur mit Recht. */
$bnAccessId = ( $tok !== '' && $access ) ? (int) $access['ma_id'] : null;
$bnUser = ( $tok !== '' && $access )
? ( trim( (string) $access['ma_label'] ) !== '' ? $access['ma_label'] : 'Mobil-Zugang' )
: ( trim( (string) ( $_SESSION['user_name'] ?? '' ) ) !== '' ? $_SESSION['user_name'] : 'Check-in' );
$bnCanWrite = ( $tok !== '' ) ? ( $access ? pacim_ma_can_notes( $access ) : false ) : true;
$bnCanAll = ( $tok === '' ); // Legacy-Hash/Admin = volle Rechte auf alle Notizen
function ci_note_can_edit( $note, $canWrite, $canAll, $accessId ) {
if ( ! $canWrite ) return false;
if ( $canAll ) return true;
return ( $accessId !== null && (int) $note['note_access'] === (int) $accessId );
}
// Kurzer, einzeiliger Auszug einer Notiz für den Aktivitäts-/Sitzungsverlauf (optional Hervorheben-Marker).
function ci_note_snip( $text, $highlight = 0 ) {
$t = trim( preg_replace( '/\s+/', ' ', (string) $text ) );
if ( mb_strlen( $t ) > 120 ) $t = mb_substr( $t, 0, 120 ) . '…';
return ( $highlight ? '★ ' : '' ) . $t;
}
/* ============================================ AJAX: Interne Notizen (Lesen vor Read-Only-Guard) */
if ( $action === 'notes' ) {
$out = array();
foreach ( pacim_bn_list( $db, $bid, true ) as $n ) { // nur in der Web-App sichtbare Notizen
$canE = ci_note_can_edit( $n, $bnCanWrite, $bnCanAll, $bnAccessId );
$out[] = array(
'id' => (int) $n['note_id'], 'text' => (string) $n['note_text'], 'user' => (string) $n['note_user'],
'highlight' => ! empty( $n['note_highlight'] ),
'created' => pacim_bn_fmt_dt( $n['note_created'] ),
'updated' => $n['note_updated'] ? pacim_bn_fmt_dt( $n['note_updated'] ) : '',
'updatedBy' => (string) $n['note_updated_by'], 'canEdit' => $canE, 'canDel' => $canE,
);
}
ci_json( array( 'ok' => true, 'notes' => $out, 'canCreate' => $bnCanWrite, 'marquee' => pacim_bn_highlighted( $db, $bid ) ) );
}
if ( $action === 'note_add' ) {
if ( ! $bnCanWrite ) ci_fail( 'Keine Berechtigung für interne Notizen.' );
if ( $storno ) ci_fail( 'Buchung ist storniert.' );
$hl = ! empty( $_POST['highlight'] ) ? 1 : 0;
$noteTxt = (string) ( $_POST['text'] ?? '' );
$id = pacim_bn_add( $db, $bid, $noteTxt, $bnUser, $bnAccessId, 1, $hl ); // Web-App-Notiz = web sichtbar
if ( ! $id ) ci_fail( 'Notiz konnte nicht gespeichert werden (leer oder Limit erreicht).' );
if ( function_exists( 'pacim_aa_log_tok' ) ) pacim_aa_log_tok( $db, $tok, $bid, (int) $r['invoice_id'], 'note_add', ci_note_snip( $noteTxt, $hl ), date( 'Y-m-d', strtotime( (string) $r['booking_begin'] ) ) );
ci_json( array( 'ok' => true, 'id' => $id ) );
}
if ( $action === 'note_edit' || $action === 'note_del' ) {
if ( ! $bnCanWrite ) ci_fail( 'Keine Berechtigung für interne Notizen.' );
$nid = (int) ( $_POST['id'] ?? 0 );
$n = pacim_bn_get( $db, $nid );
if ( ! $n || (int) $n['note_booking'] !== $bid || $n['note_deleted_at'] ) ci_fail( 'Notiz nicht gefunden.' );
if ( ! ci_note_can_edit( $n, $bnCanWrite, $bnCanAll, $bnAccessId ) ) ci_fail( 'Diese Notiz darf nicht geändert werden.' );
if ( $action === 'note_del' ) {
pacim_bn_delete( $db, $nid, $bnUser );
if ( function_exists( 'pacim_aa_log_tok' ) ) pacim_aa_log_tok( $db, $tok, $bid, (int) $r['invoice_id'], 'note_del', ci_note_snip( (string) $n['note_text'] ), date( 'Y-m-d', strtotime( (string) $r['booking_begin'] ) ) );
ci_json( array( 'ok' => true ) );
}
$hl = ! empty( $_POST['highlight'] ) ? 1 : 0; // Web-Sichtbarkeit bleibt unverändert (null)
$noteTxt = (string) ( $_POST['text'] ?? '' );
if ( ! pacim_bn_update( $db, $nid, $noteTxt, $bnUser, null, $hl ) ) ci_fail( 'Speichern fehlgeschlagen.' );
if ( function_exists( 'pacim_aa_log_tok' ) ) pacim_aa_log_tok( $db, $tok, $bid, (int) $r['invoice_id'], 'note_edit', ci_note_snip( $noteTxt, $hl ), date( 'Y-m-d', strtotime( (string) $r['booking_begin'] ) ) );
ci_json( array( 'ok' => true ) );
}
/* ============================================ AJAX: Kennzeichen-Auskunft (lokal, vor Read-Only-Guard) */
if ( $action === 'plateinfo' ) {
$pl = (string) ( $_POST['plate'] ?? $r['plate'] );
ci_json( array( 'ok' => true ) + pacim_plate_info( $pl ) );
}
// Nur-Ansicht-Zugang: keinerlei Schreibaktionen (Zahlung/Check-in/Kennzeichen).
if ( $action !== '' && $readOnly ) ci_fail( 'Dieser Zugang erlaubt nur die Ansicht.' );
/* ============================================ AJAX: Zahlung erfassen / ändern */
if ( $action === 'pay' ) {
if ( $storno ) ci_fail( 'Buchung ist storniert.' );
if ( $isAida ) ci_fail( 'AIDA-Buchung – bereits über myAIDA bezahlt.' );
$type = (int) ( $_POST['type'] ?? 0 );
$coupon = (int) ( $_POST['coupon'] ?? 0 );
if ( ! in_array( $type, array( 1, 2, 3 ), true ) ) ci_fail( 'Bitte Zahlungsart wählen.' );
if ( ! in_array( $coupon, array( 0, 1, 2 ), true ) ) $coupon = 0;
$amount = ci_amount( $pos, $coupon );
// Atomar schreiben: bei einem Fehler wird NICHTS gespeichert (keine Rechnungsnummer, keine Zahlung).
$db->startTransaction();
try {
$db->where( 'payment_invoice', $iid ); $db->delete( 'pacim_payments' ); // bei Änderung alten Eintrag ersetzen
$okIns = $db->insert( 'pacim_payments', array(
'payment_invoice' => $iid, 'payment_type' => $type, 'payment_value' => $amount,
'payment_notice' => '', 'payment_user' => trim( (string) ( $_SESSION['user_name'] ?? 'Check-in' ) ),
'payment_date' => date( 'Y-m-d' ),
) );
if ( ! $okIns ) throw new Exception( 'payment-insert' );
$upd = array(
'invoice_coupon' => $coupon ?: null,
'invoice_payment' => $type,
'invoice_paydate' => date( 'Y-m-d H:i:s' ),
);
// Rechnungs-NUMMER wird NICHT mehr automatisch vergeben – erst über „Rechnung(en) erstellen"
// (Tab Rechnungen, bzw. einzeln im 3-Punkte-Menü). Nur das Rechnungsdatum = Zahlungstag setzen,
// damit der Vorgang in der Tagesabrechnung des richtigen Tages erscheint.
if ( trim( (string) $r['invoice_no'] ) === '' ) {
$upd['invoice_date'] = date( 'Y-m-d' );
}
$db->where( 'invoice_id', $iid );
if ( ! $db->update( 'pacim_invoice', $upd ) ) throw new Exception( 'invoice-update' );
$db->commit();
} catch ( Exception $ex ) {
$db->rollback();
ci_fail( 'Zahlung konnte nicht gespeichert werden. Es wurde keine Rechnung erstellt.' );
}
try { if ( class_exists( 'mailrules' ) ) mailrules::evaluateEvent( 'payment_received', array( 'invoice_id' => $iid ), 'payment_received' ); } catch ( Exception $e ) {}
if ( function_exists( 'pacim_aa_log_tok' ) ) pacim_aa_log_tok( $db, $tok, $bid, $iid, 'payment', ci_eur( $amount ) . ' · ' . ci_payname( $type ) . ' · ' . trim( (string) $r['booking_no'] ), date( 'Y-m-d', strtotime( (string) $r['booking_begin'] ) ) );
ci_json( array( 'ok' => true, 'paid' => true, 'amount' => $amount, 'amount_fmt' => ci_eur( $amount ),
'type' => $type, 'type_name' => ci_payname( $type ), 'coupon' => $coupon ) );
}
/* ============================================ AJAX: Rechnungs-Posten hinzufügen / entfernen */
if ( $action === 'addpos' || $action === 'delpos' ) {
if ( $storno ) ci_fail( 'Buchung ist storniert.' );
if ( $isAida ) ci_fail( 'AIDA-Buchung – Posten nicht änderbar.' );
if ( ! empty( $r['invoice_payment'] ) ) ci_fail( 'Bereits bezahlt – Zahlung erst zurücknehmen.' );
if ( $action === 'addpos' ) {
$pid = (int) ( $_POST['product_id'] ?? 0 );
if ( $pid <= 2 ) ci_fail( 'Dieses Produkt kann nicht hinzugefügt werden.' );
$prod = $db->rawQueryOne(
"SELECT p.product_id, p.product_title, p.product_visible,
COALESCE((SELECT pp.price FROM pacim_product_prices pp WHERE pp.product_id = p.product_id AND pp.year = ? LIMIT 1), p.product_price) AS net
FROM pacim_products p WHERE p.product_id = ? LIMIT 1",
array( $bkYear, $pid ) );
if ( ! $prod || (int) $prod['product_visible'] !== 1 ) ci_fail( 'Produkt nicht verfügbar.' );
$db->insert( 'pacim_invoice_pos', array(
'invoice_id' => $iid, 'product_id' => $pid, 'booking_id' => $bid,
'invoice_pos_price' => (float) $prod['net'], 'invoice_pos_quantity' => 1,
'invoice_pos_title' => (string) $prod['product_title'],
) );
if ( function_exists( 'pacim_aa_log_tok' ) ) pacim_aa_log_tok( $db, $tok, $bid, $iid, 'addpos', (string) $prod['product_title'], date( 'Y-m-d', strtotime( (string) $r['booking_begin'] ) ) );
} else { // delpos – nur Extra-Positionen (Parkplatz 1/2 bleibt)
$posId = (int) ( $_POST['pos_id'] ?? 0 );
$row = $db->rawQueryOne( "SELECT product_id, invoice_pos_title FROM pacim_invoice_pos WHERE invoice_pos_id = ? AND invoice_id = ?", array( $posId, $iid ) );
if ( ! $row ) ci_fail( 'Position nicht gefunden.' );
if ( (int) $row['product_id'] <= 2 ) ci_fail( 'Parkplatz kann nicht entfernt werden.' );
$db->where( 'invoice_pos_id', $posId ); $db->where( 'invoice_id', $iid ); $db->delete( 'pacim_invoice_pos' );
if ( function_exists( 'pacim_aa_log_tok' ) ) pacim_aa_log_tok( $db, $tok, $bid, $iid, 'delpos', (string) $row['invoice_pos_title'], date( 'Y-m-d', strtotime( (string) $r['booking_begin'] ) ) );
}
// Neue Positionen + Beträge zurückgeben; invoice_price = Brutto-Summe (ohne Rabatt) konsistent halten.
$pos2 = ci_positions( $db, $iid );
$am = array( 0 => ci_amount( $pos2, 0 ), 1 => ci_amount( $pos2, 1 ), 2 => ci_amount( $pos2, 2 ) );
$db->where( 'invoice_id', $iid ); $db->update( 'pacim_invoice', array( 'invoice_price' => $am[0] ) );
ci_json( array( 'ok' => true, 'positions' => ci_pos_items( $db, $iid ), 'amounts' => $am ) );
}
/* ============================================ AJAX: Kennzeichen ändern */
if ( $action === 'plate' ) {
if ( $storno ) ci_fail( 'Buchung ist storniert.' );
$val = strtoupper( trim( (string) ( $_POST['plate'] ?? '' ) ) );
if ( $val === '' ) ci_fail( 'Bitte ein Kennzeichen eingeben.' );
if ( mb_strlen( $val ) > 20 || ! preg_match( '/^[A-ZÄÖÜ0-9 .\-]+$/u', $val ) ) ci_fail( 'Ungültiges Kennzeichen.' );
$db->where( 'booking_id', $bid ); $db->update( 'pacim_booking', array( 'booking_serial' => $val ) );
$f = $db->rawQueryOne( "SELECT license_plate(booking_serial) AS p FROM pacim_booking WHERE booking_id = ?", array( $bid ) );
$plNew = ( $f && trim( (string) $f['p'] ) !== '' ) ? $f['p'] : $val;
if ( function_exists( 'pacim_aa_log_tok' ) ) pacim_aa_log_tok( $db, $tok, $bid, (int) $r['invoice_id'], 'plate', 'Kennzeichen: ' . $plNew, date( 'Y-m-d', strtotime( (string) $r['booking_begin'] ) ) );
ci_json( array( 'ok' => true, 'plate' => $plNew ) );
}
/* ============================================ AJAX: Check-in */
if ( $action === 'checkin' ) {
if ( $storno ) ci_fail( 'Buchung ist storniert.' );
if ( ! $isAida && (int) $r['invoice_payment'] <= 0 ) ci_fail( 'Bitte zuerst die Zahlung erfassen.' );
if ( ! empty( $r['booking_checkin'] ) ) ci_json( array( 'ok' => true, 'already' => true ) );
$db->where( 'booking_id', $bid ); $db->update( 'pacim_booking', array( 'booking_checkin' => date( 'Y-m-d H:i:s' ) ) );
// Check-in abgeschlossen => einmalige Vorgangsnummer vergeben (storniert wird oben bereits geblockt).
if ( class_exists( 'vorgang' ) ) vorgang::assign( $db, $bid );
if ( function_exists( 'pacim_aa_log_tok' ) ) pacim_aa_log_tok( $db, $tok, $bid, (int) $r['invoice_id'], 'checkin', '', date( 'Y-m-d', strtotime( (string) $r['booking_begin'] ) ) );
ci_json( array( 'ok' => true, 'checked' => true, 'time' => date( 'H:i' ) ) );
}
/* ============================================ AJAX: Check-in zurücknehmen (Auschecken) */
if ( $action === 'uncheckin' ) {
if ( $storno ) ci_fail( 'Buchung ist storniert.' );
$db->where( 'booking_id', $bid ); $db->update( 'pacim_booking', array( 'booking_checkin' => null ) );
if ( function_exists( 'pacim_aa_log_tok' ) ) pacim_aa_log_tok( $db, $tok, $bid, (int) $r['invoice_id'], 'uncheckin', '', date( 'Y-m-d', strtotime( (string) $r['booking_begin'] ) ) );
ci_json( array( 'ok' => true, 'unchecked' => true ) );
}
/* ============================================ AJAX: Check-out (Abreise erfassen) */
if ( $action === 'checkout' ) {
if ( $storno ) ci_fail( 'Buchung ist storniert.' );
if ( ! empty( $r['booking_checkout'] ) ) ci_json( array( 'ok' => true, 'already' => true ) );
$db->where( 'booking_id', $bid ); $db->update( 'pacim_booking', array( 'booking_checkout' => date( 'Y-m-d H:i:s' ) ) );
if ( function_exists( 'pacim_aa_log_tok' ) ) pacim_aa_log_tok( $db, $tok, $bid, (int) $r['invoice_id'], 'checkout', '', date( 'Y-m-d', strtotime( (string) $r['booking_end'] ) ) );
ci_json( array( 'ok' => true, 'checked' => true, 'time' => date( 'H:i' ) ) );
}
/* ============================================ AJAX: Check-out zurücknehmen */
if ( $action === 'uncheckout' ) {
if ( $storno ) ci_fail( 'Buchung ist storniert.' );
$db->where( 'booking_id', $bid ); $db->update( 'pacim_booking', array( 'booking_checkout' => null ) );
if ( function_exists( 'pacim_aa_log_tok' ) ) pacim_aa_log_tok( $db, $tok, $bid, (int) $r['invoice_id'], 'uncheckout', '', date( 'Y-m-d', strtotime( (string) $r['booking_end'] ) ) );
ci_json( array( 'ok' => true, 'unchecked' => true ) );
}
// Aktueller Zustand (für den Live-Abgleich, wenn die App-Shell aus dem Cache kam und veraltet sein könnte).
if ( $action === 'state' ) {
$ckin = trim( (string) $r['checkin_time'] ); if ( $ckin === '00:00' ) $ckin = '';
$ckout = trim( (string) $r['checkout_time'] ); if ( $ckout === '00:00' ) $ckout = '';
$bd = trim( (string) $r['boarding'] ); if ( $bd === '00:00' ) $bd = '';
ci_json( array(
'ok' => true,
'arrived' => $isDep ? ! empty( $r['booking_checkout'] ) : ! empty( $r['booking_checkin'] ),
'paid' => ( $isAida || in_array( (int) $r['invoice_payment'], array( 1, 2, 3 ), true ) ),
'storno' => $storno,
'times' => array( 'boarding' => ( $isDep ? '' : $bd ), 'checkin' => $ckin, 'checkout' => $ckout ),
) );
}
/* ============================================ Seite rendern */
// Geöffneten Datensatz in den Terminal-Sitzungs-Verlauf schreiben (nur Terminal-Sitzungen, echte Seitenaufrufe).
if ( $tok !== '' && ! $embed && function_exists( 'pacim_ts_log_tok' ) ) {
$__od = trim( (string) $r['booking_no'] );
$__pl = trim( (string) $r['plate'] ); if ( $__pl !== '' && $__pl !== '—' ) $__od = trim( $__od . ( $__od !== '' ? ' · ' : '' ) . $__pl );
pacim_ts_log_tok( $db, $tok, 'open', $bid, (int) $r['invoice_id'], $__od );
}
$e = function ( $s ) { return htmlspecialchars( (string) $s, ENT_QUOTES, 'UTF-8' ); };
// Detailansicht: „Nachname, Vorname" (nur EIN Vorname, erster Token).
$fnRaw = trim( (string) $r['customer_firstname'] );
$fn = $fnRaw !== '' ? preg_split( '/\s+/', $fnRaw )[0] : '';
$ln = trim( (string) $r['customer_lastname'] );
$fullname = ( $ln !== '' && $fn !== '' ) ? ( $ln . ', ' . $fn ) : trim( $ln . ' ' . $fn );
if ( $fullname === '' ) $fullname = trim( (string) $r['customer_name'] );
/* ---- Kontaktdaten (Kontakt-Ansicht) – leere/"0"-Felder werden ausgelassen. ---- */
$cPhoneRaw = trim( (string) $r['customer_phone'] );
$cEmail = trim( (string) $r['customer_email'] );
if ( $cPhoneRaw === '0' ) $cPhoneRaw = '';
if ( $cEmail === '0' ) $cEmail = '';
$cStreet = trim( (string) $r['customer_street'] );
$cZip = trim( (string) $r['customer_zipcode'] );
$cCity = trim( (string) $r['customer_city'] );
$cCountry = trim( (string) $r['customer_country'] );
$cCityLine = trim( $cZip . ' ' . $cCity );
$hasAddress = ( $cStreet !== '' || $cCityLine !== '' || $cCountry !== '' );
// tel:-Link – nur Ziffern und ein evtl. führendes „+" behalten.
$cPhoneTel = preg_replace( '/[^0-9+]/', '', $cPhoneRaw ); if ( strpos( $cPhoneTel, '+' ) > 0 ) $cPhoneTel = str_replace( '+', '', $cPhoneTel );
// AIDA-Zusatzangaben: Buchungsnummer = 8 zusammenhängende Ziffern der Rechnungsnr.; Kabine = letzter
// Zahlenabschnitt der booking_no nach dem letzten Bindestrich.
$aidaBookNo = ''; $aidaCabin = '';
if ( $isAida ) {
if ( preg_match( '/\d{8}/', (string) $r['invoice_no'], $mm ) ) $aidaBookNo = $mm[0];
$segs = explode( '-', (string) $r['booking_no'] ); $last = trim( (string) end( $segs ) );
if ( $last !== '' && ctype_digit( $last ) ) $aidaCabin = $last;
}
$plate = trim( (string) $r['plate'] ) !== '' ? $r['plate'] : '—';
$ship = trim( (string) $r['booking_event'] ) !== '' ? $r['booking_event'] : '—';
$valet = ( (int) $r['booking_valet'] === 1 );
$days = (int) $r['trip_days'];
// Stellplatz: Halle/Außen [- VALET] + Tage (muted)
$stell = ( (int) $r['booking_type'] === 1 ? 'Halle' : 'Außen' ) . ( $valet ? ' - VALET' : '' );
$bg = strtotime( (string) $r['booking_begin'] ); $en = strtotime( (string) $r['booking_end'] );
$von = $bg ? date( 'd.m.', $bg ) : '—';
$bis = $en ? date( 'd.m.y', $en ) : '—';
$boarding = trim( (string) $r['boarding'] ); if ( $boarding === '00:00' ) $boarding = '';
// „arrived"/„ciTime" je nach Modus: Anreisen = Check-in, Abreisen = Check-out. Bei Abreisen gibt es
// keine geplante Abreisezeit (Boarding bleibt für Abreisen leer).
if ( $isDep ) {
$ciTime = trim( (string) $r['checkout_time'] ); if ( $ciTime === '00:00' ) $ciTime = '';
$arrived = ! empty( $r['booking_checkout'] );
$boarding = '';
} else {
$ciTime = trim( (string) $r['checkin_time'] ); if ( $ciTime === '00:00' ) $ciTime = '';
$arrived = ! empty( $r['booking_checkin'] );
}
// Zeit-Kachel: alle vorhandenen Zeiten (Boarding / Check-in / Check-out) – durch Antippen umschaltbar.
$ckinT = trim( (string) $r['checkin_time'] ); if ( $ckinT === '00:00' ) $ckinT = '';
$ckoutT = trim( (string) $r['checkout_time'] ); if ( $ckoutT === '00:00' ) $ckoutT = '';
$paid = $isAida || in_array( (int) $r['invoice_payment'], array( 1, 2, 3 ), true );
$coupon = (int) $r['invoice_coupon'];
$amounts = array( 0 => ci_amount( $pos, 0 ), 1 => ci_amount( $pos, 1 ), 2 => ci_amount( $pos, 2 ) );
$dateKey = substr( (string) $r['booking_begin'], 0, 10 );
$backUrl = $tok !== '' ? SITE_URL . '/day-list.php?t=' . $tok
: SITE_URL . '/day-list.php?date=' . $dateKey . '&h=' . pacim_sign( 'daylist:' . $dateKey );
// Fahrzeugbilder-Rechte: Token = Zugangsrechte, Legacy-Hash = volle Rechte.
$imgPerms = ( $tok !== '' ) ? ( $access ? pacim_ma_img_perms( $access ) : array() ) : pacim_ma_img_all();
$canViewImg = in_array( 'view', $imgPerms, true );
$canUploadImg = in_array( 'upload', $imgPerms, true );
$canNoteImg = in_array( 'note_create', $imgPerms, true );
// Notizen: Lesen darf jeder berechtigte Zugang; Erfassen/Bearbeiten nur mit Recht (siehe $bnCanWrite oben).
$canNotes = $bnCanWrite;
// Anzahlen für die Footer-Tab-Titel (x Notiz/Notizen, x Foto/Fotos).
$noteN = (int) pacim_bn_count( $db, $bid, true );
$photoN = ( $canViewImg || $canUploadImg ) ? (int) pacim_bi_count_booking( $bid ) : 0;
$cfg = array(
'b' => $bid, 'h' => $h, 't' => $tok, 'embed' => $embed, 'paid' => $paid, 'arrived' => $arrived, 'storno' => $storno, 'isAida' => $isAida, 'isDep' => $isDep,
'canAct' => $canAct, 'coupon' => $coupon, 'amounts' => $amounts, 'payType' => (int) $r['invoice_payment'], 'back' => $backUrl,
'boarding' => $boarding, 'ciTime' => $ciTime,
'times' => array( 'boarding' => ( $isDep ? '' : $boarding ), 'checkin' => $ckinT, 'checkout' => $ckoutT ),
'imgApi' => SITE_URL . '/booking-images.php', 'imgView' => $canViewImg, 'imgUpload' => $canUploadImg, 'imgNote' => $canNoteImg,
'canNotes' => $canNotes, 'hasImg' => ( $canViewImg || $canUploadImg ),
'plateHasInfo' => ( pacim_plate_lookup( $plate ) !== null ),
'notesCount' => $noteN,
'noteMarquee' => pacim_bn_highlighted( $db, $bid ),
'photosCount' => $photoN,
'plate' => $plate, 'bookingNo' => (string) $r['booking_no'],
'posItems' => $posItems, 'addProducts' => $addProducts, 'canEditPos' => ( $canAct && ! $isAida && ! $storno ),
);
header( 'Content-Type: text/html; charset=utf-8' );
echo '<!DOCTYPE html><html lang="de"><head><meta charset="utf-8">'
. '<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no, viewport-fit=cover">'
. '<meta name="robots" content="noindex,nofollow">'
. '<meta name="apple-mobile-web-app-capable" content="yes"><meta name="mobile-web-app-capable" content="yes">'
. '<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"><meta name="theme-color" content="#0d2c44">'
// Web-App (PWA): Manifest/iOS-Icon nur bei direktem Token-Aufruf (nicht im eingebetteten Overlay, nicht im CRM).
. ( ( $tok !== '' && ! $embed ) ?
'<link rel="manifest" href="' . SITE_URL . '/manifest.php?t=' . rawurlencode( $tok ) . '">'
. '<link rel="apple-touch-icon" href="' . SITE_URL . '/assets/webapp/icon-180.png">'
. '<meta name="apple-mobile-web-app-title" content="Seaside Parking">' : '' )
. '<title>Check-in · ' . $e( $plate ) . '</title>'
. '<style>@font-face{font-family:"EuroPlate";src:url("' . SITE_URL . '/assets/fonts/EuroPlate.ttf") format("truetype");font-display:swap;}'
. ci_css() . '</style></head><body' . ( $embed ? '' : ' class="ci-sa"' ) . '>';
?>
<div class="card">
<div class="hd">
<a class="back" id="ci-back" href="<?= $e( $backUrl ) ?>" title="Zurück zur Liste" aria-label="Zurück zur Liste"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"/></svg></a>
<div class="k"><?= $readOnly ? 'Übersicht' : ( $isDep ? 'Check-out' : ( $isAida ? 'Check-in' : 'Check-in & Zahlung' ) ) ?></div>
<div class="t"><?= $e( $fullname ?: '—' ) ?></div>
<div class="no"><?= $e( $r['booking_no'] ) ?><?= ( ! $isAida && ! $readOnly && $r['invoice_no'] ) ? ' · Rg. ' . $e( $r['invoice_no'] ) : '' ?></div>
<span class="hstat" id="ci-status-badge"></span>
</div>
<div class="slider" id="ci-slider"><div class="track" id="ci-track">
<!-- ============================ Tab 1: Buchung ============================ -->
<section class="pane active" id="pane-buchung" data-pane="buchung">
<?php $mqText = implode( ' ••• ', (array) $cfg['noteMarquee'] ); ?>
<!-- Hervorgehobene Notiz(en) als Endlos-Lauftext im Datenblatt -->
<div class="ci-marquee<?= $mqText === '' ? ' d-none' : '' ?>" id="ci-marquee">
<svg class="ci-mq-ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M3 11v2a1 1 0 0 0 1 1h2l4 4V6L6 10H4a1 1 0 0 0-1 1z"/><path d="M15 9a3 3 0 0 1 0 6M17.5 6.5a6 6 0 0 1 0 11"/></svg>
<div class="ci-mq-win"><div class="ci-mq-track" id="ci-mq-track"><span class="ci-mq-seg"><?= $e( $mqText ) ?></span><span class="ci-mq-seg" aria-hidden="true"><?= $e( $mqText ) ?></span></div></div>
</div>
<div class="bd">
<?php $plateRO = ( $storno || $readOnly ) ? ' data-ro="1"' : ''; ?>
<!-- Kennzeichen-Kachel -->
<div class="kachel k-plate">
<div class="k-l">Kennzeichen</div>
<div class="plate-wrap">
<div class="plate fe" id="ci-plate" spellcheck="false"<?= $plateRO ?>><?= $e( $plate ) ?></div>
<button type="button" class="plate-save d-none" id="ci-plate-save" title="Speichern" aria-label="Speichern"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M5 13l4 4L19 7"/></svg></button>
<button type="button" class="plate-info<?= $cfg['plateHasInfo'] ? '' : ' d-none' ?>" id="ci-plate-info" title="Kennzeichen-Herkunft" aria-label="Kennzeichen-Info"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9.5"/><path d="M12 11v5"/><circle cx="12" cy="7.7" r="0.7" fill="currentColor" stroke="none"/></svg></button>
</div>
</div>
<!-- Info-Kacheln -->
<div class="k-grid">
<div class="kachel"><div class="k-l">Dauer</div><div class="k-v"><?= (int) $days ?> Tage</div><div class="k-sub"><?= $e( $von ) ?> – <?= $e( $bis ) ?></div></div>
<div class="kachel"><div class="k-l">Event</div><div class="k-v"><?= $e( $ship ) ?></div></div>
<div class="kachel"><div class="k-l">Platz</div><div class="k-v"><?= $e( $stell ) ?></div></div>
<?php
// Verfügbare Zeiten in fester Reihenfolge; Standard-Anzeige = höchste (Check-out > Check-in > Boarding).
$ciTimes = array();
if ( ! $isDep && $boarding !== '' ) $ciTimes[] = array( 'l' => 'Boarding', 'v' => $boarding );
if ( $ckinT !== '' ) $ciTimes[] = array( 'l' => 'Check-in', 'v' => $ckinT );
if ( $ckoutT !== '' ) $ciTimes[] = array( 'l' => 'Check-out', 'v' => $ckoutT );
$showTime = count( $ciTimes ) > 0;
$tIdx = $showTime ? count( $ciTimes ) - 1 : 0;
$multiT = count( $ciTimes ) > 1;
$emptyLbl = $isDep ? 'Check-out' : 'Boarding'; // Label, wenn (noch) keine Zeit vorhanden -> „–"
?>
<div class="kachel<?= $multiT ? ' k-tap' : '' ?>" id="ci-timerow">
<div class="k-l" id="ci-timelabel"><span id="ci-timelbl-tx"><?= $e( $showTime ? $ciTimes[ $tIdx ]['l'] : $emptyLbl ) ?></span><svg class="k-cyc<?= $multiT ? '' : ' d-none' ?>" id="ci-timecyc" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 4v6h-6"/><path d="M1 20v-6h6"/><path d="M3.5 9a9 9 0 0 1 14.9-3.4L23 10M1 14l4.6 4.4A9 9 0 0 0 20.5 15"/></svg></div>
<div class="k-v" id="ci-timeval"><?= $showTime ? $e( $ciTimes[ $tIdx ]['v'] ) . ' Uhr' : '–' ?></div>
</div>
</div>
</div>
<?php if ( $storno ) { ?>
<div class="sec"><div class="note warn">Diese Buchung ist storniert – keine Aktion möglich.</div></div>
<?php } elseif ( $readOnly ) { ?>
<div class="sec"><div class="note info">Nur Ansicht – Check-in und Zahlung sind für diesen Zugang nicht freigegeben.</div></div>
<?php } ?>
<!-- Navigations-Karten: öffnen die jeweilige Unterseite -->
<div class="navcards">
<button type="button" class="navcard navcard-rg" data-go="rechnung">
<span class="nc-ic ic-green"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2h9l3 3v17l-3-2-3 2-3-2-3 2V2z"/><path d="M9 7h6M9 11h6M9 15h4"/></svg></span>
<span class="nc-tx"><span class="nc-t">Rechnung</span><span class="nc-s <?= $isAida ? 'paid' : 'open' ?>" id="ci-rg-cardst"><?= $isAida ? 'bezahlt' : 'offen' ?></span></span>
<span class="nc-amt <?= $isAida ? 'paid' : 'open' ?>" id="ci-nc-rg"><?= $e( ci_eur( $amounts[0] ) ) ?></span>
</button>
<div class="navtabs">
<button type="button" class="navtab" data-go="notizen">
<span class="nc-ic ic-amber"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16v12l-4 4H4z"/><path d="M16 20v-4h4M8 9h8M8 13h5"/></svg></span>
<span class="navtab-t">Notizen</span>
</button>
<?php if ( $canViewImg || $canUploadImg ) { ?>
<button type="button" class="navtab" data-go="fotos">
<span class="nc-ic ic-teal"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg></span>
<span class="navtab-t">Fotos</span>
</button>
<?php } ?>
<button type="button" class="navtab" data-go="kontakt">
<span class="nc-ic ic-blue"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M20 8v6M23 11h-6"/><circle cx="9" cy="8" r="4"/><path d="M3 21v-1a6 6 0 0 1 6-6h0a6 6 0 0 1 6 6v1"/></svg></span>
<span class="navtab-t">Kontakt</span>
</button>
</div>
</div>
<div class="status" id="ci-status"></div>
</section>
<!-- ============================ Unterseite: Rechnung ============================ -->
<section class="pane" id="pane-rechnung" data-pane="rechnung">
<div class="subhd"><button type="button" class="subback" data-go="buchung"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"/></svg> Übersicht</button><span class="subhd-t">Rechnung</span></div>
<div class="bd">
<!-- Rechnungsbetrag: riesig, rot = offen / grün = bezahlt -->
<div class="rg-box">
<div class="rg-amount <?= $isAida ? 'paid' : 'open' ?>" id="ci-rg-amount"><?= $e( ci_eur( $amounts[0] ) ) ?></div>
<div class="rg-status <?= $isAida ? 'paid' : 'open' ?>" id="ci-rg-status"><?= $isAida ? 'bezahlt · myAIDA' : 'offen' ?></div>
</div>
<!-- Posten (POS-Stil) – per JS gerendert aus CFG.posItems -->
<div class="rg-pos" id="ci-pos-list"></div>
<?php if ( $canAct && ! $isAida && ! $storno ) { ?>
<div class="rg-addwrap">
<button type="button" class="rg-addbtn" id="ci-pos-addbtn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg> Posten hinzufügen</button>
<div class="rg-picker d-none" id="ci-pos-picker"></div>
<div class="status" id="ci-pos-msg"></div>
</div>
<?php } ?>
<?php if ( ! $isAida && ! $storno && ! $readOnly ) { ?>
<!-- Zahlungs-Status (Erfassen NUR über den großen Aktions-Button unten) -->
<div class="sec d-none" id="ci-paid-sec">
<div class="note ok"><strong>Bezahlt:</strong> <span id="ci-paid-info"></span> <button type="button" class="lnk" id="ci-edit-btn">ändern</button></div>
</div>
<?php } elseif ( $storno ) { ?>
<div class="sec"><div class="note warn">Buchung storniert.</div></div>
<?php } elseif ( $readOnly ) { ?>
<div class="sec"><div class="note info">Zahlung ist für diesen Zugang nicht freigegeben.</div></div>
<?php } ?>
<?php if ( ! $isAida && $r['invoice_no'] ) { ?><div class="sec"><div class="note info">Rechnungs-Nr.: <?= $e( $r['invoice_no'] ) ?></div></div><?php } ?>
</div>
</section>
<!-- ============================ Tab 2: Notizen ============================ -->
<section class="pane" id="pane-notizen" data-pane="notizen">
<div class="subhd"><button type="button" class="subback" data-go="buchung"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"/></svg> Übersicht</button><span class="subhd-t">Notizen</span></div>
<?php $hint = trim( (string) $r['booking_hint'] ); $hintReply = trim( (string) $r['booking_hint_reply'] ); ?>
<?php if ( $hint !== '' ) { ?>
<div class="nt-hint">
<div class="nt-hint-h"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 8h.01M11 12h1v4h1"/></svg> Kundenhinweis zur Buchung</div>
<div class="nt-hint-b"><?= nl2br( $e( $hint ) ) ?></div>
<?php if ( $hintReply !== '' ) { ?>
<details class="nt-hint-reply">
<summary class="nt-hint-rh">Antwort gesendet<?= $r['hint_reply_at'] ? ' am ' . $e( $r['hint_reply_at'] ) . ' Uhr' : '' ?><?= trim( (string) $r['booking_hint_reply_by'] ) !== '' ? ' · ' . $e( $r['booking_hint_reply_by'] ) : '' ?></summary>
<div class="nt-hint-b"><?= nl2br( $e( $hintReply ) ) ?></div>
</details>
<?php } ?>
</div>
<?php } ?>
<div class="nt-block">
<div class="sec-h nt-title"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16v12l-4 4H4z"/><path d="M16 20v-4h4M8 9h8M8 13h5"/></svg> Interne Notizen <span class="ph-count" id="ci-nt-count"></span></div>
<?php if ( $canNotes && ! $storno ) { ?>
<div class="nt-add">
<textarea id="ci-nt-text" rows="3" maxlength="4000" placeholder="Interne Notiz zur Buchung …"></textarea>
<label class="nt-hl-opt"><input type="checkbox" id="ci-nt-hl"> <span>Hervorheben – läuft im Datenblatt als Lauftext durch</span></label>
<div class="modal-err d-none" id="ci-nt-err"></div>
<button type="button" class="btn btn-checkin" id="ci-nt-save">Notiz speichern</button>
</div>
<?php } ?>
<div class="nt-list" id="ci-nt-list"></div>
<div class="nt-empty d-none" id="ci-nt-empty">Noch keine internen Notizen.</div>
<?php if ( ! $canNotes ) { ?><div class="nt-foot">Dieser Zugang darf interne Notizen nur ansehen.</div><?php } ?>
</div>
</section>
<!-- ============================ Tab 3: Fotos ============================ -->
<?php if ( $canViewImg || $canUploadImg ) { ?>
<section class="pane" id="pane-fotos" data-pane="fotos">
<div class="subhd"><button type="button" class="subback" data-go="buchung"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"/></svg> Übersicht</button><span class="subhd-t">Fotos</span></div>
<div class="sec ph-sec" id="ci-photos">
<div class="ph-head">
<span class="sec-h ph-title"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg> Fahrzeugfotos <span class="ph-count" id="ci-ph-count"></span></span>
<?php if ( $canUploadImg ) { ?><button type="button" class="ph-add" id="ci-ph-add"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg> Fotos</button><?php } ?>
</div>
<div class="ph-grid" id="ci-ph-grid"></div>
<div class="ph-empty" id="ci-ph-empty"><?= $canUploadImg ? 'Noch keine Fahrzeugfotos – jetzt hinzufügen.' : 'Noch keine Fahrzeugfotos vorhanden.' ?></div>
</div>
</section>
<?php } ?>
<!-- ============================ Unterseite: Kontakt ============================ -->
<section class="pane" id="pane-kontakt" data-pane="kontakt">
<div class="subhd"><button type="button" class="subback" data-go="buchung"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"/></svg> Übersicht</button><span class="subhd-t">Kontakt</span></div>
<div class="bd">
<div class="kt-card">
<div class="kt-name"><?= $e( $fullname ?: '—' ) ?></div>
<?php if ( $hasAddress ) { ?>
<div class="kt-row"><span class="kt-lbl">Anschrift</span><span class="kt-val">
<?php if ( $cStreet !== '' ) echo $e( $cStreet ) . '<br>'; ?>
<?php if ( $cCityLine !== '' ) echo $e( $cCityLine ) . '<br>'; ?>
<?php if ( $cCountry !== '' ) echo $e( $cCountry ); ?>
</span></div>
<?php } ?>
<?php if ( $cPhoneRaw !== '' ) { ?>
<div class="kt-row"><span class="kt-lbl">Telefon</span><span class="kt-val"><?= $e( $cPhoneRaw ) ?></span></div>
<?php } ?>
<?php if ( $cEmail !== '' ) { ?>
<div class="kt-row"><span class="kt-lbl">E-Mail</span><span class="kt-val"><a href="mailto:<?= $e( $cEmail ) ?>"><?= $e( $cEmail ) ?></a></span></div>
<?php } ?>
<?php if ( $isAida && $aidaBookNo !== '' ) { ?>
<div class="kt-row"><span class="kt-lbl">Buchungsnummer</span><span class="kt-val"><?= $e( $aidaBookNo ) ?></span></div>
<?php } ?>
<?php if ( $isAida && $aidaCabin !== '' ) { ?>
<div class="kt-row"><span class="kt-lbl">Kabine</span><span class="kt-val"><?= $e( $aidaCabin ) ?></span></div>
<?php } ?>
</div>
<?php if ( $cPhoneTel !== '' ) { ?>
<a class="kt-call" href="tel:<?= $e( $cPhoneTel ) ?>"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.8 19.8 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.8 19.8 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.9.34 1.85.57 2.81.7A2 2 0 0 1 22 16.92z"/></svg> Kunde anrufen</a>
<?php } ?>
<?php if ( ! $hasAddress && $cPhoneRaw === '' && $cEmail === '' && ! ( $isAida && ( $aidaBookNo !== '' || $aidaCabin !== '' ) ) ) { ?>
<div class="sec"><div class="note info">Keine Kontaktdaten hinterlegt.</div></div>
<?php } ?>
</div>
</section>
</div></div><!-- /track /slider -->
</div>
<!-- Footer-Tab-Navigation (fixiert) -->
<nav class="tabbar" id="ci-tabbar">
<button type="button" class="tab active" data-tab="buchung">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="3" width="16" height="18" rx="2"/><path d="M8 7h8M8 11h8M8 15h5"/></svg>
<span>Buchung</span></button>
<button type="button" class="tab" data-tab="notizen">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16v12l-4 4H4z"/><path d="M16 20v-4h4M8 9h8M8 13h5"/></svg>
<span id="ci-tb-notes"><?= $noteN ?> Notiz<?= $noteN === 1 ? '' : 'en' ?></span></button>
<?php if ( $canViewImg || $canUploadImg ) { ?>
<button type="button" class="tab" data-tab="fotos">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>
<span id="ci-tb-photos"><?= $photoN ?> Foto<?= $photoN === 1 ? '' : 's' ?></span></button>
<?php } ?>
</nav>
<?php if ( ! $storno && ! $readOnly ) { ?>
<!-- Fixierter Haupt-Aktions-Button (ersetzt die Fuß-Tableiste) -->
<div class="actionbar" id="ci-actionbar"><button type="button" class="btn" id="ci-action-btn">Aktion</button></div>
<?php } ?>
<?php if ( ! $storno && ! $readOnly && ! $isAida ) { ?>
<!-- Zahlungs-Modal -->
<div class="overlay d-none" id="ci-pay-modal">
<div class="modal">
<div class="modal-h"><span>Zahlung erfassen</span><button type="button" class="modal-x" id="ci-pay-close" aria-label="Schließen">×</button></div>
<div class="modal-b">
<label class="fld-l">Rabattaktion</label>
<select id="ci-coupon" class="sel">
<option value="0">Kein Rabatt</option>
<option value="1">5 € Bestandskundenrabatt</option>
<option value="2">10 % Frühbucherrabatt</option>
</select>
<div class="amount">Zu zahlen: <strong id="ci-amount">—</strong></div>
<label class="fld-l">Zahlungsart</label>
<div class="pay-types" id="ci-types">
<button type="button" class="pt" data-type="2">EC-Karte</button>
<button type="button" class="pt" data-type="3">Bar</button>
</div>
<div class="modal-err d-none" id="ci-pay-err"></div>
<button type="button" class="btn btn-checkin" id="ci-pay-btn">Zahlung speichern</button>
</div>
</div>
</div>
<?php } ?>
<?php if ( $canUploadImg ) { ?>
<!-- Foto-Upload-Modal -->
<div class="overlay d-none" id="ci-up-modal">
<div class="modal ph-upmodal">
<div class="modal-h"><span>Fahrzeugfotos hinzufügen</span><button type="button" class="modal-x" id="ci-up-close" aria-label="Schließen">×</button></div>
<div class="modal-b">
<div class="ph-pick">
<label class="ph-pickbtn"><input type="file" id="ci-up-cam" accept="image/*" capture="environment" hidden>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>
<span>Kamera</span></label>
<label class="ph-pickbtn"><input type="file" id="ci-up-gal" accept="image/*,.heic,.heif" multiple hidden>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg>
<span>Galerie</span></label>
</div>
<div class="ph-tiles" id="ci-up-tiles"></div>
<div class="modal-err d-none" id="ci-up-err"></div>
<button type="button" class="btn btn-checkin" id="ci-up-go" disabled>Hochladen</button>
</div>
</div>
</div>
<?php } ?>
<?php if ( $canViewImg ) { ?>
<!-- Lightbox -->
<div class="overlay lb d-none" id="ci-lb">
<div class="lb-bar">
<span class="lb-pos" id="ci-lb-pos"></span>
<span class="lb-actions">
<button type="button" class="lb-btn active" id="ci-lb-mtoggle" title="Markierungen ein/aus"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 21s-7-6.3-7-11a7 7 0 0 1 14 0c0 4.7-7 11-7 11z"/><circle cx="12" cy="10" r="2.5"/></svg></button>
<button type="button" class="lb-btn" id="ci-lb-zoom" title="Zoom"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3M11 8.5v5M8.5 11h5"/></svg></button>
<button type="button" class="lb-btn" id="ci-lb-dl" style="display:none" title="Original"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12M7 10l5 5 5-5M5 21h14"/></svg></button>
<button type="button" class="lb-btn" id="ci-lb-del" style="display:none" title="Löschen"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14"/></svg></button>
<button type="button" class="lb-btn" id="ci-lb-x" title="Schließen">×</button>
</span>
</div>
<div class="lb-stage" id="ci-lb-stage"><img id="ci-lb-img" alt="Foto" draggable="false"></div>
<button type="button" class="lb-nav lb-prev" id="ci-lb-prev" aria-label="Zurück">‹</button>
<button type="button" class="lb-nav lb-next" id="ci-lb-next" aria-label="Weiter">›</button>
<div class="lb-info" id="ci-lb-info"></div>
</div>
<?php } ?>
<!-- Kennzeichen-Herkunft (lokale DB, DE/AT/CH) -->
<div class="overlay d-none" id="ci-plate-modal">
<div class="modal pl-modal">
<div class="modal-h"><span>Kennzeichen-Herkunft</span><button type="button" class="modal-x" id="ci-pl-close" aria-label="Schließen">×</button></div>
<div class="modal-b" id="ci-pl-body"></div>
</div>
</div>
<!-- Check-in Erfolg / Countdown -->
<div class="overlay d-none" id="ci-done">
<div class="done-box">
<div class="check" id="ci-done-ic"><svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M5 13l4 4L19 7"/></svg></div>
<div class="done-t" id="ci-done-t">Check-in erfasst</div>
<div class="done-x">Zurück zur Liste in <span id="ci-cd">3</span> s …</div>
<button type="button" class="btn-cancel" id="ci-cancel">Abbrechen – hier bleiben</button>
<div class="done-skip">irgendwo tippen, um sofort zur Liste zu wechseln</div>
</div>
</div>
<script>
var CFG = <?= json_encode( $cfg ) ?>;
(function(){
// Browser-Zoom unterbinden (iOS Safari ignoriert user-scalable=no)
['gesturestart','gesturechange','gestureend'].forEach(function(ev){ document.addEventListener(ev, function(e){ e.preventDefault(); }, {passive:false}); });
document.addEventListener('touchmove', function(e){ if(e.touches && e.touches.length>1) e.preventDefault(); }, {passive:false});
// Vollbild beim ersten Tippen (nur eigenständig; eingebettet übernimmt das die Tagesliste).
if(!CFG.embed){ document.addEventListener('pointerdown', function f(ev){ if(ev.target&&ev.target.closest&&ev.target.closest('a,button,input,textarea,select,label')) return; document.removeEventListener('pointerdown',f); var e=document.documentElement,r=e.requestFullscreen||e.webkitRequestFullscreen; if(r){ try{ r.call(e); }catch(_){} } }, {passive:true}); }
var API = location.pathname; // gleiche Datei
var $ = function(id){ return document.getElementById(id); };
var coupon = $('ci-coupon'), amountEl = $('ci-amount'), status = $('ci-status');
var selType = CFG.payType || 0; // keine Zahlungsart vorausgewählt
// Eingebettet (Overlay der Tagesliste): Statusänderungen an die Elternseite melden.
function ciState(){ return CFG.storno?'x':(CFG.arrived?'i':(CFG.paid?'p':'o')); }
function parentMsg(o){ if(CFG.embed && window.parent && window.parent!==window){ o.pacimCi=1; o.b=CFG.b; try{ window.parent.postMessage(o,'*'); }catch(e){} } }
// Zeit-Kachel: zeigt Boarding / Check-in / Check-out. Standard = höchste vorhandene Zeit
// (Check-out > Check-in > Boarding). Sind mehrere Zeiten da, schaltet ein Tippen durch.
var timeIdx=null;
function ciTimeList(){ var t=CFG.times||{}, list=[];
if(!CFG.isDep && t.boarding) list.push({l:'Boarding', v:t.boarding});
if(t.checkin) list.push({l:'Check-in', v:t.checkin});
if(t.checkout) list.push({l:'Check-out',v:t.checkout});
return list; }
function renderTimeRow(){
var row=$('ci-timerow'), lab=$('ci-timelbl-tx'), val=$('ci-timeval'), cyc=$('ci-timecyc'); if(!row||!lab||!val) return;
var list=ciTimeList();
row.classList.remove('d-none');
if(!list.length){ lab.textContent = CFG.isDep ? 'Check-out' : 'Boarding'; val.textContent='–'; row.classList.remove('k-tap'); if(cyc) cyc.classList.add('d-none'); return; }
if(timeIdx===null || timeIdx>=list.length) timeIdx=list.length-1; // Standard: höchste Zeit
lab.textContent=list[timeIdx].l; val.textContent=list[timeIdx].v+' Uhr';
var multi=list.length>1;
row.classList.toggle('k-tap', multi);
if(cyc) cyc.classList.toggle('d-none', !multi);
}
(function(){ var row=$('ci-timerow'); if(!row) return;
row.addEventListener('click', function(){ var list=ciTimeList(); if(list.length<2) return;
timeIdx=((timeIdx===null?list.length-1:timeIdx)+1)%list.length; renderTimeRow(); }); })();
function eur(v){ return v.toFixed(2).replace('.',',') + ' €'; }
function setStatus(t,k){ status.textContent = t||''; status.className = 'status' + (k?(' '+k):''); }
function refreshAmount(){ if(amountEl) amountEl.textContent = eur(CFG.amounts[coupon ? coupon.value : 0]); }
function markType(){ var ts=document.querySelectorAll('#ci-types .pt'); for(var i=0;i<ts.length;i++) ts[i].classList.toggle('on', parseInt(ts[i].dataset.type,10)===selType); }
function statusBadge(){
var sb=$('ci-status-badge'); if(!sb) return;
var cls,txt;
if(CFG.storno){ cls='s-red'; txt='Storniert'; }
else if(CFG.isDep){ if(CFG.arrived){ cls='s-green'; txt='Abgereist'; } else { cls='s-blue'; txt='Abreise offen'; } }
else if(CFG.arrived){ cls='s-green'; txt='Angereist'; }
else if(CFG.paid){ cls='s-blue'; txt='Check-in offen'; }
else { cls='s-orange'; txt = CFG.canAct ? (eur(CFG.amounts[CFG.coupon||0])+' offen') : 'Zahlung offen'; }
sb.className='hstat '+cls; sb.textContent=txt;
}
var PAYNAMES={1:'Überweisung',2:'EC-Karte',3:'Bar'};
function renderState(){
statusBadge();
if(CFG.storno) return;
var paidSec=$('ci-paid-sec'), btn=$('ci-action-btn');
if(paidSec){
if(CFG.paid && !CFG.isAida){
paidSec.classList.remove('d-none');
$('ci-paid-info').textContent = eur(CFG.amounts[CFG.coupon||0]) + ' · ' + (PAYNAMES[CFG.payType]||'');
} else paidSec.classList.add('d-none');
}
if(btn){
btn.disabled=false;
if(CFG.isDep){
// Abreisen: Check-out erfassen / zurücknehmen (keine Zahlungs-Logik).
if(CFG.arrived){ btn.textContent='Check-out zurücknehmen'; btn.className='btn btn-danger'; btn.dataset.act='uncheckout'; }
else { btn.textContent='Check-out erfassen'; btn.className='btn btn-checkin'; btn.dataset.act='checkout'; }
}
else if(CFG.arrived){ btn.textContent='Fahrzeug auschecken'; btn.className='btn btn-danger'; btn.dataset.act='uncheckin'; }
else if(CFG.paid){ btn.textContent='Check-in durchführen'; btn.className='btn btn-checkin'; btn.dataset.act='checkin'; }
else { btn.textContent='Zahlung erfassen'; btn.className='btn btn-warn'; btn.dataset.act='pay'; }
}
// Navigations-Karten + Rechnung/Zahlungen-Unterseiten synchron halten
var amt=eur(CFG.amounts[CFG.coupon||0]);
var isPaid = !!(CFG.paid || CFG.isAida);
function paintPaid(el){ if(!el) return; el.classList.toggle('paid', isPaid); el.classList.toggle('open', !isPaid); }
var ncrg=$('ci-nc-rg'); if(ncrg){ ncrg.textContent=amt; paintPaid(ncrg); }
var rgCs=$('ci-rg-cardst'); if(rgCs){ rgCs.textContent = isPaid ? 'bezahlt' : 'offen'; paintPaid(rgCs); }
var rgAmt=$('ci-rg-amount'); if(rgAmt){ rgAmt.textContent=amt; paintPaid(rgAmt); }
var rgSt=$('ci-rg-status'); if(rgSt){ rgSt.textContent = CFG.isAida ? 'bezahlt · myAIDA' : (CFG.paid ? ('bezahlt · '+(PAYNAMES[CFG.payType]||'')) : 'offen'); paintPaid(rgSt); }
}
if(coupon){ coupon.value = CFG.coupon || 0; coupon.addEventListener('change', refreshAmount); refreshAmount(); }
var types=document.querySelectorAll('#ci-types .pt');
for(var i=0;i<types.length;i++){ types[i].addEventListener('click', function(){ selType=parseInt(this.dataset.type,10); markType(); payClearErr(); }); }
markType();
function post(data, cb){
var fd=new URLSearchParams(); fd.set('b',CFG.b); fd.set('h',CFG.h); if(CFG.t) fd.set('t',CFG.t);
for(var k in data) fd.set(k,data[k]);
fetch(API,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded;charset=UTF-8'},body:fd.toString()})
.then(function(r){return r.json();}).then(cb).catch(function(){ setStatus('Serverfehler.','err'); });
}
// ---- Zahlungs-Modal
var payModal=$('ci-pay-modal'), payErr=$('ci-pay-err');
function payError(m){ if(payErr){ payErr.textContent=m||'Fehler.'; payErr.classList.remove('d-none'); } else setStatus(m,'err'); }
function payClearErr(){ if(payErr){ payErr.textContent=''; payErr.classList.add('d-none'); } }
function openPay(){ if(!payModal) return; selType=CFG.payType||0; if(coupon){coupon.value=CFG.coupon||0; refreshAmount();} markType(); payClearErr(); payModal.classList.remove('d-none'); }
function closePay(){ if(payModal) payModal.classList.add('d-none'); }
var payClose=$('ci-pay-close'); if(payClose) payClose.addEventListener('click', closePay);
if(payModal) payModal.addEventListener('click', function(e){ if(e.target===payModal) closePay(); });
var payBtn=$('ci-pay-btn');
if(payBtn) payBtn.addEventListener('click', function(){
payClearErr();
if(!selType){ payError('Bitte Zahlungsart wählen.'); return; }
payBtn.disabled=true;
post({action:'pay', type:selType, coupon:(coupon?coupon.value:0)}, function(res){
payBtn.disabled=false;
if(!res||!res.ok){ payError((res&&res.error)||'Fehler.'); return; } // Fehler bleibt im Modal, keine Rechnung erstellt
CFG.paid=true; CFG.coupon=res.coupon; CFG.payType=res.type;
closePay(); renderState(); parentMsg({state:ciState()});
setStatus('✓ Zahlung erfasst ('+res.amount_fmt+' · '+res.type_name+')','ok');
});
});
var editBtn=$('ci-edit-btn');
if(editBtn) editBtn.addEventListener('click', function(){ openPay(); });
var IN_ICON='<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M5 13l4 4L19 7"/></svg>';
var OUT_ICON='<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M14 3h5a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-5"/><path d="M11 16l4-4-4-4"/><path d="M15 12H3"/></svg>';
function startCountdown(mode){
var out=(mode==='out');
var ov=$('ci-done'); if(!ov){ if(CFG.embed) parentMsg({close:1}); else location.href=CFG.back; return; }
ov.classList.toggle('out', out);
var ic=$('ci-done-ic'); if(ic) ic.innerHTML = out?OUT_ICON:IN_ICON;
var tt=$('ci-done-t'); if(tt) tt.textContent = CFG.isDep ? (out?'Check-out zurückgenommen':'Abreise erfasst') : (out?'Fahrzeug ausgecheckt':'Check-in erfasst');
ov.classList.remove('d-none');
var n=3, cd=$('ci-cd'), done=false; cd.textContent=n;
var t=setInterval(function(){ n--; cd.textContent=n; if(n<=0) finish(); },1000);
function finish(){ if(done) return; done=true; clearInterval(t); if(CFG.embed){ parentMsg({close:1}); } else { location.href=CFG.back; } }
function stay(){ if(done) return; done=true; clearInterval(t); ov.classList.add('d-none'); }
var cBtn=$('ci-cancel'); if(cBtn) cBtn.onclick=function(e){ e.stopPropagation(); stay(); }; // Abbrechen -> hier bleiben
ov.onclick=function(e){ if(e.target.closest('#ci-cancel')) return; finish(); }; // woanders -> sofort weiter
}
var actBtn=$('ci-action-btn');
if(actBtn) actBtn.addEventListener('click', function(){
var act=actBtn.dataset.act;
if(act==='pay'){ openPay(); return; }
if(act==='uncheckin'){
actBtn.disabled=true; setStatus('Check-in wird zurückgenommen …');
post({action:'uncheckin'}, function(res){
if(!res||!res.ok){ actBtn.disabled=false; setStatus((res&&res.error)||'Fehler.','err'); return; }
CFG.arrived=false; CFG.ciTime=''; if(CFG.times) CFG.times.checkin=''; timeIdx=null; renderState(); renderTimeRow(); parentMsg({state:ciState()}); setStatus('Check-in zurückgenommen.','ok');
startCountdown('out');
});
return;
}
if(act==='checkin'){
actBtn.disabled=true; setStatus('Check-in …');
post({action:'checkin'}, function(res){
if(!res||!res.ok){ actBtn.disabled=false; setStatus((res&&res.error)||'Fehler.','err'); return; }
CFG.arrived=true; CFG.ciTime=res.time||CFG.ciTime; if(CFG.times) CFG.times.checkin=res.time||CFG.ciTime; timeIdx=null; renderState(); renderTimeRow(); parentMsg({state:'i'}); startCountdown('in');
});
return;
}
if(act==='checkout'){
actBtn.disabled=true; setStatus('Check-out …');
post({action:'checkout'}, function(res){
if(!res||!res.ok){ actBtn.disabled=false; setStatus((res&&res.error)||'Fehler.','err'); return; }
CFG.arrived=true; CFG.ciTime=res.time||CFG.ciTime; if(CFG.times) CFG.times.checkout=res.time||CFG.ciTime; timeIdx=null; renderState(); renderTimeRow(); parentMsg({state:'i'}); startCountdown('in');
});
return;
}
if(act==='uncheckout'){
actBtn.disabled=true; setStatus('Check-out wird zurückgenommen …');
post({action:'uncheckout'}, function(res){
if(!res||!res.ok){ actBtn.disabled=false; setStatus((res&&res.error)||'Fehler.','err'); return; }
CFG.arrived=false; CFG.ciTime=''; if(CFG.times) CFG.times.checkout=''; timeIdx=null; renderState(); renderTimeRow(); parentMsg({state:ciState()}); setStatus('Check-out zurückgenommen.','ok');
startCountdown('out');
});
return;
}
});
// Kennzeichen ändern – direkt im Kennzeichen-Feld (Klick rein aktiviert, Speichern-Button rechts)
var plate=$('ci-plate'), plSave=$('ci-plate-save'), plOrig = plate ? plate.textContent : '';
if(plate && plSave && !plate.getAttribute('data-ro')){
function startEdit(){
if(plate.isContentEditable) return;
plOrig = plate.textContent;
plate.contentEditable='true'; plate.classList.add('editing'); plSave.classList.remove('d-none');
plate.focus();
try{ var rng=document.createRange(); rng.selectNodeContents(plate); rng.collapse(false); var sel=getSelection(); sel.removeAllRanges(); sel.addRange(rng); }catch(e){}
}
function stopEdit(){ plate.contentEditable='false'; plate.classList.remove('editing'); plSave.classList.add('d-none'); }
plate.addEventListener('click', startEdit);
plate.addEventListener('keydown', function(ev){
if(ev.key==='Enter'){ ev.preventDefault(); plSave.click(); }
else if(ev.key==='Escape'){ plate.textContent=plOrig; stopEdit(); }
});
// Klick außerhalb: ohne Änderung Bearbeiten beenden; mit ungespeicherter Änderung nachfragen.
plate.addEventListener('blur', function(){
if(!plate.isContentEditable) return;
var cur=(plate.textContent||'').replace(/\s+/g,' ').trim();
var org=(plOrig||'').replace(/\s+/g,' ').trim();
if(cur===org || cur===''){ plate.textContent=plOrig; stopEdit(); return; }
if(confirm('Kennzeichen „'+cur+'" speichern?\n\nOK = Speichern · Abbrechen = Verwerfen')){ plSave.click(); }
else { plate.textContent=plOrig; stopEdit(); }
});
plSave.addEventListener('mousedown', function(ev){ ev.preventDefault(); }); // Fokus im Feld halten (kein blur beim Speichern)
plSave.addEventListener('click', function(ev){
ev.stopPropagation();
var v=(plate.textContent||'').replace(/\s+/g,' ').trim();
if(!v){ setStatus('Bitte ein Kennzeichen eingeben.','err'); return; }
setStatus('Kennzeichen wird gespeichert …');
post({action:'plate', plate:v}, function(res){
if(!res||!res.ok){ setStatus((res&&res.error)||'Fehler.','err'); plate.textContent=plOrig; return; }
plate.textContent=res.plate; plOrig=res.plate; stopEdit(); parentMsg({rows:1});
setStatus('✓ Kennzeichen aktualisiert','ok');
});
});
}
// Zurück-Button: über pointerup steuern (iOS/Standalone verschluckt oft den ERSTEN click -> Tap wirkte tot).
// Der Kompatibilitäts-click wird per Zeitfenster unterdrückt (zustandslos, kann nicht desynchronisieren).
// __wab markiert den Button, damit der globale Loader-click-Trigger ihn NICHT zusätzlich auslöst.
var backLink=$('ci-back');
if(backLink){
backLink.__wab=1;
var ciBackLt=-1;
function ciBackAct(){
// Nicht im Hauptslide (Rechnung/Notizen/Fotos/Kontakt) -> erst zurück zur Übersicht.
var ap=document.querySelector('.pane.active');
if(ap && ap.getAttribute('data-pane')!=='buchung'){ if(window.ciGoto) window.ciGoto('buchung'); return; }
// Auf dem Hauptslide: im Overlay schließen (sofort, Liste liegt schon darunter),
// sonst mit Lade-Overlay dem href zur Anreiseliste folgen.
if(CFG.embed){ parentMsg({close:1}); return; }
if(window.waLoad) window.waLoad(backLink.href);
location.assign(backLink.href);
}
backLink.addEventListener('pointerup', function(e){ if(e.button&&e.button!==0) return; ciBackLt=e.timeStamp; e.preventDefault(); ciBackAct(); });
backLink.addEventListener('click', function(e){ e.preventDefault(); if(ciBackLt>=0 && (e.timeStamp-ciBackLt)<800) return; ciBackAct(); });
}
// ---- Rechnungs-Posten (POS) rendern + hinzufügen/entfernen ----
function posEsc(s){ return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&','<':'<','>':'>','"':'"'}[c]; }); }
var posList=$('ci-pos-list'), posPicker=$('ci-pos-picker'), posAddBtn=$('ci-pos-addbtn'), posMsg=$('ci-pos-msg');
function renderPositions(){
if(!posList) return;
var items=CFG.posItems||[], h='';
for(var i=0;i<items.length;i++){ var it=items[i];
h+='<div class="rp"><span class="rp-n">'+posEsc(it.title)+(it.qty>1?' <span class="rp-q">× '+it.qty+'</span>':'')+'</span>'
+'<span class="rp-r"><span class="rp-p">'+eur(it.gross)+'</span>'
+((CFG.canEditPos && it.removable)?'<button type="button" class="rp-del" data-pos="'+it.id+'" aria-label="Entfernen">×</button>':'')
+'</span></div>';
}
h+='<div class="rp rp-total"><span class="rp-n">Gesamt</span><span class="rp-p">'+eur(CFG.amounts[0])+'</span></div>';
posList.innerHTML=h;
}
function renderPicker(){ if(!posPicker) return; var ps=CFG.addProducts||[], h='';
for(var i=0;i<ps.length;i++){ h+='<button type="button" class="rg-pick" data-add="'+ps[i].id+'"><span class="rg-pick-t">'+posEsc(ps[i].title)+'</span><span class="rg-pick-p">'+eur(ps[i].gross)+'</span></button>'; }
posPicker.innerHTML=h;
}
function posResult(res){ if(!res||!res.ok){ if(posMsg){ posMsg.textContent=(res&&res.error)||'Fehler.'; posMsg.className='status err'; } return false; }
CFG.posItems=res.positions; CFG.amounts=res.amounts; renderPositions(); renderState(); refreshAmount(); return true; }
if(posAddBtn){ renderPicker(); posAddBtn.addEventListener('click', function(){ if(posPicker) posPicker.classList.toggle('d-none'); }); }
if(posPicker) posPicker.addEventListener('click', function(e){ var b=e.target.closest('[data-add]'); if(!b) return; b.disabled=true;
post({action:'addpos', product_id:b.getAttribute('data-add')}, function(res){ b.disabled=false; if(posResult(res)){ posPicker.classList.add('d-none'); if(posMsg){ posMsg.textContent='✓ Posten hinzugefügt'; posMsg.className='status ok'; } } }); });
if(posList) posList.addEventListener('click', function(e){ var d=e.target.closest('.rp-del'); if(!d) return; d.disabled=true;
post({action:'delpos', pos_id:d.getAttribute('data-pos')}, function(res){ if(!posResult(res)) d.disabled=false; }); });
renderPositions();
renderState();
renderTimeRow();
// Live-Zustandsabgleich: Die App-Shell kann aus dem Service-Worker-Cache stammen und veraltet sein –
// z. B. wenn das Fahrzeug inzwischen (auch automatisch im Hintergrund) ein-/ausgecheckt oder bezahlt wurde.
// Ein POST geht am Cache vorbei und liefert den ECHTEN Serverzustand; Button + Zeiten werden korrigiert.
post({action:'state'}, function(res){
if(!res || !res.ok) return;
if(typeof res.arrived==='boolean') CFG.arrived=res.arrived;
if(typeof res.paid==='boolean') CFG.paid=res.paid;
if(typeof res.storno==='boolean') CFG.storno=res.storno;
if(res.times){ CFG.times=res.times; }
timeIdx=null;
renderState(); renderTimeRow();
parentMsg({state:ciState()});
});
})();
/* ============================================================ Fahrzeugfotos */
(function(){
if(!CFG.imgView && !CFG.imgUpload) return;
var $=function(id){return document.getElementById(id);};
var grid=$('ci-ph-grid'), countEl=$('ci-ph-count'), emptyEl=$('ci-ph-empty');
var API=CFG.imgApi, images=[];
function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&','<':'<','>':'>','"':'"'}[c];});}
function authParams(fd){ fd.append('b',CFG.b); if(CFG.t) fd.append('t',CFG.t); if(CFG.h) fd.append('h',CFG.h); return fd; }
function listUrl(){ var q='action=list&b='+CFG.b; if(CFG.t)q+='&t='+encodeURIComponent(CFG.t); if(CFG.h)q+='&h='+encodeURIComponent(CFG.h); return API+'?'+q; }
// ---------- Galerie
function loadGallery(){
if(!CFG.imgView){ if(emptyEl) emptyEl.style.display=''; return; }
fetch(listUrl(),{headers:{'X-Requested-With':'XMLHttpRequest'}}).then(function(r){return r.json();}).then(function(res){
if(!res||!res.success){ return; }
images=res.images||[]; renderGrid();
}).catch(function(){});
}
var NOTE_SVG='<svg viewBox="0 0 24 24" fill="none" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>';
function renderGrid(){
if(countEl){ countEl.textContent=images.length; countEl.classList.toggle('zero', images.length===0); }
var tbP=document.getElementById('ci-tb-photos'); if(tbP) tbP.textContent = images.length + (images.length===1?' Foto':' Fotos');
if(emptyEl) emptyEl.style.display = images.length? 'none':'';
if(window.ciSliderResize) window.ciSliderResize();
if(!grid) return;
var h='';
for(var i=0;i<images.length;i++){ var im=images[i];
var mb=(im.marker_count>0)?'<span class="ph-mbadge"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2a7 7 0 0 0-7 7c0 5.2 7 13 7 13s7-7.8 7-13a7 7 0 0 0-7-7zm0 9.5A2.5 2.5 0 1 1 12 6a2.5 2.5 0 0 1 0 5.5z"/></svg>'+im.marker_count+'</span>':'';
h+='<div class="ph-thumb" data-i="'+i+'"><img src="'+esc(im.thumb_url)+'" loading="lazy" alt="">'+mb+((im.note&&im.note_visible!==false)?'<span class="ph-ndot">'+NOTE_SVG+'</span>':'')+'</div>';
}
grid.innerHTML=h;
}
if(grid) grid.addEventListener('click', function(e){ var t=e.target.closest('.ph-thumb'); if(t) openLb(parseInt(t.dataset.i,10)); });
// ---------- Lightbox
var lb=$('ci-lb'), lbImg=$('ci-lb-img'), lbInfo=$('ci-lb-info'), lbPos=$('ci-lb-pos'),
lbDel=$('ci-lb-del'), lbDl=$('ci-lb-dl'), lbStage=$('ci-lb-stage');
var idx=0, scale=1, tx=0, ty=0;
// Ladespinner über der Bühne (sichtbar beim Durchsliden bis das Bild geladen ist).
var lbSpin=document.createElement('div'); lbSpin.className='ci-lb-spin'; if(lbStage) lbStage.appendChild(lbSpin);
function showSpin(){ if(lbSpin) lbSpin.classList.add('on'); }
function hideSpin(){ if(lbSpin) lbSpin.classList.remove('on'); }
function applyT(){ lbImg.style.transform='translate('+tx+'px,'+ty+'px) scale('+scale+')'; if(typeof mSync==='function') mSync(); }
function resetT(){ scale=1; tx=0; ty=0; applyT(); }
function showInfo(im){
var meta=[]; meta.push(im.created_at_formatted); if(im.created_by_name) meta.push('von '+esc(im.created_by_name));
if(CFG.plate) meta.push(esc(CFG.plate)); if(CFG.bookingNo) meta.push(esc(CFG.bookingNo));
if(im.file_size_formatted) meta.push(im.file_size_formatted+(im.width?' · '+im.width+'×'+im.height:''));
var note = (im.note&&im.note_visible!==false)? esc(im.note) : '';
var html='';
if(note) html+='<div class="lb-note">'+note+'</div>'; else html+='<div class="lb-note" style="opacity:.6;font-weight:400;font-style:italic">Keine Notiz</div>';
html+='<div class="lb-meta">'+meta.join(' · ')+'</div>';
if(im.can_edit) html+='<button type="button" class="lb-edit" id="ci-lb-noteedit">Notiz bearbeiten</button>';
lbInfo.innerHTML=html;
var ne=$('ci-lb-noteedit'); if(ne) ne.addEventListener('click', function(){ editNote(im); });
if(lbDel) lbDel.style.display=im.can_delete?'':'none';
if(lbDl) lbDl.style.display=(im.can_download&&im.original_url)?'':'none';
}
function openLb(i){ if(!lb||i<0||i>=images.length) return; idx=i; var im=images[i];
showSpin(); lbImg.src=im.image_url; resetT(); lbPos.textContent=(i+1)+' / '+images.length; showInfo(im);
lb.classList.remove('d-none'); document.documentElement.style.overflow='hidden';
if(typeof mLoad==='function') mLoad();
}
function closeLb(){ if(lb){ lb.classList.add('d-none'); lbImg.src=''; document.documentElement.style.overflow='';
if(typeof mHide==='function') mHide(); mMarkers=[]; if(mLayer) mLayer.innerHTML=''; } }
function nav(d){ var n=idx+d; if(n<0||n>=images.length) return; openLb(n); }
if($('ci-lb-x')) $('ci-lb-x').addEventListener('click', closeLb);
if($('ci-lb-prev')) $('ci-lb-prev').addEventListener('click', function(){ nav(-1); });
if($('ci-lb-next')) $('ci-lb-next').addEventListener('click', function(){ nav(1); });
if($('ci-lb-zoom')) $('ci-lb-zoom').addEventListener('click', function(){ scale = scale>1?1:2.5; if(scale===1){tx=0;ty=0;} applyT(); });
if(lbDl) lbDl.addEventListener('click', function(){ var im=images[idx]; if(im&&im.original_url) window.open(im.original_url,'_blank'); });
if(lbDel) lbDel.addEventListener('click', function(){ delImage(images[idx]); });
// Touch: Pinch-Zoom + Pan + Wisch-Navigation + Doppeltipp + Lange-drücken (Marker setzen).
var pts={}, startDist=0, startScale=1, startTx=0, startTy=0, panX=0, panY=0, lastTap=0, lpTimer=null;
function dist(a,b){ var dx=a.x-b.x, dy=a.y-b.y; return Math.sqrt(dx*dx+dy*dy); }
function lpCancel(){ if(lpTimer){ clearTimeout(lpTimer); lpTimer=null; } }
if(lbStage){
lbStage.addEventListener('pointerdown', function(e){ pts[e.pointerId]={x:e.clientX,y:e.clientY}; var k=Object.keys(pts);
if(k.length===1){ panX=e.clientX; panY=e.clientY; startTx=tx; startTy=ty;
var now=Date.now(); if(now-lastTap<300){ scale=scale>1?1:2.5; if(scale===1){tx=0;ty=0;} applyT(); } lastTap=now;
// Gedrückt halten -> Marker an dieser Stelle (nur mit Recht, nicht auf einem Marker)
lpCancel(); if(CFG.imgNote && !(e.target&&e.target.closest&&e.target.closest('.ci-mdot'))){ var lx=e.clientX, ly=e.clientY; lpTimer=setTimeout(function(){ lpTimer=null; addMarkerLP(lx,ly); }, 500); }
} else if(k.length===2){ lpCancel(); startDist=dist(pts[k[0]],pts[k[1]]); startScale=scale; }
});
lbStage.addEventListener('pointermove', function(e){ if(!pts[e.pointerId]) return; pts[e.pointerId]={x:e.clientX,y:e.clientY}; var k=Object.keys(pts);
if(lpTimer && (Math.abs(e.clientX-panX)>10 || Math.abs(e.clientY-panY)>10)) lpCancel();
if(k.length===2){ var d=dist(pts[k[0]],pts[k[1]]); if(startDist>0){ scale=Math.min(4,Math.max(1,startScale*d/startDist)); if(scale===1){tx=0;ty=0;} applyT(); } }
else if(k.length===1 && scale>1){ tx=startTx+(e.clientX-panX); ty=startTy+(e.clientY-panY); applyT(); }
});
function up(e){ lpCancel(); var wasPan=(Object.keys(pts).length===1 && scale<=1);
var sx=pts[e.pointerId]?pts[e.pointerId].x:panX; delete pts[e.pointerId];
if(wasPan){ var dx=sx-panX; if(Math.abs(dx)>50) nav(dx<0?1:-1); }
if(Object.keys(pts).length===0 && scale<=1){ tx=0;ty=0;applyT(); }
}
lbStage.addEventListener('pointerup', up); lbStage.addEventListener('pointercancel', up);
}
// Marker per Lange-drücken setzen: Punkt erfassen, 3× als Zoomstufe speichern, Notiz abfragen.
function addMarkerLP(cx,cy){
var im=images[idx]; if(!im) return; var r=lbImg.getBoundingClientRect();
if(cx<r.left||cx>r.right||cy<r.top||cy>r.bottom) return;
var rx=(cx-r.left)/r.width, ry=(cy-r.top)/r.height;
if(navigator.vibrate) try{ navigator.vibrate(30); }catch(_){}
// Zoom geräteunabhängig: 1,5× der aktuellen mobilen Anzeige (relativ zur Originalauflösung gespeichert).
var mZoom=(lbImg.offsetWidth&&lbImg.naturalWidth)?(lbImg.offsetWidth*1.5/lbImg.naturalWidth):1.5;
var v=prompt('Markierung setzen – Notiz (optional):',''); var note=(v===null)?'':v.trim();
var fd=authParams(new FormData()); fd.append('id',im.id); fd.append('rx',rx.toFixed(5)); fd.append('ry',ry.toFixed(5)); fd.append('zoom',mZoom.toFixed(3)); if(note) fd.append('note',note);
fetch(API+'?action=marker_add',{method:'POST',body:fd}).then(function(r2){return r2.json();}).then(function(res){
if(!res||!res.success){ alert((res&&res.error)||'Marker konnte nicht gesetzt werden.'); return; }
if(!mVisible){ mVisible=true; if(mTog) mTog.classList.add('active'); }
mMarkers.push(res.marker); mRender();
}).catch(function(){ alert('Fehler beim Setzen des Markers.'); });
}
function editNote(im){ if(!im.can_edit) return; var v=prompt('Notiz zum Bild:', im.note||''); if(v===null) return;
var fd=authParams(new FormData()); fd.append('id',im.id); fd.append('note',v);
fetch(API+'?action=note',{method:'POST',body:fd}).then(function(r){return r.json();}).then(function(res){
if(res&&res.success){ im.note=res.note; showInfo(im); renderGrid(); } else alert((res&&res.error)||'Fehler.');
}).catch(function(){ alert('Fehler.'); });
}
function delImage(im){ if(!im||!im.can_delete) return; if(!confirm('Dieses Foto löschen?')) return;
var fd=authParams(new FormData()); fd.append('id',im.id);
fetch(API+'?action=delete',{method:'POST',body:fd}).then(function(r){return r.json();}).then(function(res){
if(res&&res.success){ closeLb(); loadGallery(); } else alert((res&&res.error)||'Fehler.');
}).catch(function(){ alert('Fehler.'); });
}
// ---------- Upload
if(CFG.imgUpload){
var addBtn=$('ci-ph-add'), upModal=$('ci-up-modal'), tiles=$('ci-up-tiles'),
goBtn=$('ci-up-go'), upErr=$('ci-up-err'), cam=$('ci-up-cam'), gal=$('ci-up-gal');
var queue=[]; // {file, url, note, el, bar, st, status}
function openUp(){ queue=[]; tiles.innerHTML=''; goBtn.disabled=true; upErr.classList.add('d-none'); upModal.classList.remove('d-none'); }
function closeUp(){ upModal.classList.add('d-none'); queue.forEach(function(q){ if(q.url) URL.revokeObjectURL(q.url); }); queue=[]; tiles.innerHTML=''; }
if(addBtn) addBtn.addEventListener('click', openUp);
if($('ci-up-close')) $('ci-up-close').addEventListener('click', closeUp);
if(upModal) upModal.addEventListener('click', function(e){ if(e.target===upModal) closeUp(); });
function addFiles(fl){
var MAX=10;
for(var i=0;i<fl.length;i++){ if(queue.length>=MAX){ upErr.textContent='Maximal '+MAX+' Bilder auf einmal.'; upErr.classList.remove('d-none'); break; }
var f=fl[i]; if(!/^image\//.test(f.type) && !/\.(heic|heif)$/i.test(f.name||'')) continue; // HEIC: Typ oft leer -> per Endung zulassen
addTile(f);
}
goBtn.disabled = queue.length===0;
}
function addTile(f){
var url=URL.createObjectURL(f);
var el=document.createElement('div'); el.className='ph-tile';
el.innerHTML='<div class="ph-tibox"><img src="'+url+'" alt=""><button type="button" class="ph-rm">×</button><div class="ph-st d-none"></div><div class="ph-bar"></div></div>'+
(CFG.imgNote?'<input type="text" class="ph-tnote" maxlength="500" placeholder="Notiz (optional)">':'');
tiles.appendChild(el);
var item={file:f,url:url,note:'',el:el,bar:el.querySelector('.ph-bar'),st:el.querySelector('.ph-st'),status:'wait'};
var noteInp=el.querySelector('.ph-tnote'); if(noteInp) noteInp.addEventListener('input',function(){ item.note=this.value; });
el.querySelector('.ph-rm').addEventListener('click',function(){ if(item.status==='up') return; var ix=queue.indexOf(item); if(ix>=0)queue.splice(ix,1); URL.revokeObjectURL(item.url); el.remove(); goBtn.disabled=queue.length===0; });
queue.push(item);
}
if(cam) cam.addEventListener('change',function(){ addFiles(this.files); this.value=''; });
if(gal) gal.addEventListener('change',function(){ addFiles(this.files); this.value=''; });
function setSt(item,txt,show){ item.st.textContent=txt||''; item.st.classList.toggle('d-none', !show); }
function uploadItem(item, done){
// Original in voller Qualität hochladen – keine Canvas-Umwandlung/Skalierung mehr.
item.status='up'; setSt(item,'Wird hochgeladen…',true); item.bar.style.width='5%';
var fd=authParams(new FormData()); fd.append('image',item.file,item.file.name||'photo.jpg'); if(item.note) fd.append('note',item.note);
var xhr=new XMLHttpRequest(); xhr.open('POST',API+'?action=upload');
xhr.upload.onprogress=function(e){ if(e.lengthComputable){ item.bar.style.width=Math.max(5,Math.round(e.loaded/e.total*95))+'%'; } };
xhr.onload=function(){ var res; try{res=JSON.parse(xhr.responseText);}catch(_){res=null;}
if(res&&res.success){ item.status='done'; item.el.classList.add('done'); item.bar.style.width='100%'; setSt(item,'✓ Fertig',true); done(true); }
else { item.status='err'; item.el.classList.add('err'); setSt(item,(res&&res.error)||'Fehler',true); done(false); } };
xhr.onerror=function(){ item.status='err'; item.el.classList.add('err'); setSt(item,'Netzwerkfehler',true); done(false); };
xhr.send(fd);
}
if(goBtn) goBtn.addEventListener('click', function(){
var pend=queue.filter(function(q){return q.status==='wait';}); if(!pend.length) return;
goBtn.disabled=true; upErr.classList.add('d-none');
var i=0, okCount=0;
(function next(){ if(i>=pend.length){ // fertig
loadGallery();
if(okCount===pend.length){ closeUp(); }
else { upErr.textContent='Einige Bilder konnten nicht hochgeladen werden.'; upErr.classList.remove('d-none'); goBtn.disabled=false; goBtn.textContent='Erneut versuchen'; }
return; }
uploadItem(pend[i], function(ok){ if(ok)okCount++; i++; next(); });
})();
});
}
// ---------- Befund-Marker (mobil: ansehen, ein-/ausblenden; Erstellen nur im CRM mit Lupe) ----------
var mLayer=null, mMarkers=[], mPrev=null, mVisible=true, mTog=$('ci-lb-mtoggle');
function mAuthQ(){ var q=''; if(CFG.t) q+='&t='+encodeURIComponent(CFG.t); if(CFG.h) q+='&h='+encodeURIComponent(CFG.h)+'&b='+CFG.b; return q; }
function mEnsure(){ if(!lbStage) return;
if(!mLayer){ mLayer=document.createElement('div'); mLayer.className='ci-mlayer'; lbStage.appendChild(mLayer);
mLayer.addEventListener('click', function(e){ var d=e.target.closest('.ci-mdot'); if(d) mShow(parseInt(d.dataset.i,10), d); }); }
if(!mPrev){ mPrev=document.createElement('div'); mPrev.className='ci-mprev'; lbStage.appendChild(mPrev); }
}
function mHide(){ if(mPrev) mPrev.classList.remove('on'); }
function mSync(){ if(!mLayer) return; mLayer.style.transform=lbImg.style.transform||''; mLayer.style.setProperty('--mInv', scale?(1/scale):1); }
function mLoad(){ var im=images[idx]; mMarkers=[]; if(!im||!CFG.imgView){ mRender(); return; }
fetch(API+'?action=markers&id='+im.id+mAuthQ(),{headers:{'X-Requested-With':'XMLHttpRequest'}}).then(function(r){return r.json();}).then(function(res){ if(res&&res.success){ mMarkers=res.markers||[]; mRender(); } }).catch(function(){});
}
function mRender(){ mEnsure(); if(!mLayer) return; mLayer.innerHTML='';
if(!mVisible){ mLayer.style.display='none'; return; } mLayer.style.display='';
if(!mMarkers.length) return; var ow=lbImg.offsetWidth, oh=lbImg.offsetHeight; if(ow<2) return;
mLayer.style.left=lbImg.offsetLeft+'px'; mLayer.style.top=lbImg.offsetTop+'px'; mLayer.style.width=ow+'px'; mLayer.style.height=oh+'px';
for(var i=0;i<mMarkers.length;i++){ var m=mMarkers[i]; var d=document.createElement('button'); d.type='button'; d.className='ci-mdot'; d.textContent=(i+1); d.style.left=(m.rx*ow)+'px'; d.style.top=(m.ry*oh)+'px'; d.dataset.i=i; mLayer.appendChild(d); }
mSync();
}
function mShow(i, dot){ var m=mMarkers[i], im=images[idx]; if(!m||!im) return; mEnsure();
var sr=lbStage.getBoundingClientRect(), bw=210, bh=140, Z=m.zoom;
var nw=lbImg.naturalWidth||lbImg.offsetWidth, nh=lbImg.naturalHeight||lbImg.offsetHeight, bgW=nw*Z, bgH=nh*Z; // geräteunabhängig (Originalauflösung)
var zcur=lbImg.offsetWidth?(bgW/lbImg.offsetWidth):Z;
var note=(m.note&&m.note_visible!==false)?esc(m.note):'';
var h='<div class="ci-mprev-img" style="background-image:url(\''+im.image_url+'\');background-size:'+bgW+'px '+bgH+'px;background-position:'+(bw/2-m.rx*bgW)+'px '+(bh/2-m.ry*bgH)+'px"></div>';
h+='<div class="ci-mprev-b"><div class="ci-mprev-z">Zoom '+(Math.round(zcur*10)/10).toString().replace('.',',')+'×</div>';
h+='<div class="ci-mprev-note">'+(note||'<span class="ci-mprev-empty">Keine Notiz</span>')+'</div>';
h+='<div class="ci-mprev-meta">'+esc(m.created_at_formatted)+(m.created_by_name?' · '+esc(m.created_by_name):'')+'</div>';
h+='<div class="ci-mprev-act">'+(m.can_delete?'<button type="button" class="ci-mprev-del">Löschen</button>':'')+'<button type="button" class="ci-mprev-x">×</button></div></div>';
mPrev.innerHTML=h; mPrev.style.width=bw+'px';
var dr=dot?dot.getBoundingClientRect():null; var dx=dr?(dr.left+dr.width/2-sr.left):sr.width/2, dy=dr?(dr.top+dr.height/2-sr.top):sr.height/2;
var px=dx+18, py=dy-10; if(px+bw>sr.width) px=dx-bw-18; if(px<0)px=4; if(py+240>sr.height) py=sr.height-240; if(py<0)py=4;
mPrev.style.left=px+'px'; mPrev.style.top=py+'px'; mPrev.classList.add('on');
var db=mPrev.querySelector('.ci-mprev-del'); if(db) db.onclick=function(){ mDel(m); };
mPrev.querySelector('.ci-mprev-x').onclick=mHide;
}
function mDel(m){ if(!m.can_delete) return; if(!confirm('Diesen Marker löschen?')) return;
var fd=authParams(new FormData()); fd.append('mid',m.id);
fetch(API+'?action=marker_delete',{method:'POST',body:fd}).then(function(r){return r.json();}).then(function(res){ if(res&&res.success){ var ix=mMarkers.indexOf(m); if(ix>=0)mMarkers.splice(ix,1); mHide(); mRender(); } else alert((res&&res.error)||'Fehler.'); }).catch(function(){ alert('Fehler.'); });
}
if(lbImg){ lbImg.addEventListener('load', mRender); lbImg.addEventListener('load', hideSpin); lbImg.addEventListener('error', hideSpin); }
if(mTog) mTog.addEventListener('click', function(){ mVisible=!mVisible; mTog.classList.toggle('active',mVisible); if(!mVisible) mHide(); mRender(); });
loadGallery();
})();
/* ============================================================ Footer-Tabs + interne Notizen */
(function(){
var $=function(id){return document.getElementById(id);};
var tabbar=$('ci-tabbar'); if(!tabbar) return;
var panes={ buchung:$('pane-buchung'), rechnung:$('pane-rechnung'), notizen:$('pane-notizen'), fotos:$('pane-fotos'), kontakt:$('pane-kontakt') };
function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&','<':'<','>':'>','"':'"'}[c];});}
// ---- Tab-Slider (horizontal swipen, Touch + Maus). Footer-Tabs bleiben synchron.
var slider=$('ci-slider'), track=$('ci-track');
var order=['buchung','rechnung','notizen','fotos','kontakt'].filter(function(n){return !!panes[n];});
var idx=0, notesLoaded=false;
function paneH(name){ return panes[name]?panes[name].offsetHeight:0; }
function syncTabs(name){
var tabs=tabbar.querySelectorAll('.tab'), i;
for(i=0;i<tabs.length;i++) tabs[i].classList.toggle('active', tabs[i].getAttribute('data-tab')===name);
for(var k in panes){ if(panes[k]) panes[k].classList.toggle('active', k===name); }
}
function setIndex(i, scrollTop){
i=Math.max(0,Math.min(order.length-1,i)); idx=i; var name=order[i];
if(track) track.style.transform='translateX(-'+(i*100)+'%)';
syncTabs(name);
if(scrollTop && panes[name]) panes[name].scrollTop=0;
if(name==='notizen' && !notesLoaded){ notesLoaded=true; loadNotes(); }
}
function activate(name){ var i=order.indexOf(name); if(i>=0) setIndex(i,true); }
// App-Shell: Panes scrollen selbst, keine Höhenanpassung nötig (Hook bleibt für Aufrufer ohne Effekt).
function resizeActive(){}
window.ciSliderResize=resizeActive;
window.ciGoto=activate; // Navigation für Karten/Zurück-Buttons (data-go)
tabbar.addEventListener('click', function(e){ var b=e.target.closest('.tab'); if(b) activate(b.getAttribute('data-tab')); });
// Karten (Übersicht) + „Übersicht"-Zurück in den Unterseiten
document.addEventListener('click', function(e){ var g=e.target.closest('[data-go]'); if(g){ e.preventDefault(); activate(g.getAttribute('data-go')); } });
// Drag/Swipe via Pointer Events (touch-action:pan-y -> vertikales Scrollen bleibt nativ).
if(slider && track && order.length>1){
var sx=0, sy=0, dragging=false, decided=false, didSwipe=false, pid=null;
slider.addEventListener('pointerdown', function(e){
if(e.pointerType==='mouse' && e.button!==0) return;
sx=e.clientX; sy=e.clientY; dragging=true; decided=false; didSwipe=false; pid=e.pointerId;
});
slider.addEventListener('pointermove', function(e){
if(!dragging) return;
var dx=e.clientX-sx, dy=e.clientY-sy;
if(!decided){
if(Math.abs(dx)<7 && Math.abs(dy)<7) return;
if(Math.abs(dx)<=Math.abs(dy)){ dragging=false; return; } // vertikal -> nativ scrollen
decided=true; didSwipe=true;
track.classList.add('dragging');
try{ slider.setPointerCapture(pid); }catch(_){}
}
var w=slider.offsetWidth||1, pct=dx/w*100, base=-idx*100;
if((idx===0 && dx>0) || (idx===order.length-1 && dx<0)) pct*=0.35; // Rand-Widerstand
track.style.transform='translateX('+(base+pct)+'%)';
if(e.cancelable) e.preventDefault();
});
function endDrag(e){
if(!dragging) return; dragging=false;
if(!decided){ return; }
track.classList.remove('dragging');
var dx=e.clientX-sx, th=(slider.offsetWidth||1)*0.18, ni=idx;
if(dx<=-th) ni=idx+1; else if(dx>=th) ni=idx-1;
setIndex(ni,false);
}
slider.addEventListener('pointerup', endDrag);
slider.addEventListener('pointercancel', function(){ if(dragging&&decided){ track.classList.remove('dragging'); dragging=false; setIndex(idx,false); } else dragging=false; });
// Klick direkt nach einem Swipe unterdrücken (damit ein Wisch über Button/Thumb nicht auslöst).
slider.addEventListener('click', function(e){ if(didSwipe){ didSwipe=false; e.stopPropagation(); e.preventDefault(); } }, true);
}
// Initiale Badges aus CFG (vor dem ersten Öffnen)
setNotesBadge(CFG.notesCount||0);
var pc0=CFG.photosCount||0; var tbP=$('ci-tb-photos'); if(tbP) tbP.textContent = pc0 + (pc0===1?' Foto':' Fotos');
// ---- Notizen-API
var API=location.pathname, curNotes=[];
function npost(params, cb){
params.b=CFG.b; if(CFG.t)params.t=CFG.t; if(CFG.h)params.h=CFG.h;
var body=Object.keys(params).map(function(k){return encodeURIComponent(k)+'='+encodeURIComponent(params[k]);}).join('&');
fetch(API,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},body:body})
.then(function(r){return r.json();}).then(cb).catch(function(){ cb({ok:false,error:'Netzwerkfehler'}); });
}
function setNotesBadge(n){ var tb=$('ci-tb-notes'); if(tb) tb.textContent = n + (n===1?' Notiz':' Notizen'); var c=$('ci-nt-count'); if(c){ c.textContent=n; c.classList.toggle('zero', n===0); } }
// Hervorgehobene Notizen als Endlos-Lauftext im Datenblatt (Buchung-Tab).
var mqEl=$('ci-marquee'), mqTrack=$('ci-mq-track');
function setMarquee(texts){
if(!mqEl||!mqTrack) return;
var t=(texts||[]).join(' ••• ');
if(!t){ mqEl.classList.add('d-none'); mqTrack.innerHTML=''; return; }
mqEl.classList.remove('d-none');
mqTrack.innerHTML='<span class="ci-mq-seg">'+esc(t)+'</span><span class="ci-mq-seg" aria-hidden="true">'+esc(t)+'</span>';
mqTrack.style.animationDuration=Math.max(8, Math.round(t.length*0.28))+'s';
}
// Anfangs-Lauftext (server-seitig gerendert) -> Dauer passend setzen.
if(mqEl && !mqEl.classList.contains('d-none') && mqTrack){ var seg0=mqTrack.querySelector('.ci-mq-seg'); if(seg0) setMarquee([seg0.textContent]); }
function loadNotes(){ npost({action:'notes'}, function(res){ if(res&&res.ok){ renderNotes(res.notes||[]); setMarquee(res.marquee||[]); } }); }
function renderNotes(notes){
curNotes=notes; setNotesBadge(notes.length);
var list=$('ci-nt-list'), empty=$('ci-nt-empty');
if(empty) empty.classList.toggle('d-none', notes.length>0);
if(!list) return;
var h='';
for(var i=0;i<notes.length;i++){ var n=notes[i];
h+='<div class="nt-item'+(n.highlight?' nt-hl':'')+'" data-id="'+n.id+'">'
+(n.highlight?'<span class="nt-hlbadge">★ Hervorgehoben</span>':'')
+'<div class="nt-item-text">'+esc(n.text).replace(/\n/g,'<br>')+'</div>'
+'<div class="nt-item-meta">'+esc(n.user)+' · '+esc(n.created)+(n.updated?' · bearbeitet '+esc(n.updated):'')+'</div>'
+((n.canEdit||n.canDel)?'<div class="nt-item-act">'
+(n.canEdit?'<button type="button" class="nt-a nt-edit">Bearbeiten</button>':'')
+(n.canDel?'<button type="button" class="nt-a nt-del">Löschen</button>':'')
+'</div>':'')
+'</div>';
}
list.innerHTML=h;
resizeActive();
}
// Neue Notiz
var saveBtn=$('ci-nt-save'), txt=$('ci-nt-text'), err=$('ci-nt-err'), hlAdd=$('ci-nt-hl');
function showErr(m){ if(err){ err.textContent=m; err.classList.remove('d-none'); } }
function hideErr(){ if(err) err.classList.add('d-none'); }
if(saveBtn){ saveBtn.addEventListener('click', function(){
var v=(txt.value||'').trim(); hideErr();
if(!v){ showErr('Bitte einen Text eingeben.'); return; }
saveBtn.disabled=true;
npost({action:'note_add', text:v, highlight:(hlAdd&&hlAdd.checked)?1:0}, function(res){ saveBtn.disabled=false;
if(res&&res.ok){ txt.value=''; if(hlAdd) hlAdd.checked=false; loadNotes(); } else showErr((res&&res.error)||'Fehler beim Speichern.'); });
}); }
// Bearbeiten / Löschen (Inline)
var list=$('ci-nt-list');
if(list){ list.addEventListener('click', function(e){
var item=e.target.closest('.nt-item'); if(!item) return; var id=item.getAttribute('data-id');
if(e.target.closest('.nt-del')){ if(!confirm('Diese Notiz löschen?')) return;
npost({action:'note_del', id:id}, function(res){ if(res&&res.ok) loadNotes(); else alert((res&&res.error)||'Fehler.'); }); return; }
if(e.target.closest('.nt-edit')){ startEdit(item, id); return; }
if(e.target.closest('.nt-ecancel')){ renderNotes(curNotes); return; }
if(e.target.closest('.nt-esave')){ var ta=item.querySelector('.nt-eta'); var v=(ta.value||'').trim();
if(!v){ alert('Bitte einen Text eingeben.'); return; }
var eh=item.querySelector('.nt-ehl');
npost({action:'note_edit', id:id, text:v, highlight:(eh&&eh.checked)?1:0}, function(res){ if(res&&res.ok) loadNotes(); else alert((res&&res.error)||'Fehler.'); }); return; }
}); }
function startEdit(item, id){
var n=null; for(var i=0;i<curNotes.length;i++) if(String(curNotes[i].id)===String(id)) n=curNotes[i];
if(!n) return;
item.innerHTML='<textarea class="nt-eta" rows="3" maxlength="4000"></textarea>'
+'<label class="nt-hl-opt"><input type="checkbox" class="nt-ehl"'+(n.highlight?' checked':'')+'> <span>Hervorheben – Lauftext im Datenblatt</span></label>'
+'<div class="nt-eactions"><button type="button" class="nt-a nt-ecancel">Abbrechen</button><button type="button" class="nt-a nt-esave">Speichern</button></div>';
var ta=item.querySelector('.nt-eta'); ta.value=n.text; ta.focus(); resizeActive();
}
})();
/* ============================================================ Kennzeichen-Herkunft (Modal + lokale DB) */
(function(){
var $=function(id){return document.getElementById(id);};
var btn=$('ci-plate-info'), modal=$('ci-plate-modal'), body=$('ci-pl-body'), plate=$('ci-plate');
if(!btn || !modal) return;
function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&','<':'<','>':'>','"':'"'}[c];});}
function open(){ modal.classList.remove('d-none'); }
function close(){ modal.classList.add('d-none'); }
function row(l,v){ return '<div class="pl-row"><span>'+esc(l)+'</span><b>'+esc(v)+'</b></div>'; }
function render(d){
if(!d || !d.found){ body.innerHTML='<div class="pl-empty">Zu diesem Kennzeichen liegen keine Daten vor.</div>'; return; }
var title = d.ort || d.region;
var h='<div class="pl-head"><span class="pl-code">'+esc(d.code)+'</span>'
+'<div class="pl-htxt"><div class="pl-region">'+esc(title)+'</div>'
+'<div class="pl-sub">'+esc(d.state)+' · '+esc(d.countryName)+'</div></div></div>';
h+='<div class="pl-rows">';
if(d.ort && d.region.indexOf(d.ort)===0) h+=row('Stadt', d.ort); // kreisfreie Stadt ("Wuppertal, Stadt")
else if(d.ort) h+=row('Ort', d.ort)+row('Landkreis', d.region);
else h+=row(d.regionLabel, d.region);
h+=row(d.stateLabel, d.state)+row('Land', d.countryName)+'</div>';
if(d.alts && d.alts.length){
h+='<div class="pl-alt"><span>auch:</span> '+d.alts.map(function(a){ return esc(a.region)+' ('+esc(a.country)+')'; }).join(', ')+'</div>';
}
if(d.map) h+='<div class="pl-mapwrap">'+d.map+'</div>';
body.innerHTML=h;
}
btn.addEventListener('click', function(e){
e.preventDefault(); e.stopPropagation();
body.innerHTML='<div class="pl-empty">Lädt …</div>'; open();
var cur=plate?plate.textContent.trim():'';
var p='action=plateinfo&b='+CFG.b+'&plate='+encodeURIComponent(cur);
if(CFG.t)p+='&t='+encodeURIComponent(CFG.t); if(CFG.h)p+='&h='+encodeURIComponent(CFG.h);
fetch(location.pathname,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},body:p})
.then(function(r){return r.json();}).then(render).catch(function(){ body.innerHTML='<div class="pl-empty">Fehler beim Laden.</div>'; });
});
if($('ci-pl-close')) $('ci-pl-close').addEventListener('click', close);
modal.addEventListener('click', function(e){ if(e.target===modal) close(); });
document.addEventListener('keydown', function(e){ if(e.key==='Escape' && !modal.classList.contains('d-none')) close(); });
})();
</script>
<?php /* „Zum Startbildschirm hinzufügen"-Anleitung nur bei direktem Token-Aufruf (nicht eingebettet, nicht im CRM). */
if ( $tok !== '' && ! $embed ) echo pacim_webapp_install_html( 'Seaside Parking', SITE_URL . '/assets/webapp/icon-192.png' );
/* Service Worker (Offline-Navigation) + Offline-Banner – nur eigenständig, nicht im eingebetteten iframe
(dort zeigt die Eltern-Ansicht das Banner bereits). */
if ( ! $embed && $tok !== '' && function_exists( 'pacim_webapp_offline_html' ) ) echo pacim_webapp_offline_html();
/* Team-Chat-Overlay – nur eigenständig (nicht im CRM-iframe). $bid aktiviert „Diese Buchung senden";
lift hebt den Button über die Footer-Tabbar. */
if ( ! $embed && function_exists( 'pacim_chat_widget_html' ) ) echo pacim_chat_widget_html( $tok, '', (int) $bid, 76 );
// Lade-Overlay (Feedback beim Zurück-Navigieren zur Anreiseliste) – nur eigenständig, nicht im Overlay-iframe.
if ( ! $embed && function_exists( 'pacim_webapp_loader_html' ) ) echo pacim_webapp_loader_html(); ?>
</body></html>
<?php
exit();
/* ----------------------------------------- Helfer (HTML/Fehler/CSS) */
function ci_error_page( $code = 403, $msg = 'Dieser Link ist ungültig oder abgelaufen.' ) {
http_response_code( $code );
header( 'Content-Type: text/html; charset=utf-8' );
echo '<!DOCTYPE html><html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">'
. '<title>' . $code . '</title><style>' . ci_css() . '</style></head><body>'
. '<div class="err"><h1>' . $code . '</h1><p>' . htmlspecialchars( $msg, ENT_QUOTES, 'UTF-8' ) . '</p></div></body></html>';
exit();
}
function ci_css() {
return '
:root{--navy:#0d2c44;--navy2:#11436b;--blue:#1b6ca8;--green:#2fb344;--red:#d63939;--gold:#f3b53f;--ink:#16384f;--muted:#5b7488;--line:#e6eef4;}
*{box-sizing:border-box;margin:0;padding:0;}
html{-webkit-text-size-adjust:100%;text-size-adjust:100%;height:100%;}
/* App-Shell: volle Viewporthöhe, kein Body-Scroll – gescrollt wird der Inhaltsbereich (Pane). */
/* position:fixed/inset:0 statt height:100dvh – auf iOS füllt das zuverlässig den echten Viewport
(100dvh resolved dort teils kürzer → Tableiste schwebte über dem unteren Rand). */
body{font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:#fff;color:var(--ink);position:fixed;inset:0;display:flex;flex-direction:column;align-items:center;padding:0;overflow:hidden;touch-action:manipulation;}
/* iOS Standalone: Karte nicht unter Dynamic Island / Statusleiste (nur direkt geöffnet, nicht im Overlay). */
/* Randlos: Safe-Area oben übernimmt der Karten-Header (.hd) selbst; seitlich nur Notch-Schutz (Querformat). */
body.ci-sa{padding-left:env(safe-area-inset-left,0px);padding-right:env(safe-area-inset-right,0px);}
.d-none{display:none!important;}
.card{width:100%;max-width:460px;background:#fff;overflow:hidden;flex:1 1 auto;min-height:0;display:flex;flex-direction:column;}
.card>.hd{flex:0 0 auto;}
.hd{position:relative;background:linear-gradient(135deg,var(--navy) 0%,var(--navy2) 65%,var(--blue) 130%);color:#fff;padding:20px 22px 18px;}
/* Standalone (vom Home-Bildschirm): Navy-Header füllt randlos bis oben, Inhalt unter die Dynamic Island. */
body.ci-sa .hd{padding-top:calc(20px + env(safe-area-inset-top,0px));}
.hd .back{position:absolute;top:14px;right:16px;width:38px;height:38px;display:flex;align-items:center;justify-content:center;border-radius:50%;background:rgba(255,255,255,.14);color:#fff;text-decoration:none;-webkit-tap-highlight-color:transparent;transition:background .12s ease,transform .08s ease;}
.hd .back:active{transform:scale(.92);background:rgba(255,255,255,.24);}
.hd .back svg{width:22px;height:22px;}
body.ci-sa .hd .back{top:calc(14px + env(safe-area-inset-top,0px));right:calc(16px + env(safe-area-inset-right,0px));}
.hd .k{font-size:11px;font-weight:700;letter-spacing:.16em;text-transform:uppercase;opacity:.8;}
.hd .t{font-size:22px;font-weight:800;margin-top:3px;}
.hd .no{font-size:13px;opacity:.85;margin-top:2px;}
.bd{padding:14px 16px 4px;}
/* Kachel-Layout (Datenblatt) */
.bd .kachel{background:#f6f9fc;border:1px solid var(--line);border-radius:14px;padding:11px 14px;}
.bd .k-l{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);margin-bottom:3px;}
.bd .k-v{font-size:16px;font-weight:800;color:var(--navy);line-height:1.2;word-break:break-word;}
.bd .k-v .muted{font-size:12px;font-weight:600;}
.bd .k-sub{font-size:11.5px;font-weight:600;color:var(--muted);margin-top:2px;}
/* Zeit-Kachel: antippbar zum Umschalten der Zeiten (Boarding/Check-in/Check-out) */
.bd .kachel.k-tap{cursor:pointer;-webkit-tap-highlight-color:transparent;}
.bd .kachel.k-tap:active{background:#eef4f9;}
.bd .k-l{display:flex;align-items:center;gap:5px;}
.bd .k-cyc{width:12px;height:12px;color:var(--blue);opacity:.75;flex:0 0 auto;}
.bd .k-plate{margin-bottom:10px;padding:11px 14px 14px;overflow:visible;}
.bd .k-plate .plate-wrap{margin:5px 0 0;}
.bd .k-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;}
.plate-wrap{position:relative;margin:2px 0 14px;}
.plate{border:2px solid #111;border-radius:8px;text-align:center;font-family:"EuroPlate",ui-monospace,monospace;font-size:30px;font-weight:700;letter-spacing:1px;padding:9px 6px;color:#111;cursor:text;-webkit-text-stroke:.6px currentColor;}
.plate.editing{outline:2px solid var(--blue);outline-offset:1px;padding-right:64px;text-align:left;padding-left:14px;}
.plate-save{position:absolute;right:6px;top:6px;bottom:6px;width:50px;border:0;border-radius:6px;background:var(--green);color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;}
.plate-save svg{width:22px;height:22px;}
/* Info-Button (Kennzeichen-Herkunft) als Badge an der Plaketten-Ecke */
.plate-info{position:absolute;top:-14px;right:-14px;width:44px;height:44px;border-radius:50%;background:#fff;color:var(--blue);
border:2px solid var(--blue);box-shadow:0 3px 10px rgba(13,44,68,.26);display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:3;-webkit-tap-highlight-color:transparent;}
.plate-info svg{width:26px;height:26px;}
.plate-info:active{transform:scale(.92);}
.plate.editing ~ .plate-info{display:none;}
/* Kennzeichen-Herkunft-Modal */
.pl-head{display:flex;align-items:center;gap:14px;margin-bottom:14px;}
.pl-code{flex:0 0 auto;font-family:"EuroPlate",ui-monospace,monospace;font-weight:700;font-size:26px;letter-spacing:1px;
border:2px solid #111;border-radius:7px;padding:6px 12px;color:#111;-webkit-text-stroke:.5px currentColor;background:#fff;}
.pl-region{font-size:18px;font-weight:800;color:var(--navy);line-height:1.2;}
.pl-sub{font-size:13px;color:var(--muted);margin-top:2px;}
.pl-rows{border-top:1px solid var(--line);margin-bottom:14px;}
.pl-row{display:flex;justify-content:space-between;gap:12px;padding:9px 0;border-bottom:1px solid var(--line);font-size:14.5px;}
.pl-row span{color:var(--muted);} .pl-row b{color:var(--ink);text-align:right;}
.pl-alt{background:#fff8e6;border:1px solid #f3e3b0;border-radius:10px;padding:9px 12px;font-size:13px;color:#8a6400;margin-bottom:14px;}
.pl-alt span{font-weight:800;}
.pl-mapwrap{background:#eef3f7;border-radius:12px;padding:10px;display:flex;justify-content:center;}
.pl-map{width:auto;max-width:100%;max-height:46vh;height:auto;display:block;}
.pl-reg{fill:#fff;stroke:#9fb3c4;stroke-width:.8;}
.pl-reg.active{fill:#1b6ca8;stroke:#0d2c44;stroke-width:1;}
.pl-mark-halo{fill:rgba(214,57,57,.25);}
.pl-pin{fill:#d63939;stroke:#fff;stroke-width:1.2;}
.pl-pin-dot{fill:#fff;}
.pl-empty{color:var(--muted);text-align:center;padding:24px 6px;font-size:14.5px;}
.muted{color:var(--muted);font-weight:400;}
.row{display:flex;justify-content:space-between;gap:12px;padding:9px 0;border-top:1px solid var(--line);font-size:14.5px;}
.row:first-of-type{border-top:0;}
.row .l{color:var(--muted);} .row .v{font-weight:600;text-align:right;}
.row .v .v-sub{display:block;font-size:12px;font-weight:500;color:var(--muted);margin-top:1px;}
.badges{display:flex;gap:8px;margin:14px 0 2px;}
.badge{flex:1;border-radius:12px;padding:11px;text-align:center;font-weight:700;font-size:14px;}
.b-open{background:rgba(214,57,57,.10);color:var(--red);} .b-ok{background:rgba(47,179,68,.12);color:#1d7a33;} .b-warn{background:rgba(243,181,63,.16);color:#8a6400;}
/* Status-Badge im Header (ganz unten rechts, flush in der Ecke) */
.hstat{position:absolute;right:0;bottom:0;font-size:16.25px;font-weight:700;padding:8px 16px;border-radius:8px 0 0 0;white-space:nowrap;}
.hstat.s-blue{background:#2f86d6;color:#fff;} .hstat.s-orange{background:#f59f00;color:#3a2c00;}
.hstat.s-green{background:#2fb344;color:#fff;} .hstat.s-red{background:#d63939;color:#fff;}
.btn.btn-out{background:#eef4f9;color:var(--navy);}
.btn-cancel{margin-top:22px;background:#fff;color:var(--navy);border:0;border-radius:12px;padding:15px 28px;font-size:16px;font-weight:700;cursor:pointer;}
.done-skip{margin-top:14px;font-size:12.5px;opacity:.7;}
.sec{padding:14px 22px;border-top:1px solid var(--line);}
.sec-h{font-weight:700;font-size:15px;color:var(--navy);margin-bottom:10px;}
.fld-l{display:block;font-size:12px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;margin:8px 0 5px;}
.sel{width:100%;padding:12px;border:1px solid var(--line);border-radius:10px;font-size:15px;background:#fff;color:var(--ink);}
.amount{margin:12px 0;font-size:15px;}
.amount strong{font-size:22px;color:var(--navy);}
.pay-types{display:flex;gap:8px;}
.pt{flex:1;padding:13px 6px;border:2px solid var(--line);background:#fff;border-radius:12px;font-size:14px;font-weight:700;color:var(--ink);cursor:pointer;}
.pt.on{border-color:var(--blue);background:rgba(27,108,168,.08);color:var(--blue);}
.btn{display:block;width:100%;border:0;border-radius:12px;padding:15px;font-size:16px;font-weight:700;text-align:center;cursor:pointer;margin-top:12px;}
.btn-pay{background:var(--green);color:#fff;} .btn-primary{background:var(--blue);color:#fff;} .btn-light{background:#eef4f9;color:var(--navy);}
.btn-warn{background:#f59f00;color:#fff;} .btn-checkin{background:var(--green);color:#fff;} .btn-danger{background:var(--red);color:#fff;}
.btn[disabled]{background:#e7eef4;color:#9bacba;cursor:not-allowed;}
#ci-action-btn{padding:18px;font-size:17px;border-radius:14px;box-shadow:0 4px 14px rgba(13,44,68,.10);}
.lnk{background:none;border:0;color:#1d7a33;font-weight:700;text-decoration:underline;cursor:pointer;font-size:13.5px;padding:0;margin-left:4px;}
.modal{width:100%;max-width:420px;background:#fff;border-radius:0;overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,.35);}
.modal-h{display:flex;align-items:center;justify-content:space-between;background:var(--navy);color:#fff;padding:15px 18px;font-size:16px;font-weight:800;}
.modal-x{background:none;border:0;color:#cfe2f2;font-size:26px;line-height:1;cursor:pointer;padding:0 2px;}
.modal-b{padding:16px 18px 20px;}
.hint{font-size:12px;color:var(--muted);text-align:center;margin-top:8px;}
.modal-err{background:rgba(214,57,57,.10);color:var(--red);border-radius:10px;padding:11px 13px;font-size:13.5px;font-weight:600;margin-top:12px;}
.note{border-radius:10px;padding:12px 14px;font-size:14px;}
.note.ok{background:rgba(47,179,68,.10);color:#1d7a33;} .note.warn{background:rgba(243,181,63,.16);color:#8a6400;}
.note.info{background:rgba(27,108,168,.10);color:#155a8a;}
.status{padding:6px 22px 16px;font-size:13.5px;font-weight:600;text-align:center;}
.status.ok{color:#1d7a33;} .status.err{color:var(--red);}
/* ---- Footer-Tab-Navigation + Panes (App-Shell) ---- */
/* Slider füllt den Platz zwischen Header und Tab-Footer; jede Pane scrollt vertikal für sich. */
.slider{position:relative;overflow:hidden;touch-action:pan-y;flex:1 1 auto;min-height:0;}
.track{display:flex;align-items:stretch;width:100%;height:100%;transition:transform .28s cubic-bezier(.22,.61,.36,1);}
.track.dragging{transition:none;}
.pane{flex:0 0 100%;width:100%;min-width:0;height:100%;overflow-y:auto;-webkit-overflow-scrolling:touch;touch-action:pan-y;}
@media (prefers-reduced-motion: reduce){ .track{transition:none;} }
/* Bottom-Abstand: Android (kein Home-Indikator → inset≈0) per Basiswert;
iPhone mit Home-Indikator (inset>0) sitzt knapp darüber (Basis + inset). */
.tabbar{display:none;} /* Fuß-Tabs durch Navigations-Karten ersetzt; Element bleibt für die Slider-Engine im DOM */
/* Fixierter Haupt-Aktions-Button unten (wo vorher die Tabs waren) */
.actionbar{flex:0 0 auto;width:100%;max-width:460px;background:#fff;border-top:1px solid var(--line);
box-shadow:0 -2px 14px rgba(13,44,68,.10);padding:8px 16px max(6px, env(safe-area-inset-bottom,0px));}
.actionbar .btn{margin-top:0;padding:18px;font-size:17px;border-radius:14px;}
/* Navigations-Karten (Übersicht) */
.navcards{padding:4px 16px 18px;display:flex;flex-direction:column;gap:10px;}
.navcard{display:flex;align-items:center;gap:13px;width:100%;background:#fff;border:1px solid var(--line);border-radius:16px;
padding:13px 14px;cursor:pointer;text-align:left;font:inherit;box-shadow:0 2px 8px rgba(13,44,68,.05);-webkit-tap-highlight-color:transparent;}
.navcard:active{filter:brightness(.97);}
.nc-ic{flex:0 0 auto;width:44px;height:44px;border-radius:50%;display:flex;align-items:center;justify-content:center;color:#fff;}
.nc-ic svg{width:23px;height:23px;}
.ic-green{background:#2fb344;} .ic-blue{background:#1b6ca8;} .ic-amber{background:#f0a82e;} .ic-teal{background:#1c9c8f;}
.nc-tx{flex:1;min-width:0;display:flex;flex-direction:column;}
.nc-t{font-size:16px;font-weight:800;color:var(--navy);line-height:1.2;}
.nc-s{font-size:13px;color:var(--muted);margin-top:1px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.nc-chev{flex:0 0 auto;width:20px;height:20px;color:#b6c2cf;}
.nc-amt{flex:0 0 auto;margin-left:auto;font-weight:900;font-size:21px;white-space:nowrap;letter-spacing:-.3px;}
.nc-amt.open{color:#d63939;} .nc-amt.paid{color:#2fb344;}
/* Notizen/Fotos als zwei Tabs nebeneinander */
.navtabs{display:grid;grid-template-columns:repeat(auto-fit,minmax(0,1fr));gap:10px;}
.navtab{display:flex;flex-direction:column;align-items:center;gap:6px;background:#fff;border:1px solid var(--line);border-radius:16px;padding:16px 10px;cursor:pointer;font:inherit;box-shadow:0 2px 8px rgba(13,44,68,.05);-webkit-tap-highlight-color:transparent;}
.navtab:active{filter:brightness(.97);}
.navtab-t{font-size:15px;font-weight:800;color:var(--navy);}
.navtab-s{font-size:12.5px;color:var(--muted);}
/* Unterseiten-Kopf mit Zurück */
.subhd{display:flex;align-items:center;gap:11px;padding:12px 16px 4px;}
.subback{display:inline-flex;align-items:center;gap:3px;background:#eef4f9;border:0;border-radius:10px;padding:8px 12px;font:inherit;font-size:13px;font-weight:700;color:var(--navy);cursor:pointer;-webkit-tap-highlight-color:transparent;}
.subback svg{width:16px;height:16px;}
.subhd-t{font-size:16px;font-weight:800;color:var(--navy);}
/* Kontakt-Ansicht */
.kt-card{background:#fff;border:1px solid var(--line);border-radius:16px;padding:6px 16px;box-shadow:0 2px 8px rgba(13,44,68,.05);}
.kt-name{font-size:18px;font-weight:800;color:var(--navy);padding:12px 0 4px;}
.kt-row{display:flex;flex-direction:column;gap:2px;padding:11px 0;border-top:1px solid var(--line);}
.kt-lbl{font-size:11.5px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);font-weight:700;}
.kt-val{font-size:16px;color:var(--navy);word-break:break-word;line-height:1.4;}
.kt-val a{color:#1b6ca8;text-decoration:none;}
.kt-call{display:flex;align-items:center;justify-content:center;gap:9px;margin-top:14px;background:#2fb344;color:#fff;border-radius:14px;padding:15px;font-size:17px;font-weight:800;text-decoration:none;box-shadow:0 4px 14px rgba(47,179,68,.28);-webkit-tap-highlight-color:transparent;}
.kt-call svg{width:22px;height:22px;}
.kt-call:active{filter:brightness(.95);}
/* Rechnungsbetrag riesig (rot offen / grün bezahlt) + Posten */
.rg-box{text-align:center;padding:20px 14px 6px;}
.rg-amount{font-size:54px;font-weight:900;line-height:1;letter-spacing:-1.5px;}
.rg-amount.open{color:#d63939;} .rg-amount.paid{color:#2fb344;}
.rg-status{margin-top:8px;font-size:13px;font-weight:800;text-transform:uppercase;letter-spacing:.06em;}
.rg-status.open{color:#d63939;} .rg-status.paid{color:#2fb344;}
.nc-s.open{color:#d63939;font-weight:700;} .nc-s.paid{color:#2fb344;font-weight:700;}
.rg-pos{margin:8px 16px 0;border:1px solid var(--line);border-radius:14px;overflow:hidden;}
.rg-pos .rp{display:flex;justify-content:space-between;gap:10px;padding:11px 14px;border-top:1px solid var(--line);font-size:14.5px;}
.rg-pos .rp:first-child{border-top:0;}
.rg-pos .rp .rp-n{color:var(--ink);} .rg-pos .rp .rp-q{color:var(--muted);font-weight:400;}
.rg-pos .rp .rp-p{font-weight:700;white-space:nowrap;color:var(--navy);}
.rg-pos .rp .rp-r{display:inline-flex;align-items:center;gap:10px;flex:0 0 auto;}
.rg-pos .rp.rp-total{background:#f6f9fc;font-weight:800;}
.rp-del{width:26px;height:26px;border:0;border-radius:50%;background:rgba(214,57,57,.12);color:#d63939;font-size:19px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;-webkit-tap-highlight-color:transparent;}
.rp-del:active{filter:brightness(.92);}
.rg-addwrap{margin:10px 16px 0;}
.rg-addbtn{display:flex;align-items:center;justify-content:center;gap:7px;width:100%;background:#eef4f9;border:1px dashed #b6c8d8;border-radius:12px;padding:13px;font:inherit;font-size:15px;font-weight:700;color:var(--blue);cursor:pointer;-webkit-tap-highlight-color:transparent;}
.rg-addbtn svg{width:18px;height:18px;}
.rg-picker{margin-top:10px;display:flex;flex-direction:column;gap:8px;}
.rg-pick{display:flex;align-items:center;justify-content:space-between;gap:10px;width:100%;background:#fff;border:1px solid var(--line);border-radius:12px;padding:13px 14px;font:inherit;font-size:14.5px;font-weight:600;color:var(--navy);cursor:pointer;text-align:left;-webkit-tap-highlight-color:transparent;}
.rg-pick:active{filter:brightness(.97);}
.rg-pick-p{font-weight:800;white-space:nowrap;color:var(--blue);}
.tabbar .tab{flex:1;border:0;background:none;cursor:pointer;-webkit-tap-highlight-color:transparent;font:inherit;
display:flex;flex-direction:column;align-items:center;gap:3px;padding:11px 4px 10px;position:relative;
font-size:11px;font-weight:700;color:var(--muted);transition:color .14s ease;}
.tabbar .tab svg{width:23px;height:23px;}
.tabbar .tab.active{color:var(--blue);}
.tabbar .tab.active::before{content:"";position:absolute;top:0;left:22%;right:22%;height:3px;border-radius:0 0 3px 3px;background:var(--blue);}
.tabbar .tab:active{background:rgba(27,108,168,.06);}
.tb-badge{position:absolute;top:5px;left:calc(50% + 7px);min-width:16px;height:16px;padding:0 4px;border-radius:9px;
background:var(--blue);color:#fff;font-size:10px;font-weight:800;line-height:16px;text-align:center;}
/* ---- Notizen ---- */
.nt-hint{margin:14px 22px 0;background:#fff8e6;border:1px solid #f3e3b0;border-radius:12px;padding:12px 14px;}
.nt-hint-h{display:flex;align-items:center;gap:7px;font-weight:800;font-size:13.5px;color:#8a6400;margin-bottom:6px;}
.nt-hint-h svg{width:18px;height:18px;flex:0 0 auto;}
.nt-hint-b{font-size:14px;line-height:1.45;color:var(--ink);word-break:break-word;}
.nt-hint-reply{margin-top:10px;padding-top:10px;border-top:1px dashed #e6d28f;}
.nt-hint-rh{font-size:12px;color:#8a6400;font-weight:700;cursor:pointer;list-style:none;display:flex;align-items:center;gap:6px;}
.nt-hint-reply summary::-webkit-details-marker{display:none;}
.nt-hint-rh::before{content:"\25B8";display:inline-block;transition:transform .15s ease;}
.nt-hint-reply[open] .nt-hint-rh::before{transform:rotate(90deg);}
.nt-hint-rh::after{content:" – anzeigen";font-weight:400;opacity:.8;}
.nt-hint-reply[open] .nt-hint-rh::after{content:" – ausblenden";}
.nt-hint-reply .nt-hint-b{margin-top:6px;}
.nt-block{padding:14px 22px;}
.nt-title{display:flex;align-items:center;gap:7px;}
.nt-title svg{width:18px;height:18px;flex:0 0 auto;}
.nt-add{margin:4px 0 16px;}
.nt-add textarea{width:100%;border:1px solid var(--line);border-radius:10px;padding:11px 12px;font:inherit;font-size:14.5px;resize:vertical;background:#f6f9fc;}
.nt-add textarea:focus{outline:2px solid var(--blue);outline-offset:1px;background:#fff;}
.nt-add .btn{margin-top:10px;}
.nt-list{display:flex;flex-direction:column;gap:10px;}
.nt-item{background:#f6f9fc;border:1px solid var(--line);border-radius:12px;padding:11px 13px;}
.nt-item-text{font-size:14.5px;line-height:1.45;color:var(--ink);word-break:break-word;}
.nt-item-meta{font-size:11.5px;color:var(--muted);margin-top:7px;}
.nt-item-act{display:flex;gap:16px;margin-top:9px;}
.nt-a{border:0;background:none;cursor:pointer;font:inherit;font-size:13px;font-weight:700;color:var(--blue);padding:2px 0;}
.nt-a.nt-del{color:var(--red);}
.nt-eta{width:100%;border:1px solid var(--blue);border-radius:10px;padding:10px 12px;font:inherit;font-size:14.5px;resize:vertical;}
.nt-eactions{display:flex;justify-content:flex-end;gap:18px;margin-top:8px;}
.nt-empty{color:var(--muted);font-size:14px;text-align:center;padding:18px 0;}
.nt-foot{color:var(--muted);font-size:12.5px;margin-top:14px;text-align:center;}
/* Hervorheben-Option + Badge */
.nt-hl-opt{display:flex;align-items:center;gap:8px;margin:10px 0 0;font-size:13.5px;color:var(--ink);cursor:pointer;}
.nt-hl-opt input{width:18px;height:18px;flex:0 0 auto;}
.nt-item.nt-hl{border-color:#f3c34f;background:#fff8e6;}
.nt-hlbadge{display:inline-block;background:#f3b53f;color:#3a2c00;font-size:11px;font-weight:800;border-radius:6px;padding:2px 8px;margin-bottom:7px;}
/* Endlos-Lauftext (hervorgehobene Notizen) */
.ci-marquee{display:flex;align-items:center;gap:8px;background:linear-gradient(90deg,#f3b53f,#ffd87a);color:#3a2c00;padding:8px 12px;font-weight:700;font-size:14px;}
.ci-marquee .ci-mq-ic{width:18px;height:18px;flex:0 0 auto;}
.ci-mq-win{flex:1 1 auto;overflow:hidden;}
.ci-mq-track{display:inline-flex;white-space:nowrap;will-change:transform;animation:ci-mq 18s linear infinite;}
.ci-mq-seg{padding:0 1.6em;}
@keyframes ci-mq{from{transform:translateX(0);}to{transform:translateX(-50%);}}
@media (prefers-reduced-motion: reduce){ .ci-mq-track{animation:none;} }
.foot{margin-top:16px;font-size:12px;color:var(--muted);text-align:center;}
.overlay{position:fixed;inset:0;background:rgba(13,44,68,.92);display:flex;align-items:center;justify-content:center;z-index:50;}
.done-box{text-align:center;color:#fff;padding:30px;}
.check{width:92px;height:92px;border-radius:50%;background:var(--green);color:#fff;margin:0 auto 18px;display:flex;align-items:center;justify-content:center;}
.check svg{width:50px;height:50px;}
/* Check-out-Countdown: deutlich anders (rot/orange) als der Check-in-Countdown (navy/grün) */
.overlay.out{background:rgba(122,28,28,.94);}
.overlay.out .check{background:#e8590c;}
.done-t{font-size:24px;font-weight:800;} .done-x{font-size:15px;opacity:.85;margin-top:8px;}
.err{max-width:440px;background:#fff;border-radius:18px;padding:34px 26px;text-align:center;box-shadow:0 14px 40px rgba(13,44,68,.16);margin:60px auto;}
.err h1{font-size:40px;color:var(--navy);} .err p{color:var(--muted);margin-top:8px;}
/* ---- Fahrzeugfotos ---- */
.ph-head{display:flex;align-items:center;justify-content:space-between;gap:10px;}
.ph-title{display:flex;align-items:center;gap:7px;margin:0;} .ph-title svg{width:18px;height:18px;}
.ph-count{background:var(--blue);color:#fff;border-radius:999px;font-size:12px;font-weight:800;padding:1px 8px;min-width:20px;text-align:center;}
.ph-count.zero{display:none;}
.ph-add{display:inline-flex;align-items:center;gap:5px;background:var(--blue);color:#fff;border:0;border-radius:10px;padding:9px 14px;font-size:14px;font-weight:700;cursor:pointer;}
.ph-add svg{width:16px;height:16px;}
.ph-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-top:12px;}
.ph-thumb{position:relative;aspect-ratio:1/1;border-radius:10px;overflow:hidden;background:#e6eef4;cursor:pointer;}
.ph-thumb img{width:100%;height:100%;object-fit:cover;display:block;}
.ph-mbadge{position:absolute;left:5px;top:5px;display:inline-flex;align-items:center;gap:1px;background:rgba(214,57,57,.78);color:#fff;border-radius:999px;font-size:11px;font-weight:800;padding:1px 6px 1px 4px;}
.ph-mbadge svg{width:12px;height:12px;}
.ph-ndot{position:absolute;right:5px;bottom:5px;width:18px;height:18px;border-radius:50%;background:rgba(13,44,68,.82);display:flex;align-items:center;justify-content:center;}
.ph-ndot svg{width:11px;height:11px;stroke:#fff;}
.ph-empty{color:var(--muted);font-size:13.5px;margin-top:10px;}
.ph-upmodal{max-width:460px;}
.ph-pick{display:flex;gap:10px;}
.ph-pickbtn{flex:1;display:flex;flex-direction:column;align-items:center;gap:6px;border:1.5px dashed var(--line);border-radius:12px;padding:18px 8px;cursor:pointer;color:var(--navy);font-weight:700;font-size:14px;}
.ph-pickbtn svg{width:30px;height:30px;color:var(--blue);} .ph-pickbtn:active{background:#f2f7fb;}
.ph-tiles{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:14px;}
.ph-tile{border:1px solid var(--line);border-radius:10px;overflow:hidden;background:#fff;}
.ph-tibox{position:relative;aspect-ratio:4/3;background:#e6eef4;}
.ph-tibox img{width:100%;height:100%;object-fit:cover;display:block;}
.ph-rm{position:absolute;top:4px;right:4px;width:26px;height:26px;border:0;border-radius:50%;background:rgba(13,44,68,.85);color:#fff;font-size:18px;line-height:24px;cursor:pointer;}
.ph-st{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#fff;font-size:12.5px;font-weight:700;background:rgba(13,44,68,.55);}
.ph-bar{position:absolute;left:0;bottom:0;height:4px;background:var(--green);width:0;transition:width .15s;}
.ph-tnote{width:100%;border:0;border-top:1px solid var(--line);padding:8px 9px;font-size:13px;background:#fff;}
.ph-tile.done .ph-tibox{outline:2px solid var(--green);outline-offset:-2px;}
.ph-tile.err .ph-tibox{outline:2px solid var(--red);outline-offset:-2px;}
.lb{background:rgba(8,20,30,.96);}
.lb-bar{position:absolute;top:0;left:0;right:0;display:flex;align-items:center;justify-content:space-between;padding:10px 12px 26px;z-index:4;color:#fff;background:linear-gradient(180deg,rgba(0,0,0,.62),rgba(0,0,0,0));}
.lb-pos{font-size:14px;font-weight:800;text-shadow:0 1px 3px rgba(0,0,0,.7);} .lb-actions{display:flex;gap:8px;}
.lb-btn{background:rgba(0,0,0,.62);color:#fff;border:1px solid rgba(255,255,255,.28);border-radius:10px;min-width:44px;height:44px;font-size:22px;line-height:1;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;box-shadow:0 2px 8px rgba(0,0,0,.4);}
.lb-btn.active{background:#1b6ca8;border-color:#1b6ca8;}
.lb-btn svg{width:20px;height:20px;}
.lb-stage{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;overflow:hidden;touch-action:none;}
.lb-stage img{max-width:100%;max-height:100%;user-select:none;-webkit-user-drag:none;will-change:transform;}
.ci-lb-spin{position:absolute;top:50%;left:50%;width:46px;height:46px;margin:-23px 0 0 -23px;border-radius:50%;border:4px solid rgba(255,255,255,.25);border-top-color:#fff;pointer-events:none;z-index:4;opacity:0;transition:opacity .12s ease;}
.ci-lb-spin.on{opacity:1;animation:ci-lb-spin .8s linear infinite;}
@keyframes ci-lb-spin{to{transform:rotate(360deg);}}
@media (prefers-reduced-motion: reduce){ .ci-lb-spin.on{animation:none;} }
.lb-nav{position:absolute;top:50%;transform:translateY(-50%);background:rgba(0,0,0,.5);color:#fff;border:1px solid rgba(255,255,255,.2);width:44px;height:66px;font-size:30px;cursor:pointer;z-index:4;}
.lb-prev{left:0;border-radius:0 12px 12px 0;} .lb-next{right:0;border-radius:12px 0 0 12px;}
.lb-info{position:absolute;left:0;right:0;bottom:0;background:linear-gradient(transparent,rgba(0,0,0,.78));color:#fff;padding:34px 16px 16px;z-index:4;font-size:13px;}
.lb-info .lb-note{font-size:15px;font-weight:700;margin-bottom:3px;} .lb-info .lb-meta{opacity:.85;line-height:1.5;}
.lb-info .lb-edit{color:#9fd0ff;text-decoration:underline;background:none;border:0;cursor:pointer;font-size:13px;padding:0;margin-top:5px;}
/* Befund-Marker (mobil) */
.ci-mlayer{position:absolute;pointer-events:none;z-index:3;transform-origin:50% 50%;}
.ci-mdot{position:absolute;transform:translate(-50%,-50%) scale(var(--mInv,1));width:26px;height:26px;border-radius:50%;background:rgba(214,57,57,.92);color:#fff;border:2px solid #fff;font-size:12px;font-weight:800;cursor:pointer;pointer-events:auto;box-shadow:0 2px 8px rgba(0,0,0,.55);display:flex;align-items:center;justify-content:center;}
.ci-mprev{position:absolute;z-index:7;width:210px;background:#0a141e;border:1px solid rgba(255,255,255,.22);border-radius:10px;overflow:hidden;box-shadow:0 10px 30px rgba(0,0,0,.6);display:none;}
.ci-mprev.on{display:block;}
.ci-mprev-img{width:100%;height:140px;background-repeat:no-repeat;background-color:#000;border-bottom:1px solid rgba(255,255,255,.12);}
.ci-mprev-b{padding:8px 10px;color:#fff;}
.ci-mprev-z{font-size:11px;font-weight:700;color:#9fd0ff;}
.ci-mprev-note{font-size:13px;font-weight:600;margin:3px 0;word-break:break-word;} .ci-mprev-empty{opacity:.55;font-style:italic;font-weight:400;}
.ci-mprev-meta{font-size:11px;opacity:.7;}
.ci-mprev-act{display:flex;gap:6px;margin-top:6px;}
.ci-mprev-act button{background:rgba(255,255,255,.16);color:#fff;border:0;border-radius:6px;padding:4px 9px;font-size:12px;cursor:pointer;}
.ci-mprev-act .ci-mprev-x{margin-left:auto;}
/* ---- ressourcenarme Animationen: nur opacity/transform, mit Reduced-Motion-Abschaltung ---- */
@keyframes ciFadeUp{from{opacity:0;transform:translateY(10px);}to{opacity:1;transform:none;}}
@keyframes ciFade{from{opacity:0;}to{opacity:1;}}
@keyframes ciSheet{from{opacity:.3;transform:translateY(18px);}to{opacity:1;transform:none;}}
@keyframes ciPop{0%{opacity:0;transform:scale(.5);}60%{transform:scale(1.08);}100%{opacity:1;transform:none;}}
.card{animation:ciFadeUp .3s cubic-bezier(.22,.61,.36,1) both;}
.overlay:not(.d-none){animation:ciFade .18s ease;}
.modal{animation:ciSheet .24s cubic-bezier(.22,.61,.36,1);}
.check{animation:ciPop .42s cubic-bezier(.34,1.56,.64,1) both;}
.hstat{transition:background .25s ease;}
.btn,.pt,.plate-save,.lnk{transition:transform .08s ease, filter .12s ease;}
.btn:active,.pt:active,.plate-save:active{transform:scale(.97);}
@media (prefers-reduced-motion: reduce){
.card,.overlay,.modal,.check{animation:none!important;}
.btn:active,.pt:active,.plate-save:active{transform:none;}
}
';
}