From af828068a9baa80c5d09b3b8e455655f5ba941cf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jacek=20Fefli=C5=84ski?=
Date: Thu, 5 Feb 2026 12:06:38 +0100
Subject: [PATCH] Gruby refaktor
---
includes/activation.php | 140 +
includes/admin/hooks.php | 27 +
includes/admin/menu.php | 123 +
includes/admin/pages/page-activity-form.php | 303 ++
includes/admin/pages/page-activity-view.php | 263 ++
includes/admin/pages/page-dashboard.php | 195 ++
includes/admin/pages/page-equipment.php | 293 ++
includes/admin/pages/page-event-types.php | 88 +
includes/admin/pages/page-goals.php | 199 ++
includes/admin/pages/page-import-csv.php | 309 ++
includes/admin/pages/page-infographic.php | 159 +
includes/admin/pages/page-settings.php | 95 +
includes/admin/pages/page-yearly-summary.php | 292 ++
includes/core/gpx-parser.php | 139 +
includes/core/gpx-upload.php | 33 +
includes/frontend/assets.php | 18 +
includes/frontend/shortcodes.php | 393 +++
moje-statystyki.php | 2720 +-----------------
18 files changed, 3093 insertions(+), 2696 deletions(-)
create mode 100644 includes/activation.php
create mode 100644 includes/admin/hooks.php
create mode 100644 includes/admin/menu.php
create mode 100644 includes/admin/pages/page-activity-form.php
create mode 100644 includes/admin/pages/page-activity-view.php
create mode 100644 includes/admin/pages/page-dashboard.php
create mode 100644 includes/admin/pages/page-equipment.php
create mode 100644 includes/admin/pages/page-event-types.php
create mode 100644 includes/admin/pages/page-goals.php
create mode 100644 includes/admin/pages/page-import-csv.php
create mode 100644 includes/admin/pages/page-infographic.php
create mode 100644 includes/admin/pages/page-settings.php
create mode 100644 includes/admin/pages/page-yearly-summary.php
create mode 100644 includes/core/gpx-parser.php
create mode 100644 includes/core/gpx-upload.php
create mode 100644 includes/frontend/assets.php
create mode 100644 includes/frontend/shortcodes.php
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';
+
+ ?>
+
+ 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
+
+
+
+
+
+
+
Notatki i linki
+
+
+
+
+
Mapa Trasy
+
+
+
+
Wykresy
+
+
+
+ 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 »',
+ )
+ );
+ ?>
+
+
+
+
+
+
+
+ Ikona
+ Data
+ Tytuł
+ Kategoria
+ Typ
+ Sprzęt
+ Dystans (km)
+ Czas
+ Śr. prędkość
+ Akcja
+
+
+
+
+
+ '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' )
+ );
+ ?>
+
+
+ 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
+
+
+
+
+
+
+
+
+
+ Nazwa Przebieg Liczba aktywności Status Akcje
+
+
+ id );
+ ?>
+
+ 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
+
+
+
+
+
+
+
+
+
+
Całkowity przebieg: km
+ 0 ) : ?>
+
Całkowity koszt serwisu: zł
+
+
Status: status ) ); ?>
+ purchase_date ) : ?>
Data zakupu: purchase_date ); ?>
+ initial_cost ) : ?>
Koszt zakupu: initial_cost, 2, ',', ' ' ); ?> zł
+ notes ) : ?>
Notatki: notes ) ); ?>
+
+
+
+
+
+
+
+
+
+
+
+ Suma kosztów:
+ zł
+
+
+ Data Typ Opis Koszt Przebieg Akcje
+
+
+ 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ń
+
+
+
+
+
+
+ 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
+
+
+
+
+
+
+
+
+
+ Cel Postęp Akcje
+
+
+ Brak zdefiniowanych celów.
+
+
+ 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 ); ?>
+
+ 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
+ ?>
+
+
+
+
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 '
Wiersz Powód pominięcia Dane wiersza ';
+ foreach ( $skipped_details as $error ) {
+ echo '';
+ echo '' . esc_html( $error['row'] ) . ' ';
+ echo '' . esc_html( $error['reason'] ) . ' ';
+ echo '' . esc_html( wp_trim_words( $error['data'], 25, '...' ) ) . ' ';
+ echo ' ';
+ }
+ echo '
';
+ 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
+
+
+
+
+
+ Filtruj według roku
+
+
+ >
+
+
+
+
+
+
+
+
+
+
+
Dystans total_distance, 2, ',', ' ' ); ?> km
+
+
Wznios total_elevation_gain, 0, ',', ' ' ); ?> m
+
Aktywności total_activities, 0, ',', ' ' ); ?>
+
+
+
+
+
+
+
Dystans total_distance, 2, ',', ' ' ); ?> km
+
+
Wznios total_elevation_gain, 0, ',', ' ' ); ?> m
+
Aktywności total_activities, 0, ',', ' ' ); ?>
+
+
+
+
+
+
+
+
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
+
+
+
+ Filtruj według roku
+
+
+
+
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ 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 ); ?>
+ / (%)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Miesiąc Dystans (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 dystans
+ Całkowity czas
+
+
+
+
+ km
+
+
+
+
+
+
Lista aktywności
+
+
+
+ Data
+ Tytuł
+ Kategoria
+
+ Dystans
+ Czas
+ Sprzęt
+
+
+
+
+
+
+ date ) ) ); ?>
+ title ); ?>
+ category_name ); ?>
+ distance, 2, ',', ' ' ); ?> km
+ duration ); ?>
+ equipment_name ); ?>
+
+ avg_speed || $row->avg_heart_rate || $row->avg_cadence ) : ?>
+
+
+
+
+
+ avg_speed ) { echo 'Śr. prędkość '; } ?>
+ avg_heart_rate ) { echo 'Śr. tętno '; } ?>
+ avg_cadence ) { echo 'Śr. kadencja '; } ?>
+
+
+
+
+ avg_speed ) { echo '' . number_format( $row->avg_speed, 1, ',', ' ' ) . ' km/h '; } ?>
+ avg_heart_rate ) { echo '' . $row->avg_heart_rate . ' bpm '; } ?>
+ avg_cadence ) { echo '' . $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 ) ) : ?>
+ Strava Zobacz aktywność
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $label ) :
+ ?>
+
+
+
+
+
+ Oś X:
+ Dystans
+
+ Czas
+
+
+
+
+
+
+
+
+
+
+
+
+ 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ń
-
-
-
-
-
-
- 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
-
-
-
-
-
-
- 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ętu
- Całkowity Dystans
- Całkowity Czas
- Liczba 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
-
-
-
-
-
-
-
-
-
- Cel Postęp Akcje
-
-
- Brak zdefiniowanych celów.
-
-
- 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'];
- }
- ?>
-
-
- 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
-
-
-
- Filtruj według roku
-
-
-
-
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
- 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); ?>
- / (%)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Miesiąc Dystans (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
-
-
-
-
-
- Filtruj według roku
-
-
- >
-
-
-
-
-
-
-
-
-
-
-
Dystans total_distance, 2, ',', ' '); ?> km
-
-
Wznios total_elevation_gain, 0, ',', ' '); ?> m
-
Aktywności total_activities, 0, ',', ' '); ?>
-
-
-
-
-
-
-
Dystans total_distance, 2, ',', ' '); ?> km
-
-
Wznios total_elevation_gain, 0, ',', ' '); ?> m
-
Aktywności total_activities, 0, ',', ' '); ?>
-
-
-
-
-
- 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
- ?>
-
-
-
-
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 '
Wiersz Powód pominięcia Dane wiersza ';
- foreach ( $skipped_details as $error ) {
- echo '';
- echo '' . esc_html( $error['row'] ) . ' ';
- echo '' . esc_html( $error['reason'] ) . ' ';
- echo '' . esc_html( wp_trim_words( $error['data'], 25, '...' ) ) . ' ';
- echo ' ';
- }
- echo '
';
- 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
-
-
-
-
-
-
-
Notatki i linki
-
-
-
-
-
Mapa Trasy
-
-
-
-
Wykresy
-
-
-
- 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';
-
- ?>
-
- 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 »',
- ) );
- ?>
-
-
-
-
-
-
-
- Ikona
- Data
- Tytuł
- Kategoria
- Typ
- Sprzęt
- Dystans (km)
- Czas
- Śr. prędkość
- Akcja
-
-
-
-
-
- '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' ) );
- ?>
-
-
- 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 dystans
- Całkowity czas
-
-
-
-
- km
-
-
-
-
-
-
Lista aktywności
-
-
-
- Data
- Tytuł
- Kategoria
-
- Dystans
- Czas
- Sprzęt
-
-
-
-
-
-
- date ) ) ); ?>
- title ); ?>
- category_name ); ?>
- distance, 2, ',', ' ' ); ?> km
- duration ); ?>
- equipment_name ); ?>
-
- avg_speed || $row->avg_heart_rate || $row->avg_cadence ) : ?>
-
-
-
-
-
- avg_speed) echo 'Śr. prędkość '; ?>
- avg_heart_rate) echo 'Śr. tętno '; ?>
- avg_cadence) echo 'Śr. kadencja '; ?>
-
-
-
-
- avg_speed) echo '' . number_format( $row->avg_speed, 1, ',', ' ' ) . ' km/h '; ?>
- avg_heart_rate) echo '' . $row->avg_heart_rate . ' bpm '; ?>
- avg_cadence) echo '' . $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 ) ) : ?>
- Strava Zobacz aktywność
-
-
-
-
-
-
-
-
-
-
-
-
-
- $label ) : ?>
-
-
-
-
-
- Oś X:
- Dystans
-
- Czas
-
-
-
-
-
-
-
-
-
-
-
-
-