| 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
/**
* imports-api.php – JSON-Endpunkt des integrierten Import-Managers.
* Hinter dem PaCIM-Login (Session). Nutzt die importmanager-Klasse.
*
* Aktionen (?action=):
* GET search&q= -> Live-Suche (Excel) {count, html}
* POST undo_file&file_id=
* POST undo_day&date=
* POST undo_day_ship&date=&ship=
* POST excel_preview&file_path=&file_name=[&rows=] -> {status, html}
* POST run_cron -> AIDA-Import-Cron starten
*/
require_once( __DIR__ . "/includes/config.inc.php" );
require_once( __DIR__ . '/includes/functions/parkinglist-import.php' ); // ParkingList-CSV-Logik (geteilt mit Cron) – vor dem Dispatcher laden
// Saubere JSON-Antwort sicherstellen: keine PHP-Warnungen/Notices in den Output
@ini_set( 'display_errors', '0' );
header( 'Content-Type: application/json; charset=utf-8' );
header( 'Cache-Control: no-store, no-cache, must-revalidate, max-age=0' );
// --- Login-Schutz (gleiche Regel wie page::checkLogin) ---
if ( !isset( $_SESSION['user'] ) || $_SESSION['user'] != LOGIN_USER ) {
http_response_code( 403 );
echo json_encode( array( 'status' => 'error', 'success' => false, 'message' => 'Nicht angemeldet.' ) );
exit();
}
// Excel-Vorschau-Filter (vor Verwendung definieren; bedingt, da PHPExcel benötigt)
if ( !class_exists( 'ImportPreviewReadFilter' ) && interface_exists( 'PHPExcel_Reader_IReadFilter' ) ) {
class ImportPreviewReadFilter implements PHPExcel_Reader_IReadFilter {
private $startRow;
private $endRow;
public function __construct( $startRow = 1, $endRow = 250 ) {
$this->startRow = (int) $startRow;
$this->endRow = (int) $endRow;
}
public function readCell( $column, $row, $worksheetName = '' ) {
return $row >= $this->startRow && $row <= $this->endRow;
}
}
}
$im = new importmanager();
$action = isset( $_GET['action'] ) ? $_GET['action'] : ( isset( $_POST['action'] ) ? $_POST['action'] : '' );
$isPost = $_SERVER['REQUEST_METHOD'] === 'POST';
switch ( $action ) {
case 'search':
echo json_encode( $im->liveSearch( isset( $_GET['q'] ) ? $_GET['q'] : '' ) );
break;
case 'undo_file':
if ( !$isPost ) { im_bad(); }
echo json_encode( $im->undoFile( isset( $_POST['file_id'] ) ? $_POST['file_id'] : 0 ) );
break;
case 'undo_day':
if ( !$isPost ) { im_bad(); }
echo json_encode( $im->undoDay( isset( $_POST['date'] ) ? $_POST['date'] : '' ) );
break;
case 'undo_day_ship':
if ( !$isPost ) { im_bad(); }
echo json_encode( $im->undoDayShip(
isset( $_POST['date'] ) ? $_POST['date'] : '',
isset( $_POST['ship'] ) ? $_POST['ship'] : ''
) );
break;
case 'excel_preview':
if ( !$isPost ) { im_bad(); }
echo json_encode( im_excel_preview(
isset( $_POST['file_path'] ) ? (string) $_POST['file_path'] : '',
isset( $_POST['file_name'] ) ? (string) $_POST['file_name'] : '',
isset( $_POST['rows'] ) ? (int) $_POST['rows'] : 250
) );
break;
case 'run_cron':
if ( !$isPost ) { im_bad(); }
echo json_encode( im_run_cron() );
break;
case 'upload_preview':
if ( !$isPost ) { im_bad(); }
echo json_encode( im_upload_preview() );
break;
case 'parking_import':
if ( !$isPost ) { im_bad(); }
echo json_encode( im_parking_import() );
break;
case 'aida_import':
if ( !$isPost ) { im_bad(); }
echo json_encode( im_aida_import() );
break;
case 'parking_settings':
echo json_encode( im_parking_settings_get() );
break;
case 'parking_settings_save':
if ( !$isPost ) { im_bad(); }
echo json_encode( im_parking_settings_save() );
break;
case 'parking_run':
if ( !$isPost ) { im_bad(); }
echo json_encode( im_parking_run() );
break;
case 'parking_run_status':
echo json_encode( im_parking_run_status() );
break;
default:
im_bad();
}
/** ParkingList-Zugangsdaten/Status lesen (Passwort NIE ausliefern – nur „gesetzt?"). */
function im_parking_settings_get() {
return array(
'status' => 'ok', 'ok' => true,
'enabled' => ( (string) settings::get( 'parkinglist_enabled', '1' ) === '1' ),
'user' => (string) settings::get( 'parkinglist_user', '' ),
'pass_set' => ( (string) settings::get( 'parkinglist_pass', '' ) !== '' ),
);
}
/** ParkingList-Zugangsdaten speichern (Passwort verschlüsselt; leer = unverändert). */
function im_parking_settings_save() {
$user = trim( (string) ( $_POST['user'] ?? '' ) );
$enabled = ! empty( $_POST['enabled'] ) ? '1' : '0';
settings::set( 'parkinglist_user', $user );
settings::set( 'parkinglist_enabled', $enabled );
if ( isset( $_POST['pass'] ) && $_POST['pass'] !== '' ) {
settings::set( 'parkinglist_pass', function_exists( 'pacim_encrypt' ) ? pacim_encrypt( (string) $_POST['pass'] ) : (string) $_POST['pass'] );
}
return array(
'status' => 'ok', 'ok' => true, 'message' => 'Zugangsdaten gespeichert.',
'enabled' => ( $enabled === '1' ), 'user' => $user,
'pass_set' => ( (string) settings::get( 'parkinglist_pass', '' ) !== '' ),
);
}
/**
* Manuellen ParkingList-Import anstoßen (Headless-Fetch + idempotenter Import).
* Der Fetch kann länger dauern -> Lauf entkoppelt im Hintergrund (curl auf den bestehenden
* Cron-Endpunkt), Ergebnis-JSON landet in datas/parkinglist/_runs/<run>.json und wird per
* parking_run_status abgefragt.
*/
function im_parking_run() {
if ( (string) settings::get( 'parkinglist_enabled', '1' ) !== '1' )
return array( 'ok' => false, 'message' => 'Der ParkingList-Import ist deaktiviert (siehe Zugangsdaten).' );
if ( (string) settings::get( 'parkinglist_user', '' ) === '' || (string) settings::get( 'parkinglist_pass', '' ) === '' )
return array( 'ok' => false, 'message' => 'Es sind keine ParkingList-Zugangsdaten hinterlegt.' );
$dir = PATH_DATAS . '/parkinglist/_runs';
if ( ! is_dir( $dir ) ) @mkdir( $dir, 0775, true );
// Alte Läufe (> 2 h) aufräumen.
foreach ( (array) @glob( $dir . '/*.json' ) as $f ) { if ( is_file( $f ) && filemtime( $f ) < time() - 7200 ) @unlink( $f ); }
if ( ! defined( 'SITE_URL' ) ) return array( 'ok' => false, 'message' => 'SITE_URL ist nicht konfiguriert.' );
$run = bin2hex( random_bytes( 16 ) );
$out = $dir . '/' . $run . '.json';
@file_put_contents( $out, '' ); // leer = „läuft"
// curl schreibt den Body IMMER (auch bei HTTP-Fehlerstatus) -> Fehler werden sichtbar.
$url = SITE_URL . '/crons/parkinglist.php?token=eFD6dx2G4RFBkrYS7Z87';
$cmd = 'nohup curl -s -k --max-time 300 -o ' . escapeshellarg( $out ) . ' ' . escapeshellarg( $url ) . ' > /dev/null 2>&1 &';
shell_exec( $cmd );
return array( 'ok' => true, 'run' => $run );
}
/** Status/Ergebnis eines manuellen ParkingList-Laufs. */
function im_parking_run_status() {
$run = preg_replace( '/[^a-f0-9]/', '', (string) ( $_GET['run'] ?? '' ) );
if ( strlen( $run ) !== 32 ) return array( 'status' => 'error', 'message' => 'Ungültiger Lauf.' );
$out = PATH_DATAS . '/parkinglist/_runs/' . $run . '.json';
if ( ! is_file( $out ) ) return array( 'status' => 'error', 'message' => 'Lauf nicht gefunden.' );
$raw = trim( (string) @file_get_contents( $out ) );
$age = time() - (int) @filemtime( $out );
if ( $raw === '' ) return array( 'status' => 'running', 'age' => $age );
$j = json_decode( $raw, true );
if ( ! is_array( $j ) ) return array( 'status' => 'running', 'age' => $age ); // evtl. noch im Schreiben / Nicht-JSON
return array( 'status' => 'done', 'result' => $j );
}
function im_bad() {
http_response_code( 400 );
echo json_encode( array( 'status' => 'error', 'success' => false, 'message' => 'Ungültige Anfrage.' ) );
exit();
}
/* ---------- CSV-Vorschau (ParkingList o. ä.) als HTML-Tabelle ---------- */
function im_csv_preview( $filePath, $previewRows ) {
$previewRows = max( 50, min( 2000, (int) $previewRows ) );
$fh = fopen( $filePath, 'r' );
if ( !$fh ) return array( 'status' => 'error', 'message' => 'Datei nicht lesbar.' );
$esc = function ( $v ) {
$v = preg_replace( '#<br\s*/?>#i', ' · ', (string) $v );
return htmlspecialchars( strip_tags( $v ), ENT_QUOTES, 'UTF-8' );
};
$header = fgetcsv( $fh, 0, ',' );
if ( !$header ) { fclose( $fh ); return array( 'status' => 'error', 'message' => 'Leere oder ungültige CSV.' ); }
$html = '<div class="imports-preview-bar text-muted small mb-2">Vorschau: max. ' . (int) $previewRows . ' Zeilen</div>';
$html .= '<div class="table-responsive" style="max-height:75vh;"><table class="table table-sm table-bordered table-hover mb-0"><thead class="table-light"><tr>';
foreach ( $header as $h ) $html .= '<th class="text-nowrap">' . $esc( $h ) . '</th>';
$html .= '</tr></thead><tbody>';
$rows = 0;
while ( ( $r = fgetcsv( $fh, 0, ',' ) ) !== false ) {
$nonEmpty = false;
foreach ( $r as $c ) { if ( trim( (string) $c ) !== '' ) { $nonEmpty = true; break; } }
if ( !$nonEmpty ) continue;
if ( $rows++ >= $previewRows ) break;
$html .= '<tr>';
foreach ( $r as $c ) $html .= '<td class="text-nowrap small">' . $esc( $c ) . '</td>';
$html .= '</tr>';
}
fclose( $fh );
$html .= '</tbody></table></div>';
return array( 'status' => 'success', 'html' => $html );
}
/* ---------- Excel-Vorschau (portiert aus import-manager/.../read.php) ---------- */
function im_excel_preview( $filePathInput, $fileNameInput, $previewRows ) {
if ( $filePathInput === '' || $fileNameInput === '' ) {
return array( 'status' => 'error', 'message' => 'Kein Dateipfad angegeben.' );
}
$fileName = basename( $fileNameInput );
$filePath = rtrim( $filePathInput, '/' ) . '/' . $fileName;
if ( !is_file( $filePath ) || !is_readable( $filePath ) ) {
return array( 'status' => 'error', 'message' => 'Datei nicht gefunden oder nicht lesbar.' );
}
$ext = strtolower( pathinfo( $filePath, PATHINFO_EXTENSION ) );
if ( $ext === 'csv' || $ext === 'txt' ) {
return im_csv_preview( $filePath, $previewRows );
}
if ( !in_array( $ext, array( 'xls', 'xlsx' ), true ) ) {
return array( 'status' => 'error', 'message' => 'Ungültiges Dateiformat.' );
}
if ( !class_exists( 'PHPExcel_IOFactory' ) ) {
return array( 'status' => 'error', 'message' => 'PHPExcel wurde nicht geladen.' );
}
$previewRows = max( 50, min( 1000, (int) $previewRows ) );
try {
$fileMd5 = md5_file( $filePath );
$downloadUrl = SITE_URL . '/file.php?token=' . urlencode( IMPORT_DOWNLOAD_TOKEN ) . '&type=download&file=' . urlencode( $fileMd5 );
PHPExcel_Settings::setCacheStorageMethod(
PHPExcel_CachedObjectStorageFactory::cache_to_phpTemp,
array( 'memoryCacheSize' => '128MB' )
);
$reader = PHPExcel_IOFactory::createReaderForFile( $filePath );
$reader->setReadDataOnly( true );
$reader->setReadFilter( new ImportPreviewReadFilter( 1, $previewRows ) );
$excelObj = $reader->load( $filePath );
$worksheet = $excelObj->getActiveSheet();
$highestRow = min( (int) $worksheet->getHighestRow(), $previewRows );
$highestColumnIndex = PHPExcel_Cell::columnIndexFromString( $worksheet->getHighestColumn() );
$html = '<div class="imports-preview-bar d-flex justify-content-between align-items-center mb-2">';
$html .= '<span class="text-muted small">Vorschau: max. ' . (int) $previewRows . ' Zeilen</span>';
$html .= '<a href="' . htmlspecialchars( $downloadUrl, ENT_QUOTES, 'UTF-8' ) . '" class="btn btn-sm btn-primary" target="_blank">Download</a>';
$html .= '</div><div class="table-responsive" style="max-height:75vh;"><table class="table table-sm table-bordered table-hover mb-0">';
// Kopfzeile (Zeile 2) lesen, um Datumsspalten zu erkennen
$headers = array();
for ( $col = 0; $col < $highestColumnIndex; $col++ ) {
$cell = $worksheet->getCellByColumnAndRow( $col, 2 );
$headers[$col] = $cell ? trim( (string) $cell->getValue() ) : '';
}
$departureIdx = function_exists( 'findHeaderIndex' ) ? findHeaderIndex( $headers, array( 'Departure Date', 'Departure' ) ) : null;
$arrivalIdx = function_exists( 'findHeaderIndex' ) ? findHeaderIndex( $headers, array( 'Arrival Date', 'Arrival' ) ) : null;
// Kopfzeile rendern
$html .= '<thead class="table-light"><tr>';
foreach ( $headers as $h ) {
$html .= '<th class="text-nowrap">' . htmlspecialchars( $h, ENT_QUOTES, 'UTF-8' ) . '</th>';
}
$html .= '</tr></thead><tbody>';
// Datenzeilen ab Zeile 3 (Datumsspalten korrekt formatieren)
for ( $row = 3; $row <= $highestRow; $row++ ) {
$html .= '<tr>';
for ( $col = 0; $col < $highestColumnIndex; $col++ ) {
$cell = $worksheet->getCellByColumnAndRow( $col, $row );
if ( $col === $departureIdx || $col === $arrivalIdx ) {
$val = im_format_excel_date( $cell );
} else {
$val = $cell ? $cell->getFormattedValue() : '';
}
$html .= '<td class="text-nowrap">' . htmlspecialchars( (string) $val, ENT_QUOTES, 'UTF-8' ) . '</td>';
}
$html .= '</tr>';
}
$html .= '</tbody></table></div>';
$excelObj->disconnectWorksheets();
unset( $worksheet, $excelObj, $reader );
return array( 'status' => 'success', 'html' => $html );
} catch ( Exception $e ) {
error_log( 'Excel Preview Fehler: ' . $e->getMessage() );
return array( 'status' => 'error', 'message' => 'Fehler beim Lesen der Excel-Datei.' );
}
}
/* ---------- Datumszelle formatieren (Departure/Arrival -> d.m.Y) ---------- */
function im_format_excel_date( $cell ) {
if ( $cell === null ) return '';
$raw = $cell->getValue();
// Excel-Serielldatum (Zahl) -> Datum (readDataOnly liefert keine Formatmaske)
if ( is_numeric( $raw ) && (float) $raw > 0 && class_exists( 'PHPExcel_Shared_Date' ) ) {
$ts = PHPExcel_Shared_Date::ExcelToPHP( (float) $raw );
if ( $ts ) return date( 'd.m.Y', $ts );
}
// sonst String-Wert in gängigen Formaten parsen
$raw = trim( (string) $raw );
if ( $raw === '' ) return '';
foreach ( array( 'd.m.Y', 'd.m.y', 'Y-m-d', 'd/m/Y', 'd/m/y', 'Y-m-d H:i:s' ) as $fmt ) {
$dt = DateTime::createFromFormat( $fmt, $raw );
if ( $dt instanceof DateTime ) return $dt->format( 'd.m.Y' );
}
$tsp = strtotime( $raw );
return $tsp !== false ? date( 'd.m.Y', $tsp ) : $raw;
}
/* =====================================================================
* Upload-Vorschau (DRY-RUN) – erkennt automatisch AIDA-Liste vs. ParkingList.
* Es werden KEINE Datensätze importiert und KEINE Datei gespeichert.
* ===================================================================== */
function im_upload_preview() {
if ( empty( $_FILES['file'] ) || !isset( $_FILES['file']['error'] ) || $_FILES['file']['error'] !== UPLOAD_ERR_OK ) {
return array( 'ok' => false, 'message' => 'Keine Datei empfangen oder Upload-Fehler.' );
}
$tmp = $_FILES['file']['tmp_name'];
$name = (string) $_FILES['file']['name'];
$ext = strtolower( pathinfo( $name, PATHINFO_EXTENSION ) );
$size = (int) $_FILES['file']['size'];
if ( !is_uploaded_file( $tmp ) ) return array( 'ok' => false, 'message' => 'Ungültiger Upload.' );
if ( $size <= 0 ) return array( 'ok' => false, 'message' => 'Die Datei ist leer.' );
if ( $size > 20 * 1024 * 1024 ) return array( 'ok' => false, 'message' => 'Datei zu groß (max. 20 MB).' );
// ---- Typerkennung ----
$type = 'unknown';
if ( $ext === 'csv' || $ext === 'txt' ) {
$fh = fopen( $tmp, 'r' );
$headerLine = $fh ? (string) fgets( $fh, 8192 ) : '';
if ( $fh ) fclose( $fh );
$hl = mb_strtolower( $headerLine, 'UTF-8' );
if ( strpos( $hl, 'buchungsnbr' ) !== false || strpos( $hl, 'kfz' ) !== false || strpos( $hl, 'parkplatz' ) !== false ) {
$type = 'parking';
} elseif ( strpos( $hl, 'cabin' ) !== false || strpos( $hl, 'departure' ) !== false || strpos( $hl, 'arrival' ) !== false ) {
$type = 'aida';
}
} elseif ( $ext === 'xls' || $ext === 'xlsx' ) {
$type = 'aida';
}
if ( $type === 'parking' ) return im_preview_parking( $tmp, $name, $size );
if ( $type === 'aida' ) return im_preview_aida( $tmp, $name, $ext, $size );
return array( 'ok' => true, 'type' => 'unknown', 'dryRun' => true, 'filename' => $name,
'message' => 'Dateityp konnte nicht erkannt werden – weder ParkingList (CSV mit „BuchungsNbr."/„KfZ") noch AIDA-Liste (Excel bzw. „Cabin"/„Arrival").' );
}
/** ParkingList-Vorschau (Dry-Run): parsen, klassifizieren, Tabelle + Summary. */
function im_preview_parking( $tmp, $name, $size ) {
$p = im_parking_parse( $tmp );
if ( empty( $p['ok'] ) ) return $p;
$rows = $p['rows']; $warnings = $p['warnings'];
$sum = $p['sum']; $halle = $p['halle']; $aussen = $p['aussen']; $ships = $p['ships'];
$minA = $p['minA']; $maxA = $p['maxA']; $gotAddress = $p['gotAddress']; $n = $p['n'];
$db = new MysqliDb( MYSQL_HOST, MYSQL_USER, MYSQL_PASS, MYSQL_DB );
$map = im_parking_existing_map( $db, $minA, $maxA );
list( $cNew, $cUpd, $cDup ) = im_parking_classify( $rows, $map );
$esc = function ( $v ) { return htmlspecialchars( (string) $v, ENT_QUOTES, 'UTF-8' ); };
$statusBadge = function ( $row ) use ( $esc ) {
if ( $row['status'] === 'new' ) return '<span class="badge bg-success">Neu</span>';
if ( $row['status'] === 'update' ) return '<span class="badge bg-warning text-dark" title="Geändert: ' . $esc( implode( ', ', $row['diff'] ) ) . '">Änderung</span><div class="small text-muted">' . $esc( implode( ', ', $row['diff'] ) ) . '</div>';
return '<span class="badge bg-secondary">Unverändert</span>';
};
$html = '<div class="table-responsive" style="max-height:60vh;"><table class="table table-sm table-hover table-bordered mb-0"><thead class="table-light"><tr>'
. '<th>Status</th><th>BuchungsNr.</th><th>Typ</th><th>Name</th><th>Adresse</th><th>E-Mail</th><th>Telefon</th><th>Kennzeichen</th><th class="text-end">Pers.</th><th>Anreise</th><th>Schiff</th><th>Rückreise</th><th class="text-end">Preis</th></tr></thead><tbody>';
$shown = 0;
foreach ( $rows as $row ) {
if ( $shown++ >= 100 ) break;
$html .= '<tr>'
. '<td class="text-nowrap">' . $statusBadge( $row ) . '</td>'
. '<td class="text-nowrap small">' . $esc( $row['no'] ) . '</td>'
. '<td><span class="badge ' . ( $row['typ'] === 'Halle' ? 'bg-info' : 'bg-secondary' ) . '">' . $esc( $row['typ'] ) . '</span></td>'
. '<td class="text-nowrap">' . $esc( $row['name'] ) . '</td>'
. '<td class="small">' . ( $row['addr'] !== '' ? $esc( $row['addr'] ) : '<span class="text-muted">—</span>' ) . '</td>'
. '<td class="small">' . $esc( $row['mail'] ) . '</td>'
. '<td class="text-nowrap small">' . $esc( $row['phone'] ) . '</td>'
. '<td class="text-nowrap"><span class="badge bg-light text-dark border">' . $esc( $row['plate'] ) . '</span></td>'
. '<td class="text-end">' . $esc( $row['pers'] ) . '</td>'
. '<td class="text-nowrap small">' . $esc( $row['arrival'] ) . '</td>'
. '<td class="text-nowrap">' . $esc( $row['ship'] ) . '</td>'
. '<td class="text-nowrap small">' . $esc( $row['return'] ) . '</td>'
. '<td class="text-end text-nowrap">' . number_format( $row['price'], 2, ',', '.' ) . ' €</td>'
. '</tr>';
}
$html .= '</tbody></table></div>';
if ( count( $rows ) > 100 ) $html .= '<div class="small text-muted mt-1">… ' . ( count( $rows ) - 100 ) . ' weitere Zeilen (in der Vorschau ausgeblendet)</div>';
return array(
'ok' => true, 'type' => 'parking', 'dryRun' => true, 'filename' => $name,
'summary' => array(
'rows' => $n, 'halle' => $halle, 'aussen' => $aussen,
'ships' => array_keys( $ships ), 'shipCount' => count( $ships ),
'sum' => round( $sum, 2 ),
'arrivalFrom' => $minA ? date( 'd.m.Y', strtotime( $minA ) ) : null,
'arrivalTo' => $maxA ? date( 'd.m.Y', strtotime( $maxA ) ) : null,
'addressFound' => $gotAddress,
'newCount' => $cNew, 'updateCount' => $cUpd, 'duplicateCount' => $cDup,
),
'warnings' => $warnings,
'html' => $html,
);
}
/** AIDA-Liste (Excel) als Dry-Run: Kopf + erste Zeilen + Zeilenzahl, ohne Import. */
function im_preview_aida( $tmp, $name, $ext, $size ) {
if ( !class_exists( 'PHPExcel_IOFactory' ) ) {
return array( 'ok' => true, 'type' => 'aida', 'dryRun' => true, 'filename' => $name,
'message' => 'AIDA-Liste erkannt. Eine Detailvorschau ist hier nicht verfügbar (PHPExcel fehlt) – der Import läuft über den AIDA-Import.' );
}
// Tempkopie mit korrekter Endung (PHPExcel erkennt am Suffix)
$work = sys_get_temp_dir() . '/im_preview_' . uniqid() . '.' . ( $ext ? $ext : 'xlsx' );
if ( !@copy( $tmp, $work ) ) {
return array( 'ok' => false, 'message' => 'Datei konnte nicht verarbeitet werden.' );
}
try {
PHPExcel_Settings::setCacheStorageMethod( PHPExcel_CachedObjectStorageFactory::cache_to_phpTemp, array( 'memoryCacheSize' => '64MB' ) );
$reader = PHPExcel_IOFactory::createReaderForFile( $work );
$reader->setReadDataOnly( true );
$reader->setReadFilter( new ImportPreviewReadFilter( 1, 40 ) );
$xl = $reader->load( $work );
$ws = $xl->getActiveSheet();
$highestRow = (int) $ws->getHighestRow();
$colCount = PHPExcel_Cell::columnIndexFromString( $ws->getHighestColumn() );
// AIDA-ParkingOperatorlist: Kopf in Zeile 2, Daten ab Zeile 3
$headerRow = 2; $firstData = 3;
$headers = array();
for ( $c = 0; $c < $colCount; $c++ ) {
$cell = $ws->getCellByColumnAndRow( $c, $headerRow );
$headers[$c] = $cell ? trim( (string) $cell->getValue() ) : '';
}
// Falls Zeile 2 leer wirkt, Kopf in Zeile 1 annehmen
if ( count( array_filter( $headers ) ) === 0 ) {
$headerRow = 1; $firstData = 2;
for ( $c = 0; $c < $colCount; $c++ ) {
$cell = $ws->getCellByColumnAndRow( $c, $headerRow );
$headers[$c] = $cell ? trim( (string) $cell->getValue() ) : '';
}
}
$dataRows = max( 0, $highestRow - ( $firstData - 1 ) );
$esc = function ( $v ) { return htmlspecialchars( (string) $v, ENT_QUOTES, 'UTF-8' ); };
$html = '<div class="table-responsive" style="max-height:60vh;"><table class="table table-sm table-bordered table-hover mb-0"><thead class="table-light"><tr>';
foreach ( $headers as $h ) $html .= '<th class="text-nowrap">' . $esc( $h ) . '</th>';
$html .= '</tr></thead><tbody>';
$previewEnd = min( $highestRow, $firstData + 29 );
for ( $row = $firstData; $row <= $previewEnd; $row++ ) {
$html .= '<tr>';
for ( $c = 0; $c < $colCount; $c++ ) {
$cell = $ws->getCellByColumnAndRow( $c, $row );
$html .= '<td class="text-nowrap small">' . $esc( $cell ? $cell->getFormattedValue() : '' ) . '</td>';
}
$html .= '</tr>';
}
$html .= '</tbody></table></div>';
$xl->disconnectWorksheets(); unset( $ws, $xl, $reader );
@unlink( $work );
return array(
'ok' => true, 'type' => 'aida', 'dryRun' => true, 'filename' => $name,
'summary' => array( 'rows' => $dataRows, 'columns' => $colCount ),
'warnings' => array(),
'html' => $html,
);
} catch ( Exception $e ) {
@unlink( $work );
return array( 'ok' => false, 'message' => 'AIDA-Liste konnte nicht gelesen werden: ' . $e->getMessage() );
}
}
/* =====================================================================
* FINALER ParkingList-Import (anlegen/aktualisieren/überspringen).
* Keine automatische Mail. Rechnung (PAS-Nr.) entsteht über den bestehenden
* Zahlungs-Flow, sobald die Zahlung eingegangen ist (invoice_no bleibt leer).
* ===================================================================== */
function im_parking_import() {
if ( empty( $_FILES['file'] ) || !isset( $_FILES['file']['error'] ) || $_FILES['file']['error'] !== UPLOAD_ERR_OK ) return array( 'ok' => false, 'message' => 'Keine Datei empfangen.' );
$tmp = $_FILES['file']['tmp_name']; $name = basename( (string) $_FILES['file']['name'] );
if ( !is_uploaded_file( $tmp ) ) return array( 'ok' => false, 'message' => 'Ungueltiger Upload.' );
// Geteilte Logik (auch vom Cron genutzt):
return im_parking_import_file( $tmp, $name );
}
/* =====================================================================
* AIDA-Upload: Datei in den AIDA-Import-Ordner legen und bestehenden
* AIDA-Import anstoßen (kein zweiter Importer; nutzt die vorhandene Pipeline).
* ===================================================================== */
function im_aida_import() {
if ( empty( $_FILES['file'] ) || !isset( $_FILES['file']['error'] ) || $_FILES['file']['error'] !== UPLOAD_ERR_OK ) return array( 'ok' => false, 'message' => 'Keine Datei empfangen.' );
$tmp = $_FILES['file']['tmp_name']; $name = basename( (string) $_FILES['file']['name'] ); $ext = strtolower( pathinfo( $name, PATHINFO_EXTENSION ) );
if ( !is_uploaded_file( $tmp ) ) return array( 'ok' => false, 'message' => 'Ungültiger Upload.' );
if ( !in_array( $ext, array( 'xls', 'xlsx', 'csv' ), true ) ) return array( 'ok' => false, 'message' => 'AIDA-Liste muss .xls/.xlsx/.csv sein.' );
if ( !defined( 'AIDA_DIR' ) ) return array( 'ok' => false, 'message' => 'AIDA-Import-Ordner (AIDA_DIR) ist nicht konfiguriert.' );
$dir = rtrim( AIDA_DIR, '/' ) . '/';
if ( !is_dir( $dir ) ) @mkdir( $dir, 0775, true );
$dest = $dir . $name;
if ( !@copy( $tmp, $dest ) ) return array( 'ok' => false, 'message' => 'Datei konnte nicht in den AIDA-Import-Ordner gelegt werden.' );
// Bestehenden AIDA-Import best-effort anstoßen (Token-Endpunkt, im Hintergrund)
$triggered = false;
if ( function_exists( 'exec' ) && defined( 'SITE_URL' ) ) {
$url = SITE_URL . '/crons/importFiles_v2.php?token=eFD6dx2G4RFBkrYS7Z87';
@exec( 'curl -s ' . escapeshellarg( $url ) . ' > /dev/null 2>&1 &' );
$triggered = true;
}
$pending = function_exists( 'getAidaFilesCount' ) ? getAidaFilesCount() : null;
return array( 'ok' => true, 'type' => 'aida', 'filename' => $name, 'queued' => true, 'triggered' => $triggered, 'pending' => $pending,
'message' => 'AIDA-Liste in den Import-Ordner gelegt' . ( $triggered ? ' und Import angestoßen' : '' ) . '. Die Verarbeitung läuft über den bestehenden AIDA-Import.' );
}
/* ---------- AIDA-Import-Cron starten (Parität zur Alt-App) ---------- */
function im_run_cron() {
if ( !defined( 'PACIM_CRON_SCRIPT' ) || !defined( 'PACIM_CRON_LOG' ) ) {
return array( 'status' => 'error', 'message' => 'Cron-Konstanten (PACIM_CRON_SCRIPT/PACIM_CRON_LOG) sind nicht definiert.' );
}
$script = PACIM_CRON_SCRIPT;
$log = PACIM_CRON_LOG;
if ( !file_exists( $script ) ) {
return array( 'status' => 'error', 'message' => 'Cron-Script nicht gefunden.' );
}
$cmd = 'nohup bash ' . escapeshellarg( $script ) . ' >> ' . escapeshellarg( $log ) . ' 2>&1 & echo $!';
$pid = function_exists( 'shell_exec' ) ? trim( (string) @shell_exec( $cmd ) ) : '';
if ( $pid === '' ) {
return array( 'status' => 'error', 'message' => 'Cron konnte nicht gestartet werden.' );
}
return array( 'status' => 'success', 'message' => 'AIDA-Import gestartet (PID ' . $pid . ').', 'pid' => $pid );
}