diff --git a/includes/activation.php b/includes/activation.php new file mode 100644 index 0000000..a0c4b12 --- /dev/null +++ b/includes/activation.php @@ -0,0 +1,140 @@ +get_charset_collate(); + + $table_categories = $wpdb->prefix . 'mystat_categories'; + $table_activities = $wpdb->prefix . 'mystat_activities'; + $table_event_types = $wpdb->prefix . 'mystat_event_types'; + $table_equipment = $wpdb->prefix . 'mystat_equipment'; + $table_equipment_log = $wpdb->prefix . 'mystat_equipment_log'; + $table_goals = $wpdb->prefix . 'mystat_goals'; + + // SQL dla Kategorii + $sql_cat = "CREATE TABLE $table_categories ( + id mediumint(9) NOT NULL AUTO_INCREMENT, + name varchar(50) NOT NULL, + icon varchar(50) NOT NULL, + color varchar(20) NOT NULL, + PRIMARY KEY (id) + ) $charset_collate;"; + + // SQL dla Typów Wydarzeń + $sql_event_types = "CREATE TABLE $table_event_types ( + id mediumint(9) NOT NULL AUTO_INCREMENT, + name varchar(100) NOT NULL, + PRIMARY KEY (id) + ) $charset_collate;"; + + // SQL dla Sprzętu + $sql_equipment = "CREATE TABLE $table_equipment ( + id mediumint(9) NOT NULL AUTO_INCREMENT, + name varchar(100) NOT NULL, + type varchar(50) DEFAULT 'Rower' NOT NULL, + purchase_date date DEFAULT NULL, + initial_cost decimal(10,2) DEFAULT NULL, + status varchar(20) DEFAULT 'aktywny' NOT NULL, -- 'aktywny', 'sprzedany', 'wycofany' + notes text, + PRIMARY KEY (id) + ) $charset_collate;"; + + // SQL dla Dziennika Serwisowego Sprzętu + $sql_equipment_log = "CREATE TABLE $table_equipment_log ( + id bigint(20) NOT NULL AUTO_INCREMENT, + equipment_id mediumint(9) NOT NULL, + log_date date NOT NULL, + log_type varchar(50) NOT NULL, -- np. Naprawa, Zakup, Przegląd, Modyfikacja + description text NOT NULL, + cost decimal(10,2) DEFAULT NULL, + mileage int(11) DEFAULT NULL, -- Przebieg sprzętu w momencie serwisu + PRIMARY KEY (id), + KEY equipment_id (equipment_id) + ) $charset_collate;"; + + // SQL dla Celów + $sql_goals = "CREATE TABLE $table_goals ( + id mediumint(9) NOT NULL AUTO_INCREMENT, + name varchar(255) NOT NULL, + goal_type varchar(20) NOT NULL, -- 'distance', 'duration_sec', 'count' + target_value decimal(10,2) NOT NULL, + year smallint(4) NOT NULL, + month tinyint(2) UNSIGNED DEFAULT NULL, + category_id mediumint(9) DEFAULT NULL, + PRIMARY KEY (id), + KEY category_id (category_id) + ) $charset_collate;"; + + // SQL dla Aktywności + $sql_act = "CREATE TABLE $table_activities ( + id bigint(20) NOT NULL AUTO_INCREMENT, + category_id mediumint(9) NOT NULL, + date date NOT NULL, + title varchar(255) DEFAULT '' NOT NULL, + distance decimal(10,2) DEFAULT 0.00, + duration time DEFAULT '00:00:00', + calories int(11) DEFAULT 0, + comment text, + strava_url varchar(255) DEFAULT NULL, + avg_heart_rate smallint(5) UNSIGNED DEFAULT NULL, + max_heart_rate smallint(5) UNSIGNED DEFAULT NULL, + avg_speed decimal(5,2) DEFAULT NULL, + max_speed decimal(5,2) DEFAULT NULL, + avg_cadence smallint(5) UNSIGNED DEFAULT NULL, + max_cadence smallint(5) UNSIGNED DEFAULT NULL, + total_elevation_gain int(11) DEFAULT NULL, + total_elevation_loss int(11) DEFAULT NULL, + min_altitude int(11) DEFAULT NULL, + max_altitude int(11) DEFAULT NULL, + equipment_id mediumint(9) DEFAULT NULL, + gpx_url varchar(255) DEFAULT NULL, + event_type_id mediumint(9) DEFAULT NULL, + PRIMARY KEY (id) + ) $charset_collate;"; + + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + dbDelta( $sql_goals ); + dbDelta( $sql_equipment ); + dbDelta( $sql_equipment_log ); + dbDelta( $sql_cat ); + dbDelta( $sql_event_types ); + dbDelta( $sql_act ); + + // Dodanie domyślnych kategorii, jeśli tabela jest pusta + if ( 0 === $wpdb->get_var( "SELECT COUNT(*) FROM $table_categories" ) ) { + $wpdb->insert( $table_categories, array( 'name' => 'Rower', 'icon' => 'dashicons-buddicons-groups', 'color' => '#3498db' ) ); + $wpdb->insert( $table_categories, array( 'name' => 'Bieganie', 'icon' => 'dashicons-businessman', 'color' => '#e74c3c' ) ); + } + + // Dodanie domyślnych typów wydarzeń, jeśli tabela jest pusta + if ( 0 === $wpdb->get_var( "SELECT COUNT(*) FROM $table_event_types" ) ) { + $default_event_types = array( 'Bez kategorii', 'Fitness', 'Geocaching', 'Podróżowanie', 'Rekreacyjny', 'Specjalne zdarzenie', 'Transport', 'Trening', 'Wyścig' ); + foreach ( $default_event_types as $type_name ) { + $wpdb->insert( $table_event_types, array( 'name' => $type_name ) ); + } + // Ustawienie domyślnego typu "Trening" dla istniejących aktywności, które go nie mają + $training_id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $table_event_types WHERE name = %s", 'Trening' ) ); + if ( $training_id ) { + $wpdb->query( $wpdb->prepare( "UPDATE $table_activities SET event_type_id = %d WHERE event_type_id IS NULL OR event_type_id = 0", $training_id ) ); + } + } + + // Dodanie domyślnego sprzętu, jeśli tabela jest pusta + if ( 0 === $wpdb->get_var( "SELECT COUNT(*) FROM $table_equipment" ) ) { + $wpdb->insert( $table_equipment, array( 'name' => 'Giant Revolt', 'type' => 'Rower', 'status' => 'aktywny' ) ); + } +} \ No newline at end of file diff --git a/includes/admin/hooks.php b/includes/admin/hooks.php new file mode 100644 index 0000000..e64090d --- /dev/null +++ b/includes/admin/hooks.php @@ -0,0 +1,27 @@ +

Dodaj Nowy Trening

'; + // Obsługa zapisu formularza (musi być przed renderowaniem, aby wyświetlić komunikat) + mystat_handle_activity_form_submission(); + // Formularz dodawania + mystat_render_add_form(); + echo ''; +} + +function mystat_edit_activity_page() { + global $wpdb; + $activity_id = isset( $_GET['id'] ) ? intval( $_GET['id'] ) : 0; + + if ( $activity_id === 0 ) { + echo '

Błąd

Nie podano ID aktywności do edycji.

'; + return; + } + + // Handle form submission for update + mystat_handle_activity_form_submission(); + + $table_activities = $wpdb->prefix . 'mystat_activities'; + $activity = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_activities WHERE id = %d", $activity_id ) ); + + if ( ! $activity ) { + echo '

Błąd

Nie znaleziono aktywności o podanym ID.

'; + return; + } + + echo '

Edytuj Trening

'; + mystat_render_add_form( $activity ); + echo '
'; +} + +/** + * Obsługa zapisu nowego lub edytowanego wpisu do bazy danych + */ +function mystat_handle_activity_form_submission() { + global $wpdb; + + // Sprawdź czy formularz został wysłany + if ( ! isset( $_POST['mystat_submit_activity'] ) ) { + return; + } + + $activity_id = isset( $_POST['activity_id'] ) ? intval( $_POST['activity_id'] ) : 0; + $nonce_action = $activity_id > 0 ? 'mystat_edit_entry_' . $activity_id : 'mystat_add_entry'; + + // Weryfikacja bezpieczeństwa (Nonce) + if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], $nonce_action ) ) { + echo '

Błąd weryfikacji bezpieczeństwa formularza.

'; + return; + } + + $table_activities = $wpdb->prefix . 'mystat_activities'; + + // Przygotowanie danych (zamiana przecinka na kropkę w dystansie) + $distance = isset( $_POST['distance'] ) ? floatval( str_replace( ',', '.', $_POST['distance'] ) ) : 0; + + // Funkcja pomocnicza do zamiany pustych wartości na NULL, aby poprawnie zapisać je w bazie + $null_if_empty = function( $value ) { + return $value !== '' ? $value : null; + }; + + $data = array( + 'category_id' => intval( $_POST['category_id'] ), + 'date' => sanitize_text_field( $_POST['date'] ), + 'title' => sanitize_text_field( $_POST['title'] ), + 'distance' => $distance, + 'duration' => sanitize_text_field( $_POST['duration'] ), + 'calories' => intval( $_POST['calories'] ), + 'comment' => sanitize_textarea_field( $_POST['comment'] ), + 'strava_url' => $null_if_empty( esc_url_raw( $_POST['strava_url'] ) ), + 'avg_heart_rate' => $null_if_empty( intval( $_POST['avg_heart_rate'] ) ), + 'max_heart_rate' => $null_if_empty( intval( $_POST['max_heart_rate'] ) ), + 'avg_speed' => $null_if_empty( floatval( str_replace( ',', '.', $_POST['avg_speed'] ) ) ), + 'max_speed' => $null_if_empty( floatval( str_replace( ',', '.', $_POST['max_speed'] ) ) ), + 'avg_cadence' => $null_if_empty( intval( $_POST['avg_cadence'] ) ), + 'max_cadence' => $null_if_empty( intval( $_POST['max_cadence'] ) ), + 'total_elevation_gain' => $null_if_empty( intval( $_POST['total_elevation_gain'] ) ), + 'total_elevation_loss' => $null_if_empty( intval( $_POST['total_elevation_loss'] ) ), + 'min_altitude' => $null_if_empty( intval( $_POST['min_altitude'] ) ), + 'max_altitude' => $null_if_empty( intval( $_POST['max_altitude'] ) ), + 'equipment_id' => $null_if_empty( intval( $_POST['equipment_id'] ) ), + 'gpx_url' => $null_if_empty( esc_url_raw( $_POST['gpx_url'] ) ), + 'event_type_id' => $null_if_empty( intval( $_POST['event_type_id'] ) ), + ); + + // Format danych dla $wpdb->insert + $format = array( + '%d', + '%s', + '%s', + '%f', + '%s', + '%d', + '%s', // Pola podstawowe + '%s', + '%d', + '%d', + '%f', + '%f', + '%d', + '%d', // Tętno, prędkość, kadencja + '%d', + '%d', + '%d', + '%d', + '%d', + '%s', + '%d', // Wysokość, sprzęt, linki, typ wydarzenia + ); + + if ( $activity_id > 0 ) { + // UPDATE + $result = $wpdb->update( $table_activities, $data, array( 'id' => $activity_id ), $format, array( '%d' ) ); + $message = 'Trening zaktualizowany pomyślnie!'; + } else { + // INSERT + $result = $wpdb->insert( $table_activities, $data, $format ); + $message = 'Trening dodany pomyślnie!'; + } + + if ( false !== $result ) { + echo '

' . esc_html( $message ) . '

'; + } else { + echo '

Wystąpił błąd podczas zapisu do bazy.

'; + } +} + +/** + * Renderowanie formularza HTML + */ +function mystat_render_add_form( $activity = null ) { + // Enqueue media scripts for the uploader + wp_enqueue_media(); + + global $wpdb; + $table_categories = $wpdb->prefix . 'mystat_categories'; + $table_event_types = $wpdb->prefix . 'mystat_event_types'; + $table_equipment = $wpdb->prefix . 'mystat_equipment'; + $categories = $wpdb->get_results( "SELECT * FROM $table_categories ORDER BY name ASC" ); + $event_types = $wpdb->get_results( "SELECT * FROM $table_event_types ORDER BY name ASC" ); + $equipment_list = $wpdb->get_results( "SELECT * FROM $table_equipment ORDER BY name ASC" ); + + $is_edit_mode = ! is_null( $activity ); + $nonce_action = $is_edit_mode ? 'mystat_edit_entry_' . $activity->id : 'mystat_add_entry'; + $form_title = $is_edit_mode ? 'Edytuj Aktywność' : 'Dodaj Nową Aktywność'; + $button_text = $is_edit_mode ? 'Zaktualizuj Trening' : 'Zapisz Trening'; + + ?> +
+

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +

Dane szczegółowe (opcjonalne)


Linki zewnętrzne (opcjonalne)

+ + +
+

+ +

+
+ +
+
+

Błąd

Nie podano ID aktywności.

'; + return; + } + + $table_activities = $wpdb->prefix . 'mystat_activities'; + $table_categories = $wpdb->prefix . 'mystat_categories'; + $table_event_types = $wpdb->prefix . 'mystat_event_types'; + $table_equipment = $wpdb->prefix . 'mystat_equipment'; + + $sql = $wpdb->prepare( + " + SELECT a.*, c.name as category_name, c.icon, c.color, et.name as event_type_name, eq.name as equipment_name + FROM $table_activities a + LEFT JOIN $table_categories c ON a.category_id = c.id + LEFT JOIN $table_event_types et ON a.event_type_id = et.id + LEFT JOIN $table_equipment eq ON a.equipment_id = eq.id + WHERE a.id = %d + ", + $activity_id + ); + + $activity = $wpdb->get_row( $sql ); + + if ( ! $activity ) { + echo '

Błąd

Nie znaleziono aktywności o podanym ID.

'; + return; + } + + // Funkcja pomocnicza do wyświetlania wiersza, jeśli wartość istnieje + $render_row = function( $label, $value, $unit = '' ) { + if ( ! is_null( $value ) && '' !== $value ) { + echo ''; + echo '' . esc_html( $label ) . ''; + echo '' . esc_html( $value ) . ( $unit ? ' ' . esc_html( $unit ) : '' ) . ''; + echo ''; + } + }; + + // Prepare map and chart data if GPX exists + $gpx_data = array(); + $has_gpx_data = false; + if ( ! empty( $activity->gpx_url ) ) { + $gpx_data = mystat_parse_gpx_data( $activity->gpx_url ); + $has_gpx_data = ! empty( $gpx_data['points'] ); + } + + if ( $has_gpx_data ) { + wp_enqueue_style( 'leaflet-css', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css' ); + wp_enqueue_script( 'leaflet-js', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js', array(), '1.9.4', true ); + wp_enqueue_script( 'chart-js', 'https://cdn.jsdelivr.net/npm/chart.js', array(), null, true ); + + wp_register_script( 'mystat-details-loader', false ); + wp_enqueue_script( 'mystat-details-loader' ); + + // Check which profiles have data + $available_profiles = array(); + if ( ! empty( array_filter( $gpx_data['profiles']['elevation'] ) ) ) { + $available_profiles['elevation'] = 'Wysokość';} + if ( ! empty( array_filter( $gpx_data['profiles']['speed'] ) ) ) { + $available_profiles['speed'] = 'Prędkość';} + if ( ! empty( array_filter( $gpx_data['profiles']['hr'] ) ) ) { + $available_profiles['hr'] = 'Tętno';} + if ( ! empty( array_filter( $gpx_data['profiles']['cadence'] ) ) ) { + $available_profiles['cadence'] = 'Kadencja';} + + $has_time_data = ! empty( array_filter( $gpx_data['profiles']['time'], fn( $t ) => ! is_null( $t ) ) ); + + $chart_js = ' + const track_points = ' . json_encode( $gpx_data['points'] ) . '; + const profiles = ' . json_encode( $gpx_data['profiles'] ) . '; + let activeChart = null; + + if (typeof L !== "undefined" && track_points.length > 0) { + const map = L.map("mystat-activity-map"); + L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: \'© OpenStreetMap\' }).addTo(map); + const polyline = L.polyline(track_points, {color: "' . esc_js( $activity->color ) . '"}).addTo(map); + map.fitBounds(polyline.getBounds().pad(0.1)); + L.marker(track_points[0]).addTo(map).bindPopup("Start"); + L.marker(track_points[track_points.length - 1]).addTo(map).bindPopup("Koniec"); + } + + const chartConfigs = { + elevation: { label: "Wysokość", unit: "m n.p.m.", color: "#8e44ad" }, + speed: { label: "Prędkość", unit: "km/h", color: "#2980b9" }, + hr: { label: "Tętno", unit: "bpm", color: "#c0392b" }, + cadence: { label: "Kadencja", unit: "rpm", color: "#27ae60" } + }; + const xAxisConfigs = { + distance: { label: "Dystans (km)", data: profiles.distance }, + time: { + label: "Czas", data: profiles.time, + formatter: (s) => s === null ? "" : new Date(s * 1000).toISOString().substr(11, 8) + } + }; + + function renderChart() { + if (activeChart) activeChart.destroy(); + const chartType = document.querySelector(".mystat-chart-tabs .nav-tab-active").getAttribute("href").substring(1); + const xAxisType = document.querySelector(\'input[name="mystat_xaxis"]:checked\').value; + + const yData = profiles[chartType], xData = xAxisConfigs[xAxisType].data; + const filteredY = [], filteredX = []; + if(yData) { + for(let i=0; i v, maxRotation: 0, autoSkip: true, maxTicksLimit: 10 } }, + y: { title: { display: true, text: chartConfigs[chartType].unit } } + }, + plugins: { legend: { display: false } }, + interaction: { intersect: false, mode: "index" }, + } + }); + } + + document.querySelectorAll(".mystat-chart-tabs .nav-tab").forEach(t => t.addEventListener("click", e => { + e.preventDefault(); + document.querySelector(".mystat-chart-tabs .nav-tab-active").classList.remove("nav-tab-active"); + e.target.classList.add("nav-tab-active"); + renderChart(); + })); + document.querySelectorAll(\'input[name="mystat_xaxis"]\').forEach(r => r.addEventListener("change", renderChart)); + if (document.querySelector(".mystat-chart-tabs .nav-tab")) renderChart(); + '; + wp_add_inline_script( 'mystat-details-loader', 'document.addEventListener("DOMContentLoaded", function() {' . $chart_js . '});' ); + } + + ?> +
+

+ Szczegóły treningu: title ); ?> + + Edytuj + +

+ +

← Powrót do listy aktywności

+ +
+

Podsumowanie

+
+
+
+

Główne dane

+ + category_name ); ?> + date ); ?> + distance, 2, ',', ' ' ), 'km' ); ?> + duration ); ?> + calories, 'kcal' ); ?> + event_type_name ); ?> + equipment_name ); ?> +
+
+
+

Dane szczegółowe

+ + avg_speed, 1, ',', ' ' ), 'km/h' ); ?> + max_speed, 1, ',', ' ' ), 'km/h' ); ?> + avg_heart_rate, 'bpm' ); ?> + max_heart_rate, 'bpm' ); ?> + avg_cadence, 'rpm' ); ?> + max_cadence, 'rpm' ); ?> + total_elevation_gain, 'm' ); ?> + total_elevation_loss, 'm' ); ?> + min_altitude, 'm n.p.m.' ); ?> + max_altitude, 'm n.p.m.' ); ?> +
+
+
+
+

Notatki i linki

+ + comment ) ); ?> + strava_url ) ) : ?> + + + gpx_url ) ) : ?> + + +
StravaZobacz aktywność na Strava
Plik GPXPobierz plik GPX
+ + +
+

Mapa Trasy

+
+ + +

Wykresy

+
+
+ + +
+ Oś X:  + +   + +
+ + + +
+
+ +
+
+ + + gpx_url ) ) : ?> +
+
+

Nie udało się wczytać danych z pliku GPX lub plik jest uszkodzony/pusty. Brak danych do wyświetlenia mapy i wykresów.

+
+ + +
+
+
+

Moje Statystyki Sportowe

'; + mystat_render_history_table(); + echo ''; +} + +function mystat_render_history_table() { + global $wpdb; + + // Definicje nazw tabel (z uwzględnieniem prefixu WP, jeśli był użyty przy tworzeniu) + // Zakładam, że tabele nazywają się dokładnie tak jak w dokumentacji, ale dobrą praktyką jest $wpdb->prefix + // Jeśli tabele są "sztywne" (bez prefixu wp_), usuń $wpdb->prefix. + $table_activities = $wpdb->prefix . 'mystat_activities'; + $table_categories = $wpdb->prefix . 'mystat_categories'; + + // --- 1. OBSŁUGA USUWANIA (DELETE) --- + if ( isset( $_GET['action'], $_GET['id'], $_GET['_wpnonce'] ) && 'mystat_delete' === $_GET['action'] ) { + $activity_id = intval( $_GET['id'] ); + + // Weryfikacja bezpieczeństwa (Nonce) + if ( wp_verify_nonce( $_GET['_wpnonce'], 'mystat_delete_' . $activity_id ) ) { + $result = $wpdb->delete( + $table_activities, + array( 'id' => $activity_id ), + array( '%d' ) + ); + + if ( $result ) { + echo '

Aktywność została usunięta.

'; + } else { + echo '

Wystąpił błąd podczas usuwania.

'; + } + } else { + echo '

Błąd weryfikacji bezpieczeństwa (Nonce).

'; + } + } + + // --- 2. USTAWIENIA PAGINACJI --- + $items_per_page = 20; // Ile wpisów na stronę + $current_page = isset( $_GET['paged'] ) ? max( 1, intval( $_GET['paged'] ) ) : 1; + $offset = ( $current_page - 1 ) * $items_per_page; + + // --- 3. POBIERANIE DANYCH (SELECT) --- + // Pobranie całkowitej liczby wpisów do paginacji + $total_items = $wpdb->get_var( "SELECT COUNT(id) FROM $table_activities" ); + $total_pages = ceil( $total_items / $items_per_page ); + + // Pobieramy wpisy dla bieżącej strony + $sql = $wpdb->prepare( + " + SELECT a.*, c.name as category_name, c.icon, c.color, et.name as event_type_name, eq.name as equipment_name + FROM $table_activities a + LEFT JOIN $table_categories c ON a.category_id = c.id + LEFT JOIN {$wpdb->prefix}mystat_event_types et ON a.event_type_id = et.id + LEFT JOIN {$wpdb->prefix}mystat_equipment eq ON a.equipment_id = eq.id + ORDER BY a.date DESC, a.id DESC + LIMIT %d OFFSET %d + ", + $items_per_page, + $offset + ); + + $activities = $wpdb->get_results( $sql ); + + // --- 4. WIDOK TABELI (HTML) --- + ?> +
+

Historia Aktywności

+ + 1 ) : ?> +
+
+ aktywności + add_query_arg( 'paged', '%#%' ), + 'format' => '', + 'total' => $total_pages, + 'current' => $current_page, + 'prev_text' => '« Poprzednia', + 'next_text' => 'Następna »', + ) + ); + ?> +
+
+ + + + + + + + + + + + + + + + + + + + + 'mystat_delete', + 'id' => $row->id, + ) + ), + 'mystat_delete_' . $row->id + ); + + $edit_url = add_query_arg( + array( + 'page' => 'mystat-edit-activity', + 'id' => $row->id, + ), + admin_url( 'admin.php' ) + ); + + $details_url = add_query_arg( + array( + 'page' => 'mystat-view-activity', + 'id' => $row->id, + ), + admin_url( 'admin.php' ) + ); + ?> + + + + + + + + + + + + + + + + + + + +
IkonaDataTytułKategoriaTypSprzętDystans (km)CzasŚr. prędkośćAkcja
+ icon ) ) : ?> + + + date ); ?>title ? wp_trim_words( $row->title, 6 ) : '(bez tytułu)' ); ?>category_name ); ?>event_type_name ); ?>equipment_name ); ?>distance, 2, ',', ' ' ); ?>duration ); ?>avg_speed ? number_format( $row->avg_speed, 1, ',', ' ' ) . ' km/h' : '-'; ?> + Edytuj + Szczegóły + + Usuń + +
Brak zarejestrowanych aktywności. Dodaj pierwszy trening powyżej!
+ + 1 ) : ?> +
+
+ aktywności + add_query_arg( 'paged', '%#%' ), + 'format' => '', + 'total' => $total_pages, + 'current' => $current_page, + 'prev_text' => '« Poprzednia', + 'next_text' => 'Następna »', + ) + ); + ?> +
+
+ +
+ prefix . 'mystat_equipment'; + $message = ''; + $notice_class = ''; + + // Handle POST requests (add/update) + if ( isset( $_POST['submit'] ) && check_admin_referer( 'mystat_manage_equipment' ) ) { + $item_id = isset( $_POST['equipment_id'] ) ? intval( $_POST['equipment_id'] ) : 0; + $data = array( + 'name' => sanitize_text_field( $_POST['equipment_name'] ), + 'type' => sanitize_text_field( $_POST['equipment_type'] ), + 'purchase_date' => empty( $_POST['purchase_date'] ) ? null : sanitize_text_field( $_POST['purchase_date'] ), + 'initial_cost' => empty( $_POST['initial_cost'] ) ? null : floatval( str_replace( ',', '.', $_POST['initial_cost'] ) ), + 'status' => sanitize_text_field( $_POST['status'] ), + 'notes' => sanitize_textarea_field( $_POST['notes'] ), + ); + + if ( ! empty( $data['name'] ) ) { + if ( $item_id > 0 ) { // Update + $wpdb->update( $table_equipment, $data, array( 'id' => $item_id ) ); + $message = 'Sprzęt zaktualizowany.'; + $notice_class = 'notice-success'; + } else { // Insert + $wpdb->insert( $table_equipment, $data ); + $message = 'Sprzęt dodany.'; + $notice_class = 'notice-success'; + } + } else { + $message = 'Nazwa sprzętu nie może być pusta.'; + $notice_class = 'notice-error'; + } + } + + // Handle GET requests (delete) + if ( isset( $_GET['action'], $_GET['id'], $_GET['_wpnonce'] ) && 'delete' === $_GET['action'] ) { + if ( wp_verify_nonce( $_GET['_wpnonce'], 'mystat_delete_equipment_' . $_GET['id'] ) ) { + // Sprawdź, czy sprzęt nie jest używany + $usage_count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}mystat_activities WHERE equipment_id = %d", intval( $_GET['id'] ) ) ); + if ( 0 == $usage_count ) { + $wpdb->delete( $table_equipment, array( 'id' => intval( $_GET['id'] ) ) ); + // Usuń również powiązane wpisy w dzienniku + $wpdb->delete( $wpdb->prefix . 'mystat_equipment_log', array( 'equipment_id' => intval( $_GET['id'] ) ) ); + $message = 'Sprzęt usunięty.'; + $notice_class = 'notice-success'; + } else { + $message = 'Nie można usunąć sprzętu, ponieważ jest przypisany do ' . $usage_count . ' aktywności. Zmień jego status na "wycofany".'; + $notice_class = 'notice-error'; + } + } + } + + // Prepare for form (for editing) + $item_to_edit = null; + $statuses = array( 'aktywny', 'sprzedany', 'wycofany' ); + if ( isset( $_GET['action'], $_GET['id'] ) && 'edit' === $_GET['action'] ) { + $item_to_edit = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_equipment WHERE id = %d", intval( $_GET['id'] ) ) ); + } + + $table_activities = $wpdb->prefix . 'mystat_activities'; + $equipment_list = $wpdb->get_results( + " + SELECT + eq.*, + stats.total_distance, + stats.total_seconds, + stats.activity_count + FROM + {$table_equipment} eq + LEFT JOIN ( + SELECT + equipment_id, + SUM(distance) as total_distance, + SUM(TIME_TO_SEC(duration)) as total_seconds, + COUNT(id) as activity_count + FROM + {$table_activities} + WHERE equipment_id IS NOT NULL + GROUP BY + equipment_id + ) as stats ON eq.id = stats.equipment_id + ORDER BY eq.status ASC, eq.name ASC " + ); + ?> +
+

Zarządzaj Sprzętem

+ +

+ + +
+
+
+
+

+
+ + +
+
+
+
+
+
+ +
+
+
+
+
+
+ + + + + id ); + ?> + + + + + + + + + +
NazwaPrzebiegLiczba aktywnościStatusAkcje
name ); ?>
type ); ?>
total_distance ? number_format( $item->total_distance, 2, ',', ' ' ) . ' km' : '0 km'; ?>activity_count; ?>status ) ); ?> + Dziennik / Serwis + Edytuj + Usuń +
+
+
+
+
+

Błąd

Nie podano ID sprzętu.

'; + return; + } + + $table_equipment = $wpdb->prefix . 'mystat_equipment'; + $table_equipment_log = $wpdb->prefix . 'mystat_equipment_log'; + $table_activities = $wpdb->prefix . 'mystat_activities'; + $message = ''; + $notice_class = ''; + + // --- Handle Service Log form submissions (add/update/delete) --- + if ( isset( $_POST['submit_log'] ) && check_admin_referer( 'mystat_manage_equipment_log' ) ) { + $log_id = isset( $_POST['log_id'] ) ? intval( $_POST['log_id'] ) : 0; + $log_data = array( + 'equipment_id' => $equipment_id, + 'log_date' => sanitize_text_field( $_POST['log_date'] ), + 'log_type' => sanitize_text_field( $_POST['log_type'] ), + 'description' => sanitize_textarea_field( $_POST['description'] ), + 'cost' => empty( $_POST['cost'] ) ? null : floatval( str_replace( ',', '.', $_POST['cost'] ) ), + 'mileage' => empty( $_POST['mileage'] ) ? null : intval( $_POST['mileage'] ), + ); + + if ( ! empty( $log_data['log_date'] ) && ! empty( $log_data['log_type'] ) && ! empty( $log_data['description'] ) ) { + if ( $log_id > 0 ) { + $wpdb->update( $table_equipment_log, $log_data, array( 'id' => $log_id ) ); + $message = 'Wpis w dzienniku zaktualizowany.'; + } else { + $wpdb->insert( $table_equipment_log, $log_data ); + $message = 'Wpis dodany do dziennika.'; + } + $notice_class = 'notice-success'; + } else { + $message = 'Wypełnij wymagane pola (Data, Typ, Opis).'; + $notice_class = 'notice-error'; + } + } + + if ( isset( $_GET['action'], $_GET['log_id'], $_GET['_wpnonce'] ) && 'delete_log' === $_GET['action'] ) { + if ( wp_verify_nonce( $_GET['_wpnonce'], 'mystat_delete_log_' . $_GET['log_id'] ) ) { + $wpdb->delete( $table_equipment_log, array( 'id' => intval( $_GET['log_id'] ) ) ); + $message = 'Wpis z dziennika usunięty.'; + $notice_class = 'notice-success'; + } + } + + // --- Get Data --- + $equipment = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_equipment WHERE id = %d", $equipment_id ) ); + if ( ! $equipment ) { + echo '

Błąd

Nie znaleziono sprzętu o podanym ID.

'; + return; + } + + $total_mileage = $wpdb->get_var( $wpdb->prepare( "SELECT SUM(distance) FROM $table_activities WHERE equipment_id = %d", $equipment_id ) ); + $total_service_cost = $wpdb->get_var( $wpdb->prepare( "SELECT SUM(cost) FROM $table_equipment_log WHERE equipment_id = %d", $equipment_id ) ); + $service_log = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $table_equipment_log WHERE equipment_id = %d ORDER BY log_date DESC, id DESC", $equipment_id ) ); + + $log_to_edit = null; + if ( isset( $_GET['action'], $_GET['log_id'] ) && 'edit_log' === $_GET['action'] ) { + $log_to_edit = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_equipment_log WHERE id = %d", intval( $_GET['log_id'] ) ) ); + } + $log_types = array( 'Naprawa', 'Zakup części', 'Przegląd', 'Modyfikacja', 'Inne' ); + + ?> +
+

Dziennik serwisowy: name ); ?>

+

← Powrót do listy sprzętu

+ + +

+ + +
+

Podsumowanie Sprzętu

+
+
+

Całkowity przebieg: km

+ 0 ) : ?> +

Całkowity koszt serwisu:

+ +

Status: status ) ); ?>

+ purchase_date ) : ?>

Data zakupu: purchase_date ); ?>

+ initial_cost ) : ?>

Koszt zakupu: initial_cost, 2, ',', ' ' ); ?> zł

+ notes ) : ?>

Notatki:
notes ) ); ?>

+
+
+
+ +
+
+
+
+

+
+ + +
+
+
+
+

Przebieg sprzętu w momencie serwisu. Domyślnie aktualny całkowity przebieg.

+ + Anuluj edycję +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
Suma kosztów:
DataTypOpisKosztPrzebiegAkcje
Brak wpisów w dzienniku.
log_date ); ?>log_type ); ?>description ) ); ?>cost ? number_format( $log->cost, 2, ',', ' ' ) . ' zł' : '-'; ?>mileage ? number_format( $log->mileage, 0, '', ' ' ) . ' km' : '-'; ?> + Edytuj | + Usuń +
+
+
+
+
+ prefix . 'mystat_event_types'; + $message = ''; + $notice_class = ''; + + // Handle POST requests (add/update) + if ( isset( $_POST['submit'] ) && check_admin_referer( 'mystat_manage_event_type' ) ) { + $name = sanitize_text_field( $_POST['event_type_name'] ); + $type_id = isset( $_POST['event_type_id'] ) ? intval( $_POST['event_type_id'] ) : 0; + + if ( ! empty( $name ) ) { + if ( $type_id > 0 ) { // Update + $wpdb->update( $table_event_types, array( 'name' => $name ), array( 'id' => $type_id ) ); + $message = 'Typ wydarzenia zaktualizowany.'; + $notice_class = 'notice-success'; + } else { // Insert + $wpdb->insert( $table_event_types, array( 'name' => $name ) ); + $message = 'Typ wydarzenia dodany.'; + $notice_class = 'notice-success'; + } + } else { + $message = 'Nazwa typu wydarzenia nie może być pusta.'; + $notice_class = 'notice-error'; + } + } + + // Handle GET requests (delete) + if ( isset( $_GET['action'], $_GET['id'], $_GET['_wpnonce'] ) && 'delete' === $_GET['action'] ) { + if ( wp_verify_nonce( $_GET['_wpnonce'], 'mystat_delete_event_type_' . $_GET['id'] ) ) { + $wpdb->delete( $table_event_types, array( 'id' => intval( $_GET['id'] ) ) ); + $message = 'Typ wydarzenia usunięty.'; + $notice_class = 'notice-success'; + } + } + + // Prepare for form (for editing) + $item_to_edit = null; + if ( isset( $_GET['action'], $_GET['id'] ) && 'edit' === $_GET['action'] ) { + $item_to_edit = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_event_types WHERE id = %d", intval( $_GET['id'] ) ) ); + } + + $event_types = $wpdb->get_results( "SELECT * FROM $table_event_types ORDER BY name ASC" ); + ?> +
+

Typy Wydarzeń

+ +

+ + +
+
+
+
+

+
+ + +
+ + +
+ +
+
+
+
+
+
+ + + + + + + +
NazwaAkcje
name ); ?>Edytuj | Usuń
+
+
+
+
+ prefix . 'mystat_activities'; + + $sql_select = ''; + switch ( $goal->goal_type ) { + case 'distance': + $sql_select = 'SUM(distance)'; + break; + case 'duration_sec': + $sql_select = 'SUM(TIME_TO_SEC(duration))'; + break; + case 'count': + $sql_select = 'COUNT(id)'; + break; + default: + return array( + 'current_value' => 0, + 'percentage' => 0, + ); + } + + $where_clauses = array(); + $where_clauses[] = $wpdb->prepare( 'YEAR(date) = %d', $goal->year ); + + if ( ! empty( $goal->month ) ) { + $where_clauses[] = $wpdb->prepare( 'MONTH(date) = %d', $goal->month ); + } + if ( ! empty( $goal->category_id ) ) { + $where_clauses[] = $wpdb->prepare( 'category_id = %d', $goal->category_id ); + } + + $sql = "SELECT {$sql_select} FROM {$table_activities} WHERE " . implode( ' AND ', $where_clauses ); + + $current_value = (float) $wpdb->get_var( $sql ); + $percentage = ( $goal->target_value > 0 ) ? ( $current_value / $goal->target_value ) * 100 : 0; + + return array( + 'current_value' => $current_value, + 'percentage' => $percentage, + ); +} + +function mystat_goals_page() { + global $wpdb; + $table_goals = $wpdb->prefix . 'mystat_goals'; + $table_categories = $wpdb->prefix . 'mystat_categories'; + $message = ''; + $notice_class = ''; + + // Handle POST requests (add/update) + if ( isset( $_POST['submit'] ) && check_admin_referer( 'mystat_manage_goal' ) ) { + $goal_id = isset( $_POST['goal_id'] ) ? intval( $_POST['goal_id'] ) : 0; + + // Convert hours to seconds for duration goal type before saving + $target_value = floatval( str_replace( ',', '.', $_POST['target_value'] ) ); + if ( 'duration_sec' === $_POST['goal_type'] ) { + $target_value *= 3600; + } + + $data = array( + 'name' => sanitize_text_field( $_POST['goal_name'] ), + 'goal_type' => sanitize_text_field( $_POST['goal_type'] ), + 'target_value' => $target_value, + 'year' => intval( $_POST['year'] ), + 'month' => 'all' === $_POST['month'] ? null : intval( $_POST['month'] ), + 'category_id' => 'all' === $_POST['category_id'] ? null : intval( $_POST['category_id'] ), + ); + + if ( ! empty( $data['name'] ) && ! empty( $data['goal_type'] ) && $data['target_value'] > 0 && $data['year'] > 2000 ) { + if ( $goal_id > 0 ) { // Update + $wpdb->update( $table_goals, $data, array( 'id' => $goal_id ) ); + $message = 'Cel zaktualizowany.'; + $notice_class = 'notice-success'; + } else { // Insert + $wpdb->insert( $table_goals, $data ); + $message = 'Cel dodany.'; + $notice_class = 'notice-success'; + } + } else { + $message = 'Wypełnij poprawnie wszystkie wymagane pola (Nazwa, Typ, Cel, Rok).'; + $notice_class = 'notice-error'; + } + } + + // Handle GET requests (delete) + if ( isset( $_GET['action'], $_GET['id'], $_GET['_wpnonce'] ) && 'delete' === $_GET['action'] ) { + if ( wp_verify_nonce( $_GET['_wpnonce'], 'mystat_delete_goal_' . $_GET['id'] ) ) { + $wpdb->delete( $table_goals, array( 'id' => intval( $_GET['id'] ) ) ); + $message = 'Cel usunięty.'; + $notice_class = 'notice-success'; + } + } + + // Prepare for form (for editing) + $item_to_edit = null; + if ( isset( $_GET['action'], $_GET['id'] ) && 'edit' === $_GET['action'] ) { + $item_to_edit = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_goals WHERE id = %d", intval( $_GET['id'] ) ) ); + } + + $goals = $wpdb->get_results( "SELECT g.*, c.name as category_name FROM $table_goals g LEFT JOIN $table_categories c ON g.category_id = c.id ORDER BY g.year DESC, g.name ASC" ); + $categories = $wpdb->get_results( "SELECT * FROM $table_categories ORDER BY name ASC" ); + ?> +
+

Zarządzaj Celami

+ +

+ + +
+
+
+
+

+
+ + +
+
+

Dla czasu podaj wartość w godzinach (np. 100.5).

+
+
+
+ +
+
+
+
+
+
+ + + + + + + + goal_type ) { + $target_formatted = round( $goal->target_value / 3600 ) . ' godz.'; + $current_formatted = sprintf( '%d godz. %d min.', floor( $progress['current_value'] / 3600 ), floor( ( $progress['current_value'] % 3600 ) / 60 ) ); + } elseif ( 'distance' === $goal->goal_type ) { + $target_formatted = number_format( $goal->target_value, 0, ',', ' ' ) . ' km'; + $current_formatted = number_format( $progress['current_value'], 2, ',', ' ' ) . ' km'; + } else { // count + $target_formatted = (int) $goal->target_value; + $current_formatted = (int) $progress['current_value']; + } + ?> + + + + + + + + +
CelPostępAkcje
Brak zdefiniowanych celów.
+ name ); ?>
+ + year ); ?> + month ) { echo ' / ' . date_i18n( 'F', mktime( 0, 0, 0, $goal->month, 10 ) ); } ?> + category_name ) { echo ' / ' . esc_html( $goal->category_name ); } ?> + +
+
+
+
+ z (%) +
+ Edytuj | + Usuń +
+
+
+
+
+ +

Importuj aktywności z pliku CSV

'; + + // Handle the form submission + if ( 'POST' === $_SERVER['REQUEST_METHOD'] && isset( $_POST['mystat_csv_import_nonce_field'] ) ) { + mystat_handle_csv_import(); + } + + // Display the form + ?> +
+

Instrukcje i formularz importu

+
+

Aby zaimportować dane, możesz wgrać plik CSV LUB wkleić dane bezpośrednio w pole tekstowe poniżej. Ta druga opcja jest zalecana, jeśli napotykasz błędy z plikiem.

+

Wymagane kolumny: Data, Tytuł, Dystans oraz Typ aktywności (lub Kategoria).

+

Opcjonalne kolumny: Czas (format HH:MM:SS), Kalorie, Średnie tętno, Maksymalne tętno, Średnia prędkość, Maksymalna prędkość, Średni rytm pedałowania, Maksymalny rytm pedałowania, Całkowity wznios, Całkowity spadek, Minimalna wysokość, Maksymalna wysokość, Sprzęt, Typ wydarzenia.

+

Ważne: +

    +
  • Data musi być w formacie YYYY-MM-DD.
  • +
  • Dystans i prędkość: użyj kropki jako separatora dziesiętnego (np. 10.5).
  • +
  • Nazwy w kolumnach Typ aktywności, Sprzęt, Typ wydarzenia muszą dokładnie odpowiadać nazwom zdefiniowanym w ustawieniach wtyczki. Jeśli nazwa nie zostanie znaleziona, wiersz zostanie pominięty.
  • +
+

+
+
+ + + + + + + + + + +
+ +
+
+
+ '; +} + +function mystat_handle_csv_import() { + global $wpdb; + + if ( ! isset( $_POST['mystat_csv_import_nonce_field'] ) || ! wp_verify_nonce( $_POST['mystat_csv_import_nonce_field'], 'mystat_csv_import_nonce' ) ) { + echo '

Błąd weryfikacji bezpieczeństwa.

'; + return; + } + + if ( ! current_user_can( 'manage_options' ) ) { + echo '

Nie masz wystarczających uprawnień.

'; + return; + } + + // Unify input source: prefer textarea, fall back to file upload. + $csv_content = ''; + if ( ! empty( $_POST['mystat_csv_data'] ) ) { + $csv_content = stripslashes( $_POST['mystat_csv_data'] ); + } elseif ( ! empty( $_FILES['mystat_csv_file']['tmp_name'] ) && UPLOAD_ERR_OK === $_FILES['mystat_csv_file']['error'] ) { + $csv_content = file_get_contents( $_FILES['mystat_csv_file']['tmp_name'] ); + } + + if ( empty( trim( $csv_content ) ) ) { + echo '

Nie podano danych do importu. Wgraj plik lub wklej dane w pole tekstowe.

'; + 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 . 'mystat_categories'; + $table_event_types = $wpdb->prefix . 'mystat_event_types'; + $table_equipment = $wpdb->prefix . 'mystat_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 . 'mystat_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 '

Podane dane CSV są puste.

'; + 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 '

Brak wymaganych kolumn w danych CSV: ' . esc_html( implode( ', ', $missing_polish_names ) ) . '

'; + 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 '

Pomyślnie zaimportowano ' . esc_html( $imported_count ) . ' aktywności.

';} + if ( ! empty( $skipped_details ) ) { + echo '
'; + echo '

Pominięto ' . count( $skipped_details ) . ' wierszy z powodu błędów

'; + echo '
'; + echo ''; + foreach ( $skipped_details as $error ) { + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + echo '
WierszPowód pominięciaDane wiersza
' . esc_html( $error['row'] ) . '' . esc_html( $error['reason'] ) . '' . esc_html( wp_trim_words( $error['data'], 25, '...' ) ) . '
'; + echo '
'; + } + if ( 0 === $imported_count && empty( $skipped_details ) && $row_number > 1 ) { + echo '

Dane CSV nie zawierały żadnych poprawnych wierszy do importu.

'; + } elseif ( 1 === $row_number ) { + echo '

Dane CSV były puste lub zawierały tylko nagłówek.

';} +} \ No newline at end of file diff --git a/includes/admin/pages/page-infographic.php b/includes/admin/pages/page-infographic.php new file mode 100644 index 0000000..a4d297a --- /dev/null +++ b/includes/admin/pages/page-infographic.php @@ -0,0 +1,159 @@ +prefix . 'mystat_activities'; + $table_categories = $wpdb->prefix . 'mystat_categories'; + + $current_year = isset( $_GET['year'] ) ? intval( $_GET['year'] ) : current_time( 'Y' ); + + // Pobierz dostępne lata z bazy danych + $available_years = $wpdb->get_col( "SELECT DISTINCT YEAR(date) FROM $table_activities ORDER BY YEAR(date) DESC" ); + if ( empty( $available_years ) ) { + $available_years = array( current_time( 'Y' ) ); // Domyślny rok, jeśli brak danych + } + + // --- 1. Statystyki ogólne (wszystkie lata) --- + $overall_stats = $wpdb->get_row( + " + SELECT + SUM(distance) as total_distance, + SEC_TO_TIME(SUM(TIME_TO_SEC(duration))) as total_duration, + SUM(total_elevation_gain) as total_elevation_gain, + COUNT(id) as total_activities + FROM $table_activities + " + ); + + // --- 2. Statystyki dla wybranego roku --- + $yearly_stats = $wpdb->get_row( + $wpdb->prepare( + " + SELECT + SUM(distance) as total_distance, + SEC_TO_TIME(SUM(TIME_TO_SEC(duration))) as total_duration, + SUM(total_elevation_gain) as total_elevation_gain, + COUNT(id) as total_activities + FROM $table_activities + WHERE YEAR(date) = %d + ", + $current_year + ) + ); + + // --- 3. Dane dla wykresu kołowego (dystans per kategoria dla wybranego roku) --- + $category_distance_data = $wpdb->get_results( + $wpdb->prepare( + " + SELECT c.name as category_name, c.color, SUM(a.distance) as total_distance + FROM $table_activities a + LEFT JOIN $table_categories c ON a.category_id = c.id + WHERE YEAR(a.date) = %d + GROUP BY c.name, c.color + HAVING SUM(a.distance) > 0 + ORDER BY total_distance DESC + ", + $current_year + ) + ); + + $chart_labels = array(); + $chart_data = array(); + $chart_colors = array(); + foreach ( $category_distance_data as $data ) { + $chart_labels[] = $data->category_name; + $chart_data[] = round( (float) $data->total_distance, 2 ); + $chart_colors[] = $data->color; + } + + // Włączenie skryptów dla Chart.js + wp_enqueue_script( 'chart-js', 'https://cdn.jsdelivr.net/npm/chart.js', array(), null, true ); + wp_register_script( 'mystat-infographic-chart-loader', false ); + wp_enqueue_script( 'mystat-infographic-chart-loader' ); + wp_add_inline_script( + 'mystat-infographic-chart-loader', + ' + document.addEventListener("DOMContentLoaded", function() { + const ctx = document.getElementById("mystatCategoryPieChart"); + if (!ctx) return; + new Chart(ctx, { + type: "pie", + data: { + labels: ' . json_encode( $chart_labels ) . ', + datasets: [{ + data: ' . json_encode( $chart_data ) . ', + backgroundColor: ' . json_encode( $chart_colors ) . ', + hoverOffset: 4 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: "right", + }, + title: { + display: true, + text: "Dystans wg kategorii w ' . esc_js( $current_year ) . '" + } + } + } + }); + }); + ' + ); + + ?> +
+

Infografika Statystyk Sportowych

+ +
+
+
+ + + + +
+
+
+ +
+

Statystyki Ogólne (wszystkie lata)

+
+

Dystans

total_distance, 2, ',', ' ' ); ?> km

+

Czas

total_duration ); ?>

+

Wznios

total_elevation_gain, 0, ',', ' ' ); ?> m

+

Aktywności

total_activities, 0, ',', ' ' ); ?>

+
+
+ +
+

Statystyki dla

+
+

Dystans

total_distance, 2, ',', ' ' ); ?> km

+

Czas

total_duration ); ?>

+

Wznios

total_elevation_gain, 0, ',', ' ' ); ?> m

+

Aktywności

total_activities, 0, ',', ' ' ); ?>

+
+
+ +
+

Rozkład Dystansu wg Kategorii w

+
+
+ +
+
+
+
+ +
+

Ustawienia Wtyczki Statystyk

+
+ +
+
+ Zdefiniuj strefę prywatności, aby ukryć początek i koniec swoich tras GPX. Punkty wewnątrz tego okręgu nie będą wyświetlane na mapie.

'; + echo '

Aby znaleźć swoje współrzędne, kliknij prawym przyciskiem myszy na mapie Google w wybranym miejscu - współrzędne pojawią się jako pierwsza pozycja w menu.

'; +} + +function mystat_render_lat_field() { + $options = get_option( 'mystat_privacy_options' ); + $latitude = isset( $options['latitude'] ) ? esc_attr( $options['latitude'] ) : ''; + echo ""; +} + +function mystat_render_lon_field() { + $options = get_option( 'mystat_privacy_options' ); + $longitude = isset( $options['longitude'] ) ? esc_attr( $options['longitude'] ) : ''; + echo ""; +} + +function mystat_render_radius_field() { + $options = get_option( 'mystat_privacy_options' ); + $radius = isset( $options['radius'] ) ? esc_attr( $options['radius'] ) : '500'; + echo " metrów"; +} + +function mystat_sanitize_privacy_options( $input ) { + $sanitized_input = array(); + if ( isset( $input['latitude'] ) ) { + $sanitized_input['latitude'] = floatval( str_replace( ',', '.', $input['latitude'] ) ); + } + if ( isset( $input['longitude'] ) ) { + $sanitized_input['longitude'] = floatval( str_replace( ',', '.', $input['longitude'] ) ); + } + if ( isset( $input['radius'] ) ) { + $sanitized_input['radius'] = abs( intval( $input['radius'] ) ); + } + return $sanitized_input; +} \ No newline at end of file diff --git a/includes/admin/pages/page-yearly-summary.php b/includes/admin/pages/page-yearly-summary.php new file mode 100644 index 0000000..0f1000d --- /dev/null +++ b/includes/admin/pages/page-yearly-summary.php @@ -0,0 +1,292 @@ +prefix . 'mystat_activities'; + + $current_year = isset( $_GET['year'] ) ? intval( $_GET['year'] ) : current_time( 'Y' ); + + // Pobierz dostępne lata z bazy danych + $available_years = $wpdb->get_col( "SELECT DISTINCT YEAR(date) FROM $table_activities ORDER BY YEAR(date) DESC" ); + if ( empty( $available_years ) ) { + $available_years = array( current_time( 'Y' ) ); // Domyślny rok, jeśli brak danych + } + + // --- GOALS SECTION --- + $table_goals = $wpdb->prefix . 'mystat_goals'; + $goals_for_year = $wpdb->get_results( + $wpdb->prepare( + "SELECT * FROM {$table_goals} WHERE year = %d ORDER BY name ASC", + $current_year + ) + ); + + // Zapytanie SQL do grupowania danych miesięcznie + $sql = $wpdb->prepare( + " + SELECT + MONTH(date) as month_num, + SUM(distance) as total_distance, + SUM(calories) as total_calories, + SUM(TIME_TO_SEC(duration)) as total_seconds, + COUNT(id) as activity_count + FROM $table_activities + WHERE YEAR(date) = %d + GROUP BY month_num + ORDER BY month_num ASC + ", + $current_year + ); + + $monthly_summary = $wpdb->get_results( $sql, OBJECT_K ); // OBJECT_K zwróci tablicę z kluczami będącymi numerami miesięcy + + // Przygotowanie danych dla wszystkich 12 miesięcy + $full_year_summary = array(); + $total_year_distance = 0; + $total_year_calories = 0; + $total_year_seconds = 0; + + // Określ, ile miesięcy pokazać, aby uniknąć zer dla przyszłych miesięcy + $this_year = (int) current_time( 'Y' ); + $this_month = (int) current_time( 'n' ); + $loop_until_month = 12; // Domyślnie dla lat ubiegłych + + if ( $current_year === $this_year ) { + // Dla bieżącego roku, pokaż miesiące do bieżącego miesiąca + $loop_until_month = $this_month; + } elseif ( $current_year > $this_year ) { + // Dla przyszłych lat, pokaż miesiące tylko do ostatniego, w którym są dane + $last_month_with_data = $wpdb->get_var( $wpdb->prepare( "SELECT MAX(MONTH(date)) FROM $table_activities WHERE YEAR(date) = %d", $current_year ) ); + $loop_until_month = $last_month_with_data ? (int) $last_month_with_data : 0; + } + + for ( $i = 1; $i <= $loop_until_month; $i++ ) { + $month_name = date_i18n( 'F', mktime( 0, 0, 0, $i, 10 ) ); // Nazwa miesiąca + $data = isset( $monthly_summary[ $i ] ) ? $monthly_summary[ $i ] : null; + + $full_year_summary[ $i ] = (object) array( + 'month_name' => $month_name, + 'total_distance' => $data ? $data->total_distance : 0, + 'total_calories' => $data ? (int) $data->total_calories : 0, + 'total_seconds' => $data ? (int) $data->total_seconds : 0, + 'activity_count' => $data ? (int) $data->activity_count : 0, + ); + + $total_year_distance += $full_year_summary[ $i ]->total_distance; + $total_year_seconds += $full_year_summary[ $i ]->total_seconds; + $total_year_calories += $full_year_summary[ $i ]->total_calories; + } + + $total_year_hours = floor( $total_year_seconds / 3600 ); + $total_year_minutes = floor( ( $total_year_seconds % 3600 ) / 60 ); + $total_year_duration_formatted = sprintf( '%d godz. %d min.', $total_year_hours, $total_year_minutes ); + + // Przygotowanie danych dla wykresu + $chart_labels_js = array(); + $chart_datasets = array( + 'distance' => array(), + 'duration' => array(), + 'calories' => array(), + 'activities' => array(), + ); + + foreach ( $full_year_summary as $month_data ) { + $chart_labels_js[] = $month_data->month_name; + $chart_datasets['distance'][] = round( (float) $month_data->total_distance, 2 ); + $chart_datasets['duration'][] = round( $month_data->total_seconds / 3600, 2 ); // w godzinach + $chart_datasets['calories'][] = $month_data->total_calories; + $chart_datasets['activities'][] = $month_data->activity_count; + } + + // Włączenie skryptów dla Chart.js + wp_enqueue_script( 'chart-js', 'https://cdn.jsdelivr.net/npm/chart.js', array(), null, true ); + wp_register_script( 'mystat-chart-loader', false ); + wp_enqueue_script( 'mystat-chart-loader' ); + + $chart_configs = array( + 'distance' => array( + 'label' => 'Dystans (km)', + 'data' => $chart_datasets['distance'], + 'backgroundColor' => 'rgba(52, 152, 219, 0.5)', + 'borderColor' => 'rgba(52, 152, 219, 1)', + 'yAxisLabel' => 'Kilometry', + ), + 'duration' => array( + 'label' => 'Czas trwania (godz.)', + 'data' => $chart_datasets['duration'], + 'backgroundColor' => 'rgba(26, 188, 156, 0.5)', + 'borderColor' => 'rgba(26, 188, 156, 1)', + 'yAxisLabel' => 'Godziny', + ), + 'calories' => array( + 'label' => 'Kalorie (kcal)', + 'data' => $chart_datasets['calories'], + 'backgroundColor' => 'rgba(231, 76, 60, 0.5)', + 'borderColor' => 'rgba(231, 76, 60, 1)', + 'yAxisLabel' => 'kcal', + ), + 'activities' => array( + 'label' => 'Liczba aktywności', + 'data' => $chart_datasets['activities'], + 'backgroundColor' => 'rgba(241, 196, 15, 0.5)', + 'borderColor' => 'rgba(241, 196, 15, 1)', + 'yAxisLabel' => 'Ilość', + ), + ); + + wp_add_inline_script( + 'mystat-chart-loader', + ' + document.addEventListener("DOMContentLoaded", function() { + const chartLabels = ' . json_encode( $chart_labels_js ) . '; + const chartConfigs = ' . json_encode( $chart_configs ) . '; + let activeChart = null; + + const tabs = document.querySelectorAll(".nav-tab"); + tabs.forEach(tab => { + tab.addEventListener("click", function(e) { + e.preventDefault(); + tabs.forEach(t => t.classList.remove("nav-tab-active")); + this.classList.add("nav-tab-active"); + + const chartType = this.getAttribute("href").substring(1); + renderChart(chartType); + }); + }); + + function renderChart(type) { + if (activeChart) { + activeChart.destroy(); + } + const config = chartConfigs[type]; + const ctx = document.getElementById("mystatYearlyChart").getContext("2d"); + activeChart = new Chart(ctx, { + type: "bar", + data: { + labels: chartLabels, + datasets: [{ + label: config.label, + data: config.data, + backgroundColor: config.backgroundColor, + borderColor: config.borderColor, + borderWidth: 1 + }] + }, + options: { + responsive: true, maintainAspectRatio: false, + scales: { y: { beginAtZero: true, title: { display: true, text: config.yAxisLabel } } } + } + }); + } + + // Render initial chart + if (tabs.length > 0) { + renderChart(tabs[0].getAttribute("href").substring(1)); + } + }); + ' + ); + + ?> +
+

Podsumowanie Roczne

+ +
+
+ +
+ + + +
+
+
+ + +
+

Cele na

+
+
    + goal_type ) { + $target_formatted = round( $goal->target_value / 3600 ) . ' godz.'; + $current_formatted = sprintf( '%d godz. %d min.', floor( $progress['current_value'] / 3600 ), floor( ( $progress['current_value'] % 3600 ) / 60 ) ); + } elseif ( 'distance' === $goal->goal_type ) { + $target_formatted = number_format( $goal->target_value, 0, ',', ' ' ) . ' km'; + $current_formatted = number_format( $progress['current_value'], 2, ',', ' ' ) . ' km'; + } else { // count + $target_formatted = (int) $goal->target_value; + $current_formatted = (int) $progress['current_value']; + } + ?> +
  • +
    + name ); ?> + / (%) +
    +
    +
    +
    +
  • + +
+
+
+ + + +
+

Wykresy dla

+
+ +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + +
MiesiącDystans (km)Kalorie (kcal)Czas
month_name ); ?>total_distance, 2, ',', ' ' ); ?>total_calories, 0, ',', ' ' ); ?>total_seconds ) ); ?>
SUMA ROCZNA
+
+ 20 ) ); + + if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) { + return array(); + } + + $gpx_content = wp_remote_retrieve_body( $response ); + if ( empty( $gpx_content ) ) { + return array(); + } + + // --- Privacy Zone --- + $privacy_options = get_option( 'mystat_privacy_options' ); + $privacy_enabled = ! empty( $privacy_options['latitude'] ) && ! empty( $privacy_options['longitude'] ) && ! empty( $privacy_options['radius'] ); + $privacy_center_lat = $privacy_enabled ? (float) $privacy_options['latitude'] : 0; + $privacy_center_lon = $privacy_enabled ? (float) $privacy_options['longitude'] : 0; + $privacy_radius_km = $privacy_enabled ? ( (int) $privacy_options['radius'] ) / 1000 : 0; // Convert meters to km + + libxml_use_internal_errors( true ); + $gpx = simplexml_load_string( $gpx_content ); + libxml_clear_errors(); + + if ( false === $gpx ) { + return array(); + } + + // Use XPath to be more robust against different GPX structures/namespaces + $gpx->registerXPathNamespace( 'gpx', 'http://www.topografix.com/GPX/1/1' ); + $trackpoints = $gpx->xpath( '//gpx:trkpt' ); + if ( empty( $trackpoints ) ) { + $trackpoints = $gpx->xpath( '//trkpt' ); // Fallback for files without namespace + } + + $raw_points = array(); + $start_time = null; + + foreach ( $trackpoints as $trkpt ) { + if ( isset( $trkpt['lat'], $trkpt['lon'] ) ) { + $extensions = $trkpt->extensions ? $trkpt->extensions->children( 'gpxtpx', true ) : null; + $time = isset( $trkpt->time ) ? strtotime( (string) $trkpt->time ) : null; + if ( $time && is_null( $start_time ) ) { + $start_time = $time; + } + + $hr_val = ( $extensions && isset( $extensions->TrackPointExtension->hr ) ) ? (int) $extensions->TrackPointExtension->hr : null; + $cad_val = ( $extensions && isset( $extensions->TrackPointExtension->cad ) ) ? (int) $extensions->TrackPointExtension->cad : null; + + $raw_points[] = array( + 'lat' => (float) $trkpt['lat'], + 'lon' => (float) $trkpt['lon'], + 'ele' => isset( $trkpt->ele ) ? (float) $trkpt->ele : null, + 'time_offset' => $start_time && $time ? $time - $start_time : null, + 'hr' => $hr_val, + 'cad' => $cad_val, + ); + } + } + + if ( empty( $raw_points ) ) { + return array(); + } + + // Process raw points to calculate profiles + $map_points = array(); + $profiles = array( + 'distance' => array(), + 'time' => array(), + 'elevation' => array(), + 'speed' => array(), + 'hr' => array(), + 'cadence' => array(), + ); + $cumulative_distance = 0; + + $haversine = function( $lat1, $lon1, $lat2, $lon2 ) { + $earth_radius = 6371; // in km + $dLat = deg2rad( $lat2 - $lat1 ); + $dLon = deg2rad( $lon2 - $lon1 ); + $a = sin( $dLat / 2 ) * sin( $dLat / 2 ) + cos( deg2rad( $lat1 ) ) * cos( deg2rad( $lat2 ) ) * sin( $dLon / 2 ) * sin( $dLon / 2 ); + $c = 2 * atan2( sqrt( $a ), sqrt( 1 - $a ) ); + return $earth_radius * $c; + }; + + foreach ( $raw_points as $i => $point ) { + $is_in_privacy_zone = false; + if ( $privacy_enabled ) { + $distance_from_center = $haversine( $privacy_center_lat, $privacy_center_lon, $point['lat'], $point['lon'] ); + if ( $distance_from_center <= $privacy_radius_km ) { + $is_in_privacy_zone = true; + } + } + + if ( ! $is_in_privacy_zone ) { + $map_points[] = array( $point['lat'], $point['lon'] ); + $speed = null; + + if ( $i > 0 ) { + $prev_point = $raw_points[ $i - 1 ]; + $distance_delta = $haversine( $prev_point['lat'], $prev_point['lon'], $point['lat'], $point['lon'] ); // km + $cumulative_distance += $distance_delta; + + if ( ! is_null( $point['time_offset'] ) && ! is_null( $prev_point['time_offset'] ) ) { + $time_delta = $point['time_offset'] - $prev_point['time_offset']; // seconds + if ( $time_delta > 0 ) { + $speed = ( $distance_delta * 3600 ) / $time_delta; // km/h + } + } + } + + $profiles['distance'][] = round( $cumulative_distance, 3 ); + $profiles['time'][] = $point['time_offset']; + $profiles['elevation'][] = ! is_null( $point['ele'] ) ? round( $point['ele'], 2 ) : null; + $profiles['speed'][] = ! is_null( $speed ) ? round( $speed, 1 ) : null; + $profiles['hr'][] = $point['hr']; + $profiles['cadence'][] = $point['cad']; + } + } + + return array( + 'points' => $map_points, + 'profiles' => $profiles, + ); +} \ No newline at end of file diff --git a/includes/core/gpx-upload.php b/includes/core/gpx-upload.php new file mode 100644 index 0000000..460a62a --- /dev/null +++ b/includes/core/gpx-upload.php @@ -0,0 +1,33 @@ + current_time( 'Y' ), + 'month' => current_time( 'n' ), + ), + $atts, + 'moje_statystyki' + ); + + $year = intval( $atts['year'] ); + $month = intval( $atts['month'] ); + + // Pobieranie danych z bazy + $table_activities = $wpdb->prefix . 'mystat_activities'; + $sql = $wpdb->prepare( + " + SELECT a.*, c.name as category_name, eq.name as equipment_name + FROM $table_activities a + LEFT JOIN {$wpdb->prefix}mystat_categories c ON a.category_id = c.id + LEFT JOIN {$wpdb->prefix}mystat_equipment eq ON a.equipment_id = eq.id + WHERE YEAR(a.date) = %d AND MONTH(a.date) = %d + ORDER BY a.date ASC + ", + $year, + $month + ); + + $activities = $wpdb->get_results( $sql ); + + // Obliczanie podsumowań + $total_distance = 0; + $total_seconds = 0; + foreach ( $activities as $activity ) { + $total_distance += $activity->distance; + if ( ! empty( $activity->duration ) ) { + list($h, $m, $s) = explode( ':', $activity->duration ); + $total_seconds += $h * 3600 + $m * 60 + $s; + } + } + $hours = floor( $total_seconds / 3600 ); + $minutes = floor( ( $total_seconds % 3600 ) / 60 ); + $total_duration_formatted = sprintf( '%d godz. %d min.', $hours, $minutes ); + + // Rozpoczęcie buforowania wyjścia + ob_start(); + ?> +
+ +

Podsumowanie miesiąca

+ + + + + + + + + + + + + +
Całkowity dystansCałkowity czas
km
+ +

Lista aktywności

+ + + + + + + + + + + + + + + + + + + + + + + + avg_speed || $row->avg_heart_rate || $row->avg_cadence ) : ?> + + + + + + + + + + + +
DataTytułKategoriaDystansCzasSprzęt
date ) ) ); ?>title ); ?>category_name ); ?>distance, 2, ',', ' ' ); ?> kmduration ); ?>equipment_name ); ?>
+ + + + avg_speed ) { echo ''; } ?> + avg_heart_rate ) { echo ''; } ?> + avg_cadence ) { echo ''; } ?> + + + + + avg_speed ) { echo ''; } ?> + avg_heart_rate ) { echo ''; } ?> + avg_cadence ) { echo ''; } ?> + + +
Śr. prędkośćŚr. tętnoŚr. kadencja
' . number_format( $row->avg_speed, 1, ',', ' ' ) . ' km/h' . $row->avg_heart_rate . ' bpm' . $row->avg_cadence . ' rpm
+
Brak aktywności w tym miesiącu.
+
+ 0, + ), + $atts, + 'moje_statystyki_wpis' + ); + + $activity_id = intval( $atts['id'] ); + + if ( 0 === $activity_id ) { + return '

Błąd: Nie podano ID wpisu w shortcode.

'; + } + + // Pobieranie danych z bazy + $table_activities = $wpdb->prefix . 'mystat_activities'; + $sql = $wpdb->prepare( + " + SELECT a.*, c.name as category_name, c.color as category_color, et.name as event_type_name, eq.name as equipment_name + FROM $table_activities a + LEFT JOIN {$wpdb->prefix}mystat_categories c ON a.category_id = c.id + LEFT JOIN {$wpdb->prefix}mystat_event_types et ON a.event_type_id = et.id + LEFT JOIN {$wpdb->prefix}mystat_equipment eq ON a.equipment_id = eq.id + WHERE a.id = %d + ", + $activity_id + ); + + $activity = $wpdb->get_row( $sql ); + + if ( ! $activity ) { + return '

Błąd: Nie znaleziono wpisu o ID ' . esc_html( $activity_id ) . '.

'; + } + + // Funkcja pomocnicza do wyświetlania wiersza + $render_row = function( $label, $value, $unit = '' ) { + if ( ! is_null( $value ) && '' !== $value && 0 != $value ) { + echo ''; + echo '' . esc_html( $label ) . ''; + echo '' . esc_html( $value ) . ( $unit ? ' ' . esc_html( $unit ) : '' ) . ''; + echo ''; + } + }; + + ob_start(); + + // Prepare map and chart data if GPX exists + $gpx_data = array(); + $has_gpx_data = false; + if ( ! empty( $activity->gpx_url ) ) { + $gpx_data = mystat_parse_gpx_data( $activity->gpx_url ); + $has_gpx_data = ! empty( $gpx_data['points'] ); + } + + $unique_id = 'mystat-activity-' . esc_attr( $activity->id ); + + if ( $has_gpx_data ) { + wp_enqueue_style( 'leaflet-css', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css' ); + wp_enqueue_script( 'leaflet-js', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js', array(), '1.9.4', true ); + wp_enqueue_script( 'chart-js', 'https://cdn.jsdelivr.net/npm/chart.js', array(), null, true ); + + $map_id = 'map-' . $unique_id; + $chart_id = 'chart-' . $unique_id; + + $available_profiles = array(); + if ( ! empty( array_filter( $gpx_data['profiles']['elevation'] ) ) ) { + $available_profiles['elevation'] = 'Wysokość';} + if ( ! empty( array_filter( $gpx_data['profiles']['speed'] ) ) ) { + $available_profiles['speed'] = 'Prędkość';} + if ( ! empty( array_filter( $gpx_data['profiles']['hr'] ) ) ) { + $available_profiles['hr'] = 'Tętno';} + if ( ! empty( array_filter( $gpx_data['profiles']['cadence'] ) ) ) { + $available_profiles['cadence'] = 'Kadencja';} + + $has_time_data = ! empty( array_filter( $gpx_data['profiles']['time'], fn( $t ) => ! is_null( $t ) ) ); + + wp_register_script( 'mystat-shortcode-loader-' . $activity->id, false ); + wp_enqueue_script( 'mystat-shortcode-loader-' . $activity->id ); + + $js_script = ' + (function() { + document.addEventListener("DOMContentLoaded", function() { + const uniqueId = "' . esc_js( $unique_id ) . '"; + const containerEl = document.getElementById(uniqueId); + if (!containerEl || typeof L === "undefined" || typeof Chart === "undefined") return; + + const trackPoints = ' . json_encode( $gpx_data['points'] ) . '; + const profiles = ' . json_encode( $gpx_data['profiles'] ) . '; + let activeChart = null; + + const mapId = "' . esc_js( $map_id ) . '"; + const mapEl = document.getElementById(mapId); + if (mapEl && trackPoints.length > 0) { + if (mapEl._leaflet_id) mapEl._leaflet_id = null; + const map = L.map(mapId); + L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: \'© OpenStreetMap\' }).addTo(map); + const polyline = L.polyline(trackPoints, {color: "' . esc_js( $activity->category_color ) . '"}).addTo(map); + map.fitBounds(polyline.getBounds().pad(0.1)); + } + + const chartId = "' . esc_js( $chart_id ) . '"; + const chartEl = document.getElementById(chartId); + if (!chartEl) return; + + const chartConfigs = { + elevation: { label: "Wysokość", unit: "m n.p.m.", color: "#8e44ad" }, + speed: { label: "Prędkość", unit: "km/h", color: "#2980b9" }, + hr: { label: "Tętno", unit: "bpm", color: "#c0392b" }, + cadence: { label: "Kadencja", unit: "rpm", color: "#27ae60" } + }; + const xAxisConfigs = { + distance: { label: "Dystans (km)", data: profiles.distance }, + time: { label: "Czas", data: profiles.time, formatter: (s) => s === null ? "" : new Date(s * 1000).toISOString().substr(11, 8) } + }; + + function renderChart() { + if (activeChart) activeChart.destroy(); + const activeTab = containerEl.querySelector(".mystat-chart-tab.active"); + if (!activeTab) return; + const chartType = activeTab.dataset.type; + const xAxisRadio = containerEl.querySelector(\'input[name="xaxis-\' + uniqueId + \'"]:checked\'); + if (!xAxisRadio) return; + const xAxisType = xAxisRadio.value; + + const yData = profiles[chartType], xData = xAxisConfigs[xAxisType].data; + const filteredY = [], filteredX = []; + if(yData) { + for(let i=0; i v, maxRotation: 0, autoSkip: true, maxTicksLimit: 7 } }, + y: { title: { display: true, text: chartConfigs[chartType].unit } } + }, + plugins: { legend: { display: false } }, + interaction: { intersect: false, mode: "index" }, + } + }); + } + + containerEl.querySelectorAll(".mystat-chart-tab").forEach(t => t.addEventListener("click", e => { + e.preventDefault(); + containerEl.querySelector(".mystat-chart-tab.active").classList.remove("active"); + e.currentTarget.classList.add("active"); + renderChart(); + })); + containerEl.querySelectorAll(\'input[name="xaxis-\' + uniqueId + \'"]\').forEach(r => r.addEventListener("change", renderChart)); + if (containerEl.querySelector(".mystat-chart-tab")) renderChart(); + }); + })();'; + wp_add_inline_script( 'mystat-shortcode-loader-' . $activity->id, $js_script ); + } + + ?> +
+

title ); ?>

+

date ) ) ); ?>

+ +
+
+ + + distance, 2, ',', ' ' ), 'km' ); ?> + duration ); ?> + avg_speed, 1, ',', ' ' ), 'km/h' ); ?> + total_elevation_gain, 'm' ); ?> + +
+
+
+ + + category_name ); ?> + equipment_name ); ?> + strava_url ) ) : ?> + + + +
StravaZobacz aktywność
+
+
+ + +
+ + +
+
+
+ $label ) : + ?> + + +
+ +
+ Oś X:  + +   + +
+ + + +
+
+ +
+
+ + +
+ get_charset_collate(); +// --- 2. PLIKI I HOOKI PANELU ADMINA --- +require_once MYSTAT_PLUGIN_DIR . 'includes/admin/hooks.php'; +require_once MYSTAT_PLUGIN_DIR . 'includes/admin/menu.php'; +require_once MYSTAT_PLUGIN_DIR . 'includes/admin/pages/page-dashboard.php'; +require_once MYSTAT_PLUGIN_DIR . 'includes/admin/pages/page-activity-form.php'; +require_once MYSTAT_PLUGIN_DIR . 'includes/admin/pages/page-activity-view.php'; +require_once MYSTAT_PLUGIN_DIR . 'includes/admin/pages/page-event-types.php'; +require_once MYSTAT_PLUGIN_DIR . 'includes/admin/pages/page-equipment.php'; +require_once MYSTAT_PLUGIN_DIR . 'includes/admin/pages/page-goals.php'; +require_once MYSTAT_PLUGIN_DIR . 'includes/admin/pages/page-settings.php'; +require_once MYSTAT_PLUGIN_DIR . 'includes/admin/pages/page-yearly-summary.php'; +require_once MYSTAT_PLUGIN_DIR . 'includes/admin/pages/page-infographic.php'; +require_once MYSTAT_PLUGIN_DIR . 'includes/admin/pages/page-import-csv.php'; - $table_categories = $wpdb->prefix . 'mystat_categories'; - $table_activities = $wpdb->prefix . 'mystat_activities'; - $table_event_types = $wpdb->prefix . 'mystat_event_types'; - $table_equipment = $wpdb->prefix . 'mystat_equipment'; - $table_goals = $wpdb->prefix . 'mystat_goals'; - - // SQL dla Kategorii - $sql_cat = "CREATE TABLE $table_categories ( - id mediumint(9) NOT NULL AUTO_INCREMENT, - name varchar(50) NOT NULL, - icon varchar(50) NOT NULL, - color varchar(20) NOT NULL, - PRIMARY KEY (id) - ) $charset_collate;"; - - // SQL dla Typów Wydarzeń - $sql_event_types = "CREATE TABLE $table_event_types ( - id mediumint(9) NOT NULL AUTO_INCREMENT, - name varchar(100) NOT NULL, - PRIMARY KEY (id) - ) $charset_collate;"; - - // SQL dla Sprzętu - $sql_equipment = "CREATE TABLE $table_equipment ( - id mediumint(9) NOT NULL AUTO_INCREMENT, - name varchar(100) NOT NULL, - PRIMARY KEY (id) - ) $charset_collate;"; - - // SQL dla Celów - $sql_goals = "CREATE TABLE $table_goals ( - id mediumint(9) NOT NULL AUTO_INCREMENT, - name varchar(255) NOT NULL, - goal_type varchar(20) NOT NULL, -- 'distance', 'duration_sec', 'count' - target_value decimal(10,2) NOT NULL, - year smallint(4) NOT NULL, - month tinyint(2) UNSIGNED DEFAULT NULL, - category_id mediumint(9) DEFAULT NULL, - PRIMARY KEY (id), - KEY category_id (category_id) - ) $charset_collate;"; - - // SQL dla Aktywności - $sql_act = "CREATE TABLE $table_activities ( - id bigint(20) NOT NULL AUTO_INCREMENT, - category_id mediumint(9) NOT NULL, - date date NOT NULL, - title varchar(255) DEFAULT '' NOT NULL, - distance decimal(10,2) DEFAULT 0.00, - duration time DEFAULT '00:00:00', - calories int(11) DEFAULT 0, - comment text, - strava_url varchar(255) DEFAULT NULL, - avg_heart_rate smallint(5) UNSIGNED DEFAULT NULL, - max_heart_rate smallint(5) UNSIGNED DEFAULT NULL, - avg_speed decimal(5,2) DEFAULT NULL, - max_speed decimal(5,2) DEFAULT NULL, - avg_cadence smallint(5) UNSIGNED DEFAULT NULL, - max_cadence smallint(5) UNSIGNED DEFAULT NULL, - total_elevation_gain int(11) DEFAULT NULL, - total_elevation_loss int(11) DEFAULT NULL, - min_altitude int(11) DEFAULT NULL, - max_altitude int(11) DEFAULT NULL, - equipment_id mediumint(9) DEFAULT NULL, - gpx_url varchar(255) DEFAULT NULL, - event_type_id mediumint(9) DEFAULT NULL, - PRIMARY KEY (id) - ) $charset_collate;"; - - require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); - dbDelta( $sql_goals ); - dbDelta( $sql_equipment ); - dbDelta( $sql_cat ); - dbDelta( $sql_event_types ); - dbDelta( $sql_act ); - - // Dodanie domyślnych kategorii, jeśli tabela jest pusta - if ( $wpdb->get_var( "SELECT COUNT(*) FROM $table_categories" ) == 0 ) { - $wpdb->insert( $table_categories, array( 'name' => 'Rower', 'icon' => 'dashicons-buddicons-groups', 'color' => '#3498db' ) ); - $wpdb->insert( $table_categories, array( 'name' => 'Bieganie', 'icon' => 'dashicons-businessman', 'color' => '#e74c3c' ) ); - } - - // Dodanie domyślnych typów wydarzeń, jeśli tabela jest pusta - if ( $wpdb->get_var( "SELECT COUNT(*) FROM $table_event_types" ) == 0 ) { - $default_event_types = ['Bez kategorii', 'Fitness', 'Geocaching', 'Podróżowanie', 'Rekreacyjny', 'Specjalne zdarzenie', 'Transport', 'Trening', 'Wyścig']; - foreach ($default_event_types as $type_name) { - $wpdb->insert( $table_event_types, array( 'name' => $type_name ) ); - } - // Ustawienie domyślnego typu "Trening" dla istniejących aktywności, które go nie mają - $training_id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $table_event_types WHERE name = %s", 'Trening' ) ); - if ($training_id) { - $wpdb->query( $wpdb->prepare( "UPDATE $table_activities SET event_type_id = %d WHERE event_type_id IS NULL OR event_type_id = 0", $training_id ) ); - } - } - - // Dodanie domyślnego sprzętu, jeśli tabela jest pusta - if ( $wpdb->get_var( "SELECT COUNT(*) FROM $table_equipment" ) == 0 ) { - $default_equipment = ['Giant Revolt', 'Cube LTD', 'Author Agang', 'Liv Tempt 4', 'Cube Acid 24', 'Mongoose BMX', 'Nextbike - Miejski']; - foreach ($default_equipment as $eq_name) { - $wpdb->insert( $table_equipment, array( 'name' => $eq_name ) ); - } - } -} - -// --- 2. MENU ADMINA I DASHBOARD --- $mystat_plugin_hooks = []; add_action( 'admin_menu', 'mystat_add_admin_menu' ); - -/** - * Set up admin-specific hooks. - */ -function mystat_admin_init_setup() { - add_filter( 'upload_mimes', 'mystat_add_gpx_mime_type' ); - add_filter( 'wp_check_filetype_and_ext', 'mystat_fix_gpx_upload_permission', 10, 4 ); - mystat_register_settings(); -} add_action( 'admin_init', 'mystat_admin_init_setup' ); - -/** - * Enqueue admin-specific CSS. - * - * @param string $hook The current admin page hook. - */ -function mystat_enqueue_admin_styles( $hook ) { - global $mystat_plugin_hooks; - - if ( in_array( $hook, $mystat_plugin_hooks, true ) ) { - $plugin_version = '1.0'; // You can use filemtime() for cache-busting in development - wp_enqueue_style( 'mystat-admin-styles', plugin_dir_url( __FILE__ ) . 'assets/css/admin.css', [], $plugin_version ); - } -} add_action( 'admin_enqueue_scripts', 'mystat_enqueue_admin_styles' ); -/** - * Add GPX support to WordPress Media Library. - * - * @param array $mimes Allowed mime types. - * @return array Modified mime types. - */ -function mystat_add_gpx_mime_type( $mimes ) { - $mimes['gpx'] = 'application/gpx+xml'; - return $mimes; -} - -/** - * Bypasses WordPress's strict file type check for GPX files. - * This is needed because WordPress can be overly cautious with XML-based files. - * - * @param array $data File data. - * @param string $file Full path to the file. - * @param string $filename The filename. - * @param array $mimes Mime types. - * @return array Modified file data. - */ -function mystat_fix_gpx_upload_permission( $data, $file, $filename, $mimes ) { - if ( strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) ) === 'gpx' ) { - $data['ext'] = 'gpx'; - $data['type'] = 'application/gpx+xml'; - } - return $data; -} - -function mystat_add_admin_menu() { - global $mystat_plugin_hooks; - - $mystat_plugin_hooks[] = add_menu_page( - 'Moje Statystyki', // Tytuł strony - 'Statystyki', // Tytuł w menu - 'manage_options', // Wymagane uprawnienia - 'moje-statystyki', // Slug menu - 'mystat_dashboard_page', // Funkcja renderująca stronę główną (dashboard) - 'dashicons-chart-line', // Ikona - 6 // Pozycja - ); - - $mystat_plugin_hooks[] = add_submenu_page( - 'moje-statystyki', // Slug rodzica - 'Dodaj Nowy Trening', // Tytuł strony - 'Nowy trening', // Tytuł w podmenu - 'manage_options', // Wymagane uprawnienia - 'mystat-nowy-trening', // Slug podmenu - 'mystat_add_new_page' // Funkcja renderująca stronę dodawania - ); - - $mystat_plugin_hooks[] = add_submenu_page( - 'moje-statystyki', - 'Typy Wydarzeń', - 'Typy wydarzeń', - 'manage_options', - 'mystat-event-types', - 'mystat_event_types_page' - ); - - $mystat_plugin_hooks[] = add_submenu_page( - 'moje-statystyki', - 'Sprzęt', - 'Sprzęt', - 'manage_options', - 'mystat-equipment', - 'mystat_equipment_page' - ); - - $mystat_plugin_hooks[] = add_submenu_page( - 'moje-statystyki', - 'Użycie Sprzętu', - 'Użycie Sprzętu', - 'manage_options', - 'mystat-equipment-usage', - 'mystat_equipment_usage_page' - ); - - $mystat_plugin_hooks[] = add_submenu_page( - 'moje-statystyki', - 'Cele', - 'Cele', - 'manage_options', - 'mystat-goals', - 'mystat_goals_page' - ); - - - $mystat_plugin_hooks[] = add_submenu_page( - null, // Ukryta strona, nie pojawia się w menu - 'Szczegóły Treningu', // Tytuł strony - 'Szczegóły Treningu', // Tytuł w menu (nieistotny) - 'manage_options', // Wymagane uprawnienia - 'mystat-view-activity', // Slug podmenu - 'mystat_view_activity_page' // Funkcja renderująca - ); - - $mystat_plugin_hooks[] = add_submenu_page( - null, // Ukryta strona - 'Edytuj Trening', // Tytuł strony - 'Edytuj Trening', // Tytuł w menu (nieistotny) - 'manage_options', // Wymagane uprawnienia - 'mystat-edit-activity', // Slug podmenu - 'mystat_edit_activity_page' // Funkcja renderująca - ); - - $mystat_plugin_hooks[] = add_submenu_page( - 'moje-statystyki', // Slug rodzica - 'Podsumowanie Roczne', // Tytuł strony - 'Podsumowanie Roczne', // Tytuł w podmenu - 'manage_options', // Wymagane uprawnienia - 'mystat-yearly-summary', // Slug podmenu - 'mystat_yearly_summary_page'// Funkcja renderująca - ); - - $mystat_plugin_hooks[] = add_submenu_page( - 'moje-statystyki', // Slug rodzica - 'Infografika', // Tytuł strony - 'Infografika', // Tytuł w podmenu - 'manage_options', // Wymagane uprawnienia - 'mystat-infographic', // Slug podmenu - 'mystat_infographic_page' // Funkcja renderująca - ); - - $mystat_plugin_hooks[] = add_submenu_page( - 'moje-statystyki', // Slug rodzica - 'Import CSV', // Tytuł strony - 'Import CSV', // Tytuł w podmenu - 'manage_options', // Wymagane uprawnienia - 'mystat-import-csv', // Slug podmenu - 'mystat_import_csv_page' // Funkcja renderująca - ); - - $mystat_plugin_hooks[] = add_submenu_page( - 'moje-statystyki', // Slug rodzica - 'Ustawienia', // Tytuł strony - 'Ustawienia', // Tytuł w podmenu - 'manage_options', // Wymagane uprawnienia - 'mystat-settings', // Slug podmenu - 'mystat_settings_page' // Funkcja renderująca - ); -} - -function mystat_dashboard_page() { - echo '

Moje Statystyki Sportowe

'; - mystat_render_history_table(); - echo '
'; -} - -function mystat_add_new_page() { - echo '

Dodaj Nowy Trening

'; - // Obsługa zapisu formularza (musi być przed renderowaniem, aby wyświetlić komunikat) - mystat_handle_activity_form_submission(); - // Formularz dodawania - mystat_render_add_form(); - echo '
'; -} - -function mystat_event_types_page() { - global $wpdb; - $table_event_types = $wpdb->prefix . 'mystat_event_types'; - $message = ''; - $notice_class = ''; - - // Handle POST requests (add/update) - if ( isset( $_POST['submit'] ) && check_admin_referer( 'mystat_manage_event_type' ) ) { - $name = sanitize_text_field( $_POST['event_type_name'] ); - $type_id = isset( $_POST['event_type_id'] ) ? intval( $_POST['event_type_id'] ) : 0; - - if ( ! empty( $name ) ) { - if ( $type_id > 0 ) { // Update - $wpdb->update( $table_event_types, [ 'name' => $name ], [ 'id' => $type_id ] ); - $message = 'Typ wydarzenia zaktualizowany.'; - $notice_class = 'notice-success'; - } else { // Insert - $wpdb->insert( $table_event_types, [ 'name' => $name ] ); - $message = 'Typ wydarzenia dodany.'; - $notice_class = 'notice-success'; - } - } else { - $message = 'Nazwa typu wydarzenia nie może być pusta.'; - $notice_class = 'notice-error'; - } - } - - // Handle GET requests (delete) - if ( isset( $_GET['action'], $_GET['id'], $_GET['_wpnonce'] ) && $_GET['action'] === 'delete' ) { - if ( wp_verify_nonce( $_GET['_wpnonce'], 'mystat_delete_event_type_' . $_GET['id'] ) ) { - $wpdb->delete( $table_event_types, [ 'id' => intval( $_GET['id'] ) ] ); - $message = 'Typ wydarzenia usunięty.'; - $notice_class = 'notice-success'; - } - } - - // Prepare for form (for editing) - $item_to_edit = null; - if ( isset( $_GET['action'], $_GET['id'] ) && $_GET['action'] === 'edit' ) { - $item_to_edit = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_event_types WHERE id = %d", intval( $_GET['id'] ) ) ); - } - - $event_types = $wpdb->get_results( "SELECT * FROM $table_event_types ORDER BY name ASC" ); - ?> -
-

Typy Wydarzeń

- -

- - -
-
-
-
-

-
- - -
- - -
- -
-
-
-
-
-
- - - - - - - -
NazwaAkcje
name ); ?>Edytuj | Usuń
-
-
-
-
- prefix . 'mystat_equipment'; - $message = ''; - $notice_class = ''; - - // Handle POST requests (add/update) - if ( isset( $_POST['submit'] ) && check_admin_referer( 'mystat_manage_equipment' ) ) { - $name = sanitize_text_field( $_POST['equipment_name'] ); - $item_id = isset( $_POST['equipment_id'] ) ? intval( $_POST['equipment_id'] ) : 0; - - if ( ! empty( $name ) ) { - if ( $item_id > 0 ) { // Update - $wpdb->update( $table_equipment, [ 'name' => $name ], [ 'id' => $item_id ] ); - $message = 'Sprzęt zaktualizowany.'; - $notice_class = 'notice-success'; - } else { // Insert - $wpdb->insert( $table_equipment, [ 'name' => $name ] ); - $message = 'Sprzęt dodany.'; - $notice_class = 'notice-success'; - } - } else { - $message = 'Nazwa sprzętu nie może być pusta.'; - $notice_class = 'notice-error'; - } - } - - // Handle GET requests (delete) - if ( isset( $_GET['action'], $_GET['id'], $_GET['_wpnonce'] ) && $_GET['action'] === 'delete' ) { - if ( wp_verify_nonce( $_GET['_wpnonce'], 'mystat_delete_equipment_' . $_GET['id'] ) ) { - $wpdb->delete( $table_equipment, [ 'id' => intval( $_GET['id'] ) ] ); - $message = 'Sprzęt usunięty.'; - $notice_class = 'notice-success'; - } - } - - // Prepare for form (for editing) - $item_to_edit = null; - if ( isset( $_GET['action'], $_GET['id'] ) && $_GET['action'] === 'edit' ) { - $item_to_edit = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_equipment WHERE id = %d", intval( $_GET['id'] ) ) ); - } - - $equipment_list = $wpdb->get_results( "SELECT * FROM $table_equipment ORDER BY name ASC" ); - ?> -
-

Zarządzaj Sprzętem

- -

- - -
-
-
-
-

-
- - -
- - -
- -
-
-
-
-
-
- - - - - - - -
NazwaAkcje
name ); ?>Edytuj | Usuń
-
-
-
-
- prefix . 'mystat_activities'; - $table_equipment = $wpdb->prefix . 'mystat_equipment'; - - $sql = " - SELECT - eq.id, - eq.name, - SUM(a.distance) as total_distance, - SUM(TIME_TO_SEC(a.duration)) as total_seconds, - COUNT(a.id) as activity_count - FROM - $table_equipment eq - LEFT JOIN - $table_activities a ON eq.id = a.equipment_id - GROUP BY - eq.id, eq.name - ORDER BY - total_distance DESC - "; - - $equipment_usage = $wpdb->get_results($sql); - ?> -
-

Użycie Sprzętu

-

Tutaj znajdziesz podsumowanie wykorzystania Twojego sprzętu we wszystkich zarejestrowanych aktywnościach.

- - - - - - - - - - - - - - - - - - - - -
Nazwa SprzętuCałkowity DystansCałkowity CzasLiczba Aktywności
name); ?>total_distance ? number_format($item->total_distance, 2, ',', ' ') . ' km' : 'Brak danych'; ?>total_seconds ? sprintf('%d godz. %d min.', floor($item->total_seconds / 3600), floor(($item->total_seconds % 3600) / 60)) : 'Brak danych'; ?>activity_count; ?>
-
- prefix . 'mystat_activities'; - - $sql_select = ''; - switch ($goal->goal_type) { - case 'distance': - $sql_select = 'SUM(distance)'; - break; - case 'duration_sec': - $sql_select = 'SUM(TIME_TO_SEC(duration))'; - break; - case 'count': - $sql_select = 'COUNT(id)'; - break; - default: - return ['current_value' => 0, 'percentage' => 0]; - } - - $where_clauses = []; - $where_clauses[] = $wpdb->prepare('YEAR(date) = %d', $goal->year); - - if (!empty($goal->month)) { - $where_clauses[] = $wpdb->prepare('MONTH(date) = %d', $goal->month); - } - if (!empty($goal->category_id)) { - $where_clauses[] = $wpdb->prepare('category_id = %d', $goal->category_id); - } - - $sql = "SELECT {$sql_select} FROM {$table_activities} WHERE " . implode(' AND ', $where_clauses); - - $current_value = (float) $wpdb->get_var($sql); - $percentage = ($goal->target_value > 0) ? ($current_value / $goal->target_value) * 100 : 0; - - return [ - 'current_value' => $current_value, - 'percentage' => $percentage, - ]; -} - -function mystat_goals_page() { - global $wpdb; - $table_goals = $wpdb->prefix . 'mystat_goals'; - $table_categories = $wpdb->prefix . 'mystat_categories'; - $message = ''; - $notice_class = ''; - - // Handle POST requests (add/update) - if ( isset( $_POST['submit'] ) && check_admin_referer( 'mystat_manage_goal' ) ) { - $goal_id = isset( $_POST['goal_id'] ) ? intval( $_POST['goal_id'] ) : 0; - $data = [ - 'name' => sanitize_text_field($_POST['goal_name']), - 'goal_type' => sanitize_text_field($_POST['goal_type']), - 'target_value' => floatval(str_replace(',', '.', $_POST['target_value'])), - 'year' => intval($_POST['year']), - 'month' => $_POST['month'] === 'all' ? null : intval($_POST['month']), - 'category_id' => $_POST['category_id'] === 'all' ? null : intval($_POST['category_id']), - ]; - - if ( !empty($data['name']) && !empty($data['goal_type']) && $data['target_value'] > 0 && $data['year'] > 2000 ) { - if ( $goal_id > 0 ) { // Update - $wpdb->update( $table_goals, $data, [ 'id' => $goal_id ] ); - $message = 'Cel zaktualizowany.'; - $notice_class = 'notice-success'; - } else { // Insert - $wpdb->insert( $table_goals, $data ); - $message = 'Cel dodany.'; - $notice_class = 'notice-success'; - } - } else { - $message = 'Wypełnij poprawnie wszystkie wymagane pola (Nazwa, Typ, Cel, Rok).'; - $notice_class = 'notice-error'; - } - } - - // Handle GET requests (delete) - if ( isset( $_GET['action'], $_GET['id'], $_GET['_wpnonce'] ) && $_GET['action'] === 'delete' ) { - if ( wp_verify_nonce( $_GET['_wpnonce'], 'mystat_delete_goal_' . $_GET['id'] ) ) { - $wpdb->delete( $table_goals, [ 'id' => intval( $_GET['id'] ) ] ); - $message = 'Cel usunięty.'; - $notice_class = 'notice-success'; - } - } - - // Prepare for form (for editing) - $item_to_edit = null; - if ( isset( $_GET['action'], $_GET['id'] ) && $_GET['action'] === 'edit' ) { - $item_to_edit = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_goals WHERE id = %d", intval( $_GET['id'] ) ) ); - } - - $goals = $wpdb->get_results( "SELECT g.*, c.name as category_name FROM $table_goals g LEFT JOIN $table_categories c ON g.category_id = c.id ORDER BY g.year DESC, g.name ASC" ); - $categories = $wpdb->get_results( "SELECT * FROM $table_categories ORDER BY name ASC" ); - ?> -
-

Zarządzaj Celami

- -

- - -
-
-
-
-

-
- - -
-
-

Dla czasu podaj wartość w godzinach (np. 100.5).

-
-
-
- -
-
-
-
-
-
- - - - - - - - goal_type === 'duration_sec'; - if ($is_duration) { - $goal->target_value = $goal->target_value * 3600; // Convert hours back to seconds for calculation - } - $progress = mystat_get_goal_progress($goal); - $percentage = min(100, $progress['percentage']); - - $target_formatted = ''; - $current_formatted = ''; - if ($is_duration) { - $target_formatted = round($goal->target_value / 3600) . ' godz.'; - $current_formatted = sprintf('%d godz. %d min.', floor($progress['current_value'] / 3600), floor(($progress['current_value'] % 3600) / 60)); - } elseif ($goal->goal_type === 'distance') { - $target_formatted = number_format($goal->target_value, 0, ',', ' ') . ' km'; - $current_formatted = number_format($progress['current_value'], 2, ',', ' ') . ' km'; - } else { // count - $target_formatted = (int)$goal->target_value; - $current_formatted = (int)$progress['current_value']; - } - ?> - - - - - - - - -
CelPostępAkcje
Brak zdefiniowanych celów.
- name); ?>
- - year); ?> - month) echo ' / ' . date_i18n('F', mktime(0,0,0,$goal->month,10)); ?> - category_name) echo ' / ' . esc_html($goal->category_name); ?> - -
-
-
-
- z (%) -
- Edytuj | - Usuń -
-
-
-
-
- - -
-

Ustawienia Wtyczki Statystyk

-
- -
-
- Zdefiniuj strefę prywatności, aby ukryć początek i koniec swoich tras GPX. Punkty wewnątrz tego okręgu nie będą wyświetlane na mapie.

'; - echo '

Aby znaleźć swoje współrzędne, kliknij prawym przyciskiem myszy na mapie Google w wybranym miejscu - współrzędne pojawią się jako pierwsza pozycja w menu.

'; -} - -function mystat_render_lat_field() { - $options = get_option( 'mystat_privacy_options' ); - $latitude = isset( $options['latitude'] ) ? esc_attr( $options['latitude'] ) : ''; - echo ""; -} - -function mystat_render_lon_field() { - $options = get_option( 'mystat_privacy_options' ); - $longitude = isset( $options['longitude'] ) ? esc_attr( $options['longitude'] ) : ''; - echo ""; -} - -function mystat_render_radius_field() { - $options = get_option( 'mystat_privacy_options' ); - $radius = isset( $options['radius'] ) ? esc_attr( $options['radius'] ) : '500'; - echo " metrów"; -} - -function mystat_sanitize_privacy_options( $input ) { - $sanitized_input = []; - if ( isset( $input['latitude'] ) ) { - $sanitized_input['latitude'] = floatval( str_replace( ',', '.', $input['latitude'] ) ); - } - if ( isset( $input['longitude'] ) ) { - $sanitized_input['longitude'] = floatval( str_replace( ',', '.', $input['longitude'] ) ); - } - if ( isset( $input['radius'] ) ) { - $sanitized_input['radius'] = abs( intval( $input['radius'] ) ); - } - return $sanitized_input; -} - -function mystat_yearly_summary_page() { - global $wpdb; - $table_activities = $wpdb->prefix . 'mystat_activities'; - - $current_year = isset( $_GET['year'] ) ? intval( $_GET['year'] ) : current_time( 'Y' ); - - // Pobierz dostępne lata z bazy danych - $available_years = $wpdb->get_col( "SELECT DISTINCT YEAR(date) FROM $table_activities ORDER BY YEAR(date) DESC" ); - if ( empty( $available_years ) ) { - $available_years = [current_time('Y')]; // Domyślny rok, jeśli brak danych - } - - // --- GOALS SECTION --- - $table_goals = $wpdb->prefix . 'mystat_goals'; - $goals_for_year = $wpdb->get_results($wpdb->prepare( - "SELECT * FROM {$table_goals} WHERE year = %d ORDER BY name ASC", - $current_year - )); - - // Zapytanie SQL do grupowania danych miesięcznie - $sql = $wpdb->prepare(" - SELECT - MONTH(date) as month_num, - SUM(distance) as total_distance, - SUM(calories) as total_calories, - SUM(TIME_TO_SEC(duration)) as total_seconds, - COUNT(id) as activity_count - FROM $table_activities - WHERE YEAR(date) = %d - GROUP BY month_num - ORDER BY month_num ASC - ", $current_year); - - - $monthly_summary = $wpdb->get_results( $sql, OBJECT_K ); // OBJECT_K zwróci tablicę z kluczami będącymi numerami miesięcy - - // Przygotowanie danych dla wszystkich 12 miesięcy - $full_year_summary = []; - $total_year_distance = 0; - $total_year_calories = 0; - $total_year_seconds = 0; - - // Określ, ile miesięcy pokazać, aby uniknąć zer dla przyszłych miesięcy - $this_year = (int) current_time('Y'); - $this_month = (int) current_time('n'); - $loop_until_month = 12; // Domyślnie dla lat ubiegłych - - if ( $current_year == $this_year ) { - // Dla bieżącego roku, pokaż miesiące do bieżącego miesiąca - $loop_until_month = $this_month; - } elseif ( $current_year > $this_year ) { - // Dla przyszłych lat, pokaż miesiące tylko do ostatniego, w którym są dane - $last_month_with_data = $wpdb->get_var( $wpdb->prepare( "SELECT MAX(MONTH(date)) FROM $table_activities WHERE YEAR(date) = %d", $current_year ) ); - $loop_until_month = $last_month_with_data ? (int) $last_month_with_data : 0; - } - - for ( $i = 1; $i <= $loop_until_month; $i++ ) { - $month_name = date_i18n( 'F', mktime( 0, 0, 0, $i, 10 ) ); // Nazwa miesiąca - $data = isset( $monthly_summary[$i] ) ? $monthly_summary[$i] : null; - - $full_year_summary[$i] = (object) [ - 'month_name' => $month_name, - 'total_distance' => $data ? $data->total_distance : 0, - 'total_calories' => $data ? (int)$data->total_calories : 0, - 'total_seconds' => $data ? (int)$data->total_seconds : 0, - 'activity_count' => $data ? (int)$data->activity_count : 0, - ]; - - $total_year_distance += $full_year_summary[$i]->total_distance; - $total_year_seconds += $full_year_summary[$i]->total_seconds; - $total_year_calories += $full_year_summary[$i]->total_calories; - } - - $total_year_hours = floor($total_year_seconds / 3600); - $total_year_minutes = floor(($total_year_seconds % 3600) / 60); - $total_year_duration_formatted = sprintf('%d godz. %d min.', $total_year_hours, $total_year_minutes); - - // Przygotowanie danych dla wykresu - $chart_labels_js = []; - $chart_datasets = [ - 'distance' => [], - 'duration' => [], - 'calories' => [], - 'activities' => [], - ]; - - foreach ($full_year_summary as $month_data) { - $chart_labels_js[] = $month_data->month_name; - $chart_datasets['distance'][] = round((float)$month_data->total_distance, 2); - $chart_datasets['duration'][] = round($month_data->total_seconds / 3600, 2); // w godzinach - $chart_datasets['calories'][] = $month_data->total_calories; - $chart_datasets['activities'][] = $month_data->activity_count; - } - - // Włączenie skryptów dla Chart.js - wp_enqueue_script('chart-js', 'https://cdn.jsdelivr.net/npm/chart.js', [], null, true); - wp_register_script('mystat-chart-loader', false); - wp_enqueue_script('mystat-chart-loader'); - - $chart_configs = [ - 'distance' => [ - 'label' => 'Dystans (km)', - 'data' => $chart_datasets['distance'], - 'backgroundColor' => 'rgba(52, 152, 219, 0.5)', - 'borderColor' => 'rgba(52, 152, 219, 1)', - 'yAxisLabel' => 'Kilometry' - ], - 'duration' => [ - 'label' => 'Czas trwania (godz.)', - 'data' => $chart_datasets['duration'], - 'backgroundColor' => 'rgba(26, 188, 156, 0.5)', - 'borderColor' => 'rgba(26, 188, 156, 1)', - 'yAxisLabel' => 'Godziny' - ], - 'calories' => [ - 'label' => 'Kalorie (kcal)', - 'data' => $chart_datasets['calories'], - 'backgroundColor' => 'rgba(231, 76, 60, 0.5)', - 'borderColor' => 'rgba(231, 76, 60, 1)', - 'yAxisLabel' => 'kcal' - ], - 'activities' => [ - 'label' => 'Liczba aktywności', - 'data' => $chart_datasets['activities'], - 'backgroundColor' => 'rgba(241, 196, 15, 0.5)', - 'borderColor' => 'rgba(241, 196, 15, 1)', - 'yAxisLabel' => 'Ilość' - ], - ]; - - wp_add_inline_script('mystat-chart-loader', ' - document.addEventListener("DOMContentLoaded", function() { - const chartLabels = ' . json_encode($chart_labels_js) . '; - const chartConfigs = ' . json_encode($chart_configs) . '; - let activeChart = null; - - const tabs = document.querySelectorAll(".nav-tab"); - tabs.forEach(tab => { - tab.addEventListener("click", function(e) { - e.preventDefault(); - tabs.forEach(t => t.classList.remove("nav-tab-active")); - this.classList.add("nav-tab-active"); - - const chartType = this.getAttribute("href").substring(1); - renderChart(chartType); - }); - }); - - function renderChart(type) { - if (activeChart) { - activeChart.destroy(); - } - const config = chartConfigs[type]; - const ctx = document.getElementById("mystatYearlyChart").getContext("2d"); - activeChart = new Chart(ctx, { - type: "bar", - data: { - labels: chartLabels, - datasets: [{ - label: config.label, - data: config.data, - backgroundColor: config.backgroundColor, - borderColor: config.borderColor, - borderWidth: 1 - }] - }, - options: { - responsive: true, maintainAspectRatio: false, - scales: { y: { beginAtZero: true, title: { display: true, text: config.yAxisLabel } } } - } - }); - } - - // Render initial chart - if (tabs.length > 0) { - renderChart(tabs[0].getAttribute("href").substring(1)); - } - }); - '); - - ?> -
-

Podsumowanie Roczne

- -
-
- -
- - - -
-
-
- - -
-

Cele na

-
-
    - - goal_type === 'duration_sec') { - $target_formatted = round($goal->target_value / 3600) . ' godz.'; - $current_formatted = sprintf('%d godz. %d min.', floor($progress['current_value'] / 3600), floor(($progress['current_value'] % 3600) / 60)); - } elseif ($goal->goal_type === 'distance') { - $target_formatted = number_format($goal->target_value, 0, ',', ' ') . ' km'; - $current_formatted = number_format($progress['current_value'], 2, ',', ' ') . ' km'; - } else { // count - $target_formatted = (int)$goal->target_value; - $current_formatted = (int)$progress['current_value']; - } - ?> -
  • -
    - name); ?> - / (%) -
    -
    -
    -
    -
  • - -
-
-
- - - -
-

Wykresy dla

-
- -
- -
-
-
- - - - - - - - - - - - - - - - - - - - - -
MiesiącDystans (km)Kalorie (kcal)Czas
month_name ); ?>total_distance, 2, ',', ' ' ); ?>total_calories, 0, ',', ' ' ); ?>total_seconds) ); ?>
SUMA ROCZNA
-
- 20 ] ); - - if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) { - return []; - } - - $gpx_content = wp_remote_retrieve_body( $response ); - if ( empty( $gpx_content ) ) { - return []; - } - - // --- Privacy Zone --- - $privacy_options = get_option( 'mystat_privacy_options' ); - $privacy_enabled = ! empty( $privacy_options['latitude'] ) && ! empty( $privacy_options['longitude'] ) && ! empty( $privacy_options['radius'] ); - $privacy_center_lat = $privacy_enabled ? (float) $privacy_options['latitude'] : 0; - $privacy_center_lon = $privacy_enabled ? (float) $privacy_options['longitude'] : 0; - $privacy_radius_km = $privacy_enabled ? ( (int) $privacy_options['radius'] ) / 1000 : 0; // Convert meters to km - - libxml_use_internal_errors( true ); - $gpx = simplexml_load_string( $gpx_content ); - libxml_clear_errors(); - - if ( $gpx === false ) { - return []; - } - - // Use XPath to be more robust against different GPX structures/namespaces - $gpx->registerXPathNamespace( 'gpx', 'http://www.topografix.com/GPX/1/1' ); - $trackpoints = $gpx->xpath( '//gpx:trkpt' ); - if ( empty( $trackpoints ) ) { - $trackpoints = $gpx->xpath( '//trkpt' ); // Fallback for files without namespace - } - - $raw_points = []; - $start_time = null; - - foreach ( $trackpoints as $trkpt ) { - if ( isset( $trkpt['lat'], $trkpt['lon'] ) ) { - $extensions = $trkpt->extensions ? $trkpt->extensions->children( 'gpxtpx', true ) : null; - $time = isset( $trkpt->time ) ? strtotime( (string) $trkpt->time ) : null; - if ( $time && is_null( $start_time ) ) { - $start_time = $time; - } - - $hr_val = ( $extensions && isset( $extensions->TrackPointExtension->hr ) ) ? (int) $extensions->TrackPointExtension->hr : null; - $cad_val = ( $extensions && isset( $extensions->TrackPointExtension->cad ) ) ? (int) $extensions->TrackPointExtension->cad : null; - - $raw_points[] = [ - 'lat' => (float) $trkpt['lat'], - 'lon' => (float) $trkpt['lon'], - 'ele' => isset( $trkpt->ele ) ? (float) $trkpt->ele : null, - 'time_offset' => $start_time && $time ? $time - $start_time : null, - 'hr' => $hr_val, - 'cad' => $cad_val, - ]; - } - } - - if ( empty( $raw_points ) ) { - return []; - } - - // Process raw points to calculate profiles - $map_points = []; - $profiles = [ 'distance' => [], 'time' => [], 'elevation' => [], 'speed' => [], 'hr' => [], 'cadence' => [] ]; - $cumulative_distance = 0; - - $haversine = function( $lat1, $lon1, $lat2, $lon2 ) { - $earth_radius = 6371; // in km - $dLat = deg2rad( $lat2 - $lat1 ); - $dLon = deg2rad( $lon2 - $lon1 ); - $a = sin( $dLat / 2 ) * sin( $dLat / 2 ) + cos( deg2rad( $lat1 ) ) * cos( deg2rad( $lat2 ) ) * sin( $dLon / 2 ) * sin( $dLon / 2 ); - $c = 2 * atan2( sqrt( $a ), sqrt( 1 - $a ) ); - return $earth_radius * $c; - }; - - foreach ( $raw_points as $i => $point ) { - $is_in_privacy_zone = false; - if ( $privacy_enabled ) { - $distance_from_center = $haversine( $privacy_center_lat, $privacy_center_lon, $point['lat'], $point['lon'] ); - if ( $distance_from_center <= $privacy_radius_km ) { - $is_in_privacy_zone = true; - } - } - - if ( ! $is_in_privacy_zone ) { - $map_points[] = [ $point['lat'], $point['lon'] ]; - $speed = null; - - if ( $i > 0 ) { - $prev_point = $raw_points[ $i - 1 ]; - $distance_delta = $haversine( $prev_point['lat'], $prev_point['lon'], $point['lat'], $point['lon'] ); // km - $cumulative_distance += $distance_delta; - - if ( ! is_null( $point['time_offset'] ) && ! is_null( $prev_point['time_offset'] ) ) { - $time_delta = $point['time_offset'] - $prev_point['time_offset']; // seconds - if ( $time_delta > 0 ) { - $speed = ( $distance_delta * 3600 ) / $time_delta; // km/h - } - } - } - - $profiles['distance'][] = round( $cumulative_distance, 3 ); - $profiles['time'][] = $point['time_offset']; - $profiles['elevation'][] = ! is_null( $point['ele'] ) ? round( $point['ele'], 2 ) : null; - $profiles['speed'][] = ! is_null( $speed ) ? round( $speed, 1 ) : null; - $profiles['hr'][] = $point['hr']; - $profiles['cadence'][] = $point['cad']; - } - } - - return [ - 'points' => $map_points, - 'profiles' => $profiles, - ]; -} - -function mystat_infographic_page() { - global $wpdb; - $table_activities = $wpdb->prefix . 'mystat_activities'; - $table_categories = $wpdb->prefix . 'mystat_categories'; - - $current_year = isset( $_GET['year'] ) ? intval( $_GET['year'] ) : current_time( 'Y' ); - - // Pobierz dostępne lata z bazy danych - $available_years = $wpdb->get_col( "SELECT DISTINCT YEAR(date) FROM $table_activities ORDER BY YEAR(date) DESC" ); - if ( empty( $available_years ) ) { - $available_years = [current_time('Y')]; // Domyślny rok, jeśli brak danych - } - - // --- 1. Statystyki ogólne (wszystkie lata) --- - $overall_stats = $wpdb->get_row(" - SELECT - SUM(distance) as total_distance, - SEC_TO_TIME(SUM(TIME_TO_SEC(duration))) as total_duration, - SUM(total_elevation_gain) as total_elevation_gain, - COUNT(id) as total_activities - FROM $table_activities - "); - - // --- 2. Statystyki dla wybranego roku --- - $yearly_stats = $wpdb->get_row($wpdb->prepare(" - SELECT - SUM(distance) as total_distance, - SEC_TO_TIME(SUM(TIME_TO_SEC(duration))) as total_duration, - SUM(total_elevation_gain) as total_elevation_gain, - COUNT(id) as total_activities - FROM $table_activities - WHERE YEAR(date) = %d - ", $current_year)); - - // --- 3. Dane dla wykresu kołowego (dystans per kategoria dla wybranego roku) --- - $category_distance_data = $wpdb->get_results($wpdb->prepare(" - SELECT c.name as category_name, c.color, SUM(a.distance) as total_distance - FROM $table_activities a - LEFT JOIN $table_categories c ON a.category_id = c.id - WHERE YEAR(a.date) = %d - GROUP BY c.name, c.color - HAVING SUM(a.distance) > 0 - ORDER BY total_distance DESC - ", $current_year)); - - $chart_labels = []; - $chart_data = []; - $chart_colors = []; - foreach ($category_distance_data as $data) { - $chart_labels[] = $data->category_name; - $chart_data[] = round((float)$data->total_distance, 2); - $chart_colors[] = $data->color; - } - - // Włączenie skryptów dla Chart.js - wp_enqueue_script('chart-js', 'https://cdn.jsdelivr.net/npm/chart.js', [], null, true); - wp_register_script('mystat-infographic-chart-loader', false); - wp_enqueue_script('mystat-infographic-chart-loader'); - wp_add_inline_script('mystat-infographic-chart-loader', ' - document.addEventListener("DOMContentLoaded", function() { - const ctx = document.getElementById("mystatCategoryPieChart"); - if (!ctx) return; - new Chart(ctx, { - type: "pie", - data: { - labels: ' . json_encode($chart_labels) . ', - datasets: [{ - data: ' . json_encode($chart_data) . ', - backgroundColor: ' . json_encode($chart_colors) . ', - hoverOffset: 4 - }] - }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - position: "right", - }, - title: { - display: true, - text: "Dystans wg kategorii w ' . esc_js($current_year) . '" - } - } - } - }); - }); - '); - - ?> -
-

Infografika Statystyk Sportowych

- -
-
-
- - - - -
-
-
- -
-

Statystyki Ogólne (wszystkie lata)

-
-

Dystans

total_distance, 2, ',', ' '); ?> km

-

Czas

total_duration); ?>

-

Wznios

total_elevation_gain, 0, ',', ' '); ?> m

-

Aktywności

total_activities, 0, ',', ' '); ?>

-
-
- -
-

Statystyki dla

-
-

Dystans

total_distance, 2, ',', ' '); ?> km

-

Czas

total_duration); ?>

-

Wznios

total_elevation_gain, 0, ',', ' '); ?> m

-

Aktywności

total_activities, 0, ',', ' '); ?>

-
-
- -
-

Rozkład Dystansu wg Kategorii w

-
-
- -
-
-
-
-

Importuj aktywności z pliku CSV

'; - - // Handle the form submission - if ( 'POST' === $_SERVER['REQUEST_METHOD'] && isset( $_POST['mystat_csv_import_nonce_field'] ) ) { - mystat_handle_csv_import(); - } - - // Display the form - ?> -
-

Instrukcje i formularz importu

-
-

Aby zaimportować dane, możesz wgrać plik CSV LUB wkleić dane bezpośrednio w pole tekstowe poniżej. Ta druga opcja jest zalecana, jeśli napotykasz błędy z plikiem.

-

Wymagane kolumny: Data, Tytuł, Dystans oraz Typ aktywności (lub Kategoria).

-

Opcjonalne kolumny: Czas (format HH:MM:SS), Kalorie, Średnie tętno, Maksymalne tętno, Średnia prędkość, Maksymalna prędkość, Średni rytm pedałowania, Maksymalny rytm pedałowania, Całkowity wznios, Całkowity spadek, Minimalna wysokość, Maksymalna wysokość, Sprzęt, Typ wydarzenia.

-

Ważne: -

    -
  • Data musi być w formacie YYYY-MM-DD.
  • -
  • Dystans i prędkość: użyj kropki jako separatora dziesiętnego (np. 10.5).
  • -
  • Nazwy w kolumnach Typ aktywności, Sprzęt, Typ wydarzenia muszą dokładnie odpowiadać nazwom zdefiniowanym w ustawieniach wtyczki. Jeśli nazwa nie zostanie znaleziona, wiersz zostanie pominięty.
  • -
-

-
-
- - - - - - - - - - -
- -
-
-
- '; -} - -function mystat_handle_csv_import() { - global $wpdb; - - if ( ! isset( $_POST['mystat_csv_import_nonce_field'] ) || ! wp_verify_nonce( $_POST['mystat_csv_import_nonce_field'], 'mystat_csv_import_nonce' ) ) { - echo '

Błąd weryfikacji bezpieczeństwa.

'; - return; - } - - if ( ! current_user_can( 'manage_options' ) ) { - echo '

Nie masz wystarczających uprawnień.

'; - return; - } - - // Unify input source: prefer textarea, fall back to file upload. - $csv_content = ''; - if ( ! empty( $_POST['mystat_csv_data'] ) ) { - $csv_content = stripslashes( $_POST['mystat_csv_data'] ); - } elseif ( ! empty( $_FILES['mystat_csv_file']['tmp_name'] ) && UPLOAD_ERR_OK === $_FILES['mystat_csv_file']['error'] ) { - $csv_content = file_get_contents( $_FILES['mystat_csv_file']['tmp_name'] ); - } - - if ( empty( trim( $csv_content ) ) ) { - echo '

Nie podano danych do importu. Wgraj plik lub wklej dane w pole tekstowe.

'; - return; - } - - // Mapowanie polskich i angielskich nazw kolumn na wewnętrzne klucze - $column_map = [ - // 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 . 'mystat_categories'; - $table_event_types = $wpdb->prefix . 'mystat_event_types'; - $table_equipment = $wpdb->prefix . 'mystat_equipment'; - - $create_lookup = function( $table_name ) use ( $wpdb ) { - $items = $wpdb->get_results( "SELECT id, name FROM {$table_name}" ); - $lookup = []; - 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 . 'mystat_activities'; - $imported_count = 0; - $skipped_details = []; - $row_number = 1; // Header is row 1 - - // Normalize line endings and split into lines - $lines = str_replace( ["\r\n", "\r"], "\n", $csv_content ); - $lines = explode( "\n", $lines ); - - if ( empty($lines) || empty(trim($lines[0])) ) { - echo '

Podane dane CSV są puste.

'; - 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 = []; - foreach ($header_raw as $col) { - $header[] = $column_map[strtolower($col)] ?? 'ignored_' . uniqid(); - } - - $required_internal_keys = ['date', 'title', 'category_name', 'distance']; - $missing_keys = array_diff($required_internal_keys, $header); - if (!empty($missing_keys)) { - $key_to_polish_map = [ - '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 '

Brak wymaganych kolumn w danych CSV: ' . esc_html(implode(', ', $missing_polish_names)) . '

'; - return; - } - - $parse_and_round_int = fn($val) => round(floatval(str_replace(',', '.', $val))); - - $null_if_empty = fn($value) => $value !== '' ? $value : null; - - foreach ( $lines as $line ) { // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.Found - $row_number++; - if ( empty( trim( $line ) ) ) { - continue; // Skip empty lines - } - $data = str_getcsv( $line, $delimiter ); - - if (count($data) !== count($header)) { - $skipped_details[] = [ - '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 = []; - 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'] ?? ''; - // Use a robust trim to handle various whitespace characters and make it case-insensitive - $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[] = [ - '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 = [ - '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[] = [ - 'row' => $row_number, - 'reason' => 'Błąd zapisu do bazy danych. (' . esc_html( $wpdb->last_error ) . ')', - 'data' => $line, - ]; - } - } - - if ( $imported_count > 0 ) echo '

Pomyślnie zaimportowano ' . esc_html( $imported_count ) . ' aktywności.

'; - if ( ! empty( $skipped_details ) ) { - echo '
'; - echo '

Pominięto ' . count( $skipped_details ) . ' wierszy z powodu błędów

'; - echo '
'; - echo ''; - foreach ( $skipped_details as $error ) { - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - } - echo '
WierszPowód pominięciaDane wiersza
' . esc_html( $error['row'] ) . '' . esc_html( $error['reason'] ) . '' . esc_html( wp_trim_words( $error['data'], 25, '...' ) ) . '
'; - echo '
'; - } - if ( $imported_count === 0 && empty($skipped_details) && $row_number > 1) echo '

Dane CSV nie zawierały żadnych poprawnych wierszy do importu.

'; - elseif ($row_number === 1) echo '

Dane CSV były puste lub zawierały tylko nagłówek.

'; -} - -function mystat_edit_activity_page() { - global $wpdb; - $activity_id = isset( $_GET['id'] ) ? intval( $_GET['id'] ) : 0; - - if ( $activity_id === 0 ) { - echo '

Błąd

Nie podano ID aktywności do edycji.

'; - return; - } - - // Handle form submission for update - mystat_handle_activity_form_submission(); - - $table_activities = $wpdb->prefix . 'mystat_activities'; - $activity = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_activities WHERE id = %d", $activity_id ) ); - - if ( ! $activity ) { - echo '

Błąd

Nie znaleziono aktywności o podanym ID.

'; - return; - } - - echo '

Edytuj Trening

'; - mystat_render_add_form( $activity ); - echo '
'; -} - -function mystat_view_activity_page() { - global $wpdb; - - $activity_id = isset( $_GET['id'] ) ? intval( $_GET['id'] ) : 0; - - if ( $activity_id === 0 ) { - echo '

Błąd

Nie podano ID aktywności.

'; - return; - } - - $table_activities = $wpdb->prefix . 'mystat_activities'; - $table_categories = $wpdb->prefix . 'mystat_categories'; - $table_event_types = $wpdb->prefix . 'mystat_event_types'; - $table_equipment = $wpdb->prefix . 'mystat_equipment'; - - $sql = $wpdb->prepare(" - SELECT a.*, c.name as category_name, c.icon, c.color, et.name as event_type_name, eq.name as equipment_name - FROM $table_activities a - LEFT JOIN $table_categories c ON a.category_id = c.id - LEFT JOIN $table_event_types et ON a.event_type_id = et.id - LEFT JOIN $table_equipment eq ON a.equipment_id = eq.id - WHERE a.id = %d - ", $activity_id); - - $activity = $wpdb->get_row( $sql ); - - if ( ! $activity ) { - echo '

Błąd

Nie znaleziono aktywności o podanym ID.

'; - return; - } - - // Funkcja pomocnicza do wyświetlania wiersza, jeśli wartość istnieje - $render_row = function($label, $value, $unit = '') { - if ( ! is_null($value) && $value !== '' ) { - echo ''; - echo '' . esc_html($label) . ''; - echo '' . esc_html($value) . ($unit ? ' ' . esc_html($unit) : '') . ''; - echo ''; - } - }; - - // Prepare map and chart data if GPX exists - $gpx_data = []; - $has_gpx_data = false; - if ( ! empty( $activity->gpx_url ) ) { - $gpx_data = mystat_parse_gpx_data( $activity->gpx_url ); - $has_gpx_data = !empty($gpx_data['points']); - } - - if ($has_gpx_data) { - wp_enqueue_style('leaflet-css', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'); - wp_enqueue_script('leaflet-js', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js', [], '1.9.4', true); - wp_enqueue_script('chart-js', 'https://cdn.jsdelivr.net/npm/chart.js', [], null, true); - - wp_register_script('mystat-details-loader', false); - wp_enqueue_script('mystat-details-loader'); - - // Check which profiles have data - $available_profiles = []; - if (!empty(array_filter($gpx_data['profiles']['elevation']))) $available_profiles['elevation'] = 'Wysokość'; - if (!empty(array_filter($gpx_data['profiles']['speed']))) $available_profiles['speed'] = 'Prędkość'; - if (!empty(array_filter($gpx_data['profiles']['hr']))) $available_profiles['hr'] = 'Tętno'; - if (!empty(array_filter($gpx_data['profiles']['cadence']))) $available_profiles['cadence'] = 'Kadencja'; - - $has_time_data = !empty(array_filter($gpx_data['profiles']['time'], fn($t) => !is_null($t))); - - $chart_js = ' - const track_points = ' . json_encode($gpx_data['points']) . '; - const profiles = ' . json_encode($gpx_data['profiles']) . '; - let activeChart = null; - - if (typeof L !== "undefined" && track_points.length > 0) { - const map = L.map("mystat-activity-map"); - L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: \'© OpenStreetMap\' }).addTo(map); - const polyline = L.polyline(track_points, {color: "' . esc_js( $activity->color ) . '"}).addTo(map); - map.fitBounds(polyline.getBounds().pad(0.1)); - L.marker(track_points[0]).addTo(map).bindPopup("Start"); - L.marker(track_points[track_points.length - 1]).addTo(map).bindPopup("Koniec"); - } - - const chartConfigs = { - elevation: { label: "Wysokość", unit: "m n.p.m.", color: "#8e44ad" }, - speed: { label: "Prędkość", unit: "km/h", color: "#2980b9" }, - hr: { label: "Tętno", unit: "bpm", color: "#c0392b" }, - cadence: { label: "Kadencja", unit: "rpm", color: "#27ae60" } - }; - const xAxisConfigs = { - distance: { label: "Dystans (km)", data: profiles.distance }, - time: { - label: "Czas", data: profiles.time, - formatter: (s) => s === null ? "" : new Date(s * 1000).toISOString().substr(11, 8) - } - }; - - function renderChart() { - if (activeChart) activeChart.destroy(); - const chartType = document.querySelector(".mystat-chart-tabs .nav-tab-active").getAttribute("href").substring(1); - const xAxisType = document.querySelector(\'input[name="mystat_xaxis"]:checked\').value; - - const yData = profiles[chartType], xData = xAxisConfigs[xAxisType].data; - const filteredY = [], filteredX = []; - if(yData) { - for(let i=0; i v, maxRotation: 0, autoSkip: true, maxTicksLimit: 10 } }, - y: { title: { display: true, text: chartConfigs[chartType].unit } } - }, - plugins: { legend: { display: false } }, - interaction: { intersect: false, mode: "index" }, - } - }); - } - - document.querySelectorAll(".mystat-chart-tabs .nav-tab").forEach(t => t.addEventListener("click", e => { - e.preventDefault(); - document.querySelector(".mystat-chart-tabs .nav-tab-active").classList.remove("nav-tab-active"); - e.target.classList.add("nav-tab-active"); - renderChart(); - })); - document.querySelectorAll(\'input[name="mystat_xaxis"]\').forEach(r => r.addEventListener("change", renderChart)); - if (document.querySelector(".mystat-chart-tabs .nav-tab")) renderChart(); - '; - wp_add_inline_script('mystat-details-loader', 'document.addEventListener("DOMContentLoaded", function() {' . $chart_js . '});'); - } - - ?> -
-

- Szczegóły treningu: title ); ?> - - Edytuj - -

- -

← Powrót do listy aktywności

- -
-

Podsumowanie

-
-
-
-

Główne dane

- - category_name); ?> - date); ?> - distance, 2, ',', ' '), 'km'); ?> - duration); ?> - calories, 'kcal'); ?> - event_type_name); ?> - equipment_name); ?> -
-
-
-

Dane szczegółowe

- - avg_speed, 1, ',', ' '), 'km/h'); ?> - max_speed, 1, ',', ' '), 'km/h'); ?> - avg_heart_rate, 'bpm'); ?> - max_heart_rate, 'bpm'); ?> - avg_cadence, 'rpm'); ?> - max_cadence, 'rpm'); ?> - total_elevation_gain, 'm'); ?> - total_elevation_loss, 'm'); ?> - min_altitude, 'm n.p.m.'); ?> - max_altitude, 'm n.p.m.'); ?> -
-
-
-
-

Notatki i linki

- - comment)); ?> - strava_url ) ) : ?> - - - gpx_url ) ) : ?> - - -
StravaZobacz aktywność na Strava
Plik GPXPobierz plik GPX
- - -
-

Mapa Trasy

-
- - -

Wykresy

-
-
- - -
- Oś X:  - -   - -
- - - -
-
- -
-
- - - gpx_url ) ) : ?> -
-
-

Nie udało się wczytać danych z pliku GPX lub plik jest uszkodzony/pusty. Brak danych do wyświetlenia mapy i wykresów.

-
- - -
-
-
- 0 ? 'mystat_edit_entry_' . $activity_id : 'mystat_add_entry'; - - // Weryfikacja bezpieczeństwa (Nonce) - if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], $nonce_action ) ) { - echo '

Błąd weryfikacji bezpieczeństwa formularza.

'; - return; - } - - $table_activities = $wpdb->prefix . 'mystat_activities'; - - // Przygotowanie danych (zamiana przecinka na kropkę w dystansie) - $distance = isset($_POST['distance']) ? floatval( str_replace( ',', '.', $_POST['distance'] ) ) : 0; - - // Funkcja pomocnicza do zamiany pustych wartości na NULL, aby poprawnie zapisać je w bazie - $null_if_empty = function($value) { - return $value !== '' ? $value : null; - }; - - $data = array( - 'category_id' => intval( $_POST['category_id'] ), - 'date' => sanitize_text_field( $_POST['date'] ), - 'title' => sanitize_text_field( $_POST['title'] ), - 'distance' => $distance, - 'duration' => sanitize_text_field( $_POST['duration'] ), - 'calories' => intval( $_POST['calories'] ), - 'comment' => sanitize_textarea_field( $_POST['comment'] ), - 'strava_url' => $null_if_empty( esc_url_raw( $_POST['strava_url'] ) ), - 'avg_heart_rate' => $null_if_empty( intval( $_POST['avg_heart_rate'] ) ), - 'max_heart_rate' => $null_if_empty( intval( $_POST['max_heart_rate'] ) ), - 'avg_speed' => $null_if_empty( floatval( str_replace( ',', '.', $_POST['avg_speed'] ) ) ), - 'max_speed' => $null_if_empty( floatval( str_replace( ',', '.', $_POST['max_speed'] ) ) ), - 'avg_cadence' => $null_if_empty( intval( $_POST['avg_cadence'] ) ), - 'max_cadence' => $null_if_empty( intval( $_POST['max_cadence'] ) ), - 'total_elevation_gain' => $null_if_empty( intval( $_POST['total_elevation_gain'] ) ), - 'total_elevation_loss' => $null_if_empty( intval( $_POST['total_elevation_loss'] ) ), - 'min_altitude' => $null_if_empty( intval( $_POST['min_altitude'] ) ), - 'max_altitude' => $null_if_empty( intval( $_POST['max_altitude'] ) ), - 'equipment_id' => $null_if_empty( intval( $_POST['equipment_id'] ) ), - 'gpx_url' => $null_if_empty( esc_url_raw( $_POST['gpx_url'] ) ), - 'event_type_id' => $null_if_empty( intval( $_POST['event_type_id'] ) ), - ); - - // Format danych dla $wpdb->insert - $format = array( - '%d', '%s', '%s', '%f', '%s', '%d', '%s', // Pola podstawowe - '%s', '%d', '%d', '%f', '%f', '%d', '%d', // Tętno, prędkość, kadencja - '%d', '%d', '%d', '%d', '%d', '%s', '%d' // Wysokość, sprzęt, linki, typ wydarzenia - ); - - if ( $activity_id > 0 ) { - // UPDATE - $result = $wpdb->update( $table_activities, $data, [ 'id' => $activity_id ], $format, [ '%d' ] ); - $message = 'Trening zaktualizowany pomyślnie!'; - } else { - // INSERT - $result = $wpdb->insert( $table_activities, $data, $format ); - $message = 'Trening dodany pomyślnie!'; - } - - if ( $result !== false ) { - echo '

' . esc_html( $message ) . '

'; - } else { - echo '

Wystąpił błąd podczas zapisu do bazy.

'; - } -} - -/** - * Renderowanie formularza HTML - */ -function mystat_render_add_form( $activity = null ) { - // Enqueue media scripts for the uploader - wp_enqueue_media(); - - global $wpdb; - $table_categories = $wpdb->prefix . 'mystat_categories'; - $table_event_types = $wpdb->prefix . 'mystat_event_types'; - $table_equipment = $wpdb->prefix . 'mystat_equipment'; - $categories = $wpdb->get_results( "SELECT * FROM $table_categories ORDER BY name ASC" ); - $event_types = $wpdb->get_results( "SELECT * FROM $table_event_types ORDER BY name ASC" ); - $equipment_list = $wpdb->get_results( "SELECT * FROM $table_equipment ORDER BY name ASC" ); - - $is_edit_mode = ! is_null( $activity ); - $nonce_action = $is_edit_mode ? 'mystat_edit_entry_' . $activity->id : 'mystat_add_entry'; - $form_title = $is_edit_mode ? 'Edytuj Aktywność' : 'Dodaj Nową Aktywność'; - $button_text = $is_edit_mode ? 'Zaktualizuj Trening' : 'Zapisz Trening'; - - ?> -
-

-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
- -

Dane szczegółowe (opcjonalne)


Linki zewnętrzne (opcjonalne)

- - -
-

- -

-
- -
-
- prefix - // Jeśli tabele są "sztywne" (bez prefixu wp_), usuń $wpdb->prefix. - $table_activities = $wpdb->prefix . 'mystat_activities'; - $table_categories = $wpdb->prefix . 'mystat_categories'; - - // --- 1. OBSŁUGA USUWANIA (DELETE) --- - if ( isset( $_GET['action'], $_GET['id'], $_GET['_wpnonce'] ) && $_GET['action'] === 'mystat_delete' ) { - $activity_id = intval( $_GET['id'] ); - - // Weryfikacja bezpieczeństwa (Nonce) - if ( wp_verify_nonce( $_GET['_wpnonce'], 'mystat_delete_' . $activity_id ) ) { - $result = $wpdb->delete( - $table_activities, - array( 'id' => $activity_id ), - array( '%d' ) - ); - - if ( $result ) { - echo '

Aktywność została usunięta.

'; - } else { - echo '

Wystąpił błąd podczas usuwania.

'; - } - } else { - echo '

Błąd weryfikacji bezpieczeństwa (Nonce).

'; - } - } - - // --- 2. USTAWIENIA PAGINACJI --- - $items_per_page = 20; // Ile wpisów na stronę - $current_page = isset( $_GET['paged'] ) ? max( 1, intval( $_GET['paged'] ) ) : 1; - $offset = ( $current_page - 1 ) * $items_per_page; - - // --- 3. POBIERANIE DANYCH (SELECT) --- - // Pobranie całkowitej liczby wpisów do paginacji - $total_items = $wpdb->get_var( "SELECT COUNT(id) FROM $table_activities" ); - $total_pages = ceil( $total_items / $items_per_page ); - - // Pobieramy wpisy dla bieżącej strony - $sql = $wpdb->prepare(" - SELECT a.*, c.name as category_name, c.icon, c.color, et.name as event_type_name, eq.name as equipment_name - FROM $table_activities a - LEFT JOIN $table_categories c ON a.category_id = c.id - LEFT JOIN {$wpdb->prefix}mystat_event_types et ON a.event_type_id = et.id - LEFT JOIN {$wpdb->prefix}mystat_equipment eq ON a.equipment_id = eq.id - ORDER BY a.date DESC, a.id DESC - LIMIT %d OFFSET %d - ", $items_per_page, $offset); - - $activities = $wpdb->get_results( $sql ); - - // --- 4. WIDOK TABELI (HTML) --- - ?> -
-

Historia Aktywności

- - 1 ) : ?> -
-
- aktywności - add_query_arg( 'paged', '%#%' ), - 'format' => '', - 'total' => $total_pages, - 'current' => $current_page, - 'prev_text' => '« Poprzednia', - 'next_text' => 'Następna »', - ) ); - ?> -
-
- - - - - - - - - - - - - - - - - - - - - 'mystat_delete', - 'id' => $row->id, - ) ), 'mystat_delete_' . $row->id ); - - $edit_url = add_query_arg( array( - 'page' => 'mystat-edit-activity', - 'id' => $row->id - ), admin_url( 'admin.php' ) ); - - $details_url = add_query_arg( array( - 'page' => 'mystat-view-activity', - 'id' => $row->id - ), admin_url( 'admin.php' ) ); - ?> - - - - - - - - - - - - - - - - - - - -
IkonaDataTytułKategoriaTypSprzętDystans (km)CzasŚr. prędkośćAkcja
- icon ) ) : ?> - - - date ); ?>title ? wp_trim_words( $row->title, 6 ) : '(bez tytułu)' ); ?>category_name ); ?>event_type_name ); ?>equipment_name ); ?>distance, 2, ',', ' ' ); ?>duration ); ?>avg_speed ? number_format( $row->avg_speed, 1, ',', ' ' ) . ' km/h' : '-'; ?> - Edytuj - Szczegóły - - Usuń - -
Brak zarejestrowanych aktywności. Dodaj pierwszy trening powyżej!
- - 1 ) : ?> -
-
- aktywności - add_query_arg( 'paged', '%#%' ), - 'format' => '', - 'total' => $total_pages, - 'current' => $current_page, - 'prev_text' => '« Poprzednia', - 'next_text' => 'Następna »', - ) ); - ?> -
-
- -
- current_time( 'Y' ), - 'month' => current_time( 'n' ), - ), $atts, 'moje_statystyki' ); - - $year = intval( $atts['year'] ); - $month = intval( $atts['month'] ); - - // Pobieranie danych z bazy - $table_activities = $wpdb->prefix . 'mystat_activities'; - $sql = $wpdb->prepare(" - SELECT a.*, c.name as category_name, eq.name as equipment_name - FROM $table_activities a - LEFT JOIN {$wpdb->prefix}mystat_categories c ON a.category_id = c.id - LEFT JOIN {$wpdb->prefix}mystat_equipment eq ON a.equipment_id = eq.id - WHERE YEAR(a.date) = %d AND MONTH(a.date) = %d - ORDER BY a.date ASC - ", $year, $month); - - $activities = $wpdb->get_results( $sql ); - - // Obliczanie podsumowań - $total_distance = 0; - $total_seconds = 0; - foreach ($activities as $activity) { - $total_distance += $activity->distance; - if ( ! empty( $activity->duration ) ) { - list($h, $m, $s) = explode(':', $activity->duration); - $total_seconds += $h * 3600 + $m * 60 + $s; - } - } - $hours = floor($total_seconds / 3600); - $minutes = floor(($total_seconds % 3600) / 60); - $total_duration_formatted = sprintf('%d godz. %d min.', $hours, $minutes); - - // Rozpoczęcie buforowania wyjścia - ob_start(); - ?> -
- -

Podsumowanie miesiąca

- - - - - - - - - - - - - -
Całkowity dystansCałkowity czas
km
- -

Lista aktywności

- - - - - - - - - - - - - - - - - - - - - - - - avg_speed || $row->avg_heart_rate || $row->avg_cadence ) : ?> - - - - - - - - - - - -
DataTytułKategoriaDystansCzasSprzęt
date ) ) ); ?>title ); ?>category_name ); ?>distance, 2, ',', ' ' ); ?> kmduration ); ?>equipment_name ); ?>
- - - - avg_speed) echo ''; ?> - avg_heart_rate) echo ''; ?> - avg_cadence) echo ''; ?> - - - - - avg_speed) echo ''; ?> - avg_heart_rate) echo ''; ?> - avg_cadence) echo ''; ?> - - -
Śr. prędkośćŚr. tętnoŚr. kadencja
' . number_format( $row->avg_speed, 1, ',', ' ' ) . ' km/h' . $row->avg_heart_rate . ' bpm' . $row->avg_cadence . ' rpm
-
Brak aktywności w tym miesiącu.
-
- - - 0, - ), $atts, 'moje_statystyki_wpis' ); - - $activity_id = intval( $atts['id'] ); - - if ( $activity_id === 0 ) { - return '

Błąd: Nie podano ID wpisu w shortcode.

'; - } - - // Pobieranie danych z bazy - $table_activities = $wpdb->prefix . 'mystat_activities'; - $sql = $wpdb->prepare(" - SELECT a.*, c.name as category_name, c.color as category_color, et.name as event_type_name, eq.name as equipment_name - FROM $table_activities a - LEFT JOIN {$wpdb->prefix}mystat_categories c ON a.category_id = c.id - LEFT JOIN {$wpdb->prefix}mystat_event_types et ON a.event_type_id = et.id - LEFT JOIN {$wpdb->prefix}mystat_equipment eq ON a.equipment_id = eq.id - WHERE a.id = %d - ", $activity_id); - - $activity = $wpdb->get_row( $sql ); - - if ( ! $activity ) { - return '

Błąd: Nie znaleziono wpisu o ID ' . esc_html($activity_id) . '.

'; - } - - // Funkcja pomocnicza do wyświetlania wiersza - $render_row = function($label, $value, $unit = '') { - if ( ! is_null($value) && $value !== '' && $value != 0) { - echo ''; - echo '' . esc_html($label) . ''; - echo '' . esc_html($value) . ($unit ? ' ' . esc_html($unit) : '') . ''; - echo ''; - } - }; - - ob_start(); - - // Prepare map and chart data if GPX exists - $gpx_data = []; - $has_gpx_data = false; - if ( ! empty( $activity->gpx_url ) ) { - $gpx_data = mystat_parse_gpx_data( $activity->gpx_url ); - $has_gpx_data = !empty($gpx_data['points']); - } - - $unique_id = 'mystat-activity-' . esc_attr( $activity->id ); - - if ($has_gpx_data) { - wp_enqueue_style('leaflet-css', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'); - wp_enqueue_script('leaflet-js', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js', [], '1.9.4', true); - wp_enqueue_script('chart-js', 'https://cdn.jsdelivr.net/npm/chart.js', [], null, true); - - $map_id = 'map-' . $unique_id; - $chart_id = 'chart-' . $unique_id; - - $available_profiles = []; - if (!empty(array_filter($gpx_data['profiles']['elevation']))) $available_profiles['elevation'] = 'Wysokość'; - if (!empty(array_filter($gpx_data['profiles']['speed']))) $available_profiles['speed'] = 'Prędkość'; - if (!empty(array_filter($gpx_data['profiles']['hr']))) $available_profiles['hr'] = 'Tętno'; - if (!empty(array_filter($gpx_data['profiles']['cadence']))) $available_profiles['cadence'] = 'Kadencja'; - - $has_time_data = !empty(array_filter($gpx_data['profiles']['time'], fn($t) => !is_null($t))); - - wp_register_script('mystat-shortcode-loader-' . $activity->id, false); - wp_enqueue_script('mystat-shortcode-loader-' . $activity->id); - - $js_script = ' - (function() { - document.addEventListener("DOMContentLoaded", function() { - const uniqueId = "' . esc_js($unique_id) . '"; - const containerEl = document.getElementById(uniqueId); - if (!containerEl || typeof L === "undefined" || typeof Chart === "undefined") return; - - const trackPoints = ' . json_encode( $gpx_data['points'] ) . '; - const profiles = ' . json_encode( $gpx_data['profiles'] ) . '; - let activeChart = null; - - const mapId = "' . esc_js($map_id) . '"; - const mapEl = document.getElementById(mapId); - if (mapEl && trackPoints.length > 0) { - if (mapEl._leaflet_id) mapEl._leaflet_id = null; - const map = L.map(mapId); - L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: \'© OpenStreetMap\' }).addTo(map); - const polyline = L.polyline(trackPoints, {color: "' . esc_js( $activity->category_color ) . '"}).addTo(map); - map.fitBounds(polyline.getBounds().pad(0.1)); - } - - const chartId = "' . esc_js($chart_id) . '"; - const chartEl = document.getElementById(chartId); - if (!chartEl) return; - - const chartConfigs = { - elevation: { label: "Wysokość", unit: "m n.p.m.", color: "#8e44ad" }, - speed: { label: "Prędkość", unit: "km/h", color: "#2980b9" }, - hr: { label: "Tętno", unit: "bpm", color: "#c0392b" }, - cadence: { label: "Kadencja", unit: "rpm", color: "#27ae60" } - }; - const xAxisConfigs = { - distance: { label: "Dystans (km)", data: profiles.distance }, - time: { label: "Czas", data: profiles.time, formatter: (s) => s === null ? "" : new Date(s * 1000).toISOString().substr(11, 8) } - }; - - function renderChart() { - if (activeChart) activeChart.destroy(); - const activeTab = containerEl.querySelector(".mystat-chart-tab.active"); - if (!activeTab) return; - const chartType = activeTab.dataset.type; - const xAxisRadio = containerEl.querySelector(\'input[name="xaxis-\' + uniqueId + \'"]:checked\'); - if (!xAxisRadio) return; - const xAxisType = xAxisRadio.value; - - const yData = profiles[chartType], xData = xAxisConfigs[xAxisType].data; - const filteredY = [], filteredX = []; - if(yData) { - for(let i=0; i v, maxRotation: 0, autoSkip: true, maxTicksLimit: 7 } }, - y: { title: { display: true, text: chartConfigs[chartType].unit } } - }, - plugins: { legend: { display: false } }, - interaction: { intersect: false, mode: "index" }, - } - }); - } - - containerEl.querySelectorAll(".mystat-chart-tab").forEach(t => t.addEventListener("click", e => { - e.preventDefault(); - containerEl.querySelector(".mystat-chart-tab.active").classList.remove("active"); - e.currentTarget.classList.add("active"); - renderChart(); - })); - containerEl.querySelectorAll(\'input[name="xaxis-\' + uniqueId + \'"]\').forEach(r => r.addEventListener("change", renderChart)); - if (containerEl.querySelector(".mystat-chart-tab")) renderChart(); - }); - })();'; - wp_add_inline_script('mystat-shortcode-loader-' . $activity->id, $js_script); - } - - ?> -
-

title ); ?>

-

date ) ) ); ?>

- -
-
- - - distance, 2, ',', ' '), 'km'); ?> - duration); ?> - avg_speed, 1, ',', ' '), 'km/h'); ?> - total_elevation_gain, 'm'); ?> - -
-
-
- - - category_name); ?> - equipment_name); ?> - strava_url ) ) : ?> - - - -
StravaZobacz aktywność
-
-
- - -
- - -
-
-
- $label ) : ?> - - -
- -
- Oś X:  - -   - -
- - - -
-
- -
-
- - -
-