Files
wp-cycling-stats/includes/admin/pages/page-import-csv.php
T
2026-02-12 22:34:54 +01:00

309 lines
15 KiB
PHP

<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
function statpress_import_csv_page() {
echo '<div class="wrap"><h1>Importuj aktywności z pliku CSV</h1>';
// Handle the form submission
if ( 'POST' === $_SERVER['REQUEST_METHOD'] && isset( $_POST['statpress_csv_import_nonce_field'] ) ) {
statpress_handle_csv_import();
}
// Display the form
?>
<div class="postbox">
<div class="postbox-header"><h2 class="hndle">Instrukcje i formularz importu</h2></div>
<div class="inside">
<p>Aby zaimportować dane, możesz wgrać plik CSV <strong>LUB</strong> wkleić dane bezpośrednio w pole tekstowe poniżej. Ta druga opcja jest zalecana, jeśli napotykasz błędy z plikiem.</p>
<p><strong>Wymagane kolumny:</strong> <code>Data</code>, <code>Tytuł</code>, <code>Dystans</code> oraz <code>Typ aktywności</code> (lub <code>Kategoria</code>).</p>
<p><strong>Opcjonalne kolumny:</strong> <code>Czas</code> (format HH:MM:SS), <code>Kalorie</code>, <code>Średnie tętno</code>, <code>Maksymalne tętno</code>, <code>Średnia prędkość</code>, <code>Maksymalna prędkość</code>, <code>Średni rytm pedałowania</code>, <code>Maksymalny rytm pedałowania</code>, <code>Całkowity wznios</code>, <code>Całkowity spadek</code>, <code>Minimalna wysokość</code>, <code>Maksymalna wysokość</code>, <code>Sprzęt</code>, <code>Typ wydarzenia</code>.</p>
<p><strong>Ważne:</strong>
<ul>
<li>Data musi być w formacie <code>YYYY-MM-DD</code>.</li>
<li>Dystans i prędkość: użyj kropki jako separatora dziesiętnego (np. <code>10.5</code>).</li>
<li>Nazwy w kolumnach <code>Typ aktywności</code>, <code>Sprzęt</code>, <code>Typ wydarzenia</code> muszą dokładnie odpowiadać nazwom zdefiniowanym w ustawieniach wtyczki. Jeśli nazwa nie zostanie znaleziona, wiersz zostanie pominięty.</li>
</ul>
</p>
<hr>
<form method="post" enctype="multipart/form-data">
<?php wp_nonce_field( 'statpress_csv_import_nonce', 'statpress_csv_import_nonce_field' ); ?>
<table class="form-table">
<tr valign="top">
<th scope="row"><label for="statpress_csv_file">Opcja 1: Wgraj plik CSV</label></th>
<td><input type="file" id="statpress_csv_file" name="statpress_csv_file" accept=".csv,text/csv" /></td>
</tr>
<tr valign="top">
<th scope="row"><label for="statpress_csv_data">Opcja 2: Wklej dane CSV</label></th>
<td><textarea name="statpress_csv_data" id="statpress_csv_data" rows="15" class="large-text" placeholder="Wklej tutaj zawartość swojego pliku CSV...&#10;Typ aktywności,Data,Tytuł,Dystans,...&#10;Rower,2025-01-01,Nowy Rok,10.5,..."></textarea></td>
</tr>
</table>
<?php submit_button( 'Importuj' ); ?>
</form>
</div>
</div>
<?php
echo '</div>';
}
function statpress_handle_csv_import() {
global $wpdb;
if ( ! isset( $_POST['statpress_csv_import_nonce_field'] ) || ! wp_verify_nonce( $_POST['statpress_csv_import_nonce_field'], 'statpress_csv_import_nonce' ) ) {
echo '<div class="notice notice-error"><p>Błąd weryfikacji bezpieczeństwa.</p></div>';
return;
}
if ( ! current_user_can( 'manage_options' ) ) {
echo '<div class="notice notice-error"><p>Nie masz wystarczających uprawnień.</p></div>';
return;
}
// Unify input source: prefer textarea, fall back to file upload.
$csv_content = '';
if ( ! empty( $_POST['statpress_csv_data'] ) ) {
$csv_content = stripslashes( $_POST['statpress_csv_data'] );
} elseif ( ! empty( $_FILES['statpress_csv_file']['tmp_name'] ) && UPLOAD_ERR_OK === $_FILES['statpress_csv_file']['error'] ) {
$csv_content = file_get_contents( $_FILES['statpress_csv_file']['tmp_name'] );
}
if ( empty( trim( $csv_content ) ) ) {
echo '<div class="notice notice-error"><p>Nie podano danych do importu. Wgraj plik lub wklej dane w pole tekstowe.</p></div>';
return;
}
// Mapowanie polskich i angielskich nazw kolumn na wewnętrzne klucze
$column_map = array(
// Polish => English
'typ aktywności' => 'category_name',
'data' => 'date',
'tytuł' => 'title',
'dystans' => 'distance',
'kalorie' => 'calories',
'czas' => 'duration',
'średnie tętno' => 'avg_heart_rate',
'maksymalne tętno' => 'max_heart_rate',
'średnia prędkość' => 'avg_speed',
'maksymalna prędkość' => 'max_speed',
'średni rytm pedałowania' => 'avg_cadence',
'maksymalny rytm pedałowania' => 'max_cadence',
'całkowity wznios' => 'total_elevation_gain',
'całkowity spadek' => 'total_elevation_loss',
'minimalna wysokość' => 'min_altitude',
'maksymalna wysokość' => 'max_altitude',
'sprzęt' => 'equipment_name',
'typ wydarzenia' => 'event_type_name',
'komentarz' => 'comment',
'link do strava' => 'strava_url',
// English keys for compatibility
'category_name' => 'category_name',
'date' => 'date',
'title' => 'title',
'distance' => 'distance',
'calories' => 'calories',
'duration' => 'duration',
'avg_heart_rate' => 'avg_heart_rate',
'max_heart_rate' => 'max_heart_rate',
'avg_speed' => 'avg_speed',
'max_speed' => 'max_speed',
'avg_cadence' => 'avg_cadence',
'max_cadence' => 'max_cadence',
'total_elevation_gain' => 'total_elevation_gain',
'total_elevation_loss' => 'total_elevation_loss',
'min_altitude' => 'min_altitude',
'max_altitude' => 'max_altitude',
'equipment_name' => 'equipment_name',
'event_type_name' => 'event_type_name',
'comment' => 'comment',
'strava_url' => 'strava_url',
);
// --- START: Robust, case-insensitive lookup ---
$table_categories = $wpdb->prefix . 'statpress_categories';
$table_event_types = $wpdb->prefix . 'statpress_event_types';
$table_equipment = $wpdb->prefix . 'statpress_equipment';
$create_lookup = function( $table_name ) use ( $wpdb ) {
$items = $wpdb->get_results( "SELECT id, name FROM {$table_name}" );
$lookup = array();
if ( is_array( $items ) ) {
foreach ( $items as $item ) {
// Use a robust trim to handle various whitespace characters and make it case-insensitive
$clean_name = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $item->name );
$lookup[ mb_strtolower( $clean_name, 'UTF-8' ) ] = $item->id;
}
}
return $lookup;
};
$categories_lookup = $create_lookup( $table_categories );
$event_types_lookup = $create_lookup( $table_event_types );
$equipment_lookup = $create_lookup( $table_equipment );
// --- END: Robust, case-insensitive lookup ---
// Process the CSV file
$table_activities = $wpdb->prefix . 'statpress_activities';
$imported_count = 0;
$skipped_details = array();
$row_number = 1; // Header is row 1
// Normalize line endings and split into lines
$lines = str_replace( array( "\r\n", "\r" ), "\n", $csv_content );
$lines = explode( "\n", $lines );
if ( empty( $lines ) || empty( trim( $lines[0] ) ) ) {
echo '<div class="notice notice-info"><p>Podane dane CSV są puste.</p></div>';
return;
}
// --- Delimiter and BOM detection ---
$first_line = $lines[0];
$delimiter = ( substr_count( $first_line, ';' ) > substr_count( $first_line, ',' ) ) ? ';' : ',';
// --- BOM removal from first header element ---
$bom = "\xEF\xBB\xBF";
if ( substr( $first_line, 0, 3 ) === $bom ) {
$lines[0] = substr( $first_line, 3 );
}
$header_raw = str_getcsv( array_shift( $lines ), $delimiter );
$header_raw = array_map( 'trim', $header_raw );
// Translate header from Polish/English to internal keys
$header = array();
foreach ( $header_raw as $col ) {
$header[] = $column_map[ strtolower( $col ) ] ?? 'ignored_' . uniqid();
}
$required_internal_keys = array( 'date', 'title', 'category_name', 'distance' );
$missing_keys = array_diff( $required_internal_keys, $header );
if ( ! empty( $missing_keys ) ) {
$key_to_polish_map = array(
'date' => 'Data',
'title' => 'Tytuł',
'category_name' => 'Kategoria / Typ aktywności',
'distance' => 'Dystans',
);
$missing_polish_names = array_map( fn( $key ) => $key_to_polish_map[ $key ] ?? $key, $missing_keys );
echo '<div class="notice notice-error"><p>Brak wymaganych kolumn w danych CSV: ' . esc_html( implode( ', ', $missing_polish_names ) ) . '</p></div>';
return;
}
$parse_and_round_int = fn( $val ) => round( floatval( str_replace( ',', '.', $val ) ) );
$null_if_empty = fn( $value ) => '' !== $value ? $value : null;
foreach ( $lines as $line ) {
$row_number++;
if ( empty( trim( $line ) ) ) {
continue; // Skip empty lines
}
$data = str_getcsv( $line, $delimiter );
if ( count( $data ) !== count( $header ) ) {
$skipped_details[] = array(
'row' => $row_number,
'reason' => 'Nieprawidłowa liczba kolumn (oczekiwano ' . count( $header ) . ', otrzymano ' . count( $data ) . ').',
'data' => $line,
);
continue;
}
$row_data = array_combine( $header, $data );
// Detailed validation
$validation_errors = array();
if ( empty( $row_data['date'] ) ) {
$validation_errors[] = 'brak daty';
}
if ( empty( $row_data['title'] ) ) {
$validation_errors[] = 'brak tytułu';
}
if ( ! isset( $row_data['distance'] ) || '' === $row_data['distance'] ) {
$validation_errors[] = 'brak dystansu';
}
$category_name = $row_data['category_name'] ?? '';
$clean_category_name = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $category_name );
$category_id = $categories_lookup[ mb_strtolower( $clean_category_name, 'UTF-8' ) ] ?? null;
if ( empty( $clean_category_name ) ) {
$validation_errors[] = 'brak nazwy kategorii';
} elseif ( is_null( $category_id ) ) {
$available_categories_from_db = $wpdb->get_col( "SELECT name FROM $table_categories ORDER BY name" );
$validation_errors[] = 'nieznana kategoria: "' . esc_html( $category_name ) . '". Sprawdź, czy nazwa jest poprawna. Dostępne w bazie: "' . esc_html( implode( '", "', $available_categories_from_db ) ) . '".';
}
if ( ! empty( $validation_errors ) ) {
$skipped_details[] = array(
'row' => $row_number,
'reason' => ucfirst( implode( ', ', $validation_errors ) ) . '.',
'data' => $line,
);
continue;
}
// Get IDs for optional fields using the same robust method
$get_id = function( $name, $lookup_table ) {
$clean_name = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $name );
return $lookup_table[ mb_strtolower( $clean_name, 'UTF-8' ) ] ?? null;
};
$equipment_id = $get_id( $row_data['equipment_name'] ?? '', $equipment_lookup );
$event_type_id = $get_id( $row_data['event_type_name'] ?? '', $event_types_lookup );
$insert_data = array(
'date' => sanitize_text_field( $row_data['date'] ),
'title' => sanitize_text_field( $row_data['title'] ),
'category_id' => $category_id,
'distance' => floatval( str_replace( ',', '.', $row_data['distance'] ) ),
'duration' => isset( $row_data['duration'] ) ? sanitize_text_field( $row_data['duration'] ) : '00:00:00',
'calories' => isset( $row_data['calories'] ) ? intval( str_replace( ',', '.', $row_data['calories'] ) ) : 0,
'comment' => isset( $row_data['comment'] ) ? sanitize_textarea_field( $row_data['comment'] ) : null,
'strava_url' => isset( $row_data['strava_url'] ) ? $null_if_empty( esc_url_raw( $row_data['strava_url'] ) ) : null,
'avg_heart_rate' => isset( $row_data['avg_heart_rate'] ) ? $null_if_empty( $parse_and_round_int( $row_data['avg_heart_rate'] ) ) : null,
'max_heart_rate' => isset( $row_data['max_heart_rate'] ) ? $null_if_empty( $parse_and_round_int( $row_data['max_heart_rate'] ) ) : null,
'avg_speed' => isset( $row_data['avg_speed'] ) ? $null_if_empty( floatval( str_replace( ',', '.', $row_data['avg_speed'] ) ) ) : null,
'max_speed' => isset( $row_data['max_speed'] ) ? $null_if_empty( floatval( str_replace( ',', '.', $row_data['max_speed'] ) ) ) : null,
'avg_cadence' => isset( $row_data['avg_cadence'] ) ? $null_if_empty( $parse_and_round_int( $row_data['avg_cadence'] ) ) : null,
'max_cadence' => isset( $row_data['max_cadence'] ) ? $null_if_empty( $parse_and_round_int( $row_data['max_cadence'] ) ) : null,
'total_elevation_gain' => isset( $row_data['total_elevation_gain'] ) ? $null_if_empty( $parse_and_round_int( $row_data['total_elevation_gain'] ) ) : null,
'total_elevation_loss' => isset( $row_data['total_elevation_loss'] ) ? $null_if_empty( $parse_and_round_int( $row_data['total_elevation_loss'] ) ) : null,
'min_altitude' => isset( $row_data['min_altitude'] ) ? $null_if_empty( $parse_and_round_int( $row_data['min_altitude'] ) ) : null,
'max_altitude' => isset( $row_data['max_altitude'] ) ? $null_if_empty( $parse_and_round_int( $row_data['max_altitude'] ) ) : null,
'equipment_id' => $equipment_id,
'event_type_id' => $event_type_id,
);
if ( $wpdb->insert( $table_activities, $insert_data ) ) {
$imported_count++;
} else {
$skipped_details[] = array(
'row' => $row_number,
'reason' => 'Błąd zapisu do bazy danych. (' . esc_html( $wpdb->last_error ) . ')',
'data' => $line,
);
}
}
if ( $imported_count > 0 ) {
echo '<div class="notice notice-success is-dismissible"><p>Pomyślnie zaimportowano ' . esc_html( $imported_count ) . ' aktywności.</p></div>';}
if ( ! empty( $skipped_details ) ) {
echo '<div class="notice notice-warning">';
echo '<h4>Pominięto ' . count( $skipped_details ) . ' wierszy z powodu błędów</h4>';
echo '<div style="max-height: 300px; overflow-y: auto; border: 1px solid #c3c4c7; padding: 10px; background: #fff; margin-top: 10px; font-size: 12px;">';
echo '<table class="wp-list-table widefat striped" style="margin:0;"><thead><tr><th style="width:80px">Wiersz</th><th>Powód pominięcia</th><th>Dane wiersza</th></tr></thead><tbody>';
foreach ( $skipped_details as $error ) {
echo '<tr>';
echo '<td>' . esc_html( $error['row'] ) . '</td>';
echo '<td>' . esc_html( $error['reason'] ) . '</td>';
echo '<td><small>' . esc_html( wp_trim_words( $error['data'], 25, '...' ) ) . '</small></td>';
echo '</tr>';
}
echo '</tbody></table>';
echo '</div></div>';
}
if ( 0 === $imported_count && empty( $skipped_details ) && $row_number > 1 ) {
echo '<div class="notice notice-info"><p>Dane CSV nie zawierały żadnych poprawnych wierszy do importu.</p></div>';
} elseif ( 1 === $row_number ) {
echo '<div class="notice notice-info"><p>Dane CSV były puste lub zawierały tylko nagłówek.</p></div>';}
}