diff --git a/LICENSE b/LICENSE index 795d267..b69d491 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2026 Jacek Fefliński - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) 2026 Jacek Fefliński + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 9c6ee1d..0883ad8 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -# wp-cycling-ststs -WP plugin for stats +# wp-cycling-ststs +WP plugin for stats diff --git a/assets/css/admin.css b/assets/css/admin.css index 3eb6aac..0b2ef94 100644 --- a/assets/css/admin.css +++ b/assets/css/admin.css @@ -1,60 +1,101 @@ -/* Styles for WordPress Activity Stats Plugin - Admin Area */ - -/* Infografika */ -.statpress-infographic-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 20px; -} -.statpress-infographic-card { - background: #fff; - border: 1px solid #e0e0e0; - border-radius: 5px; - padding: 15px; - text-align: center; - box-shadow: 0 2px 4px rgba(0,0,0,0.05); -} -.statpress-infographic-card h3 { - margin-top: 0; - color: #555; - font-size: 1.1em; -} -.statpress-infographic-card p { - font-size: 1.8em; - font-weight: bold; - color: #333; - margin-bottom: 0; -} - -/* Kontener dla szczegółów aktywności */ -#statpress-details-container { - display: flex; - flex-wrap: wrap; - gap: 2%; -} -/* Szczegóły aktywności */ -.statpress-details-col { - flex: 1 1 48%; /* Pozwala na elastyczne dopasowanie, z bazową szerokością 48% */ - min-width: 300px; /* Zapobiega zbytniemu ściskaniu kolumn na mniejszych ekranach */ -} - -/* Chart Controls */ -.statpress-chart-controls { - display: flex; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - margin-bottom: 15px; - border-bottom: 1px solid #ddd; - padding-bottom: 10px; -} - -.statpress-chart-tabs.nav-tab-wrapper { - border-bottom: none; - margin-bottom: 0; -} - -.statpress-xaxis-switcher { - padding-top: 5px; - white-space: nowrap; -} +/* Styles for WordPress Activity Stats Plugin - Admin Area */ + +/* Infografika */ +.statpress-infographic-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 20px; +} +.statpress-infographic-card { + background: #fff; + border: 1px solid #e0e0e0; + border-radius: 5px; + padding: 15px; + text-align: center; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); +} +.statpress-infographic-card h3 { + margin-top: 0; + color: #555; + font-size: 1.1em; +} +.statpress-infographic-card p { + font-size: 1.8em; + font-weight: bold; + color: #333; + margin-bottom: 0; +} + +/* Kontener dla szczegółów aktywności */ +#statpress-details-container { + display: flex; + flex-wrap: wrap; + gap: 2%; +} +/* Szczegóły aktywności */ +.statpress-details-col { + flex: 1 1 48%; /* Pozwala na elastyczne dopasowanie, z bazową szerokością 48% */ + min-width: 300px; /* Zapobiega zbytniemu ściskaniu kolumn na mniejszych ekranach */ +} + +/* Chart Controls */ +.statpress-chart-controls { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + margin-bottom: 15px; + border-bottom: 1px solid #ddd; + padding-bottom: 10px; +} + +.statpress-chart-tabs.nav-tab-wrapper { + border-bottom: none; + margin-bottom: 0; +} + +.statpress-xaxis-switcher { + padding-top: 5px; + white-space: nowrap; +} + +/* Form layout improvements */ +.statpress-form-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; + margin-top: 1em; +} + +.statpress-form-group { + background: #fdfdfd; + border: 1px solid #e5e5e5; + padding: 15px; + border-radius: 4px; +} + +.statpress-form-group h3 { + margin-top: 0; + font-size: 1em; + border-bottom: 1px solid #eee; + padding-bottom: 10px; + margin-bottom: 15px; +} + +.statpress-form-group p { + margin: 0 0 1em; +} + +.statpress-form-group label { + display: block; + margin-bottom: 5px; +} + +/* GPX Parser Status */ +#gpx_parse_status { + vertical-align: middle; + color: #50575e; +} +#gpx_parse_status .spinner { + margin-top: -2px; +} diff --git a/assets/css/frontend.css b/assets/css/frontend.css index 81580d8..b2b1bb3 100644 --- a/assets/css/frontend.css +++ b/assets/css/frontend.css @@ -1,103 +1,103 @@ -/* Styles for WordPress Activity Stats Plugin - Frontend Shortcodes */ - -/* Shortcode [moje_statystyki] */ -.mystats-shortcode-container table { - width: 100%; - border-collapse: collapse; - margin-bottom: 2em; -} -.mystats-shortcode-container th, -.mystats-shortcode-container td { - padding: 8px 12px; - border: 1px solid #ddd; - text-align: left; -} -.mystats-shortcode-container th { - background-color: #f4f4f4; -} - -.mystats-activity-table td:nth-child(4), -.mystats-activity-table td:nth-child(5) { - text-align: right; -} - -/* Styles for the nested details table */ -.mystats-activity-details-row > .mystats-activity-details-cell { - padding: 0 !important; - border-top: none; - border-bottom: 1px solid #ddd; -} - -.mystats-nested-details-table { - width: 100%; - margin: 0; - background-color: #fdfdfd; - border: none !important; - border-collapse: collapse; -} - -.mystats-nested-details-table th, -.mystats-nested-details-table td { - border: none !important; - padding: 6px 12px; - text-align: center; - font-size: 0.9em; - width: 33.33%; -} - -.mystats-nested-details-table th { - background-color: transparent; - font-weight: normal; - color: #777; - padding-top: 8px; - padding-bottom: 2px; -} - -.mystats-nested-details-table td { - font-weight: bold; - padding-top: 0; - padding-bottom: 8px; -} - -/* Shortcode [moje_statystyki_wpis] */ -.mystat-single-activity-shortcode { - border: 1px solid #eee; - padding: 15px; - margin-bottom: 1.5em; - border-radius: 5px; - background: #f9f9f9; -} -.mystat-single-activity-shortcode h4 { - margin-top: 0; -} -.mystat-single-activity-shortcode p em { - color: #777; - font-size: 0.9em; -} -.mystat-single-columns-container { - display: flex; - flex-wrap: wrap; - gap: 30px; - margin-bottom: 15px; -} -.mystat-single-col { - flex: 1; - min-width: 240px; -} -.mystat-single-summary-table { - width: 100%; - border-collapse: collapse; -} -.mystat-single-summary-table th, -.mystat-single-summary-table td { - padding: 4px 0; - border: none; - text-align: left; - vertical-align: top; -} -.mystat-single-summary-table th { - font-weight: bold; - padding-right: 1em; - white-space: nowrap; - width: 1%; -} +/* Styles for WordPress Activity Stats Plugin - Frontend Shortcodes */ + +/* Shortcode [statpress_summary] */ +.statpress-shortcode-container table { + width: 100%; + border-collapse: collapse; + margin-bottom: 2em; +} +.statpress-shortcode-container th, +.statpress-shortcode-container td { + padding: 8px 12px; + border: 1px solid #ddd; + text-align: left; +} +.statpress-shortcode-container th { + background-color: #f4f4f4; +} + +.statpress-activity-table td:nth-child(4), +.statpress-activity-table td:nth-child(5) { + text-align: right; +} + +/* Styles for the nested details table */ +.statpress-activity-details-row > .statpress-activity-details-cell { + padding: 0 !important; + border-top: none; + border-bottom: 1px solid #ddd; +} + +.statpress-nested-details-table { + width: 100%; + margin: 0; + background-color: #fdfdfd; + border: none !important; + border-collapse: collapse; +} + +.statpress-nested-details-table th, +.statpress-nested-details-table td { + border: none !important; + padding: 6px 12px; + text-align: center; + font-size: 0.9em; + width: 33.33%; +} + +.statpress-nested-details-table th { + background-color: transparent; + font-weight: normal; + color: #777; + padding-top: 8px; + padding-bottom: 2px; +} + +.statpress-nested-details-table td { + font-weight: bold; + padding-top: 0; + padding-bottom: 8px; +} + +/* Shortcode [statpress_activity] */ +.statpress-single-activity-shortcode { + border: 1px solid #eee; + padding: 15px; + margin-bottom: 1.5em; + border-radius: 5px; + background: #f9f9f9; +} +.statpress-single-activity-shortcode h4 { + margin-top: 0; +} +.statpress-single-activity-shortcode p em { + color: #777; + font-size: 0.9em; +} +.statpress-single-columns-container { + display: flex; + flex-wrap: wrap; + gap: 30px; + margin-bottom: 15px; +} +.statpress-single-col { + flex: 1; + min-width: 240px; +} +.statpress-single-summary-table { + width: 100%; + border-collapse: collapse; +} +.statpress-single-summary-table th, +.statpress-single-summary-table td { + padding: 4px 0; + border: none; + text-align: left; + vertical-align: top; +} +.statpress-single-summary-table th { + font-weight: bold; + padding-right: 1em; + white-space: nowrap; + width: 1%; +} diff --git a/includes/activation.php b/includes/activation.php index a0c4b12..e815486 100644 --- a/includes/activation.php +++ b/includes/activation.php @@ -1,140 +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' ) ); - } +get_charset_collate(); + + $table_categories = $wpdb->prefix . 'statpress_categories'; + $table_activities = $wpdb->prefix . 'statpress_activities'; + $table_event_types = $wpdb->prefix . 'statpress_event_types'; + $table_equipment = $wpdb->prefix . 'statpress_equipment'; + $table_equipment_log = $wpdb->prefix . 'statpress_equipment_log'; + $table_goals = $wpdb->prefix . 'statpress_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 index e64090d..451cd35 100644 --- a/includes/admin/hooks.php +++ b/includes/admin/hooks.php @@ -1,27 +1,27 @@ -

Dodaj Nowy Trening

'; - // Obsługa zapisu formularza (musi być przed renderowaniem, aby wyświetlić komunikat) - statpress_handle_activity_form_submission(); - // Formularz dodawania - statpress_render_add_form(); - echo ''; -} - -function statpress_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 - statpress_handle_activity_form_submission(); - - $table_activities = $wpdb->prefix . 'statpress_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

'; - statpress_render_add_form( $activity ); - echo '
'; -} - -/** - * Obsługa zapisu nowego lub edytowanego wpisu do bazy danych - */ -function statpress_handle_activity_form_submission() { - global $wpdb; - - // Sprawdź czy formularz został wysłany - if ( ! isset( $_POST['statpress_submit_activity'] ) ) { - return; - } - - $activity_id = isset( $_POST['activity_id'] ) ? intval( $_POST['activity_id'] ) : 0; - $nonce_action = $activity_id > 0 ? 'statpress_edit_entry_' . $activity_id : 'statpress_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; - } - - // Use the refactored function to save data. - // We can pass $_POST directly as the function will sanitize it. - $result = statpress_save_activity_data( $_POST, $activity_id ); - - if ( $activity_id > 0 ) { - $message = 'Trening zaktualizowany pomyślnie!'; - } else { - $message = 'Trening dodany pomyślnie!'; - } - - if ( $result ) { - echo '

' . esc_html( $message ) . '

'; - } else { - echo '

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

'; - } -} - -/** - * Renderowanie formularza HTML - */ -function statpress_render_add_form( $activity = null ) { - // Enqueue media scripts for the uploader - wp_enqueue_media(); - - global $wpdb; - $table_categories = $wpdb->prefix . 'statpress_categories'; - $table_event_types = $wpdb->prefix . 'statpress_event_types'; - $table_equipment = $wpdb->prefix . 'statpress_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 ? 'statpress_edit_entry_' . $activity->id : 'statpress_add_entry'; - $form_title = $is_edit_mode ? 'Edytuj Aktywność' : 'Dodaj Nową Aktywność'; - $button_text = $is_edit_mode ? 'Zaktualizuj Trening' : 'Zapisz Trening'; - - ?> -
-

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

Dane szczegółowe (opcjonalne)


Linki zewnętrzne (opcjonalne)

- - -
-

- -

-
- -
-
-

Dodaj Nowy Trening

'; + // Obsługa zapisu formularza (musi być przed renderowaniem, aby wyświetlić komunikat) + statpress_handle_activity_form_submission(); + // Formularz dodawania + statpress_render_add_form(); + echo ''; +} + +function statpress_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 + statpress_handle_activity_form_submission(); + + $table_activities = $wpdb->prefix . 'statpress_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

'; + statpress_render_add_form( $activity ); + echo '
'; +} + +/** + * Obsługa zapisu nowego lub edytowanego wpisu do bazy danych + */ +function statpress_handle_activity_form_submission() { + global $wpdb; + + // Sprawdź czy formularz został wysłany + if ( ! isset( $_POST['statpress_submit_activity'] ) ) { + return; + } + + $activity_id = isset( $_POST['activity_id'] ) ? intval( $_POST['activity_id'] ) : 0; + $nonce_action = $activity_id > 0 ? 'statpress_edit_entry_' . $activity_id : 'statpress_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; + } + + // Use the refactored function to save data. + // We can pass $_POST directly as the function will sanitize it. + $result = statpress_save_activity_data( $_POST, $activity_id ); + + if ( $activity_id > 0 ) { + $message = 'Trening zaktualizowany pomyślnie!'; + } else { + $message = 'Trening dodany pomyślnie!'; + } + + if ( $result ) { + echo '

' . esc_html( $message ) . '

'; + } else { + echo '

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

'; + } +} + +/** + * Renderowanie formularza HTML + */ +function statpress_render_add_form( $activity = null ) { + // Enqueue media scripts for the uploader + wp_enqueue_media(); + + global $wpdb; + $table_categories = $wpdb->prefix . 'statpress_categories'; + $table_event_types = $wpdb->prefix . 'statpress_event_types'; + $table_equipment = $wpdb->prefix . 'statpress_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 ? 'statpress_edit_entry_' . $activity->id : 'statpress_add_entry'; + $form_title = $is_edit_mode ? 'Edytuj Aktywność' : 'Dodaj Nową Aktywność'; + $button_text = $is_edit_mode ? 'Zaktualizuj Trening' : 'Zapisz Trening'; + + ?> +
+

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

Wklej link lub wgraj plik GPX, aby automatycznie uzupełnić pola poniżej.

+

+ +
+ +
+ +
+
+

Dane szczegółowe (opcjonalne)

+
+
+

Prędkość

+

+

+
+
+

Tętno

+

+

+
+
+

Rytm

+

+

+
+
+

Wysokość

+

+

+

+

+
+
+
+
+

+ +

+
+ +
+
+

Błąd

Nie podano ID aktywności.

'; - return; - } - - $table_activities = $wpdb->prefix . 'statpress_activities'; - $table_categories = $wpdb->prefix . 'statpress_categories'; - $table_event_types = $wpdb->prefix . 'statpress_event_types'; - $table_equipment = $wpdb->prefix . 'statpress_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 = statpress_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( 'statpress-details-loader', false ); - wp_enqueue_script( 'statpress-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("statpress-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(".statpress-chart-tabs .nav-tab-active").getAttribute("href").substring(1); - const xAxisType = document.querySelector(\'input[name="statpress_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(".statpress-chart-tabs .nav-tab").forEach(t => t.addEventListener("click", e => { - e.preventDefault(); - document.querySelector(".statpress-chart-tabs .nav-tab-active").classList.remove("nav-tab-active"); - e.target.classList.add("nav-tab-active"); - renderChart(); - })); - document.querySelectorAll(\'input[name="statpress_xaxis"]\').forEach(r => r.addEventListener("change", renderChart)); - if (document.querySelector(".statpress-chart-tabs .nav-tab")) renderChart(); - '; - wp_add_inline_script( 'statpress-details-loader', 'document.addEventListener("DOMContentLoaded", function() {' . $chart_js . '});' ); - } - - ?> -
-

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

- -

← Powrót do listy aktywności

- -
-

Podsumowanie

-
-
-
-

Główne dane

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

Dane szczegółowe

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

Notatki i linki

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

Mapa Trasy

-
- - -

Wykresy

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

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

-
- - -
-
-
-

Błąd

Nie podano ID aktywności.

'; + return; + } + + $table_activities = $wpdb->prefix . 'statpress_activities'; + $table_categories = $wpdb->prefix . 'statpress_categories'; + $table_event_types = $wpdb->prefix . 'statpress_event_types'; + $table_equipment = $wpdb->prefix . 'statpress_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 = statpress_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( 'statpress-details-loader', false ); + wp_enqueue_script( 'statpress-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("statpress-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(".statpress-chart-tabs .nav-tab-active").getAttribute("href").substring(1); + const xAxisType = document.querySelector(\'input[name="statpress_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(".statpress-chart-tabs .nav-tab").forEach(t => t.addEventListener("click", e => { + e.preventDefault(); + document.querySelector(".statpress-chart-tabs .nav-tab-active").classList.remove("nav-tab-active"); + e.target.classList.add("nav-tab-active"); + renderChart(); + })); + document.querySelectorAll(\'input[name="statpress_xaxis"]\').forEach(r => r.addEventListener("change", renderChart)); + if (document.querySelector(".statpress-chart-tabs .nav-tab")) renderChart(); + '; + wp_add_inline_script( 'statpress-details-loader', 'document.addEventListener("DOMContentLoaded", function() {' . $chart_js . '});' ); + } + + ?> +
+

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

+ +

← Powrót do listy aktywności

+ +
+

Podsumowanie

+
+
+
+

Główne dane

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

Dane szczegółowe

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

Notatki i linki

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

Mapa Trasy

+
+ + +

Wykresy

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

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

+
+ + +
+
+
+

StatPress Dashboard

'; - statpress_render_history_table(); - echo ''; -} - -function statpress_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 . 'statpress_activities'; - $table_categories = $wpdb->prefix . 'statpress_categories'; - - // --- 1. OBSŁUGA USUWANIA (DELETE) --- - if ( isset( $_GET['action'], $_GET['id'], $_GET['_wpnonce'] ) && 'statpress_delete' === $_GET['action'] ) { - $activity_id = intval( $_GET['id'] ); - - // Weryfikacja bezpieczeństwa (Nonce) - if ( wp_verify_nonce( $_GET['_wpnonce'], 'statpress_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}statpress_event_types et ON a.event_type_id = et.id - LEFT JOIN {$wpdb->prefix}statpress_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 »', - ) - ); - ?> -
-
- - - - - - - - - - - - - - - - - - - - - 'statpress_delete', - 'id' => $row->id, - ) - ), - 'statpress_delete_' . $row->id - ); - - $edit_url = add_query_arg( - array( - 'page' => 'statpress-edit-activity', - 'id' => $row->id, - ), - admin_url( 'admin.php' ) - ); - - $details_url = add_query_arg( - array( - 'page' => 'statpress-view-activity', - 'id' => $row->id, - ), - admin_url( 'admin.php' ) - ); - ?> - - - - - - - - - - - - - - - - - - - -
IkonaDataTytułKategoriaTypSprzętDystans (km)CzasŚr. prędkośćAkcja
- icon ) ) : ?> - - - date ); ?>title ? wp_trim_words( $row->title, 6 ) : '(bez tytułu)' ); ?>category_name ); ?>event_type_name ); ?>equipment_name ); ?>distance, 2, ',', ' ' ); ?>duration ); ?>avg_speed ? number_format( $row->avg_speed, 1, ',', ' ' ) . ' km/h' : '-'; ?> - Edytuj - Szczegóły - - Usuń - -
Brak zarejestrowanych aktywności. Dodaj pierwszy trening powyżej!
- - 1 ) : ?> -
-
- aktywności - add_query_arg( 'paged', '%#%' ), - 'format' => '', - 'total' => $total_pages, - 'current' => $current_page, - 'prev_text' => '« Poprzednia', - 'next_text' => 'Następna »', - ) - ); - ?> -
-
- -
-

StatPress Dashboard

'; + + // --- MIGRATION NOTICE --- + // Show the migration button if it hasn't been completed yet. + if ( ! get_option( 'statpress_migration_complete' ) ) { + $migration_url = wp_nonce_url( + admin_url( 'admin.php?page=statpress-dashboard&action=statpress_migrate_data' ), + 'statpress_migration_nonce' + ); + echo '
'; + echo '

Migracja danych StatPress

'; + echo '

Wygląda na to, że istnieją dane w starych tabelach (mystat_*), które można przenieść. Kliknij przycisk poniżej, aby rozpocząć proces.

'; + echo '

Ważne: Ta operacja jest jednorazowa i powinna być wykonana tylko raz, na pustej instalacji wtyczki StatPress.

'; + echo 'Rozpocznij migrację danych'; + echo '
'; + } + + // Show the results of the migration after it's done. + $migration_results = get_transient( 'statpress_migration_results' ); + if ( $migration_results ) { + echo '
'; + echo '

Migracja zakończona!

'; + echo '
    '; + foreach ( $migration_results as $table => $result ) { + if ( 'success' === $result['status'] ) { + echo '
  • Tabela ' . esc_html( $table ) . ': Przeniesiono ' . esc_html( $result['count'] ) . ' wierszy.
  • '; + } elseif ( 'skipped' === $result['status'] ) { + echo '
  • Tabela ' . esc_html( $table ) . ': Pominięto, ponieważ nowa tabela zawiera już dane (' . esc_html( $result['count'] ) . ' wierszy).
  • '; + } elseif ( 'failure' === $result['status'] ) { + echo '
  • Tabela ' . esc_html( $table ) . ': Migracja nieudana. Błąd bazy danych:
    ' . esc_html( $result['error'] ) . '
  • '; + } + } + echo '
'; + echo '

Twoje dane powinny być teraz widoczne. Stare tabele (' . esc_html( $GLOBALS['wpdb']->prefix ) . 'mystat_*) wciąż istnieją w bazie danych, ale nie są już używane. Możesz je usunąć ręcznie (np. przez phpMyAdmin), jeśli wszystko działa poprawnie.

'; + echo '
'; + delete_transient( 'statpress_migration_results' ); + } + // --- END MIGRATION NOTICE --- + + statpress_render_history_table(); + echo ''; +} + +function statpress_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 . 'statpress_activities'; + $table_categories = $wpdb->prefix . 'statpress_categories'; + + // --- 1. OBSŁUGA USUWANIA (DELETE) --- + if ( isset( $_GET['action'], $_GET['id'], $_GET['_wpnonce'] ) && 'statpress_delete' === $_GET['action'] ) { + $activity_id = intval( $_GET['id'] ); + + // Weryfikacja bezpieczeństwa (Nonce) + if ( wp_verify_nonce( $_GET['_wpnonce'], 'statpress_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}statpress_event_types et ON a.event_type_id = et.id + LEFT JOIN {$wpdb->prefix}statpress_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 »', + ) + ); + ?> +
+
+ + + + + + + + + + + + + + + + + + + + + 'statpress_delete', + 'id' => $row->id, + ) + ), + 'statpress_delete_' . $row->id + ); + + $edit_url = add_query_arg( + array( + 'page' => 'statpress-edit-activity', + 'id' => $row->id, + ), + admin_url( 'admin.php' ) + ); + + $details_url = add_query_arg( + array( + 'page' => 'statpress-view-activity', + 'id' => $row->id, + ), + admin_url( 'admin.php' ) + ); + ?> + + + + + + + + + + + + + + + + + + + +
IkonaDataTytułKategoriaTypSprzętDystans (km)CzasŚr. prędkośćAkcja
+ icon ) ) : ?> + + + date ); ?>title ? wp_trim_words( $row->title, 6 ) : '(bez tytułu)' ); ?>category_name ); ?>event_type_name ); ?>equipment_name ); ?>distance, 2, ',', ' ' ); ?>duration ); ?>avg_speed ? number_format( $row->avg_speed, 1, ',', ' ' ) . ' km/h' : '-'; ?> + Edytuj + Szczegóły + + Usuń + +
Brak zarejestrowanych aktywności. Dodaj pierwszy trening powyżej!
+ + 1 ) : ?> +
+
+ aktywności + add_query_arg( 'paged', '%#%' ), + 'format' => '', + 'total' => $total_pages, + 'current' => $current_page, + 'prev_text' => '« Poprzednia', + 'next_text' => 'Następna »', + ) + ); + ?> +
+
+ +
+ prefix . 'statpress_equipment'; - $message = ''; - $notice_class = ''; - - // Handle POST requests (add/update) - if ( isset( $_POST['submit'] ) && check_admin_referer( 'statpress_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 - $result = $wpdb->update( $table_equipment, $data, array( 'id' => $item_id ) ); - if ( false !== $result ) { - $message = 'Sprzęt zaktualizowany.'; - $notice_class = 'notice-success'; - } else { - $message = 'Błąd podczas aktualizacji sprzętu: ' . $wpdb->last_error; - $notice_class = 'notice-error'; - } - } else { // Insert - $result = $wpdb->insert( $table_equipment, $data ); - if ( false !== $result ) { - $message = 'Sprzęt dodany.'; - $notice_class = 'notice-success'; - } else { - $message = 'Błąd podczas dodawania sprzętu: ' . $wpdb->last_error; - $notice_class = 'notice-error'; - } - } - } 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'], 'statpress_delete_equipment_' . $_GET['id'] ) ) { - // Sprawdź, czy sprzęt nie jest używany - $usage_count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}statpress_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 . 'statpress_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 . 'statpress_activities'; - $equipment_list = $wpdb->get_results(" - SELECT - eq.id, - eq.name, - eq.type, - eq.status, - SUM(a.distance) as total_distance, - 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, eq.type, eq.status - ORDER BY - eq.status ASC, eq.name ASC" - ); - ?> -
-

Zarządzaj Sprzętem

- -

- - -
-
-
-
-

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

Błąd

Nie podano ID sprzętu.

'; - return; - } - - $table_equipment = $wpdb->prefix . 'statpress_equipment'; - $table_equipment_log = $wpdb->prefix . 'statpress_equipment_log'; - $table_activities = $wpdb->prefix . 'statpress_activities'; - $message = ''; - $notice_class = ''; - - // --- Handle Service Log form submissions (add/update/delete) --- - if ( isset( $_POST['submit_log'] ) && check_admin_referer( 'statpress_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'], 'statpress_delete_log_' . $_GET['log_id'] ) ) { - $wpdb->delete( $table_equipment_log, array( 'id' => intval( $_GET['log_id'] ) ) ); - $message = 'Wpis z dziennika usunięty.'; - $notice_class = 'notice-success'; - } - } - - // --- Get Data --- - $equipment = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_equipment WHERE id = %d", $equipment_id ) ); - if ( ! $equipment ) { - echo '

Błąd

Nie znaleziono sprzętu o podanym ID.

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

Dziennik serwisowy: name ); ?>

-

← Powrót do listy sprzętu

- - -

- - -
-

Podsumowanie Sprzętu

-
-
-

Całkowity przebieg: km

- 0 ) : ?> -

Całkowity koszt serwisu:

- -

Status: status ) ); ?>

- purchase_date ) : ?>

Data zakupu: purchase_date ); ?>

- initial_cost ) : ?>

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

- notes ) : ?>

Notatki:
notes ) ); ?>

-
-
-
- -
-
-
-
-

-
- - -
-
-
-
-

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

- - Anuluj edycję -
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - -
Suma kosztów:
DataTypOpisKosztPrzebiegAkcje
Brak wpisów w dzienniku.
log_date ); ?>log_type ); ?>description ) ); ?>cost ? number_format( $log->cost, 2, ',', ' ' ) . ' zł' : '-'; ?>mileage ? number_format( $log->mileage, 0, '', ' ' ) . ' km' : '-'; ?> - Edytuj | - Usuń -
-
-
-
-
- prefix . 'statpress_equipment'; + $message = ''; + $notice_class = ''; + + // Handle POST requests (add/update) + if ( isset( $_POST['submit'] ) && check_admin_referer( 'statpress_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 + $result = $wpdb->update( $table_equipment, $data, array( 'id' => $item_id ) ); + if ( false !== $result ) { + $message = 'Sprzęt zaktualizowany.'; + $notice_class = 'notice-success'; + } else { + $message = 'Błąd podczas aktualizacji sprzętu: ' . $wpdb->last_error; + $notice_class = 'notice-error'; + } + } else { // Insert + $result = $wpdb->insert( $table_equipment, $data ); + if ( false !== $result ) { + $message = 'Sprzęt dodany.'; + $notice_class = 'notice-success'; + } else { + $message = 'Błąd podczas dodawania sprzętu: ' . $wpdb->last_error; + $notice_class = 'notice-error'; + } + } + } 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'], 'statpress_delete_equipment_' . $_GET['id'] ) ) { + // Sprawdź, czy sprzęt nie jest używany + $usage_count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}statpress_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 . 'statpress_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 . 'statpress_activities'; + $equipment_list = $wpdb->get_results(" + SELECT + eq.id, + eq.name, + eq.type, + eq.status, + SUM(a.distance) as total_distance, + 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, eq.type, eq.status + ORDER BY + eq.status ASC, eq.name ASC" + ); + ?> +
+

Zarządzaj Sprzętem

+ +

+ + +
+
+
+
+

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

Błąd

Nie podano ID sprzętu.

'; + return; + } + + $table_equipment = $wpdb->prefix . 'statpress_equipment'; + $table_equipment_log = $wpdb->prefix . 'statpress_equipment_log'; + $table_activities = $wpdb->prefix . 'statpress_activities'; + $message = ''; + $notice_class = ''; + + // --- Handle Service Log form submissions (add/update/delete) --- + if ( isset( $_POST['submit_log'] ) && check_admin_referer( 'statpress_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'], 'statpress_delete_log_' . $_GET['log_id'] ) ) { + $wpdb->delete( $table_equipment_log, array( 'id' => intval( $_GET['log_id'] ) ) ); + $message = 'Wpis z dziennika usunięty.'; + $notice_class = 'notice-success'; + } + } + + // --- Get Data --- + $equipment = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_equipment WHERE id = %d", $equipment_id ) ); + if ( ! $equipment ) { + echo '

Błąd

Nie znaleziono sprzętu o podanym ID.

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

Dziennik serwisowy: name ); ?>

+

← Powrót do listy sprzętu

+ + +

+ + +
+

Podsumowanie Sprzętu

+
+
+

Całkowity przebieg: km

+ 0 ) : ?> +

Całkowity koszt serwisu:

+ +

Status: status ) ); ?>

+ purchase_date ) : ?>

Data zakupu: purchase_date ); ?>

+ initial_cost ) : ?>

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

+ notes ) : ?>

Notatki:
notes ) ); ?>

+
+
+
+ +
+
+
+
+

+
+ + +
+
+
+
+

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

+ + Anuluj edycję +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
Suma kosztów:
DataTypOpisKosztPrzebiegAkcje
Brak wpisów w dzienniku.
log_date ); ?>log_type ); ?>description ) ); ?>cost ? number_format( $log->cost, 2, ',', ' ' ) . ' zł' : '-'; ?>mileage ? number_format( $log->mileage, 0, '', ' ' ) . ' km' : '-'; ?> + Edytuj | + Usuń +
+
+
+
+
+ prefix . 'statpress_event_types'; - $message = ''; - $notice_class = ''; - - // Handle POST requests (add/update) - if ( isset( $_POST['submit'] ) && check_admin_referer( 'statpress_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'], 'statpress_delete_event_type_' . $_GET['id'] ) ) { - $wpdb->delete( $table_event_types, array( 'id' => intval( $_GET['id'] ) ) ); - $message = 'Typ wydarzenia usunięty.'; - $notice_class = 'notice-success'; - } - } - - // Prepare for form (for editing) - $item_to_edit = null; - if ( isset( $_GET['action'], $_GET['id'] ) && 'edit' === $_GET['action'] ) { - $item_to_edit = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_event_types WHERE id = %d", intval( $_GET['id'] ) ) ); - } - - $event_types = $wpdb->get_results( "SELECT * FROM $table_event_types ORDER BY name ASC" ); - ?> -
-

Typy Wydarzeń

- -

- - -
-
-
-
-

-
- - -
- - -
- -
-
-
-
-
-
- - - - - - - -
NazwaAkcje
name ); ?>Edytuj | Usuń
-
-
-
-
- prefix . 'statpress_event_types'; + $message = ''; + $notice_class = ''; + + // Handle POST requests (add/update) + if ( isset( $_POST['submit'] ) && check_admin_referer( 'statpress_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'], 'statpress_delete_event_type_' . $_GET['id'] ) ) { + $wpdb->delete( $table_event_types, array( 'id' => intval( $_GET['id'] ) ) ); + $message = 'Typ wydarzenia usunięty.'; + $notice_class = 'notice-success'; + } + } + + // Prepare for form (for editing) + $item_to_edit = null; + if ( isset( $_GET['action'], $_GET['id'] ) && 'edit' === $_GET['action'] ) { + $item_to_edit = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_event_types WHERE id = %d", intval( $_GET['id'] ) ) ); + } + + $event_types = $wpdb->get_results( "SELECT * FROM $table_event_types ORDER BY name ASC" ); + ?> +
+

Typy Wydarzeń

+ +

+ + +
+
+
+
+

+
+ + +
+ + +
+ +
+
+
+
+
+
+ + + + + + + +
NazwaAkcje
name ); ?>Edytuj | Usuń
+
+
+
+
+ prefix . 'statpress_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 statpress_goals_page() { - global $wpdb; - $table_goals = $wpdb->prefix . 'statpress_goals'; - $table_categories = $wpdb->prefix . 'statpress_categories'; - $message = ''; - $notice_class = ''; - - // Handle POST requests (add/update) - if ( isset( $_POST['submit'] ) && check_admin_referer( 'statpress_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'], 'statpress_delete_goal_' . $_GET['id'] ) ) { - $wpdb->delete( $table_goals, array( 'id' => intval( $_GET['id'] ) ) ); - $message = 'Cel usunięty.'; - $notice_class = 'notice-success'; - } - } - - // Prepare for form (for editing) - $item_to_edit = null; - if ( isset( $_GET['action'], $_GET['id'] ) && 'edit' === $_GET['action'] ) { - $item_to_edit = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_goals WHERE id = %d", intval( $_GET['id'] ) ) ); - } - - $goals = $wpdb->get_results( "SELECT g.*, c.name as category_name FROM $table_goals g LEFT JOIN $table_categories c ON g.category_id = c.id ORDER BY g.year DESC, g.name ASC" ); - $categories = $wpdb->get_results( "SELECT * FROM $table_categories ORDER BY name ASC" ); - ?> -
-

Zarządzaj Celami

- -

- - -
-
-
-
-

-
- - -
-
-

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

-
-
-
- -
-
-
-
-
-
- - - - - - - - goal_type ) { - $target_formatted = round( $goal->target_value / 3600 ) . ' godz.'; - $current_formatted = sprintf( '%d godz. %d min.', floor( $progress['current_value'] / 3600 ), floor( ( $progress['current_value'] % 3600 ) / 60 ) ); - } elseif ( 'distance' === $goal->goal_type ) { - $target_formatted = number_format( $goal->target_value, 0, ',', ' ' ) . ' km'; - $current_formatted = number_format( $progress['current_value'], 2, ',', ' ' ) . ' km'; - } else { // count - $target_formatted = (int) $goal->target_value; - $current_formatted = (int) $progress['current_value']; - } - ?> - - - - - - - - -
CelPostępAkcje
Brak zdefiniowanych celów.
- name ); ?>
- - year ); ?> - month ) { echo ' / ' . date_i18n( 'F', mktime( 0, 0, 0, $goal->month, 10 ) ); } ?> - category_name ) { echo ' / ' . esc_html( $goal->category_name ); } ?> - -
-
-
-
- z (%) -
- Edytuj | - Usuń -
-
-
-
-
- - prefix . 'statpress_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 statpress_goals_page() { + global $wpdb; + $table_goals = $wpdb->prefix . 'statpress_goals'; + $table_categories = $wpdb->prefix . 'statpress_categories'; + $message = ''; + $notice_class = ''; + + // Handle POST requests (add/update) + if ( isset( $_POST['submit'] ) && check_admin_referer( 'statpress_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'], 'statpress_delete_goal_' . $_GET['id'] ) ) { + $wpdb->delete( $table_goals, array( 'id' => intval( $_GET['id'] ) ) ); + $message = 'Cel usunięty.'; + $notice_class = 'notice-success'; + } + } + + // Prepare for form (for editing) + $item_to_edit = null; + if ( isset( $_GET['action'], $_GET['id'] ) && 'edit' === $_GET['action'] ) { + $item_to_edit = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_goals WHERE id = %d", intval( $_GET['id'] ) ) ); + } + + $goals = $wpdb->get_results( "SELECT g.*, c.name as category_name FROM $table_goals g LEFT JOIN $table_categories c ON g.category_id = c.id ORDER BY g.year DESC, g.name ASC" ); + $categories = $wpdb->get_results( "SELECT * FROM $table_categories ORDER BY name ASC" ); + ?> +
+

Zarządzaj Celami

+ +

+ + +
+
+
+
+

+
+ + +
+
+

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

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

Importuj aktywności z pliku CSV

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

Instrukcje i formularz importu

-
-

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

-

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

-

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

-

Ważne: -

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

-
-
- - - - - - - - - - -
- -
-
-
- '; -} - -function statpress_handle_csv_import() { - global $wpdb; - - if ( ! isset( $_POST['statpress_csv_import_nonce_field'] ) || ! wp_verify_nonce( $_POST['statpress_csv_import_nonce_field'], 'statpress_csv_import_nonce' ) ) { - echo '

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['statpress_csv_data'] ) ) { - $csv_content = stripslashes( $_POST['statpress_csv_data'] ); - } elseif ( ! empty( $_FILES['statpress_csv_file']['tmp_name'] ) && UPLOAD_ERR_OK === $_FILES['statpress_csv_file']['error'] ) { - $csv_content = file_get_contents( $_FILES['statpress_csv_file']['tmp_name'] ); - } - - if ( empty( trim( $csv_content ) ) ) { - echo '

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 . 'statpress_categories'; - $table_event_types = $wpdb->prefix . 'statpress_event_types'; - $table_equipment = $wpdb->prefix . 'statpress_equipment'; - - $create_lookup = function( $table_name ) use ( $wpdb ) { - $items = $wpdb->get_results( "SELECT id, name FROM {$table_name}" ); - $lookup = array(); - if ( is_array( $items ) ) { - foreach ( $items as $item ) { - // Use a robust trim to handle various whitespace characters and make it case-insensitive - $clean_name = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $item->name ); - $lookup[ mb_strtolower( $clean_name, 'UTF-8' ) ] = $item->id; - } - } - return $lookup; - }; - - $categories_lookup = $create_lookup( $table_categories ); - $event_types_lookup = $create_lookup( $table_event_types ); - $equipment_lookup = $create_lookup( $table_equipment ); - // --- END: Robust, case-insensitive lookup --- - - // Process the CSV file - $table_activities = $wpdb->prefix . 'statpress_activities'; - $imported_count = 0; - $skipped_details = array(); - $row_number = 1; // Header is row 1 - - // Normalize line endings and split into lines - $lines = str_replace( array( "\r\n", "\r" ), "\n", $csv_content ); - $lines = explode( "\n", $lines ); - - if ( empty( $lines ) || empty( trim( $lines[0] ) ) ) { - echo '

Podane dane CSV są puste.

'; - return; - } - - // --- Delimiter and BOM detection --- - $first_line = $lines[0]; - $delimiter = ( substr_count( $first_line, ';' ) > substr_count( $first_line, ',' ) ) ? ';' : ','; - - // --- BOM removal from first header element --- - $bom = "\xEF\xBB\xBF"; - if ( substr( $first_line, 0, 3 ) === $bom ) { - $lines[0] = substr( $first_line, 3 ); - } - - $header_raw = str_getcsv( array_shift( $lines ), $delimiter ); - $header_raw = array_map( 'trim', $header_raw ); - - // Translate header from Polish/English to internal keys - $header = array(); - foreach ( $header_raw as $col ) { - $header[] = $column_map[ strtolower( $col ) ] ?? 'ignored_' . uniqid(); - } - - $required_internal_keys = array( 'date', 'title', 'category_name', 'distance' ); - $missing_keys = array_diff( $required_internal_keys, $header ); - if ( ! empty( $missing_keys ) ) { - $key_to_polish_map = array( - 'date' => 'Data', - 'title' => 'Tytuł', - 'category_name' => 'Kategoria / Typ aktywności', - 'distance' => 'Dystans', - ); - $missing_polish_names = array_map( fn( $key ) => $key_to_polish_map[ $key ] ?? $key, $missing_keys ); - echo '

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

'; - return; - } - - $parse_and_round_int = fn( $val ) => round( floatval( str_replace( ',', '.', $val ) ) ); - $null_if_empty = fn( $value ) => '' !== $value ? $value : null; - - foreach ( $lines as $line ) { - $row_number++; - if ( empty( trim( $line ) ) ) { - continue; // Skip empty lines - } - $data = str_getcsv( $line, $delimiter ); - - if ( count( $data ) !== count( $header ) ) { - $skipped_details[] = array( - 'row' => $row_number, - 'reason' => 'Nieprawidłowa liczba kolumn (oczekiwano ' . count( $header ) . ', otrzymano ' . count( $data ) . ').', - 'data' => $line, - ); - continue; - } - $row_data = array_combine( $header, $data ); - - // Detailed validation - $validation_errors = array(); - if ( empty( $row_data['date'] ) ) { - $validation_errors[] = 'brak daty'; - } - if ( empty( $row_data['title'] ) ) { - $validation_errors[] = 'brak tytułu'; - } - if ( ! isset( $row_data['distance'] ) || '' === $row_data['distance'] ) { - $validation_errors[] = 'brak dystansu'; - } - - $category_name = $row_data['category_name'] ?? ''; - $clean_category_name = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $category_name ); - $category_id = $categories_lookup[ mb_strtolower( $clean_category_name, 'UTF-8' ) ] ?? null; - - if ( empty( $clean_category_name ) ) { - $validation_errors[] = 'brak nazwy kategorii'; - } elseif ( is_null( $category_id ) ) { - $available_categories_from_db = $wpdb->get_col( "SELECT name FROM $table_categories ORDER BY name" ); - $validation_errors[] = 'nieznana kategoria: "' . esc_html( $category_name ) . '". Sprawdź, czy nazwa jest poprawna. Dostępne w bazie: "' . esc_html( implode( '", "', $available_categories_from_db ) ) . '".'; - } - - if ( ! empty( $validation_errors ) ) { - $skipped_details[] = array( - 'row' => $row_number, - 'reason' => ucfirst( implode( ', ', $validation_errors ) ) . '.', - 'data' => $line, - ); - continue; - } - - // Get IDs for optional fields using the same robust method - $get_id = function( $name, $lookup_table ) { - $clean_name = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $name ); - return $lookup_table[ mb_strtolower( $clean_name, 'UTF-8' ) ] ?? null; - }; - - $equipment_id = $get_id( $row_data['equipment_name'] ?? '', $equipment_lookup ); - $event_type_id = $get_id( $row_data['event_type_name'] ?? '', $event_types_lookup ); - - $insert_data = array( - 'date' => sanitize_text_field( $row_data['date'] ), - 'title' => sanitize_text_field( $row_data['title'] ), - 'category_id' => $category_id, - 'distance' => floatval( str_replace( ',', '.', $row_data['distance'] ) ), - 'duration' => isset( $row_data['duration'] ) ? sanitize_text_field( $row_data['duration'] ) : '00:00:00', - 'calories' => isset( $row_data['calories'] ) ? intval( str_replace( ',', '.', $row_data['calories'] ) ) : 0, - 'comment' => isset( $row_data['comment'] ) ? sanitize_textarea_field( $row_data['comment'] ) : null, - 'strava_url' => isset( $row_data['strava_url'] ) ? $null_if_empty( esc_url_raw( $row_data['strava_url'] ) ) : null, - 'avg_heart_rate' => isset( $row_data['avg_heart_rate'] ) ? $null_if_empty( $parse_and_round_int( $row_data['avg_heart_rate'] ) ) : null, - 'max_heart_rate' => isset( $row_data['max_heart_rate'] ) ? $null_if_empty( $parse_and_round_int( $row_data['max_heart_rate'] ) ) : null, - 'avg_speed' => isset( $row_data['avg_speed'] ) ? $null_if_empty( floatval( str_replace( ',', '.', $row_data['avg_speed'] ) ) ) : null, - 'max_speed' => isset( $row_data['max_speed'] ) ? $null_if_empty( floatval( str_replace( ',', '.', $row_data['max_speed'] ) ) ) : null, - 'avg_cadence' => isset( $row_data['avg_cadence'] ) ? $null_if_empty( $parse_and_round_int( $row_data['avg_cadence'] ) ) : null, - 'max_cadence' => isset( $row_data['max_cadence'] ) ? $null_if_empty( $parse_and_round_int( $row_data['max_cadence'] ) ) : null, - 'total_elevation_gain' => isset( $row_data['total_elevation_gain'] ) ? $null_if_empty( $parse_and_round_int( $row_data['total_elevation_gain'] ) ) : null, - 'total_elevation_loss' => isset( $row_data['total_elevation_loss'] ) ? $null_if_empty( $parse_and_round_int( $row_data['total_elevation_loss'] ) ) : null, - 'min_altitude' => isset( $row_data['min_altitude'] ) ? $null_if_empty( $parse_and_round_int( $row_data['min_altitude'] ) ) : null, - 'max_altitude' => isset( $row_data['max_altitude'] ) ? $null_if_empty( $parse_and_round_int( $row_data['max_altitude'] ) ) : null, - 'equipment_id' => $equipment_id, - 'event_type_id' => $event_type_id, - ); - - if ( $wpdb->insert( $table_activities, $insert_data ) ) { - $imported_count++; - } else { - $skipped_details[] = array( - 'row' => $row_number, - 'reason' => 'Błąd zapisu do bazy danych. (' . esc_html( $wpdb->last_error ) . ')', - 'data' => $line, - ); - } - } - - if ( $imported_count > 0 ) { - echo '

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

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

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

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

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

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

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

';} +

Importuj aktywności z pliku CSV

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

Instrukcje i formularz importu

+
+

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

+

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

+

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

+

Ważne: +

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

+
+
+ + + + + + + + + + +
+ +
+
+
+ '; +} + +function statpress_handle_csv_import() { + global $wpdb; + + if ( ! isset( $_POST['statpress_csv_import_nonce_field'] ) || ! wp_verify_nonce( $_POST['statpress_csv_import_nonce_field'], 'statpress_csv_import_nonce' ) ) { + echo '

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['statpress_csv_data'] ) ) { + $csv_content = stripslashes( $_POST['statpress_csv_data'] ); + } elseif ( ! empty( $_FILES['statpress_csv_file']['tmp_name'] ) && UPLOAD_ERR_OK === $_FILES['statpress_csv_file']['error'] ) { + $csv_content = file_get_contents( $_FILES['statpress_csv_file']['tmp_name'] ); + } + + if ( empty( trim( $csv_content ) ) ) { + echo '

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 . 'statpress_categories'; + $table_event_types = $wpdb->prefix . 'statpress_event_types'; + $table_equipment = $wpdb->prefix . 'statpress_equipment'; + + $create_lookup = function( $table_name ) use ( $wpdb ) { + $items = $wpdb->get_results( "SELECT id, name FROM {$table_name}" ); + $lookup = array(); + if ( is_array( $items ) ) { + foreach ( $items as $item ) { + // Use a robust trim to handle various whitespace characters and make it case-insensitive + $clean_name = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $item->name ); + $lookup[ mb_strtolower( $clean_name, 'UTF-8' ) ] = $item->id; + } + } + return $lookup; + }; + + $categories_lookup = $create_lookup( $table_categories ); + $event_types_lookup = $create_lookup( $table_event_types ); + $equipment_lookup = $create_lookup( $table_equipment ); + // --- END: Robust, case-insensitive lookup --- + + // Process the CSV file + $table_activities = $wpdb->prefix . 'statpress_activities'; + $imported_count = 0; + $skipped_details = array(); + $row_number = 1; // Header is row 1 + + // Normalize line endings and split into lines + $lines = str_replace( array( "\r\n", "\r" ), "\n", $csv_content ); + $lines = explode( "\n", $lines ); + + if ( empty( $lines ) || empty( trim( $lines[0] ) ) ) { + echo '

Podane dane CSV są puste.

'; + return; + } + + // --- Delimiter and BOM detection --- + $first_line = $lines[0]; + $delimiter = ( substr_count( $first_line, ';' ) > substr_count( $first_line, ',' ) ) ? ';' : ','; + + // --- BOM removal from first header element --- + $bom = "\xEF\xBB\xBF"; + if ( substr( $first_line, 0, 3 ) === $bom ) { + $lines[0] = substr( $first_line, 3 ); + } + + $header_raw = str_getcsv( array_shift( $lines ), $delimiter ); + $header_raw = array_map( 'trim', $header_raw ); + + // Translate header from Polish/English to internal keys + $header = array(); + foreach ( $header_raw as $col ) { + $header[] = $column_map[ strtolower( $col ) ] ?? 'ignored_' . uniqid(); + } + + $required_internal_keys = array( 'date', 'title', 'category_name', 'distance' ); + $missing_keys = array_diff( $required_internal_keys, $header ); + if ( ! empty( $missing_keys ) ) { + $key_to_polish_map = array( + 'date' => 'Data', + 'title' => 'Tytuł', + 'category_name' => 'Kategoria / Typ aktywności', + 'distance' => 'Dystans', + ); + $missing_polish_names = array_map( fn( $key ) => $key_to_polish_map[ $key ] ?? $key, $missing_keys ); + echo '

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

'; + return; + } + + $parse_and_round_int = fn( $val ) => round( floatval( str_replace( ',', '.', $val ) ) ); + $null_if_empty = fn( $value ) => '' !== $value ? $value : null; + + foreach ( $lines as $line ) { + $row_number++; + if ( empty( trim( $line ) ) ) { + continue; // Skip empty lines + } + $data = str_getcsv( $line, $delimiter ); + + if ( count( $data ) !== count( $header ) ) { + $skipped_details[] = array( + 'row' => $row_number, + 'reason' => 'Nieprawidłowa liczba kolumn (oczekiwano ' . count( $header ) . ', otrzymano ' . count( $data ) . ').', + 'data' => $line, + ); + continue; + } + $row_data = array_combine( $header, $data ); + + // Detailed validation + $validation_errors = array(); + if ( empty( $row_data['date'] ) ) { + $validation_errors[] = 'brak daty'; + } + if ( empty( $row_data['title'] ) ) { + $validation_errors[] = 'brak tytułu'; + } + if ( ! isset( $row_data['distance'] ) || '' === $row_data['distance'] ) { + $validation_errors[] = 'brak dystansu'; + } + + $category_name = $row_data['category_name'] ?? ''; + $clean_category_name = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $category_name ); + $category_id = $categories_lookup[ mb_strtolower( $clean_category_name, 'UTF-8' ) ] ?? null; + + if ( empty( $clean_category_name ) ) { + $validation_errors[] = 'brak nazwy kategorii'; + } elseif ( is_null( $category_id ) ) { + $available_categories_from_db = $wpdb->get_col( "SELECT name FROM $table_categories ORDER BY name" ); + $validation_errors[] = 'nieznana kategoria: "' . esc_html( $category_name ) . '". Sprawdź, czy nazwa jest poprawna. Dostępne w bazie: "' . esc_html( implode( '", "', $available_categories_from_db ) ) . '".'; + } + + if ( ! empty( $validation_errors ) ) { + $skipped_details[] = array( + 'row' => $row_number, + 'reason' => ucfirst( implode( ', ', $validation_errors ) ) . '.', + 'data' => $line, + ); + continue; + } + + // Get IDs for optional fields using the same robust method + $get_id = function( $name, $lookup_table ) { + $clean_name = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $name ); + return $lookup_table[ mb_strtolower( $clean_name, 'UTF-8' ) ] ?? null; + }; + + $equipment_id = $get_id( $row_data['equipment_name'] ?? '', $equipment_lookup ); + $event_type_id = $get_id( $row_data['event_type_name'] ?? '', $event_types_lookup ); + + $insert_data = array( + 'date' => sanitize_text_field( $row_data['date'] ), + 'title' => sanitize_text_field( $row_data['title'] ), + 'category_id' => $category_id, + 'distance' => floatval( str_replace( ',', '.', $row_data['distance'] ) ), + 'duration' => isset( $row_data['duration'] ) ? sanitize_text_field( $row_data['duration'] ) : '00:00:00', + 'calories' => isset( $row_data['calories'] ) ? intval( str_replace( ',', '.', $row_data['calories'] ) ) : 0, + 'comment' => isset( $row_data['comment'] ) ? sanitize_textarea_field( $row_data['comment'] ) : null, + 'strava_url' => isset( $row_data['strava_url'] ) ? $null_if_empty( esc_url_raw( $row_data['strava_url'] ) ) : null, + 'avg_heart_rate' => isset( $row_data['avg_heart_rate'] ) ? $null_if_empty( $parse_and_round_int( $row_data['avg_heart_rate'] ) ) : null, + 'max_heart_rate' => isset( $row_data['max_heart_rate'] ) ? $null_if_empty( $parse_and_round_int( $row_data['max_heart_rate'] ) ) : null, + 'avg_speed' => isset( $row_data['avg_speed'] ) ? $null_if_empty( floatval( str_replace( ',', '.', $row_data['avg_speed'] ) ) ) : null, + 'max_speed' => isset( $row_data['max_speed'] ) ? $null_if_empty( floatval( str_replace( ',', '.', $row_data['max_speed'] ) ) ) : null, + 'avg_cadence' => isset( $row_data['avg_cadence'] ) ? $null_if_empty( $parse_and_round_int( $row_data['avg_cadence'] ) ) : null, + 'max_cadence' => isset( $row_data['max_cadence'] ) ? $null_if_empty( $parse_and_round_int( $row_data['max_cadence'] ) ) : null, + 'total_elevation_gain' => isset( $row_data['total_elevation_gain'] ) ? $null_if_empty( $parse_and_round_int( $row_data['total_elevation_gain'] ) ) : null, + 'total_elevation_loss' => isset( $row_data['total_elevation_loss'] ) ? $null_if_empty( $parse_and_round_int( $row_data['total_elevation_loss'] ) ) : null, + 'min_altitude' => isset( $row_data['min_altitude'] ) ? $null_if_empty( $parse_and_round_int( $row_data['min_altitude'] ) ) : null, + 'max_altitude' => isset( $row_data['max_altitude'] ) ? $null_if_empty( $parse_and_round_int( $row_data['max_altitude'] ) ) : null, + 'equipment_id' => $equipment_id, + 'event_type_id' => $event_type_id, + ); + + if ( $wpdb->insert( $table_activities, $insert_data ) ) { + $imported_count++; + } else { + $skipped_details[] = array( + 'row' => $row_number, + 'reason' => 'Błąd zapisu do bazy danych. (' . esc_html( $wpdb->last_error ) . ')', + 'data' => $line, + ); + } + } + + if ( $imported_count > 0 ) { + echo '

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

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

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

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

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

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

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

';} } \ No newline at end of file diff --git a/includes/admin/pages/page-infographic.php b/includes/admin/pages/page-infographic.php index 09e0003..bad342f 100644 --- a/includes/admin/pages/page-infographic.php +++ b/includes/admin/pages/page-infographic.php @@ -1,159 +1,159 @@ -prefix . 'statpress_activities'; - $table_categories = $wpdb->prefix . 'statpress_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( 'statpress-infographic-chart-loader', false ); - wp_enqueue_script( 'statpress-infographic-chart-loader' ); - wp_add_inline_script( - 'statpress-infographic-chart-loader', - ' - document.addEventListener("DOMContentLoaded", function() { - const ctx = document.getElementById("statpressCategoryPieChart"); - if (!ctx) return; - new Chart(ctx, { - type: "pie", - data: { - labels: ' . json_encode( $chart_labels ) . ', - datasets: [{ - data: ' . json_encode( $chart_data ) . ', - backgroundColor: ' . json_encode( $chart_colors ) . ', - hoverOffset: 4 - }] - }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - position: "right", - }, - title: { - display: true, - text: "Dystans wg kategorii w ' . esc_js( $current_year ) . '" - } - } - } - }); - }); - ' - ); - - ?> -
-

Infografika Statystyk Sportowych

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

Statystyki Ogólne (wszystkie lata)

-
-

Dystans

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

-

Czas

total_duration ); ?>

-

Wznios

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

-

Aktywności

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

-
-
- -
-

Statystyki dla

-
-

Dystans

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

-

Czas

total_duration ); ?>

-

Wznios

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

-

Aktywności

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

-
-
- -
-

Rozkład Dystansu wg Kategorii w

-
-
- -
-
-
-
- prefix . 'statpress_activities'; + $table_categories = $wpdb->prefix . 'statpress_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( 'statpress-infographic-chart-loader', false ); + wp_enqueue_script( 'statpress-infographic-chart-loader' ); + wp_add_inline_script( + 'statpress-infographic-chart-loader', + ' + document.addEventListener("DOMContentLoaded", function() { + const ctx = document.getElementById("statpressCategoryPieChart"); + if (!ctx) return; + new Chart(ctx, { + type: "pie", + data: { + labels: ' . json_encode( $chart_labels ) . ', + datasets: [{ + data: ' . json_encode( $chart_data ) . ', + backgroundColor: ' . json_encode( $chart_colors ) . ', + hoverOffset: 4 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: "right", + }, + title: { + display: true, + text: "Dystans wg kategorii w ' . esc_js( $current_year ) . '" + } + } + } + }); + }); + ' + ); + + ?> +
+

Infografika Statystyk Sportowych

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

Statystyki Ogólne (wszystkie lata)

+
+

Dystans

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

+

Czas

total_duration ); ?>

+

Wznios

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

+

Aktywności

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

+
+
+ +
+

Statystyki dla

+
+

Dystans

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

+

Czas

total_duration ); ?>

+

Wznios

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

+

Aktywności

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

+
+
+ +
+

Rozkład Dystansu wg Kategorii w

+
+
+ +
+
+
+
+ -
-

Ustawienia Wtyczki Statystyk

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

'; - echo '

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

'; -} - -function statpress_render_lat_field() { - $options = get_option( 'statpress_privacy_options' ); - $latitude = isset( $options['latitude'] ) ? esc_attr( $options['latitude'] ) : ''; - echo ""; -} - -function statpress_render_lon_field() { - $options = get_option( 'statpress_privacy_options' ); - $longitude = isset( $options['longitude'] ) ? esc_attr( $options['longitude'] ) : ''; - echo ""; -} - -function statpress_render_radius_field() { - $options = get_option( 'statpress_privacy_options' ); - $radius = isset( $options['radius'] ) ? esc_attr( $options['radius'] ) : '500'; - echo " metrów"; -} - -function statpress_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; -} - -function statpress_api_section_callback() { - echo '

Ustawienia związane z integracją wtyczki z zewnętrznymi aplikacjami, np. mobilnymi.

'; -} - -function statpress_render_enable_api_field() { - $options = get_option( 'statpress_api_options' ); - $checked = isset( $options['enable_api'] ) && $options['enable_api'] ? 'checked' : ''; - echo ""; - echo '

Umożliwia zewnętrznym aplikacjom (np. na Androida) komunikację z wtyczką w celu dodawania i odczytywania aktywności. Jeśli nie korzystasz z takich integracji, możesz to wyłączyć dla większego bezpieczeństwa.

'; -} - -function statpress_sanitize_api_options( $input ) { - $sanitized_input = array(); - // If the checkbox is not checked, it won't be in the $input array. - // So we check for its existence to determine if it's on or off. - $sanitized_input['enable_api'] = isset( $input['enable_api'] ) ? 1 : 0; - return $sanitized_input; + +
+

Status migracji został zresetowany. Wróć do głównego panelu, aby ponownie uruchomić migrację.

'; + delete_transient( 'statpress_migration_reset_notice' ); + } + ?> +

Ustawienia Wtyczki Statystyk

+
+ +
+ +
+

Narzędzia deweloperskie

+
+

Resetowanie migracji

+
+

Jeśli migracja danych nie powiodła się, a przycisk do jej ponownego uruchomienia zniknął, możesz użyć tego narzędzia, aby zresetować status migracji. Po kliknięciu, przycisk w głównym panelu wtyczki pojawi się ponownie.

+
+
+
+
+ + + 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 statpress_render_lat_field() { + $options = get_option( 'statpress_privacy_options' ); + $latitude = isset( $options['latitude'] ) ? esc_attr( $options['latitude'] ) : ''; + echo ""; +} + +function statpress_render_lon_field() { + $options = get_option( 'statpress_privacy_options' ); + $longitude = isset( $options['longitude'] ) ? esc_attr( $options['longitude'] ) : ''; + echo ""; +} + +function statpress_render_radius_field() { + $options = get_option( 'statpress_privacy_options' ); + $radius = isset( $options['radius'] ) ? esc_attr( $options['radius'] ) : '500'; + echo " metrów"; +} + +function statpress_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; +} + +function statpress_api_section_callback() { + echo '

Ustawienia związane z integracją wtyczki z zewnętrznymi aplikacjami, np. mobilnymi.

'; +} + +function statpress_render_enable_api_field() { + $options = get_option( 'statpress_api_options' ); + $checked = isset( $options['enable_api'] ) && $options['enable_api'] ? 'checked' : ''; + echo ""; + echo '

Umożliwia zewnętrznym aplikacjom (np. na Androida) komunikację z wtyczką w celu dodawania i odczytywania aktywności. Jeśli nie korzystasz z takich integracji, możesz to wyłączyć dla większego bezpieczeństwa.

'; +} + +function statpress_sanitize_api_options( $input ) { + $sanitized_input = array(); + // If the checkbox is not checked, it won't be in the $input array. + // So we check for its existence to determine if it's on or off. + $sanitized_input['enable_api'] = isset( $input['enable_api'] ) ? 1 : 0; + 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 index 9d97e43..5cc66fc 100644 --- a/includes/admin/pages/page-yearly-summary.php +++ b/includes/admin/pages/page-yearly-summary.php @@ -1,292 +1,292 @@ -prefix . 'statpress_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 . 'statpress_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( 'statpress-chart-loader', false ); - wp_enqueue_script( 'statpress-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( - 'statpress-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("statpressYearlyChart").getContext("2d"); - activeChart = new Chart(ctx, { - type: "bar", - data: { - labels: chartLabels, - datasets: [{ - label: config.label, - data: config.data, - backgroundColor: config.backgroundColor, - borderColor: config.borderColor, - borderWidth: 1 - }] - }, - options: { - responsive: true, maintainAspectRatio: false, - scales: { y: { beginAtZero: true, title: { display: true, text: config.yAxisLabel } } } - } - }); - } - - // Render initial chart - if (tabs.length > 0) { - renderChart(tabs[0].getAttribute("href").substring(1)); - } - }); - ' - ); - - ?> -
-

Podsumowanie Roczne

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

Cele na

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

Wykresy dla

-
- -
- -
-
-
- - - - - - - - - - - - - - - - - - - - - -
MiesiącDystans (km)Kalorie (kcal)Czas
month_name ); ?>total_distance, 2, ',', ' ' ); ?>total_calories, 0, ',', ' ' ); ?>total_seconds ) ); ?>
SUMA ROCZNA
-
- prefix . 'statpress_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 . 'statpress_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( 'statpress-chart-loader', false ); + wp_enqueue_script( 'statpress-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( + 'statpress-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("statpressYearlyChart").getContext("2d"); + activeChart = new Chart(ctx, { + type: "bar", + data: { + labels: chartLabels, + datasets: [{ + label: config.label, + data: config.data, + backgroundColor: config.backgroundColor, + borderColor: config.borderColor, + borderWidth: 1 + }] + }, + options: { + responsive: true, maintainAspectRatio: false, + scales: { y: { beginAtZero: true, title: { display: true, text: config.yAxisLabel } } } + } + }); + } + + // Render initial chart + if (tabs.length > 0) { + renderChart(tabs[0].getAttribute("href").substring(1)); + } + }); + ' + ); + + ?> +
+

Podsumowanie Roczne

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

Cele na

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

Wykresy dla

+
+ +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + +
MiesiącDystans (km)Kalorie (kcal)Czas
month_name ); ?>total_distance, 2, ',', ' ' ); ?>total_calories, 0, ',', ' ' ); ?>total_seconds ) ); ?>
SUMA ROCZNA
+
+ WP_REST_Server::READABLE, - 'callback' => 'statpress_get_activities_api', - 'permission_callback' => 'statpress_api_permissions_check', - 'args' => array( - 'page' => array( - 'validate_callback' => 'is_numeric', - ), - 'per_page' => array( - 'validate_callback' => 'is_numeric', - ), - ), - ), - array( - 'methods' => WP_REST_Server::CREATABLE, - 'callback' => 'statpress_create_activity_api', - 'permission_callback' => 'statpress_api_permissions_check', - ), - ) - ); - - // Route for a single activity - register_rest_route( - $namespace, - '/activities/(?P[\d]+)', - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => 'statpress_get_activity_api', - 'permission_callback' => 'statpress_api_permissions_check', - ), - array( - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => 'statpress_update_activity_api', - 'permission_callback' => 'statpress_api_permissions_check', - ), - array( - 'methods' => WP_REST_Server::DELETABLE, - 'callback' => 'statpress_delete_activity_api', - 'permission_callback' => 'statpress_api_permissions_check', - ), - ) - ); -} - -/** - * Permission check for API endpoints. - * - * @return bool - */ -function statpress_api_permissions_check() { - return current_user_can( 'manage_options' ); -} - -/** - * Get a collection of activities. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_REST_Response|WP_Error - */ -function statpress_get_activities_api( WP_REST_Request $request ) { - global $wpdb; - $table_activities = $wpdb->prefix . 'statpress_activities'; - - $per_page = $request->get_param( 'per_page' ) ? (int) $request->get_param( 'per_page' ) : 20; - $page = $request->get_param( 'page' ) ? (int) $request->get_param( 'page' ) : 1; - $offset = ( $page - 1 ) * $per_page; - - $sql = $wpdb->prepare( - "SELECT a.*, c.name as category_name, et.name as event_type_name, eq.name as equipment_name - FROM $table_activities a - LEFT JOIN {$wpdb->prefix}statpress_categories c ON a.category_id = c.id - LEFT JOIN {$wpdb->prefix}statpress_event_types et ON a.event_type_id = et.id - LEFT JOIN {$wpdb->prefix}statpress_equipment eq ON a.equipment_id = eq.id - ORDER BY a.date DESC, a.id DESC - LIMIT %d OFFSET %d", - $per_page, - $offset - ); - - $results = $wpdb->get_results( $sql ); - - return new WP_REST_Response( $results, 200 ); -} - -/** - * Get a single activity. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_REST_Response|WP_Error - */ -function statpress_get_activity_api( WP_REST_Request $request ) { - global $wpdb; - $id = (int) $request['id']; - - $sql = $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}statpress_activities WHERE id = %d", $id ); - $activity = $wpdb->get_row( $sql ); - - if ( ! $activity ) { - return new WP_Error( 'not_found', 'Activity not found', array( 'status' => 404 ) ); - } - - return new WP_REST_Response( $activity, 200 ); -} - -/** - * Create a new activity. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_REST_Response|WP_Error - */ -function statpress_create_activity_api( WP_REST_Request $request ) { - $params = $request->get_json_params(); - $activity_id = statpress_save_activity_data( $params ); - - if ( ! $activity_id ) { - return new WP_Error( 'cant-create', 'Error creating activity', array( 'status' => 500 ) ); - } - - $response = statpress_get_activity_api( new WP_REST_Request( 'GET', "/statpress/v1/activities/{$activity_id}" ) ); - $response->set_status( 201 ); // 201 Created - return $response; -} - -/** - * Update an existing activity. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_REST_Response|WP_Error - */ -function statpress_update_activity_api( WP_REST_Request $request ) { - $id = (int) $request['id']; - $params = $request->get_json_params(); - $activity_id = statpress_save_activity_data( $params, $id ); - - if ( ! $activity_id ) { - return new WP_Error( 'cant-update', 'Error updating activity', array( 'status' => 500 ) ); - } - - return statpress_get_activity_api( $request ); -} - -/** - * Delete an activity. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_REST_Response|WP_Error - */ -function statpress_delete_activity_api( WP_REST_Request $request ) { - global $wpdb; - $id = (int) $request['id']; - - $result = $wpdb->delete( $wpdb->prefix . 'statpress_activities', array( 'id' => $id ), array( '%d' ) ); - - if ( ! $result ) { - return new WP_Error( 'cant-delete', 'Error deleting activity', array( 'status' => 500 ) ); - } - - return new WP_REST_Response( array( 'message' => 'Activity deleted successfully.' ), 200 ); + WP_REST_Server::READABLE, + 'callback' => 'statpress_get_activities_api', + 'permission_callback' => 'statpress_api_permissions_check', + 'args' => array( + 'page' => array( + 'validate_callback' => 'is_numeric', + ), + 'per_page' => array( + 'validate_callback' => 'is_numeric', + ), + ), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => 'statpress_create_activity_api', + 'permission_callback' => 'statpress_api_permissions_check', + ), + ) + ); + + // Route for a single activity + register_rest_route( + $namespace, + '/activities/(?P[\d]+)', + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => 'statpress_get_activity_api', + 'permission_callback' => 'statpress_api_permissions_check', + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => 'statpress_update_activity_api', + 'permission_callback' => 'statpress_api_permissions_check', + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => 'statpress_delete_activity_api', + 'permission_callback' => 'statpress_api_permissions_check', + ), + ) + ); + + // Route for parsing GPX file summary + register_rest_route( + $namespace, + '/gpx/parse-summary', + array( + 'methods' => WP_REST_Server::CREATABLE, // Use POST to send URL in the body + 'callback' => 'statpress_parse_gpx_summary_api', + 'permission_callback' => 'statpress_api_permissions_check', + 'args' => array( + 'gpx_url' => array( + 'required' => true, + 'validate_callback' => 'esc_url_raw', + ), + ), + ) + ); +} + +/** + * Permission check for API endpoints. + * + * @return bool + */ +function statpress_api_permissions_check() { + return current_user_can( 'manage_options' ); +} + +/** + * Get a collection of activities. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ +function statpress_get_activities_api( WP_REST_Request $request ) { + global $wpdb; + $table_activities = $wpdb->prefix . 'statpress_activities'; + + $per_page = $request->get_param( 'per_page' ) ? (int) $request->get_param( 'per_page' ) : 20; + $page = $request->get_param( 'page' ) ? (int) $request->get_param( 'page' ) : 1; + $offset = ( $page - 1 ) * $per_page; + + // Get total items for pagination headers + $total_items = (int) $wpdb->get_var( "SELECT COUNT(id) FROM $table_activities" ); + $total_pages = ceil( $total_items / $per_page ); + + $sql = $wpdb->prepare( + "SELECT a.*, c.name as category_name, et.name as event_type_name, eq.name as equipment_name + FROM $table_activities a + LEFT JOIN {$wpdb->prefix}statpress_categories c ON a.category_id = c.id + LEFT JOIN {$wpdb->prefix}statpress_event_types et ON a.event_type_id = et.id + LEFT JOIN {$wpdb->prefix}statpress_equipment eq ON a.equipment_id = eq.id + ORDER BY a.date DESC, a.id DESC + LIMIT %d OFFSET %d", + $per_page, + $offset + ); + + $results = $wpdb->get_results( $sql ); + + $response = new WP_REST_Response( $results, 200 ); + $response->header( 'X-WP-Total', $total_items ); + $response->header( 'X-WP-TotalPages', $total_pages ); + + return $response; +} + +/** + * Get a single activity. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ +function statpress_get_activity_api( WP_REST_Request $request ) { + global $wpdb; + $id = (int) $request['id']; + + // Use the same rich query as the collection endpoint for consistency. + $sql = $wpdb->prepare( + "SELECT a.*, c.name as category_name, et.name as event_type_name, eq.name as equipment_name + FROM {$wpdb->prefix}statpress_activities a + LEFT JOIN {$wpdb->prefix}statpress_categories c ON a.category_id = c.id + LEFT JOIN {$wpdb->prefix}statpress_event_types et ON a.event_type_id = et.id + LEFT JOIN {$wpdb->prefix}statpress_equipment eq ON a.equipment_id = eq.id + WHERE a.id = %d", + $id + ); + $activity = $wpdb->get_row( $sql ); + + if ( ! $activity ) { + return new WP_Error( 'not_found', 'Activity not found', array( 'status' => 404 ) ); + } + + return new WP_REST_Response( $activity, 200 ); +} + +/** + * Create a new activity. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ +function statpress_create_activity_api( WP_REST_Request $request ) { + $params = $request->get_json_params(); + $activity_id = statpress_save_activity_data( $params ); + + if ( ! $activity_id ) { + return new WP_Error( 'cant-create', 'Error creating activity', array( 'status' => 500 ) ); + } + + // Create a new request object to fetch the newly created activity. + $new_request = new WP_REST_Request(); + $new_request->set_param( 'id', $activity_id ); + $response = statpress_get_activity_api( $new_request ); + $response->set_status( 201 ); // 201 Created + return $response; +} + +/** + * Update an existing activity. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ +function statpress_update_activity_api( WP_REST_Request $request ) { + $id = (int) $request['id']; + $params = $request->get_json_params(); + $activity_id = statpress_save_activity_data( $params, $id ); + + if ( ! $activity_id ) { + return new WP_Error( 'cant-update', 'Error updating activity', array( 'status' => 500 ) ); + } + + return statpress_get_activity_api( $request ); +} + +/** + * Delete an activity. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ +function statpress_delete_activity_api( WP_REST_Request $request ) { + global $wpdb; + $id = (int) $request['id']; + + $result = $wpdb->delete( $wpdb->prefix . 'statpress_activities', array( 'id' => $id ), array( '%d' ) ); + + if ( ! $result ) { + return new WP_Error( 'cant-delete', 'Error deleting activity', array( 'status' => 500 ) ); + } + + return new WP_REST_Response( array( 'message' => 'Activity deleted successfully.' ), 200 ); +} + +/** + * API callback to parse a GPX file and return its summary data. + * + * @param WP_REST_Request $request The request object. + * @return WP_REST_Response|WP_Error A response object with summary data or an error. + */ +function statpress_parse_gpx_summary_api( WP_REST_Request $request ) { + $params = $request->get_json_params(); + $gpx_url = $params['gpx_url'] ?? ''; + + if ( empty( $gpx_url ) ) { + return new WP_Error( 'no_url_provided', 'Nie podano adresu URL do pliku GPX.', array( 'status' => 400 ) ); + } + + // This new function will do the heavy lifting of calculation. + $summary = statpress_calculate_gpx_summary( $gpx_url ); + + if ( empty( $summary ) || ! $summary['distance'] > 0 ) { + return new WP_Error( 'gpx_parse_error', 'Nie udało się przetworzyć pliku GPX. Sprawdź, czy plik jest poprawny i zawiera dane trasy.', array( 'status' => 400 ) ); + } + + return new WP_REST_Response( $summary, 200 ); } \ No newline at end of file diff --git a/includes/core/crud-activity.php b/includes/core/crud-activity.php index 4b603e5..70c8415 100644 --- a/includes/core/crud-activity.php +++ b/includes/core/crud-activity.php @@ -1,95 +1,95 @@ -prefix . 'statpress_activities'; - - // Helper to convert empty strings to NULL for the database. - $null_if_empty = function( $value ) { - // Also check for 0 as we don't want to nullify it for numeric fields. - return ( '' !== $value && ! is_null( $value ) ) ? $value : null; - }; - - // Sanitize and prepare data. - $prepared_data = array( - 'category_id' => isset( $data['category_id'] ) ? intval( $data['category_id'] ) : null, - 'date' => isset( $data['date'] ) ? sanitize_text_field( $data['date'] ) : current_time( 'Y-m-d' ), - 'title' => isset( $data['title'] ) ? sanitize_text_field( $data['title'] ) : '', - 'distance' => isset( $data['distance'] ) ? floatval( str_replace( ',', '.', $data['distance'] ) ) : 0, - 'duration' => isset( $data['duration'] ) ? sanitize_text_field( $data['duration'] ) : '00:00:00', - 'calories' => isset( $data['calories'] ) ? intval( $data['calories'] ) : null, - 'comment' => isset( $data['comment'] ) ? sanitize_textarea_field( $data['comment'] ) : null, - 'strava_url' => isset( $data['strava_url'] ) ? $null_if_empty( esc_url_raw( $data['strava_url'] ) ) : null, - 'avg_heart_rate' => isset( $data['avg_heart_rate'] ) ? $null_if_empty( intval( $data['avg_heart_rate'] ) ) : null, - 'max_heart_rate' => isset( $data['max_heart_rate'] ) ? $null_if_empty( intval( $data['max_heart_rate'] ) ) : null, - 'avg_speed' => isset( $data['avg_speed'] ) ? $null_if_empty( floatval( str_replace( ',', '.', $data['avg_speed'] ) ) ) : null, - 'max_speed' => isset( $data['max_speed'] ) ? $null_if_empty( floatval( str_replace( ',', '.', $data['max_speed'] ) ) ) : null, - 'avg_cadence' => isset( $data['avg_cadence'] ) ? $null_if_empty( intval( $data['avg_cadence'] ) ) : null, - 'max_cadence' => isset( $data['max_cadence'] ) ? $null_if_empty( intval( $data['max_cadence'] ) ) : null, - 'total_elevation_gain' => isset( $data['total_elevation_gain'] ) ? $null_if_empty( intval( $data['total_elevation_gain'] ) ) : null, - 'total_elevation_loss' => isset( $data['total_elevation_loss'] ) ? $null_if_empty( intval( $data['total_elevation_loss'] ) ) : null, - 'min_altitude' => isset( $data['min_altitude'] ) ? $null_if_empty( intval( $data['min_altitude'] ) ) : null, - 'max_altitude' => isset( $data['max_altitude'] ) ? $null_if_empty( intval( $data['max_altitude'] ) ) : null, - 'equipment_id' => isset( $data['equipment_id'] ) ? $null_if_empty( intval( $data['equipment_id'] ) ) : null, - 'gpx_url' => isset( $data['gpx_url'] ) ? $null_if_empty( esc_url_raw( $data['gpx_url'] ) ) : null, - 'event_type_id' => isset( $data['event_type_id'] ) ? $null_if_empty( intval( $data['event_type_id'] ) ) : null, - ); - - // Data formats for $wpdb. - $format = array( - '%d', // category_id - '%s', // date - '%s', // title - '%f', // distance - '%s', // duration - '%d', // calories - '%s', // comment - '%s', // strava_url - '%d', // avg_heart_rate - '%d', // max_heart_rate - '%f', // avg_speed - '%f', // max_speed - '%d', // avg_cadence - '%d', // max_cadence - '%d', // total_elevation_gain - '%d', // total_elevation_loss - '%d', // min_altitude - '%d', // max_altitude - '%d', // equipment_id - '%s', // gpx_url - '%d', // event_type_id - ); - - if ( $activity_id > 0 ) { - // UPDATE - $result = $wpdb->update( $table_activities, $prepared_data, array( 'id' => $activity_id ), $format, array( '%d' ) ); - if ( false !== $result ) { - return $activity_id; - } - } else { - // INSERT - $result = $wpdb->insert( $table_activities, $prepared_data, $format ); - if ( $result ) { - return $wpdb->insert_id; - } - } - - return false; +prefix . 'statpress_activities'; + + // Helper to convert empty strings to NULL for the database. + $null_if_empty = function( $value ) { + // Also check for 0 as we don't want to nullify it for numeric fields. + return ( '' !== $value && ! is_null( $value ) ) ? $value : null; + }; + + // Sanitize and prepare data. + $prepared_data = array( + 'category_id' => isset( $data['category_id'] ) ? intval( $data['category_id'] ) : null, + 'date' => isset( $data['date'] ) ? sanitize_text_field( $data['date'] ) : current_time( 'Y-m-d' ), + 'title' => isset( $data['title'] ) ? sanitize_text_field( $data['title'] ) : '', + 'distance' => isset( $data['distance'] ) ? floatval( str_replace( ',', '.', $data['distance'] ) ) : 0, + 'duration' => isset( $data['duration'] ) ? sanitize_text_field( $data['duration'] ) : '00:00:00', + 'calories' => isset( $data['calories'] ) ? intval( $data['calories'] ) : null, + 'comment' => isset( $data['comment'] ) ? sanitize_textarea_field( $data['comment'] ) : null, + 'strava_url' => isset( $data['strava_url'] ) ? $null_if_empty( esc_url_raw( $data['strava_url'] ) ) : null, + 'avg_heart_rate' => isset( $data['avg_heart_rate'] ) ? $null_if_empty( intval( $data['avg_heart_rate'] ) ) : null, + 'max_heart_rate' => isset( $data['max_heart_rate'] ) ? $null_if_empty( intval( $data['max_heart_rate'] ) ) : null, + 'avg_speed' => isset( $data['avg_speed'] ) ? $null_if_empty( floatval( str_replace( ',', '.', $data['avg_speed'] ) ) ) : null, + 'max_speed' => isset( $data['max_speed'] ) ? $null_if_empty( floatval( str_replace( ',', '.', $data['max_speed'] ) ) ) : null, + 'avg_cadence' => isset( $data['avg_cadence'] ) ? $null_if_empty( intval( $data['avg_cadence'] ) ) : null, + 'max_cadence' => isset( $data['max_cadence'] ) ? $null_if_empty( intval( $data['max_cadence'] ) ) : null, + 'total_elevation_gain' => isset( $data['total_elevation_gain'] ) ? $null_if_empty( intval( $data['total_elevation_gain'] ) ) : null, + 'total_elevation_loss' => isset( $data['total_elevation_loss'] ) ? $null_if_empty( intval( $data['total_elevation_loss'] ) ) : null, + 'min_altitude' => isset( $data['min_altitude'] ) ? $null_if_empty( intval( $data['min_altitude'] ) ) : null, + 'max_altitude' => isset( $data['max_altitude'] ) ? $null_if_empty( intval( $data['max_altitude'] ) ) : null, + 'equipment_id' => isset( $data['equipment_id'] ) ? $null_if_empty( intval( $data['equipment_id'] ) ) : null, + 'gpx_url' => isset( $data['gpx_url'] ) ? $null_if_empty( esc_url_raw( $data['gpx_url'] ) ) : null, + 'event_type_id' => isset( $data['event_type_id'] ) ? $null_if_empty( intval( $data['event_type_id'] ) ) : null, + ); + + // Data formats for $wpdb. + $format = array( + '%d', // category_id + '%s', // date + '%s', // title + '%f', // distance + '%s', // duration + '%d', // calories + '%s', // comment + '%s', // strava_url + '%d', // avg_heart_rate + '%d', // max_heart_rate + '%f', // avg_speed + '%f', // max_speed + '%d', // avg_cadence + '%d', // max_cadence + '%d', // total_elevation_gain + '%d', // total_elevation_loss + '%d', // min_altitude + '%d', // max_altitude + '%d', // equipment_id + '%s', // gpx_url + '%d', // event_type_id + ); + + if ( $activity_id > 0 ) { + // UPDATE + $result = $wpdb->update( $table_activities, $prepared_data, array( 'id' => $activity_id ), $format, array( '%d' ) ); + if ( false !== $result ) { + return $activity_id; + } + } else { + // INSERT + $result = $wpdb->insert( $table_activities, $prepared_data, $format ); + if ( $result ) { + return $wpdb->insert_id; + } + } + + return false; } \ No newline at end of file diff --git a/includes/core/gpx-parser.php b/includes/core/gpx-parser.php index e307880..9417c79 100644 --- a/includes/core/gpx-parser.php +++ b/includes/core/gpx-parser.php @@ -1,139 +1,297 @@ - 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( 'statpress_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, - ); + 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( 'statpress_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, + ); +} + +/** + * Fetches and calculates a full summary from a GPX file. + * This function parses the entire file to get accurate summary stats, ignoring privacy zones. + * + * @param string $gpx_url The URL of the GPX file. + * @return array An associative array with summary statistics. + */ +function statpress_calculate_gpx_summary( $gpx_url ) { + if ( empty( $gpx_url ) ) { + return array(); + } + + $response = wp_remote_get( $gpx_url, array( 'timeout' => 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(); + } + + libxml_use_internal_errors( true ); + $gpx = simplexml_load_string( $gpx_content ); + libxml_clear_errors(); + + if ( false === $gpx ) { + return array(); + } + + $gpx->registerXPathNamespace( 'gpx', 'http://www.topografix.com/GPX/1/1' ); + $trackpoints = $gpx->xpath( '//gpx:trkpt' ); + if ( empty( $trackpoints ) ) { + $trackpoints = $gpx->xpath( '//trkpt' ); + } + + if ( empty( $trackpoints ) ) { + return array(); + } + + $haversine = function( $lat1, $lon1, $lat2, $lon2 ) { + $earth_radius = 6371; // 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; + }; + + $stats = array( + 'distance' => 0, + 'total_elevation_gain' => 0, + 'total_elevation_loss' => 0, + 'min_altitude' => null, + 'max_altitude' => null, + 'max_speed' => 0, + 'max_heart_rate' => 0, + 'max_cadence' => 0, + 'hr_sum' => 0, + 'hr_count' => 0, + 'cad_sum' => 0, + 'cad_count' => 0, + 'speed_sum' => 0, + 'speed_count' => 0, + 'start_time' => null, + 'end_time' => null, + ); + + $prev_point = null; + + foreach ( $trackpoints as $trkpt ) { + $point = array( + 'lat' => (float) $trkpt['lat'], + 'lon' => (float) $trkpt['lon'], + 'ele' => isset( $trkpt->ele ) ? (float) $trkpt->ele : null, + 'time' => isset( $trkpt->time ) ? strtotime( (string) $trkpt->time ) : null, + ); + + if ( is_null( $stats['start_time'] ) && ! is_null( $point['time'] ) ) { + $stats['start_time'] = $point['time']; + } + $stats['end_time'] = $point['time']; + + if ( ! is_null( $point['ele'] ) ) { + if ( is_null( $stats['min_altitude'] ) || $point['ele'] < $stats['min_altitude'] ) { + $stats['min_altitude'] = $point['ele']; + } + if ( is_null( $stats['max_altitude'] ) || $point['ele'] > $stats['max_altitude'] ) { + $stats['max_altitude'] = $point['ele']; + } + } + + $extensions = $trkpt->extensions ? $trkpt->extensions->children( 'gpxtpx', true ) : null; + if ( $extensions ) { + if ( isset( $extensions->TrackPointExtension->hr ) ) { + $hr = (int) $extensions->TrackPointExtension->hr; + $stats['hr_sum'] += $hr; + $stats['hr_count']++; + if ( $hr > $stats['max_heart_rate'] ) { + $stats['max_heart_rate'] = $hr; + } + } + if ( isset( $extensions->TrackPointExtension->cad ) ) { + $cad = (int) $extensions->TrackPointExtension->cad; + $stats['cad_sum'] += $cad; + $stats['cad_count']++; + if ( $cad > $stats['max_cadence'] ) { + $stats['max_cadence'] = $cad; + } + } + } + + if ( $prev_point ) { + $distance_delta = $haversine( $prev_point['lat'], $prev_point['lon'], $point['lat'], $point['lon'] ); + $stats['distance'] += $distance_delta; + + if ( ! is_null( $point['ele'] ) && ! is_null( $prev_point['ele'] ) ) { + $ele_delta = $point['ele'] - $prev_point['ele']; + if ( $ele_delta > 0 ) { + $stats['total_elevation_gain'] += $ele_delta; + } else { + $stats['total_elevation_loss'] += abs( $ele_delta ); + } + } + + if ( ! is_null( $point['time'] ) && ! is_null( $prev_point['time'] ) ) { + $time_delta = $point['time'] - $prev_point['time']; + if ( $time_delta > 0 ) { + $speed = ( $distance_delta * 3600 ) / $time_delta; // km/h + $stats['speed_sum'] += $speed; + $stats['speed_count']++; + if ( $speed > $stats['max_speed'] ) { + $stats['max_speed'] = $speed; + } + } + } + } + $prev_point = $point; + } + + $duration_sec = ( ! is_null( $stats['end_time'] ) && ! is_null( $stats['start_time'] ) ) ? ( $stats['end_time'] - $stats['start_time'] ) : 0; + + return array( + 'distance' => round( $stats['distance'], 2 ), + 'duration' => gmdate( 'H:i:s', $duration_sec ), + 'avg_speed' => ( $stats['distance'] > 0 && $duration_sec > 0 ) ? round( ( $stats['distance'] / ( $duration_sec / 3600 ) ), 1 ) : 0, + 'max_speed' => round( $stats['max_speed'], 1 ), + 'total_elevation_gain' => round( $stats['total_elevation_gain'] ), + 'total_elevation_loss' => round( $stats['total_elevation_loss'] ), + 'min_altitude' => ! is_null( $stats['min_altitude'] ) ? round( $stats['min_altitude'] ) : null, + 'max_altitude' => ! is_null( $stats['max_altitude'] ) ? round( $stats['max_altitude'] ) : null, + 'avg_heart_rate' => $stats['hr_count'] > 0 ? round( $stats['hr_sum'] / $stats['hr_count'] ) : 0, + 'max_heart_rate' => $stats['max_heart_rate'], + 'avg_cadence' => $stats['cad_count'] > 0 ? round( $stats['cad_sum'] / $stats['cad_count'] ) : 0, + 'max_cadence' => $stats['max_cadence'], + ); } \ No newline at end of file diff --git a/includes/core/gpx-upload.php b/includes/core/gpx-upload.php index 836413a..170443c 100644 --- a/includes/core/gpx-upload.php +++ b/includes/core/gpx-upload.php @@ -1,33 +1,33 @@ - current_time( 'Y' ), - 'month' => current_time( 'n' ), - ), - $atts, - 'moje_statystyki' - ); - - $year = intval( $atts['year'] ); - $month = intval( $atts['month'] ); - - // Pobieranie danych z bazy - $table_activities = $wpdb->prefix . 'mystat_activities'; - $sql = $wpdb->prepare( - " - SELECT a.*, c.name as category_name, eq.name as equipment_name - FROM $table_activities a - LEFT JOIN {$wpdb->prefix}mystat_categories c ON a.category_id = c.id - LEFT JOIN {$wpdb->prefix}mystat_equipment eq ON a.equipment_id = eq.id - WHERE YEAR(a.date) = %d AND MONTH(a.date) = %d - ORDER BY a.date ASC - ", - $year, - $month - ); - - $activities = $wpdb->get_results( $sql ); - - // Obliczanie podsumowań - $total_distance = 0; - $total_seconds = 0; - foreach ( $activities as $activity ) { - $total_distance += $activity->distance; - if ( ! empty( $activity->duration ) ) { - list($h, $m, $s) = explode( ':', $activity->duration ); - $total_seconds += $h * 3600 + $m * 60 + $s; - } - } - $hours = floor( $total_seconds / 3600 ); - $minutes = floor( ( $total_seconds % 3600 ) / 60 ); - $total_duration_formatted = sprintf( '%d godz. %d min.', $hours, $minutes ); - - // Rozpoczęcie buforowania wyjścia - ob_start(); - ?> -
- -

Podsumowanie miesiąca

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

Lista aktywności

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

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

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

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

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

title ); ?>

-

date ) ) ); ?>

- -
-
- - - distance, 2, ',', ' ' ), 'km' ); ?> - duration ); ?> - avg_speed, 1, ',', ' ' ), 'km/h' ); ?> - total_elevation_gain, 'm' ); ?> - -
-
-
- - - category_name ); ?> - equipment_name ); ?> - strava_url ) ) : ?> - - - -
StravaZobacz aktywność
-
-
- - -
- - -
-
-
- $label ) : - ?> - - -
- -
- Oś X:  - -   - -
- - - -
-
- -
-
- - -
- current_time( 'Y' ), + 'month' => current_time( 'n' ), + ), + $atts, + 'statpress_summary' + ); + + $year = intval( $atts['year'] ); + $month = intval( $atts['month'] ); + + // Pobieranie danych z bazy + $table_activities = $wpdb->prefix . 'statpress_activities'; + $sql = $wpdb->prepare( + " + SELECT a.*, c.name as category_name, eq.name as equipment_name + FROM $table_activities a + LEFT JOIN {$wpdb->prefix}statpress_categories c ON a.category_id = c.id + LEFT JOIN {$wpdb->prefix}statpress_equipment eq ON a.equipment_id = eq.id + WHERE YEAR(a.date) = %d AND MONTH(a.date) = %d + ORDER BY a.date ASC + ", + $year, + $month + ); + + $activities = $wpdb->get_results( $sql ); + + // Obliczanie podsumowań + $total_distance = 0; + $total_seconds = 0; + foreach ( $activities as $activity ) { + $total_distance += $activity->distance; + if ( ! empty( $activity->duration ) ) { + list($h, $m, $s) = explode( ':', $activity->duration ); + $total_seconds += $h * 3600 + $m * 60 + $s; + } + } + $hours = floor( $total_seconds / 3600 ); + $minutes = floor( ( $total_seconds % 3600 ) / 60 ); + $total_duration_formatted = sprintf( '%d godz. %d min.', $hours, $minutes ); + + // Rozpoczęcie buforowania wyjścia + ob_start(); + ?> +
+ +

Podsumowanie miesiąca

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

Lista aktywności

+ + + + + + + + + + + + + + + + + + + + + + + + avg_speed || $row->avg_heart_rate || $row->avg_cadence ) : ?> + + + + + + + + + + + +
DataTytułKategoriaDystansCzasSprzęt
date ) ) ); ?>title ); ?>category_name ); ?>distance, 2, ',', ' ' ); ?> kmduration ); ?>equipment_name ); ?>
+ + + + avg_speed ) { echo ''; } ?> + avg_heart_rate ) { echo ''; } ?> + avg_cadence ) { echo ''; } ?> + + + + + avg_speed ) { echo ''; } ?> + avg_heart_rate ) { echo ''; } ?> + avg_cadence ) { echo ''; } ?> + + +
Śr. prędkośćŚr. tętnoŚr. kadencja
' . number_format( $row->avg_speed, 1, ',', ' ' ) . ' km/h' . $row->avg_heart_rate . ' bpm' . $row->avg_cadence . ' rpm
+
Brak aktywności w tym miesiącu.
+
+ 0, + ), + $atts, + 'statpress_activity' + ); + + $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 . 'statpress_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}statpress_categories c ON a.category_id = c.id + LEFT JOIN {$wpdb->prefix}statpress_event_types et ON a.event_type_id = et.id + LEFT JOIN {$wpdb->prefix}statpress_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 = statpress_parse_gpx_data( $activity->gpx_url ); + $has_gpx_data = ! empty( $gpx_data['points'] ); + } + + $unique_id = 'statpress-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( 'statpress-shortcode-loader-' . $activity->id, false ); + wp_enqueue_script( 'statpress-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(".statpress-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(".statpress-chart-tab").forEach(t => t.addEventListener("click", e => { + e.preventDefault(); + containerEl.querySelector(".statpress-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(".statpress-chart-tab")) renderChart(); + }); + })();'; + wp_add_inline_script( 'statpress-shortcode-loader-' . $activity->id, $js_script ); + } + + ?> +
+

title ); ?>

+

date ) ) ); ?>

+ +
+
+ + + distance, 2, ',', ' ' ), 'km' ); ?> + duration ); ?> + avg_speed, 1, ',', ' ' ), 'km/h' ); ?> + total_elevation_gain, 'm' ); ?> + +
+
+
+ + + category_name ); ?> + equipment_name ); ?> + strava_url ) ) : ?> + + + +
StravaZobacz aktywność
+
+
+ + +
+ + +
+
+
+ $label ) : + ?> + + +
+ +
+ Oś X:  + +   + +
+ + + +
+
+ +
+
+ + +
+ 'id, category_id, date, title, distance, duration, calories, comment, strava_url, avg_heart_rate, max_heart_rate, avg_speed, max_speed, avg_cadence, max_cadence, total_elevation_gain, total_elevation_loss, min_altitude, max_altitude, equipment_id, gpx_url, event_type_id', + 'categories' => 'id, name, icon, color', + 'equipment' => 'id, name, type, purchase_date, initial_cost, status, notes', + 'equipment_log' => 'id, equipment_id, log_date, log_type, description, cost, mileage', + 'event_types' => 'id, name', + 'goals' => 'id, name, goal_type, target_value, year, month, category_id', + ); + + $results = array(); + $all_successful = true; + + foreach ( $table_columns as $table_suffix => $columns ) { + $old_table = $wpdb->prefix . 'mystat_' . $table_suffix; + $new_table = $wpdb->prefix . 'statpress_' . $table_suffix; + + // Check if old table exists and new table is empty + if ( $wpdb->get_var( "SHOW TABLES LIKE '{$old_table}'" ) === $old_table ) { + // Check if there's anything to migrate + $old_table_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$old_table}" ); + $new_table_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$new_table}" ); + + // Only migrate if the new table is empty but the old one has data. + if ( $old_table_count > 0 && 0 === $new_table_count ) { + // Use explicit column list for a robust query + $query = "INSERT INTO {$new_table} ({$columns}) SELECT {$columns} FROM {$old_table}"; + $copied_rows = $wpdb->query( $query ); + + if ( false === $copied_rows ) { + $all_successful = false; + $results[ $table_suffix ] = array( + 'status' => 'failure', + 'error' => $wpdb->last_error, + ); + } else { + $results[ $table_suffix ] = array( + 'status' => 'success', + 'count' => $copied_rows, + ); + } + } else { + // If the new table is not empty, skip it. + $results[ $table_suffix ] = array( + 'status' => 'skipped', + 'count' => $new_table_count, + ); + } + } + } + + // Store the results in a transient to display a notice + set_transient( 'statpress_migration_results', $results, 60 ); + + // Mark migration as complete only if everything was successful or skipped. + if ( $all_successful ) { + update_option( 'statpress_migration_complete', true ); + } + + // Redirect to the main dashboard to show the notice and remove query args + wp_safe_redirect( admin_url( 'admin.php?page=statpress-dashboard' ) ); + exit; +} + +/** + * Handles various admin tool actions, like resetting the migration flag. + */ +function statpress_handle_admin_tools() { + // Check if the reset migration action is triggered + if ( isset( $_POST['statpress_action'] ) && 'reset_migration' === $_POST['statpress_action'] ) { + // Verify nonce and permissions + if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'statpress_reset_migration_nonce' ) ) { + return; + } + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + + // Delete the option that hides the migration button + delete_option( 'statpress_migration_complete' ); + + // Set a transient to show a success notice on the settings page + set_transient( 'statpress_migration_reset_notice', true, 60 ); + wp_safe_redirect( admin_url( 'admin.php?page=statpress-settings' ) ); + exit; + } +}