Update repo

This commit is contained in:
2026-04-22 12:51:16 +02:00
parent d303a55638
commit d31591e287
24 changed files with 3994 additions and 3501 deletions
+21 -21
View File
@@ -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.
+2 -2
View File
@@ -1,2 +1,2 @@
# wp-cycling-ststs
WP plugin for stats
# wp-cycling-ststs
WP plugin for stats
+101 -60
View File
@@ -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;
}
+103 -103
View File
@@ -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%;
}
+139 -139
View File
@@ -1,140 +1,140 @@
<?php
/**
* Funkcje związane z aktywacją wtyczki, np. tworzenie tabel w bazie danych.
*
* @package WordPress Activity Stats
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Główna funkcja aktywacyjna. Tworzy wszystkie niezbędne tabele w bazie danych.
*
* @return void
*/
function mystat_activate() {
global $wpdb;
$charset_collate = $wpdb->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' ) );
}
<?php
/**
* Funkcje związane z aktywacją wtyczki, np. tworzenie tabel w bazie danych.
*
* @package WordPress Activity Stats
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Główna funkcja aktywacyjna. Tworzy wszystkie niezbędne tabele w bazie danych.
*
* @return void
*/
function statpress_activate() {
global $wpdb;
$charset_collate = $wpdb->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' ) );
}
}
+26 -26
View File
@@ -1,27 +1,27 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Set up admin-specific hooks.
*/
function mystat_admin_init_setup() {
add_filter( 'upload_mimes', 'mystat_add_gpx_mime_type' );
add_filter( 'wp_check_filetype_and_ext', 'mystat_fix_gpx_upload_permission', 10, 4 );
mystat_register_settings();
}
/**
* Enqueue admin-specific CSS.
*
* @param string $hook The current admin page hook.
*/
function mystat_enqueue_admin_styles( $hook ) {
global $mystat_plugin_hooks;
if ( in_array( $hook, $mystat_plugin_hooks, true ) ) {
$plugin_version = '1.0'; // You can use filemtime() for cache-busting in development
wp_enqueue_style( 'mystat-admin-styles', MYSTAT_PLUGIN_URL . 'assets/css/admin.css', array(), $plugin_version );
}
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Set up admin-specific hooks.
*/
function statpress_admin_init_setup() {
add_filter( 'upload_mimes', 'statpress_add_gpx_mime_type' );
add_filter( 'wp_check_filetype_and_ext', 'statpress_fix_gpx_upload_permission', 10, 4 );
statpress_register_settings();
}
/**
* Enqueue admin-specific CSS.
*
* @param string $hook The current admin page hook.
*/
function statpress_enqueue_admin_styles( $hook ) {
global $statpress_plugin_hooks;
if ( is_array( $statpress_plugin_hooks ) && in_array( $hook, $statpress_plugin_hooks, true ) ) {
$plugin_version = '1.0'; // You can use filemtime() for cache-busting in development
wp_enqueue_style( 'statpress-admin-styles', STATPRESS_PLUGIN_URL . 'assets/css/admin.css', array(), $plugin_version );
}
}
+122 -122
View File
@@ -1,123 +1,123 @@
<?php
/**
* Admin Menu setup for the plugin.
*
* @package WordPress Activity Stats
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Adds the main menu and sub-menu pages for the plugin.
*
* @return void
*/
function mystat_add_admin_menu() {
global $mystat_plugin_hooks;
$mystat_plugin_hooks[] = add_menu_page(
'Moje Statystyki', // Tytuł strony
'Statystyki', // Tytuł w menu
'manage_options', // Wymagane uprawnienia
'moje-statystyki', // Slug menu
'mystat_dashboard_page', // Funkcja renderująca stronę główną (dashboard)
'dashicons-chart-line', // Ikona
6 // Pozycja
);
$mystat_plugin_hooks[] = add_submenu_page(
'moje-statystyki', // Slug rodzica
'Dodaj Nowy Trening', // Tytuł strony
'Nowy trening', // Tytuł w podmenu
'manage_options', // Wymagane uprawnienia
'mystat-nowy-trening', // Slug podmenu
'mystat_add_new_page' // Funkcja renderująca stronę dodawania
);
$mystat_plugin_hooks[] = add_submenu_page(
'moje-statystyki',
'Typy Wydarzeń',
'Typy wydarzeń',
'manage_options',
'mystat-event-types',
'mystat_event_types_page'
);
$mystat_plugin_hooks[] = add_submenu_page(
'moje-statystyki',
'Sprzęt',
'Sprzęt',
'manage_options',
'mystat-equipment',
'mystat_equipment_page'
);
$mystat_plugin_hooks[] = add_submenu_page(
'moje-statystyki',
'Cele',
'Cele',
'manage_options',
'mystat-goals',
'mystat_goals_page'
);
$mystat_plugin_hooks[] = add_submenu_page(
null, 'Dziennik Serwisowy', 'Dziennik Serwisowy', 'manage_options', 'mystat-equipment-details', 'mystat_equipment_details_page'
);
$mystat_plugin_hooks[] = add_submenu_page(
null, // Ukryta strona, nie pojawia się w menu
'Szczegóły Treningu', // Tytuł strony
'Szczegóły Treningu', // Tytuł w menu (nieistotny)
'manage_options', // Wymagane uprawnienia
'mystat-view-activity', // Slug podmenu
'mystat_view_activity_page' // Funkcja renderująca
);
$mystat_plugin_hooks[] = add_submenu_page(
null, // Ukryta strona
'Edytuj Trening', // Tytuł strony
'Edytuj Trening', // Tytuł w menu (nieistotny)
'manage_options', // Wymagane uprawnienia
'mystat-edit-activity', // Slug podmenu
'mystat_edit_activity_page' // Funkcja renderująca
);
$mystat_plugin_hooks[] = add_submenu_page(
'moje-statystyki', // Slug rodzica
'Podsumowanie Roczne', // Tytuł strony
'Podsumowanie Roczne', // Tytuł w podmenu
'manage_options', // Wymagane uprawnienia
'mystat-yearly-summary', // Slug podmenu
'mystat_yearly_summary_page'// Funkcja renderująca
);
$mystat_plugin_hooks[] = add_submenu_page(
'moje-statystyki', // Slug rodzica
'Infografika', // Tytuł strony
'Infografika', // Tytuł w podmenu
'manage_options', // Wymagane uprawnienia
'mystat-infographic', // Slug podmenu
'mystat_infographic_page' // Funkcja renderująca
);
$mystat_plugin_hooks[] = add_submenu_page(
'moje-statystyki', // Slug rodzica
'Import CSV', // Tytuł strony
'Import CSV', // Tytuł w podmenu
'manage_options', // Wymagane uprawnienia
'mystat-import-csv', // Slug podmenu
'mystat_import_csv_page' // Funkcja renderująca
);
$mystat_plugin_hooks[] = add_submenu_page(
'moje-statystyki', // Slug rodzica
'Ustawienia', // Tytuł strony
'Ustawienia', // Tytuł w podmenu
'manage_options', // Wymagane uprawnienia
'mystat-settings', // Slug podmenu
'mystat_settings_page' // Funkcja renderująca
);
<?php
/**
* Admin Menu setup for the plugin.
*
* @package WordPress Activity Stats
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Adds the main menu and sub-menu pages for the plugin.
*
* @return void
*/
function statpress_add_admin_menu() {
global $statpress_plugin_hooks;
$statpress_plugin_hooks[] = add_menu_page(
'StatPress Dashboard', // Tytuł strony
'StatPress', // Tytuł w menu
'manage_options', // Wymagane uprawnienia
'statpress-dashboard', // Slug menu
'statpress_dashboard_page', // Funkcja renderująca stronę główną (dashboard)
'dashicons-chart-line', // Ikona
6 // Pozycja
);
$statpress_plugin_hooks[] = add_submenu_page(
'statpress-dashboard', // Slug rodzica
'Dodaj Nowy Trening', // Tytuł strony
'Nowy trening', // Tytuł w podmenu
'manage_options', // Wymagane uprawnienia
'statpress-add-new', // Slug podmenu
'statpress_add_new_page' // Funkcja renderująca stronę dodawania
);
$statpress_plugin_hooks[] = add_submenu_page(
'statpress-dashboard',
'Typy Wydarzeń',
'Typy wydarzeń',
'manage_options',
'statpress-event-types',
'statpress_event_types_page'
);
$statpress_plugin_hooks[] = add_submenu_page(
'statpress-dashboard',
'Sprzęt',
'Sprzęt',
'manage_options',
'statpress-equipment',
'statpress_equipment_page'
);
$statpress_plugin_hooks[] = add_submenu_page(
'statpress-dashboard',
'Cele',
'Cele',
'manage_options',
'statpress-goals',
'statpress_goals_page'
);
$statpress_plugin_hooks[] = add_submenu_page(
null, 'Dziennik Serwisowy', 'Dziennik Serwisowy', 'manage_options', 'statpress-equipment-details', 'statpress_equipment_details_page'
);
$statpress_plugin_hooks[] = add_submenu_page(
null, // Ukryta strona, nie pojawia się w menu
'Szczegóły Treningu', // Tytuł strony
'Szczegóły Treningu', // Tytuł w menu (nieistotny)
'manage_options', // Wymagane uprawnienia
'statpress-view-activity', // Slug podmenu
'statpress_view_activity_page' // Funkcja renderująca
);
$statpress_plugin_hooks[] = add_submenu_page(
null, // Ukryta strona
'Edytuj Trening', // Tytuł strony
'Edytuj Trening', // Tytuł w menu (nieistotny)
'manage_options', // Wymagane uprawnienia
'statpress-edit-activity', // Slug podmenu
'statpress_edit_activity_page' // Funkcja renderująca
);
$statpress_plugin_hooks[] = add_submenu_page(
'statpress-dashboard', // Slug rodzica
'Podsumowanie Roczne', // Tytuł strony
'Podsumowanie Roczne', // Tytuł w podmenu
'manage_options', // Wymagane uprawnienia
'statpress-yearly-summary', // Slug podmenu
'statpress_yearly_summary_page'// Funkcja renderująca
);
$statpress_plugin_hooks[] = add_submenu_page(
'statpress-dashboard', // Slug rodzica
'Infografika', // Tytuł strony
'Infografika', // Tytuł w podmenu
'manage_options', // Wymagane uprawnienia
'statpress-infographic', // Slug podmenu
'statpress_infographic_page' // Funkcja renderująca
);
$statpress_plugin_hooks[] = add_submenu_page(
'statpress-dashboard', // Slug rodzica
'Import CSV', // Tytuł strony
'Import CSV', // Tytuł w podmenu
'manage_options', // Wymagane uprawnienia
'statpress-import-csv', // Slug podmenu
'statpress_import_csv_page' // Funkcja renderująca
);
$statpress_plugin_hooks[] = add_submenu_page(
'statpress-dashboard', // Slug rodzica
'Ustawienia', // Tytuł strony
'Ustawienia', // Tytuł w podmenu
'manage_options', // Wymagane uprawnienia
'statpress-settings', // Slug podmenu
'statpress_settings_page' // Funkcja renderująca
);
}
+303 -243
View File
@@ -1,244 +1,304 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
function statpress_add_new_page() {
echo '<div class="wrap"><h1>Dodaj Nowy Trening</h1>';
// Obsługa zapisu formularza (musi być przed renderowaniem, aby wyświetlić komunikat)
statpress_handle_activity_form_submission();
// Formularz dodawania
statpress_render_add_form();
echo '</div>';
}
function statpress_edit_activity_page() {
global $wpdb;
$activity_id = isset( $_GET['id'] ) ? intval( $_GET['id'] ) : 0;
if ( $activity_id === 0 ) {
echo '<div class="wrap"><h1>Błąd</h1><p>Nie podano ID aktywności do edycji.</p></div>';
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 '<div class="wrap"><h1>Błąd</h1><p>Nie znaleziono aktywności o podanym ID.</p></div>';
return;
}
echo '<div class="wrap"><h1>Edytuj Trening</h1>';
statpress_render_add_form( $activity );
echo '</div>';
}
/**
* 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 '<div class="notice notice-error"><p>Błąd weryfikacji bezpieczeństwa formularza.</p></div>';
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 '<div class="notice notice-success is-dismissible"><p>' . esc_html( $message ) . '</p></div>';
} else {
echo '<div class="notice notice-error is-dismissible"><p>Wystąpił błąd podczas zapisu do bazy.</p></div>';
}
}
/**
* 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';
?>
<div class="postbox">
<div class="postbox-header"><h2 class="hndle"><?php echo esc_html( $form_title ); ?></h2></div>
<div class="inside">
<form method="post" action="">
<input type="hidden" name="activity_id" value="<?php echo $is_edit_mode ? esc_attr( $activity->id ) : '0'; ?>">
<?php wp_nonce_field( $nonce_action, '_wpnonce' ); ?>
<table class="form-table">
<tr>
<th scope="row"><label for="title">Tytuł</label></th>
<td><input type="text" name="title" id="title" class="large-text" placeholder="np. Poranny trening w lesie" value="<?php echo $is_edit_mode ? esc_attr( $activity->title ) : ''; ?>" required></td>
</tr>
<tr>
<th scope="row"><label for="category_id">Kategoria</label></th>
<td>
<select name="category_id" id="category_id" required>
<?php foreach ( $categories as $cat ) : ?>
<option value="<?php echo esc_attr( $cat->id ); ?>" <?php echo $is_edit_mode ? selected( $activity->category_id, $cat->id, false ) : selected( $cat->name, 'Rower', false ); ?>><?php echo esc_html( $cat->name ); ?></option>
<?php endforeach; ?>
</select>
</td>
</tr>
<tr>
<th scope="row"><label for="date">Data</label></th>
<td><input type="date" name="date" id="date" value="<?php echo $is_edit_mode ? esc_attr( $activity->date ) : current_time( 'Y-m-d' ); ?>" class="regular-text" required></td>
</tr>
<tr>
<th scope="row"><label for="distance">Dystans (km)</label></th>
<td><input type="text" name="distance" id="distance" class="regular-text" placeholder="0,00" value="<?php echo $is_edit_mode ? esc_attr( number_format( (float) $activity->distance, 2, ',', '' ) ) : ''; ?>"></td>
</tr>
<tr>
<th scope="row"><label for="duration">Czas (HH:MM:SS)</label></th>
<td><input type="time" name="duration" id="duration" step="1" value="<?php echo $is_edit_mode ? esc_attr( $activity->duration ) : '00:00:00'; ?>" class="regular-text"></td>
</tr>
<tr>
<th scope="row"><label for="calories">Kalorie (kcal)</label></th>
<td><input type="number" name="calories" id="calories" class="regular-text" placeholder="0" value="<?php echo $is_edit_mode ? esc_attr( $activity->calories ) : ''; ?>"></td>
</tr>
<tr>
<th scope="row"><label for="event_type_id">Typ wydarzenia</label></th>
<td>
<select name="event_type_id" id="event_type_id">
<option value="">-- Wybierz --</option>
<?php foreach ( $event_types as $type ) : ?>
<option value="<?php echo esc_attr( $type->id ); ?>" <?php echo $is_edit_mode ? selected( $activity->event_type_id, $type->id, false ) : selected( $type->name, 'Trening', false ); ?>><?php echo esc_html( $type->name ); ?></option>
<?php endforeach; ?>
</select>
</td>
</tr>
<tr>
<th scope="row"><label for="equipment_id">Sprzęt</label></th>
<td>
<select name="equipment_id" id="equipment_id">
<option value="">-- Wybierz --</option>
<?php foreach ( $equipment_list as $item ) : ?>
<option value="<?php echo esc_attr( $item->id ); ?>" <?php if ( $is_edit_mode ) {
selected( $activity->equipment_id, $item->id );} ?>><?php echo esc_html( $item->name ); ?></option>
<?php endforeach; ?>
</select>
</td>
</tr>
<tr class="form-field">
<td colspan="2"><hr><h4>Dane szczegółowe (opcjonalne)</h4></td>
</tr>
<tr>
<th scope="row"><label for="avg_speed">Śr. prędkość (km/h)</label></th>
<td><input type="text" name="avg_speed" id="avg_speed" class="small-text" placeholder="0,0" value="<?php echo $is_edit_mode ? esc_attr( number_format( (float) $activity->avg_speed, 2, ',', '' ) ) : ''; ?>"></td>
</tr>
<tr>
<th scope="row"><label for="max_speed">Maks. prędkość (km/h)</label></th>
<td><input type="text" name="max_speed" id="max_speed" class="small-text" placeholder="0,0" value="<?php echo $is_edit_mode ? esc_attr( number_format( (float) $activity->max_speed, 2, ',', '' ) ) : ''; ?>"></td>
</tr>
<tr>
<th scope="row"><label for="avg_heart_rate">Śr. tętno</label></th>
<td><input type="number" name="avg_heart_rate" id="avg_heart_rate" class="small-text" placeholder="0" value="<?php echo $is_edit_mode ? esc_attr( $activity->avg_heart_rate ) : ''; ?>"></td>
</tr>
<tr>
<th scope="row"><label for="max_heart_rate">Maks. tętno</label></th>
<td><input type="number" name="max_heart_rate" id="max_heart_rate" class="small-text" placeholder="0" value="<?php echo $is_edit_mode ? esc_attr( $activity->max_heart_rate ) : ''; ?>"></td>
</tr>
<tr>
<th scope="row"><label for="avg_cadence">Śr. rytm pedałowania</label></th>
<td><input type="number" name="avg_cadence" id="avg_cadence" class="small-text" placeholder="0" value="<?php echo $is_edit_mode ? esc_attr( $activity->avg_cadence ) : ''; ?>"></td>
</tr>
<tr>
<th scope="row"><label for="max_cadence">Maks. rytm pedałowania</label></th>
<td><input type="number" name="max_cadence" id="max_cadence" class="small-text" placeholder="0" value="<?php echo $is_edit_mode ? esc_attr( $activity->max_cadence ) : ''; ?>"></td>
</tr>
<tr>
<th scope="row"><label for="total_elevation_gain">Całkowity wznios (m)</label></th>
<td><input type="number" name="total_elevation_gain" id="total_elevation_gain" class="small-text" placeholder="0" value="<?php echo $is_edit_mode ? esc_attr( $activity->total_elevation_gain ) : ''; ?>"></td>
</tr>
<tr>
<th scope="row"><label for="total_elevation_loss">Całkowity spadek (m)</label></th>
<td><input type="number" name="total_elevation_loss" id="total_elevation_loss" class="small-text" placeholder="0" value="<?php echo $is_edit_mode ? esc_attr( $activity->total_elevation_loss ) : ''; ?>"></td>
</tr>
<tr>
<th scope="row"><label for="min_altitude">Min. wysokość (m)</labe></th>
<td><input type="number" name="min_altitude" id="min_altitude" class="small-text" placeholder="0" value="<?php echo $is_edit_mode ? esc_attr( $activity->min_altitude ) : ''; ?>"></td>
</tr>
<tr>
<th scope="row"><label for="max_altitude">Maks. wysokość (m)</label></th>
<td><input type="number" name="max_altitude" id="max_altitude" class="small-text" placeholder="0" value="<?php echo $is_edit_mode ? esc_attr( $activity->max_altitude ) : ''; ?>"></td>
</tr>
<tr>
<th scope="row"><label for="comment">Komentarz</label></th>
<td><textarea name="comment" id="comment" rows="3" class="large-text"><?php echo $is_edit_mode ? esc_textarea( $activity->comment ) : ''; ?></textarea></td>
</tr>
<tr class="form-field">
<td colspan="2"><hr><h4>Linki zewnętrzne (opcjonalne)</h4></td>
</tr>
<tr>
<th scope="row"><label for="strava_url">Link do Strava</label></th>
<td><input type="url" name="strava_url" id="strava_url" class="large-text" placeholder="https://www.strava.com/activities/..." value="<?php echo $is_edit_mode ? esc_attr( $activity->strava_url ) : ''; ?>"></td>
</tr>
<tr>
<th scope="row"><label for="gpx_url">Link do pliku GPX</label></th>
<td>
<input type="text" name="gpx_url" id="gpx_url" class="large-text" placeholder="Wklej URL lub wgraj plik..." value="<?php echo $is_edit_mode ? esc_attr( $activity->gpx_url ) : ''; ?>">
<button type="button" class="button" id="upload_gpx_button" style="margin-top: 5px;">Wgraj lub wybierz plik</button>
</td>
</tr>
</table>
<p class="submit">
<input type="submit" name="statpress_submit_activity" id="submit" class="button button-primary" value="<?php echo esc_attr( $button_text ); ?>">
</p>
</form>
<script>
jQuery(document).ready(function($) {
$('#upload_gpx_button').click(function(e) {
e.preventDefault();
var gpx_uploader = wp.media({
title: 'Wybierz plik GPX',
button: { text: 'Użyj tego pliku' },
multiple: false,
library: { type: ['application/gpx+xml', 'application/xml', 'text/plain'] }
})
.on('select', function() {
var attachment = gpx_uploader.state().get('selection').first().toJSON();
$('#gpx_url').val(attachment.url).trigger('change');
}).open();
});
});
</script>
</div>
</div>
<?php
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
function statpress_add_new_page() {
echo '<div class="wrap"><h1>Dodaj Nowy Trening</h1>';
// Obsługa zapisu formularza (musi być przed renderowaniem, aby wyświetlić komunikat)
statpress_handle_activity_form_submission();
// Formularz dodawania
statpress_render_add_form();
echo '</div>';
}
function statpress_edit_activity_page() {
global $wpdb;
$activity_id = isset( $_GET['id'] ) ? intval( $_GET['id'] ) : 0;
if ( $activity_id === 0 ) {
echo '<div class="wrap"><h1>Błąd</h1><p>Nie podano ID aktywności do edycji.</p></div>';
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 '<div class="wrap"><h1>Błąd</h1><p>Nie znaleziono aktywności o podanym ID.</p></div>';
return;
}
echo '<div class="wrap"><h1>Edytuj Trening</h1>';
statpress_render_add_form( $activity );
echo '</div>';
}
/**
* 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 '<div class="notice notice-error"><p>Błąd weryfikacji bezpieczeństwa formularza.</p></div>';
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 '<div class="notice notice-success is-dismissible"><p>' . esc_html( $message ) . '</p></div>';
} else {
echo '<div class="notice notice-error is-dismissible"><p>Wystąpił błąd podczas zapisu do bazy.</p></div>';
}
}
/**
* 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';
?>
<div class="postbox">
<div class="postbox-header"><h2 class="hndle"><?php echo esc_html( $form_title ); ?></h2></div>
<div class="inside">
<form method="post" action="">
<input type="hidden" name="activity_id" value="<?php echo $is_edit_mode ? esc_attr( $activity->id ) : '0'; ?>">
<?php wp_nonce_field( $nonce_action, '_wpnonce' ); ?>
<table class="form-table">
<tr>
<th scope="row"><label for="title">Tytuł</label></th>
<td><input type="text" name="title" id="title" class="large-text" placeholder="np. Poranny trening w lesie" value="<?php echo $is_edit_mode ? esc_attr( $activity->title ) : ''; ?>" required></td>
</tr>
<tr>
<th scope="row"><label for="gpx_url">Link do pliku GPX</label></th>
<td>
<input type="text" name="gpx_url" id="gpx_url" class="large-text" placeholder="Wklej URL lub wgraj plik..." value="<?php echo $is_edit_mode ? esc_attr( $activity->gpx_url ) : ''; ?>" style="width: calc(100% - 150px); margin-right: 10px;">
<span id="gpx_parse_status"></span>
<button type="button" class="button" id="upload_gpx_button" style="margin-top: 5px;">Wgraj lub wybierz plik</button>
<p class="description">Wklej link lub wgraj plik GPX, aby automatycznie uzupełnić pola poniżej.</p>
</td>
</tr>
<tr>
<td colspan="2" style="padding: 15px 0 5px;"><hr></td>
</tr>
<tr>
<th scope="row"><label for="category_id">Kategoria</label></th>
<td>
<select name="category_id" id="category_id" required>
<?php foreach ( $categories as $cat ) : ?>
<option value="<?php echo esc_attr( $cat->id ); ?>" <?php echo $is_edit_mode ? selected( $activity->category_id, $cat->id, false ) : selected( $cat->name, 'Rower', false ); ?>><?php echo esc_html( $cat->name ); ?></option>
<?php endforeach; ?>
</select>
</td>
</tr>
<tr>
<th scope="row"><label for="date">Data</label></th>
<td><input type="date" name="date" id="date" value="<?php echo $is_edit_mode ? esc_attr( $activity->date ) : current_time( 'Y-m-d' ); ?>" class="regular-text" required></td>
</tr>
<tr>
<th scope="row"><label for="distance">Dystans (km)</label></th>
<td><input type="text" name="distance" id="distance" class="regular-text" placeholder="0,00" value="<?php echo $is_edit_mode ? esc_attr( number_format( (float) $activity->distance, 2, ',', '' ) ) : ''; ?>"></td>
</tr>
<tr>
<th scope="row"><label for="duration">Czas (HH:MM:SS)</label></th>
<td><input type="time" name="duration" id="duration" step="1" value="<?php echo $is_edit_mode ? esc_attr( $activity->duration ) : '00:00:00'; ?>" class="regular-text"></td>
</tr>
<tr>
<th scope="row"><label for="calories">Kalorie (kcal)</label></th>
<td><input type="number" name="calories" id="calories" class="regular-text" placeholder="0" value="<?php echo $is_edit_mode ? esc_attr( $activity->calories ) : ''; ?>"></td>
</tr>
<tr>
<th scope="row"><label for="event_type_id">Typ wydarzenia</label></th>
<td>
<select name="event_type_id" id="event_type_id">
<option value="">-- Wybierz --</option>
<?php foreach ( $event_types as $type ) : ?>
<option value="<?php echo esc_attr( $type->id ); ?>" <?php echo $is_edit_mode ? selected( $activity->event_type_id, $type->id, false ) : selected( $type->name, 'Trening', false ); ?>><?php echo esc_html( $type->name ); ?></option>
<?php endforeach; ?>
</select>
</td>
</tr>
<tr>
<th scope="row"><label for="equipment_id">Sprzęt</label></th>
<td>
<select name="equipment_id" id="equipment_id">
<option value="">-- Wybierz --</option>
<?php foreach ( $equipment_list as $item ) : ?>
<option value="<?php echo esc_attr( $item->id ); ?>" <?php if ( $is_edit_mode ) {
selected( $activity->equipment_id, $item->id );} ?>><?php echo esc_html( $item->name ); ?></option>
<?php endforeach; ?>
</select>
</td>
</tr>
<tr>
<td colspan="2" style="padding: 20px 0;">
<hr>
<h3 style="font-size: 1.2em; margin: 1em 0;">Dane szczegółowe (opcjonalne)</h3>
<div class="statpress-form-grid">
<div class="statpress-form-group">
<h3><span class="dashicons dashicons-dashboard" style="vertical-align: middle;"></span> Prędkość</h3>
<p><label for="avg_speed">Średnia (km/h)</label><input type="text" name="avg_speed" id="avg_speed" placeholder="0,0" value="<?php echo $is_edit_mode ? esc_attr( number_format( (float) $activity->avg_speed, 2, ',', '' ) ) : ''; ?>"></p>
<p><label for="max_speed">Maksymalna (km/h)</label><input type="text" name="max_speed" id="max_speed" placeholder="0,0" value="<?php echo $is_edit_mode ? esc_attr( number_format( (float) $activity->max_speed, 2, ',', '' ) ) : ''; ?>"></p>
</div>
<div class="statpress-form-group">
<h3><span class="dashicons dashicons-heart" style="vertical-align: middle;"></span> Tętno</h3>
<p><label for="avg_heart_rate">Średnie (bpm)</label><input type="number" name="avg_heart_rate" id="avg_heart_rate" placeholder="0" value="<?php echo $is_edit_mode ? esc_attr( $activity->avg_heart_rate ) : ''; ?>"></p>
<p><label for="max_heart_rate">Maksymalne (bpm)</label><input type="number" name="max_heart_rate" id="max_heart_rate" placeholder="0" value="<?php echo $is_edit_mode ? esc_attr( $activity->max_heart_rate ) : ''; ?>"></p>
</div>
<div class="statpress-form-group">
<h3><span class="dashicons dashicons-update" style="vertical-align: middle;"></span> Rytm</h3>
<p><label for="avg_cadence">Średni (rpm)</label><input type="number" name="avg_cadence" id="avg_cadence" placeholder="0" value="<?php echo $is_edit_mode ? esc_attr( $activity->avg_cadence ) : ''; ?>"></p>
<p><label for="max_cadence">Maksymalny (rpm)</label><input type="number" name="max_cadence" id="max_cadence" placeholder="0" value="<?php echo $is_edit_mode ? esc_attr( $activity->max_cadence ) : ''; ?>"></p>
</div>
<div class="statpress-form-group">
<h3><span class="dashicons dashicons-chart-area" style="vertical-align: middle;"></span> Wysokość</h3>
<p><label for="total_elevation_gain">Suma wzniosów (m)</label><input type="number" name="total_elevation_gain" id="total_elevation_gain" placeholder="0" value="<?php echo $is_edit_mode ? esc_attr( $activity->total_elevation_gain ) : ''; ?>"></p>
<p><label for="total_elevation_loss">Suma spadków (m)</label><input type="number" name="total_elevation_loss" id="total_elevation_loss" placeholder="0" value="<?php echo $is_edit_mode ? esc_attr( $activity->total_elevation_loss ) : ''; ?>"></p>
<p><label for="min_altitude">Min. wysokość (m n.p.m.)</label><input type="number" name="min_altitude" id="min_altitude" placeholder="0" value="<?php echo $is_edit_mode ? esc_attr( $activity->min_altitude ) : ''; ?>"></p>
<p><label for="max_altitude">Maks. wysokość (m n.p.m.)</label><input type="number" name="max_altitude" id="max_altitude" placeholder="0" value="<?php echo $is_edit_mode ? esc_attr( $activity->max_altitude ) : ''; ?>"></p>
</div>
</div>
<hr style="margin-top: 2em;">
</td>
</tr>
<tr>
<th scope="row"><label for="comment">Komentarz</label></th>
<td><textarea name="comment" id="comment" rows="3" class="large-text"><?php echo $is_edit_mode ? esc_textarea( $activity->comment ) : ''; ?></textarea></td>
</tr>
<tr>
<th scope="row"><label for="strava_url">Link do Strava</label></th>
<td><input type="url" name="strava_url" id="strava_url" class="large-text" placeholder="https://www.strava.com/activities/..." value="<?php echo $is_edit_mode ? esc_attr( $activity->strava_url ) : ''; ?>"></td>
</tr>
</table>
<p class="submit">
<input type="submit" name="statpress_submit_activity" id="submit" class="button button-primary" value="<?php echo esc_attr( $button_text ); ?>">
</p>
</form>
<script>
jQuery(document).ready(function($) {
$('#upload_gpx_button').click(function(e) {
e.preventDefault();
var gpx_uploader = wp.media({
title: 'Wybierz plik GPX',
button: { text: 'Użyj tego pliku' },
multiple: false,
library: { type: ['application/gpx+xml', 'application/xml', 'text/plain'] }
})
.on('select', function() {
var attachment = gpx_uploader.state().get('selection').first().toJSON();
$('#gpx_url').val(attachment.url).trigger('change');
}).open();
});
// --- GPX Auto-fill Feature ---
const gpxUrlInput = $('#gpx_url');
const statusEl = $('#gpx_parse_status');
let debounceTimer;
gpxUrlInput.on('input change', function() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(function() {
const url = gpxUrlInput.val();
if (url && url.toLowerCase().endsWith('.gpx')) {
parseGpx(url);
}
}, 500); // 500ms delay to avoid firing on every keystroke
});
function parseGpx(url) {
statusEl.html('<span class="spinner is-active" style="float:none; vertical-align: middle;"></span> Analizuję plik...');
wp.apiFetch({
path: '/statpress/v1/gpx/parse-summary',
method: 'POST',
data: { gpx_url: url }
})
.then(function(data) {
statusEl.html('<span class="dashicons dashicons-yes-alt" style="color: #46b450;"></span> Dane wczytane!');
const fieldsToCheck = [
'#distance', '#duration', '#calories', '#avg_speed', '#max_speed',
'#avg_heart_rate', '#max_heart_rate', '#avg_cadence', '#max_cadence',
'#total_elevation_gain', '#total_elevation_loss', '#min_altitude', '#max_altitude'
];
let hasExistingData = false;
fieldsToCheck.forEach(function(selector) {
const field = $(selector);
if (field.val() && field.val() !== '00:00:00' && field.val() !== '0' && field.val() !== '0,00') {
hasExistingData = true;
}
});
let proceed = true;
if (hasExistingData) {
proceed = confirm('Niektóre pola formularza zawierają już dane. Czy na pewno chcesz je nadpisać danymi z pliku GPX?');
}
if (proceed) {
const formatNum = (num) => String(num).replace('.', ',');
if (data.distance) $('#distance').val(formatNum(data.distance));
if (data.duration) $('#duration').val(data.duration);
if (data.avg_speed) $('#avg_speed').val(formatNum(data.avg_speed));
if (data.max_speed) $('#max_speed').val(formatNum(data.max_speed));
if (data.avg_heart_rate) $('#avg_heart_rate').val(data.avg_heart_rate);
if (data.max_heart_rate) $('#max_heart_rate').val(data.max_heart_rate);
if (data.avg_cadence) $('#avg_cadence').val(data.avg_cadence);
if (data.max_cadence) $('#max_cadence').val(data.max_cadence);
if (data.total_elevation_gain) $('#total_elevation_gain').val(data.total_elevation_gain);
if (data.total_elevation_loss) $('#total_elevation_loss').val(data.total_elevation_loss);
if (data.min_altitude) $('#min_altitude').val(data.min_altitude);
if (data.max_altitude) $('#max_altitude').val(data.max_altitude);
}
setTimeout(() => statusEl.html(''), 4000);
})
.catch(function(error) {
const errorMsg = error.message || 'Nieznany błąd.';
statusEl.html('<span class="dashicons dashicons-warning" style="color: #d63638;"></span> Błąd: ' + errorMsg);
});
}
});
</script>
</div>
</div>
<?php
}
+262 -262
View File
@@ -1,263 +1,263 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
function statpress_view_activity_page() {
global $wpdb;
$activity_id = isset( $_GET['id'] ) ? intval( $_GET['id'] ) : 0;
if ( $activity_id === 0 ) {
echo '<div class="wrap"><h1>Błąd</h1><p>Nie podano ID aktywności.</p></div>';
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 '<div class="wrap"><h1>Błąd</h1><p>Nie znaleziono aktywności o podanym ID.</p></div>';
return;
}
// Funkcja pomocnicza do wyświetlania wiersza, jeśli wartość istnieje
$render_row = function( $label, $value, $unit = '' ) {
if ( ! is_null( $value ) && '' !== $value ) {
echo '<tr>';
echo '<th scope="row">' . esc_html( $label ) . '</th>';
echo '<td>' . esc_html( $value ) . ( $unit ? ' ' . esc_html( $unit ) : '' ) . '</td>';
echo '</tr>';
}
};
// 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: \'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>\' }).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<yData.length; i++) {
if (yData[i] !== null) {
filteredY.push(yData[i]);
filteredX.push(xData[i]);
}
}
}
const ctx = document.getElementById("statpress-details-chart").getContext("2d");
activeChart = new Chart(ctx, {
type: "line",
data: {
labels: filteredX,
datasets: [{
label: chartConfigs[chartType].label, data: filteredY,
borderColor: chartConfigs[chartType].color, backgroundColor: chartConfigs[chartType].color + "20",
fill: true, pointRadius: 0, tension: 0.1
}]
},
options: {
responsive: true, maintainAspectRatio: false,
scales: {
x: { title: { display: true, text: xAxisConfigs[xAxisType].label }, ticks: { callback: xAxisType === "time" ? xAxisConfigs.time.formatter : (v) => 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 . '});' );
}
?>
<div class="wrap">
<h1>
Szczegóły treningu: <?php echo esc_html( $activity->title ); ?>
<a href="<?php echo esc_url( add_query_arg( array( 'page' => 'statpress-edit-activity', 'id' => $activity->id ), admin_url( 'admin.php' ) ) ); ?>" class="page-title-action">
Edytuj
</a>
</h1>
<p><a href="<?php echo esc_url( admin_url( 'admin.php?page=statpress-dashboard' ) ); ?>">&larr; Powrót do listy aktywności</a></p>
<div class="postbox" style="margin-top: 20px;">
<div class="postbox-header"><h2 class="hndle">Podsumowanie</h2></div>
<div class="inside">
<div id="statpress-details-container">
<div class="statpress-details-col">
<h3>Główne dane</h3>
<table class="form-table">
<?php $render_row( 'Kategoria', $activity->category_name ); ?>
<?php $render_row( 'Data', $activity->date ); ?>
<?php $render_row( 'Dystans', number_format( $activity->distance, 2, ',', ' ' ), 'km' ); ?>
<?php $render_row( 'Czas trwania', $activity->duration ); ?>
<?php $render_row( 'Spalone kalorie', $activity->calories, 'kcal' ); ?>
<?php $render_row( 'Typ wydarzenia', $activity->event_type_name ); ?>
<?php $render_row( 'Sprzęt', $activity->equipment_name ); ?>
</table>
</div>
<div class="statpress-details-col">
<h3>Dane szczegółowe</h3>
<table class="form-table">
<?php $render_row( 'Średnia prędkość', number_format( $activity->avg_speed, 1, ',', ' ' ), 'km/h' ); ?>
<?php $render_row( 'Maksymalna prędkość', number_format( $activity->max_speed, 1, ',', ' ' ), 'km/h' ); ?>
<?php $render_row( 'Średnie tętno', $activity->avg_heart_rate, 'bpm' ); ?>
<?php $render_row( 'Maksymalne tętno', $activity->max_heart_rate, 'bpm' ); ?>
<?php $render_row( 'Średni rytm', $activity->avg_cadence, 'rpm' ); ?>
<?php $render_row( 'Maksymalny rytm', $activity->max_cadence, 'rpm' ); ?>
<?php $render_row( 'Suma wzniosów', $activity->total_elevation_gain, 'm' ); ?>
<?php $render_row( 'Suma spadków', $activity->total_elevation_loss, 'm' ); ?>
<?php $render_row( 'Minimalna wysokość', $activity->min_altitude, 'm n.p.m.' ); ?>
<?php $render_row( 'Maksymalna wysokość', $activity->max_altitude, 'm n.p.m.' ); ?>
</table>
</div>
</div>
<hr>
<h3>Notatki i linki</h3>
<table class="form-table">
<?php $render_row( 'Komentarz', nl2br( $activity->comment ) ); ?>
<?php if ( ! empty( $activity->strava_url ) ) : ?>
<tr><th scope="row">Strava</th><td><a href="<?php echo esc_url( $activity->strava_url ); ?>" target="_blank" rel="noopener noreferrer">Zobacz aktywność na Strava</a></td></tr>
<?php endif; ?>
<?php if ( ! empty( $activity->gpx_url ) ) : ?>
<tr><th scope="row">Plik GPX</th><td><a href="<?php echo esc_url( $activity->gpx_url ); ?>" target="_blank">Pobierz plik GPX</a></td></tr>
<?php endif; ?>
</table>
<?php if ( $has_gpx_data ) : ?>
<hr>
<h3>Mapa Trasy</h3>
<div id="statpress-activity-map" style="height: 450px; width: 100%; border: 1px solid #ddd; margin-bottom: 20px;"></div>
<?php if ( ! empty( $available_profiles ) ) : ?>
<h3>Wykresy</h3>
<div class="statpress-charts-container">
<div class="statpress-chart-controls">
<nav class="nav-tab-wrapper statpress-chart-tabs">
<?php
$is_first = true;
foreach ( $available_profiles as $key => $label ) :
?>
<a href="#<?php echo esc_attr( $key ); ?>" class="nav-tab <?php
if ( $is_first ) {
echo 'nav-tab-active';
$is_first = false; }
?>
"><?php echo esc_html( $label ); ?></a>
<?php endforeach; ?>
</nav>
<?php if ( $has_time_data ) : ?>
<div class="statpress-xaxis-switcher">
<strong>Oś X:</strong>&nbsp;
<label><input type="radio" name="statpress_xaxis" value="distance" checked> Dystans</label>
&nbsp;
<label><input type="radio" name="statpress_xaxis" value="time"> Czas</label>
</div>
<?php else : ?>
<input type="hidden" name="statpress_xaxis" value="distance" checked>
<?php endif; ?>
</div>
<div style="position: relative; height:250px; width:100%;">
<canvas id="statpress-details-chart"></canvas>
</div>
</div>
<?php endif; ?>
<?php elseif ( ! empty( $activity->gpx_url ) ) : ?>
<hr>
<div class="notice notice-warning inline">
<p>Nie udało się wczytać danych z pliku GPX lub plik jest uszkodzony/pusty. Brak danych do wyświetlenia mapy i wykresów.</p>
</div>
<?php endif; ?>
</div>
</div>
</div>
<?php
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
function statpress_view_activity_page() {
global $wpdb;
$activity_id = isset( $_GET['id'] ) ? intval( $_GET['id'] ) : 0;
if ( $activity_id === 0 ) {
echo '<div class="wrap"><h1>Błąd</h1><p>Nie podano ID aktywności.</p></div>';
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 '<div class="wrap"><h1>Błąd</h1><p>Nie znaleziono aktywności o podanym ID.</p></div>';
return;
}
// Funkcja pomocnicza do wyświetlania wiersza, jeśli wartość istnieje
$render_row = function( $label, $value, $unit = '' ) {
if ( ! is_null( $value ) && '' !== $value ) {
echo '<tr>';
echo '<th scope="row">' . esc_html( $label ) . '</th>';
echo '<td>' . esc_html( $value ) . ( $unit ? ' ' . esc_html( $unit ) : '' ) . '</td>';
echo '</tr>';
}
};
// 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: \'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>\' }).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<yData.length; i++) {
if (yData[i] !== null) {
filteredY.push(yData[i]);
filteredX.push(xData[i]);
}
}
}
const ctx = document.getElementById("statpress-details-chart").getContext("2d");
activeChart = new Chart(ctx, {
type: "line",
data: {
labels: filteredX,
datasets: [{
label: chartConfigs[chartType].label, data: filteredY,
borderColor: chartConfigs[chartType].color, backgroundColor: chartConfigs[chartType].color + "20",
fill: true, pointRadius: 0, tension: 0.1
}]
},
options: {
responsive: true, maintainAspectRatio: false,
scales: {
x: { title: { display: true, text: xAxisConfigs[xAxisType].label }, ticks: { callback: xAxisType === "time" ? xAxisConfigs.time.formatter : (v) => 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 . '});' );
}
?>
<div class="wrap">
<h1>
Szczegóły treningu: <?php echo esc_html( $activity->title ); ?>
<a href="<?php echo esc_url( add_query_arg( array( 'page' => 'statpress-edit-activity', 'id' => $activity->id ), admin_url( 'admin.php' ) ) ); ?>" class="page-title-action">
Edytuj
</a>
</h1>
<p><a href="<?php echo esc_url( admin_url( 'admin.php?page=statpress-dashboard' ) ); ?>">&larr; Powrót do listy aktywności</a></p>
<div class="postbox" style="margin-top: 20px;">
<div class="postbox-header"><h2 class="hndle">Podsumowanie</h2></div>
<div class="inside">
<div id="statpress-details-container">
<div class="statpress-details-col">
<h3>Główne dane</h3>
<table class="form-table">
<?php $render_row( 'Kategoria', $activity->category_name ); ?>
<?php $render_row( 'Data', $activity->date ); ?>
<?php $render_row( 'Dystans', number_format( $activity->distance, 2, ',', ' ' ), 'km' ); ?>
<?php $render_row( 'Czas trwania', $activity->duration ); ?>
<?php $render_row( 'Spalone kalorie', $activity->calories, 'kcal' ); ?>
<?php $render_row( 'Typ wydarzenia', $activity->event_type_name ); ?>
<?php $render_row( 'Sprzęt', $activity->equipment_name ); ?>
</table>
</div>
<div class="statpress-details-col">
<h3>Dane szczegółowe</h3>
<table class="form-table">
<?php $render_row( 'Średnia prędkość', number_format( $activity->avg_speed, 1, ',', ' ' ), 'km/h' ); ?>
<?php $render_row( 'Maksymalna prędkość', number_format( $activity->max_speed, 1, ',', ' ' ), 'km/h' ); ?>
<?php $render_row( 'Średnie tętno', $activity->avg_heart_rate, 'bpm' ); ?>
<?php $render_row( 'Maksymalne tętno', $activity->max_heart_rate, 'bpm' ); ?>
<?php $render_row( 'Średni rytm', $activity->avg_cadence, 'rpm' ); ?>
<?php $render_row( 'Maksymalny rytm', $activity->max_cadence, 'rpm' ); ?>
<?php $render_row( 'Suma wzniosów', $activity->total_elevation_gain, 'm' ); ?>
<?php $render_row( 'Suma spadków', $activity->total_elevation_loss, 'm' ); ?>
<?php $render_row( 'Minimalna wysokość', $activity->min_altitude, 'm n.p.m.' ); ?>
<?php $render_row( 'Maksymalna wysokość', $activity->max_altitude, 'm n.p.m.' ); ?>
</table>
</div>
</div>
<hr>
<h3>Notatki i linki</h3>
<table class="form-table">
<?php $render_row( 'Komentarz', nl2br( $activity->comment ) ); ?>
<?php if ( ! empty( $activity->strava_url ) ) : ?>
<tr><th scope="row">Strava</th><td><a href="<?php echo esc_url( $activity->strava_url ); ?>" target="_blank" rel="noopener noreferrer">Zobacz aktywność na Strava</a></td></tr>
<?php endif; ?>
<?php if ( ! empty( $activity->gpx_url ) ) : ?>
<tr><th scope="row">Plik GPX</th><td><a href="<?php echo esc_url( $activity->gpx_url ); ?>" target="_blank">Pobierz plik GPX</a></td></tr>
<?php endif; ?>
</table>
<?php if ( $has_gpx_data ) : ?>
<hr>
<h3>Mapa Trasy</h3>
<div id="statpress-activity-map" style="height: 450px; width: 100%; border: 1px solid #ddd; margin-bottom: 20px;"></div>
<?php if ( ! empty( $available_profiles ) ) : ?>
<h3>Wykresy</h3>
<div class="statpress-charts-container">
<div class="statpress-chart-controls">
<nav class="nav-tab-wrapper statpress-chart-tabs">
<?php
$is_first = true;
foreach ( $available_profiles as $key => $label ) :
?>
<a href="#<?php echo esc_attr( $key ); ?>" class="nav-tab <?php
if ( $is_first ) {
echo 'nav-tab-active';
$is_first = false; }
?>
"><?php echo esc_html( $label ); ?></a>
<?php endforeach; ?>
</nav>
<?php if ( $has_time_data ) : ?>
<div class="statpress-xaxis-switcher">
<strong>Oś X:</strong>&nbsp;
<label><input type="radio" name="statpress_xaxis" value="distance" checked> Dystans</label>
&nbsp;
<label><input type="radio" name="statpress_xaxis" value="time"> Czas</label>
</div>
<?php else : ?>
<input type="hidden" name="statpress_xaxis" value="distance" checked>
<?php endif; ?>
</div>
<div style="position: relative; height:250px; width:100%;">
<canvas id="statpress-details-chart"></canvas>
</div>
</div>
<?php endif; ?>
<?php elseif ( ! empty( $activity->gpx_url ) ) : ?>
<hr>
<div class="notice notice-warning inline">
<p>Nie udało się wczytać danych z pliku GPX lub plik jest uszkodzony/pusty. Brak danych do wyświetlenia mapy i wykresów.</p>
</div>
<?php endif; ?>
</div>
</div>
</div>
<?php
}
+232 -194
View File
@@ -1,195 +1,233 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
function statpress_dashboard_page() {
echo '<div class="wrap"><h1>StatPress Dashboard</h1>';
statpress_render_history_table();
echo '</div>';
}
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 '<div class="notice notice-success is-dismissible"><p>Aktywność została usunięta.</p></div>';
} else {
echo '<div class="notice notice-error is-dismissible"><p>Wystąpił błąd podczas usuwania.</p></div>';
}
} else {
echo '<div class="notice notice-error is-dismissible"><p>Błąd weryfikacji bezpieczeństwa (Nonce).</p></div>';
}
}
// --- 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) ---
?>
<div class="wrap">
<h2>Historia Aktywności</h2>
<?php if ( $total_pages > 1 ) : ?>
<div class="tablenav top">
<div class="tablenav-pages">
<span class="displaying-num"><?php echo esc_html( $total_items ); ?> aktywności</span>
<?php
echo paginate_links(
array(
'base' => add_query_arg( 'paged', '%#%' ),
'format' => '',
'total' => $total_pages,
'current' => $current_page,
'prev_text' => '&laquo; Poprzednia',
'next_text' => 'Następna &raquo;',
)
);
?>
</div>
</div>
<?php endif; ?>
<table class="wp-list-table widefat fixed striped table-view-list">
<thead>
<tr>
<th scope="col" style="width: 50px;">Ikona</th>
<th scope="col">Data</th>
<th scope="col">Tytuł</th>
<th scope="col" style="width: 120px;">Kategoria</th>
<th scope="col">Typ</th>
<th scope="col">Sprzęt</th>
<th scope="col">Dystans (km)</th>
<th scope="col">Czas</th>
<th scope="col">Śr. prędkość</th>
<th scope="col" style="width: 180px;">Akcja</th>
</tr>
</thead>
<tbody>
<?php if ( ! empty( $activities ) ) : ?>
<?php foreach ( $activities as $row ) : ?>
<?php
// Generowanie URL-i akcji z zachowaniem paginacji
$delete_url = wp_nonce_url(
add_query_arg(
array(
'action' => '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' )
);
?>
<tr>
<td>
<?php if ( ! empty( $row->icon ) ) : ?>
<span class="dashicons <?php echo esc_attr( $row->icon ); ?>" style="color: <?php echo esc_attr( $row->color ); ?>;"></span>
<?php endif; ?>
</td>
<td><?php echo esc_html( $row->date ); ?></td>
<td><strong><a href="<?php echo esc_url( $details_url ); ?>"><?php echo esc_html( $row->title ? wp_trim_words( $row->title, 6 ) : '(bez tytułu)' ); ?></a></strong></td>
<td><?php echo esc_html( $row->category_name ); ?></td>
<td><?php echo esc_html( $row->event_type_name ); ?></td>
<td><?php echo esc_html( $row->equipment_name ); ?></td>
<td><?php echo number_format( $row->distance, 2, ',', ' ' ); ?></td>
<td><?php echo esc_html( $row->duration ); ?></td>
<td><?php echo $row->avg_speed ? number_format( $row->avg_speed, 1, ',', ' ' ) . ' km/h' : '-'; ?></td>
<td>
<a href="<?php echo esc_url( $edit_url ); ?>" class="button button-small">Edytuj</a>
<a href="<?php echo esc_url( $details_url ); ?>" class="button button-small">Szczegóły</a>
<a href="<?php echo esc_url( $delete_url ); ?>"
class="button button-small button-link-delete"
onclick="return confirm('Czy na pewno chcesz usunąć ten wpis?');">
Usuń
</a>
</td>
</tr>
<?php endforeach; ?>
<?php else : ?>
<tr>
<td colspan="10">Brak zarejestrowanych aktywności. Dodaj pierwszy trening powyżej!</td>
</tr>
<?php endif; ?>
</tbody>
</table>
<?php if ( $total_pages > 1 ) : ?>
<div class="tablenav bottom">
<div class="tablenav-pages">
<span class="displaying-num"><?php echo esc_html( $total_items ); ?> aktywności</span>
<?php
echo paginate_links(
array(
'base' => add_query_arg( 'paged', '%#%' ),
'format' => '',
'total' => $total_pages,
'current' => $current_page,
'prev_text' => '&laquo; Poprzednia',
'next_text' => 'Następna &raquo;',
)
);
?>
</div>
</div>
<?php endif; ?>
</div>
<?php
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
function statpress_dashboard_page() {
echo '<div class="wrap"><h1>StatPress Dashboard</h1>';
// --- 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 '<div class="notice notice-warning is-dismissible" style="padding-bottom: 10px;">';
echo '<h4>Migracja danych StatPress</h4>';
echo '<p>Wygląda na to, że istnieją dane w starych tabelach (<code>mystat_*</code>), które można przenieść. Kliknij przycisk poniżej, aby rozpocząć proces.</p>';
echo '<p><strong>Ważne:</strong> Ta operacja jest jednorazowa i powinna być wykonana tylko raz, na pustej instalacji wtyczki StatPress.</p>';
echo '<a href="' . esc_url( $migration_url ) . '" class="button button-primary">Rozpocznij migrację danych</a>';
echo '</div>';
}
// Show the results of the migration after it's done.
$migration_results = get_transient( 'statpress_migration_results' );
if ( $migration_results ) {
echo '<div class="notice notice-success is-dismissible">';
echo '<h4>Migracja zakończona!</h4>';
echo '<ul>';
foreach ( $migration_results as $table => $result ) {
if ( 'success' === $result['status'] ) {
echo '<li>Tabela <strong>' . esc_html( $table ) . '</strong>: Przeniesiono <strong>' . esc_html( $result['count'] ) . '</strong> wierszy.</li>';
} elseif ( 'skipped' === $result['status'] ) {
echo '<li>Tabela <strong>' . esc_html( $table ) . '</strong>: Pominięto, ponieważ nowa tabela zawiera już dane (' . esc_html( $result['count'] ) . ' wierszy).</li>';
} elseif ( 'failure' === $result['status'] ) {
echo '<li>Tabela <strong>' . esc_html( $table ) . '</strong>: <strong style="color:red;">Migracja nieudana.</strong> Błąd bazy danych: <pre style="display:inline;white-space:pre-wrap;">' . esc_html( $result['error'] ) . '</pre></li>';
}
}
echo '</ul>';
echo '<p>Twoje dane powinny być teraz widoczne. Stare tabele (<code>' . esc_html( $GLOBALS['wpdb']->prefix ) . 'mystat_*</code>) 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.</p>';
echo '</div>';
delete_transient( 'statpress_migration_results' );
}
// --- END MIGRATION NOTICE ---
statpress_render_history_table();
echo '</div>';
}
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 '<div class="notice notice-success is-dismissible"><p>Aktywność została usunięta.</p></div>';
} else {
echo '<div class="notice notice-error is-dismissible"><p>Wystąpił błąd podczas usuwania.</p></div>';
}
} else {
echo '<div class="notice notice-error is-dismissible"><p>Błąd weryfikacji bezpieczeństwa (Nonce).</p></div>';
}
}
// --- 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) ---
?>
<div class="wrap">
<h2>Historia Aktywności</h2>
<?php if ( $total_pages > 1 ) : ?>
<div class="tablenav top">
<div class="tablenav-pages">
<span class="displaying-num"><?php echo esc_html( $total_items ); ?> aktywności</span>
<?php
echo paginate_links(
array(
'base' => add_query_arg( 'paged', '%#%' ),
'format' => '',
'total' => $total_pages,
'current' => $current_page,
'prev_text' => '&laquo; Poprzednia',
'next_text' => 'Następna &raquo;',
)
);
?>
</div>
</div>
<?php endif; ?>
<table class="wp-list-table widefat fixed striped table-view-list">
<thead>
<tr>
<th scope="col" style="width: 50px;">Ikona</th>
<th scope="col">Data</th>
<th scope="col">Tytuł</th>
<th scope="col" style="width: 120px;">Kategoria</th>
<th scope="col">Typ</th>
<th scope="col">Sprzęt</th>
<th scope="col">Dystans (km)</th>
<th scope="col">Czas</th>
<th scope="col">Śr. prędkość</th>
<th scope="col" style="width: 180px;">Akcja</th>
</tr>
</thead>
<tbody>
<?php if ( ! empty( $activities ) ) : ?>
<?php foreach ( $activities as $row ) : ?>
<?php
// Generowanie URL-i akcji z zachowaniem paginacji
$delete_url = wp_nonce_url(
add_query_arg(
array(
'action' => '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' )
);
?>
<tr>
<td>
<?php if ( ! empty( $row->icon ) ) : ?>
<span class="dashicons <?php echo esc_attr( $row->icon ); ?>" style="color: <?php echo esc_attr( $row->color ); ?>;"></span>
<?php endif; ?>
</td>
<td><?php echo esc_html( $row->date ); ?></td>
<td><strong><a href="<?php echo esc_url( $details_url ); ?>"><?php echo esc_html( $row->title ? wp_trim_words( $row->title, 6 ) : '(bez tytułu)' ); ?></a></strong></td>
<td><?php echo esc_html( $row->category_name ); ?></td>
<td><?php echo esc_html( $row->event_type_name ); ?></td>
<td><?php echo esc_html( $row->equipment_name ); ?></td>
<td><?php echo number_format( $row->distance, 2, ',', ' ' ); ?></td>
<td><?php echo esc_html( $row->duration ); ?></td>
<td><?php echo $row->avg_speed ? number_format( $row->avg_speed, 1, ',', ' ' ) . ' km/h' : '-'; ?></td>
<td>
<a href="<?php echo esc_url( $edit_url ); ?>" class="button button-small">Edytuj</a>
<a href="<?php echo esc_url( $details_url ); ?>" class="button button-small">Szczegóły</a>
<a href="<?php echo esc_url( $delete_url ); ?>"
class="button button-small button-link-delete"
onclick="return confirm('Czy na pewno chcesz usunąć ten wpis?');">
Usuń
</a>
</td>
</tr>
<?php endforeach; ?>
<?php else : ?>
<tr>
<td colspan="10">Brak zarejestrowanych aktywności. Dodaj pierwszy trening powyżej!</td>
</tr>
<?php endif; ?>
</tbody>
</table>
<?php if ( $total_pages > 1 ) : ?>
<div class="tablenav bottom">
<div class="tablenav-pages">
<span class="displaying-num"><?php echo esc_html( $total_items ); ?> aktywności</span>
<?php
echo paginate_links(
array(
'base' => add_query_arg( 'paged', '%#%' ),
'format' => '',
'total' => $total_pages,
'current' => $current_page,
'prev_text' => '&laquo; Poprzednia',
'next_text' => 'Następna &raquo;',
)
);
?>
</div>
</div>
<?php endif; ?>
</div>
<?php
}
+296 -296
View File
@@ -1,297 +1,297 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
function statpress_equipment_page() {
global $wpdb;
$table_equipment = $wpdb->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"
);
?>
<div class="wrap">
<h1>Zarządzaj Sprzętem</h1>
<?php if ( ! empty( $message ) ) : ?>
<div class="notice <?php echo esc_attr( $notice_class ); ?> is-dismissible"><p><?php echo esc_html( $message ); ?></p></div>
<?php endif; ?>
<div id="col-container" class="wp-clearfix">
<div id="col-left">
<div class="col-wrap">
<div class="form-wrap">
<h2><?php echo $item_to_edit ? 'Edytuj pozycję' : 'Dodaj nowy sprzęt'; ?></h2>
<form method="post">
<input type="hidden" name="equipment_id" value="<?php echo $item_to_edit ? esc_attr( $item_to_edit->id ) : '0'; ?>">
<?php wp_nonce_field( 'statpress_manage_equipment' ); ?>
<div class="form-field form-required"><label for="equipment_name">Nazwa</label><input type="text" name="equipment_name" id="equipment_name" value="<?php echo $item_to_edit ? esc_attr( $item_to_edit->name ) : ''; ?>" required></div>
<div class="form-field"><label for="equipment_type">Typ</label><input type="text" name="equipment_type" id="equipment_type" value="<?php echo $item_to_edit ? esc_attr( $item_to_edit->type ) : 'Rower'; ?>"></div>
<div class="form-field"><label for="purchase_date">Data zakupu</label><input type="date" name="purchase_date" id="purchase_date" value="<?php echo $item_to_edit ? esc_attr( $item_to_edit->purchase_date ) : ''; ?>"></div>
<div class="form-field"><label for="initial_cost">Koszt zakupu</label><input type="text" name="initial_cost" id="initial_cost" value="<?php echo $item_to_edit ? esc_attr( $item_to_edit->initial_cost ) : ''; ?>" placeholder="0.00"></div>
<div class="form-field"><label for="status">Status</label><select name="status" id="status"><?php foreach ( $statuses as $s ) : ?><option value="<?php echo esc_attr( $s ); ?>" <?php if ( $item_to_edit ) {
selected( $item_to_edit->status, $s );} ?>><?php echo esc_html( ucfirst( $s ) ); ?></option><?php endforeach; ?></select></div>
<div class="form-field"><label for="notes">Notatki</label><textarea name="notes" id="notes" rows="3"><?php echo $item_to_edit ? esc_textarea( $item_to_edit->notes ) : ''; ?></textarea></div>
<?php submit_button( $item_to_edit ? 'Zaktualizuj' : 'Dodaj' ); ?>
</form>
</div>
</div>
</div>
<div id="col-right">
<div class="col-wrap">
<table class="wp-list-table widefat striped">
<thead><tr><th>Nazwa</th><th>Przebieg</th><th>Liczba aktywności</th><th>Status</th><th style="width: 220px;">Akcje</th></tr></thead>
<tbody>
<?php foreach ( $equipment_list as $item ) : ?>
<?php
$details_url = admin_url( 'admin.php?page=statpress-equipment-details&id=' . $item->id );
?>
<tr>
<td><strong><a href="<?php echo esc_url( $details_url ); ?>"><?php echo esc_html( $item->name ); ?></a></strong><br><small><?php echo esc_html( $item->type ); ?></small></td>
<td><?php echo $item->total_distance ? number_format( $item->total_distance, 2, ',', ' ' ) . ' km' : '0 km'; ?></td>
<td><?php echo (int) $item->activity_count; ?></td>
<td><?php echo esc_html( ucfirst( $item->status ) ); ?></td>
<td>
<a href="<?php echo esc_url( $details_url ); ?>" class="button button-small">Dziennik / Serwis</a>
<a href="<?php echo esc_url( add_query_arg( array( 'action' => 'edit', 'id' => $item->id ) ) ); ?>" class="button button-small">Edytuj</a>
<a href="<?php echo esc_url( wp_nonce_url( add_query_arg( array( 'action' => 'delete', 'id' => $item->id ) ), 'statpress_delete_equipment_' . $item->id ) ); ?>" onclick="return confirm('Czy na pewno chcesz usunąć ten sprzęt?')" class="button button-small button-link-delete">Usuń</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<?php
}
function statpress_equipment_details_page() {
global $wpdb;
$equipment_id = isset( $_GET['id'] ) ? intval( $_GET['id'] ) : 0;
if ( 0 === $equipment_id ) {
echo '<div class="wrap"><h1>Błąd</h1><p>Nie podano ID sprzętu.</p></div>';
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 '<div class="wrap"><h1>Błąd</h1><p>Nie znaleziono sprzętu o podanym ID.</p></div>';
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' );
?>
<div class="wrap">
<h1>Dziennik serwisowy: <?php echo esc_html( $equipment->name ); ?></h1>
<p><a href="<?php echo esc_url( admin_url( 'admin.php?page=statpress-equipment' ) ); ?>">&larr; Powrót do listy sprzętu</a></p>
<?php if ( ! empty( $message ) ) : ?>
<div class="notice <?php echo esc_attr( $notice_class ); ?> is-dismissible"><p><?php echo esc_html( $message ); ?></p></div>
<?php endif; ?>
<div class="postbox">
<div class="postbox-header"><h2 class="hndle">Podsumowanie Sprzętu</h2></div>
<div class="inside">
<div class="main">
<p><strong>Całkowity przebieg:</strong> <?php echo number_format( (float) $total_mileage, 2, ',', ' ' ); ?> km</p>
<?php if ( $total_service_cost > 0 ) : ?>
<p><strong>Całkowity koszt serwisu:</strong> <?php echo number_format( $total_service_cost, 2, ',', ' ' ); ?> zł</p>
<?php endif; ?>
<p><strong>Status:</strong> <?php echo esc_html( ucfirst( $equipment->status ) ); ?></p>
<?php if ( $equipment->purchase_date ) : ?><p><strong>Data zakupu:</strong> <?php echo esc_html( $equipment->purchase_date ); ?></p><?php endif; ?>
<?php if ( $equipment->initial_cost ) : ?><p><strong>Koszt zakupu:</strong> <?php echo number_format( $equipment->initial_cost, 2, ',', ' ' ); ?> zł</p><?php endif; ?>
<?php if ( $equipment->notes ) : ?><p><strong>Notatki:</strong><br><?php echo nl2br( esc_html( $equipment->notes ) ); ?></p><?php endif; ?>
</div>
</div>
</div>
<div id="col-container" class="wp-clearfix">
<div id="col-left">
<div class="col-wrap">
<div class="form-wrap">
<h2><?php echo $log_to_edit ? 'Edytuj wpis' : 'Dodaj wpis do dziennika'; ?></h2>
<form method="post">
<input type="hidden" name="log_id" value="<?php echo $log_to_edit ? esc_attr( $log_to_edit->id ) : '0'; ?>">
<?php wp_nonce_field( 'statpress_manage_equipment_log' ); ?>
<div class="form-field form-required"><label for="log_date">Data</label><input type="date" name="log_date" id="log_date" value="<?php echo $log_to_edit ? esc_attr( $log_to_edit->log_date ) : current_time( 'Y-m-d' ); ?>" required></div>
<div class="form-field form-required"><label for="log_type">Typ</label><select name="log_type" id="log_type" required><?php foreach ( $log_types as $type ) : ?><option value="<?php echo esc_attr( $type ); ?>" <?php if ( $log_to_edit ) {
selected( $log_to_edit->log_type, $type );} ?>><?php echo esc_html( $type ); ?></option><?php endforeach; ?></select></div>
<div class="form-field form-required"><label for="description">Opis</label><textarea name="description" id="description" rows="4" required><?php echo $log_to_edit ? esc_textarea( $log_to_edit->description ) : ''; ?></textarea></div>
<div class="form-field"><label for="cost">Koszt (zł)</label><input type="text" name="cost" id="cost" value="<?php echo $log_to_edit ? esc_attr( $log_to_edit->cost ) : ''; ?>" placeholder="0.00"></div>
<div class="form-field"><label for="mileage">Przebieg (km)</label><input type="number" name="mileage" id="mileage" value="<?php echo $log_to_edit ? esc_attr( $log_to_edit->mileage ) : round( (float) $total_mileage ); ?>"><p class="description">Przebieg sprzętu w momencie serwisu. Domyślnie aktualny całkowity przebieg.</p></div>
<?php submit_button( $log_to_edit ? 'Zaktualizuj wpis' : 'Dodaj wpis', 'primary', 'submit_log' ); ?>
<?php if ( $log_to_edit ) : ?><a href="<?php echo esc_url( remove_query_arg( array( 'action', 'log_id' ) ) ); ?>" class="button">Anuluj edycję</a><?php endif; ?>
</form>
</div>
</div>
</div>
<div id="col-right">
<div class="col-wrap">
<table class="wp-list-table widefat fixed striped">
<tfoot>
<tr>
<th colspan="3" style="text-align:right;">Suma kosztów:</th>
<th colspan="3"><strong><?php echo number_format( (float) $total_service_cost, 2, ',', ' ' ); ?> zł</strong></th>
</tr>
</tfoot>
<thead><tr><th>Data</th><th>Typ</th><th>Opis</th><th>Koszt</th><th>Przebieg</th><th style="width: 100px;">Akcje</th></tr></thead>
<tbody>
<?php if ( empty( $service_log ) ) : ?>
<tr><td colspan="6">Brak wpisów w dzienniku.</td></tr>
<?php else : ?>
<?php foreach ( $service_log as $log ) : ?>
<tr>
<td><?php echo esc_html( $log->log_date ); ?></td>
<td><?php echo esc_html( $log->log_type ); ?></td>
<td><?php echo nl2br( esc_html( $log->description ) ); ?></td>
<td><?php echo $log->cost ? number_format( $log->cost, 2, ',', ' ' ) . ' zł' : '-'; ?></td>
<td><?php echo $log->mileage ? number_format( $log->mileage, 0, '', ' ' ) . ' km' : '-'; ?></td>
<td>
<a href="<?php echo esc_url( add_query_arg( array( 'action' => 'edit_log', 'log_id' => $log->id ) ) ); ?>">Edytuj</a> |
<a href="<?php echo esc_url( wp_nonce_url( add_query_arg( array( 'action' => 'delete_log', 'log_id' => $log->id ) ), 'statpress_delete_log_' . $log->id ) ); ?>" onclick="return confirm('Czy na pewno chcesz usunąć ten wpis?')" style="color: #a00;">Usuń</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<?php
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
function statpress_equipment_page() {
global $wpdb;
$table_equipment = $wpdb->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"
);
?>
<div class="wrap">
<h1>Zarządzaj Sprzętem</h1>
<?php if ( ! empty( $message ) ) : ?>
<div class="notice <?php echo esc_attr( $notice_class ); ?> is-dismissible"><p><?php echo esc_html( $message ); ?></p></div>
<?php endif; ?>
<div id="col-container" class="wp-clearfix">
<div id="col-left">
<div class="col-wrap">
<div class="form-wrap">
<h2><?php echo $item_to_edit ? 'Edytuj pozycję' : 'Dodaj nowy sprzęt'; ?></h2>
<form method="post">
<input type="hidden" name="equipment_id" value="<?php echo $item_to_edit ? esc_attr( $item_to_edit->id ) : '0'; ?>">
<?php wp_nonce_field( 'statpress_manage_equipment' ); ?>
<div class="form-field form-required"><label for="equipment_name">Nazwa</label><input type="text" name="equipment_name" id="equipment_name" value="<?php echo $item_to_edit ? esc_attr( $item_to_edit->name ) : ''; ?>" required></div>
<div class="form-field"><label for="equipment_type">Typ</label><input type="text" name="equipment_type" id="equipment_type" value="<?php echo $item_to_edit ? esc_attr( $item_to_edit->type ) : 'Rower'; ?>"></div>
<div class="form-field"><label for="purchase_date">Data zakupu</label><input type="date" name="purchase_date" id="purchase_date" value="<?php echo $item_to_edit ? esc_attr( $item_to_edit->purchase_date ) : ''; ?>"></div>
<div class="form-field"><label for="initial_cost">Koszt zakupu</label><input type="text" name="initial_cost" id="initial_cost" value="<?php echo $item_to_edit ? esc_attr( $item_to_edit->initial_cost ) : ''; ?>" placeholder="0.00"></div>
<div class="form-field"><label for="status">Status</label><select name="status" id="status"><?php foreach ( $statuses as $s ) : ?><option value="<?php echo esc_attr( $s ); ?>" <?php if ( $item_to_edit ) {
selected( $item_to_edit->status, $s );} ?>><?php echo esc_html( ucfirst( $s ) ); ?></option><?php endforeach; ?></select></div>
<div class="form-field"><label for="notes">Notatki</label><textarea name="notes" id="notes" rows="3"><?php echo $item_to_edit ? esc_textarea( $item_to_edit->notes ) : ''; ?></textarea></div>
<?php submit_button( $item_to_edit ? 'Zaktualizuj' : 'Dodaj' ); ?>
</form>
</div>
</div>
</div>
<div id="col-right">
<div class="col-wrap">
<table class="wp-list-table widefat striped">
<thead><tr><th>Nazwa</th><th>Przebieg</th><th>Liczba aktywności</th><th>Status</th><th style="width: 220px;">Akcje</th></tr></thead>
<tbody>
<?php foreach ( $equipment_list as $item ) : ?>
<?php
$details_url = admin_url( 'admin.php?page=statpress-equipment-details&id=' . $item->id );
?>
<tr>
<td><strong><a href="<?php echo esc_url( $details_url ); ?>"><?php echo esc_html( $item->name ); ?></a></strong><br><small><?php echo esc_html( $item->type ); ?></small></td>
<td><?php echo $item->total_distance ? number_format( $item->total_distance, 2, ',', ' ' ) . ' km' : '0 km'; ?></td>
<td><?php echo (int) $item->activity_count; ?></td>
<td><?php echo esc_html( ucfirst( $item->status ) ); ?></td>
<td>
<a href="<?php echo esc_url( $details_url ); ?>" class="button button-small">Dziennik / Serwis</a>
<a href="<?php echo esc_url( add_query_arg( array( 'action' => 'edit', 'id' => $item->id ) ) ); ?>" class="button button-small">Edytuj</a>
<a href="<?php echo esc_url( wp_nonce_url( add_query_arg( array( 'action' => 'delete', 'id' => $item->id ) ), 'statpress_delete_equipment_' . $item->id ) ); ?>" onclick="return confirm('Czy na pewno chcesz usunąć ten sprzęt?')" class="button button-small button-link-delete">Usuń</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<?php
}
function statpress_equipment_details_page() {
global $wpdb;
$equipment_id = isset( $_GET['id'] ) ? intval( $_GET['id'] ) : 0;
if ( 0 === $equipment_id ) {
echo '<div class="wrap"><h1>Błąd</h1><p>Nie podano ID sprzętu.</p></div>';
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 '<div class="wrap"><h1>Błąd</h1><p>Nie znaleziono sprzętu o podanym ID.</p></div>';
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' );
?>
<div class="wrap">
<h1>Dziennik serwisowy: <?php echo esc_html( $equipment->name ); ?></h1>
<p><a href="<?php echo esc_url( admin_url( 'admin.php?page=statpress-equipment' ) ); ?>">&larr; Powrót do listy sprzętu</a></p>
<?php if ( ! empty( $message ) ) : ?>
<div class="notice <?php echo esc_attr( $notice_class ); ?> is-dismissible"><p><?php echo esc_html( $message ); ?></p></div>
<?php endif; ?>
<div class="postbox">
<div class="postbox-header"><h2 class="hndle">Podsumowanie Sprzętu</h2></div>
<div class="inside">
<div class="main">
<p><strong>Całkowity przebieg:</strong> <?php echo number_format( (float) $total_mileage, 2, ',', ' ' ); ?> km</p>
<?php if ( $total_service_cost > 0 ) : ?>
<p><strong>Całkowity koszt serwisu:</strong> <?php echo number_format( $total_service_cost, 2, ',', ' ' ); ?> zł</p>
<?php endif; ?>
<p><strong>Status:</strong> <?php echo esc_html( ucfirst( $equipment->status ) ); ?></p>
<?php if ( $equipment->purchase_date ) : ?><p><strong>Data zakupu:</strong> <?php echo esc_html( $equipment->purchase_date ); ?></p><?php endif; ?>
<?php if ( $equipment->initial_cost ) : ?><p><strong>Koszt zakupu:</strong> <?php echo number_format( $equipment->initial_cost, 2, ',', ' ' ); ?> zł</p><?php endif; ?>
<?php if ( $equipment->notes ) : ?><p><strong>Notatki:</strong><br><?php echo nl2br( esc_html( $equipment->notes ) ); ?></p><?php endif; ?>
</div>
</div>
</div>
<div id="col-container" class="wp-clearfix">
<div id="col-left">
<div class="col-wrap">
<div class="form-wrap">
<h2><?php echo $log_to_edit ? 'Edytuj wpis' : 'Dodaj wpis do dziennika'; ?></h2>
<form method="post">
<input type="hidden" name="log_id" value="<?php echo $log_to_edit ? esc_attr( $log_to_edit->id ) : '0'; ?>">
<?php wp_nonce_field( 'statpress_manage_equipment_log' ); ?>
<div class="form-field form-required"><label for="log_date">Data</label><input type="date" name="log_date" id="log_date" value="<?php echo $log_to_edit ? esc_attr( $log_to_edit->log_date ) : current_time( 'Y-m-d' ); ?>" required></div>
<div class="form-field form-required"><label for="log_type">Typ</label><select name="log_type" id="log_type" required><?php foreach ( $log_types as $type ) : ?><option value="<?php echo esc_attr( $type ); ?>" <?php if ( $log_to_edit ) {
selected( $log_to_edit->log_type, $type );} ?>><?php echo esc_html( $type ); ?></option><?php endforeach; ?></select></div>
<div class="form-field form-required"><label for="description">Opis</label><textarea name="description" id="description" rows="4" required><?php echo $log_to_edit ? esc_textarea( $log_to_edit->description ) : ''; ?></textarea></div>
<div class="form-field"><label for="cost">Koszt (zł)</label><input type="text" name="cost" id="cost" value="<?php echo $log_to_edit ? esc_attr( $log_to_edit->cost ) : ''; ?>" placeholder="0.00"></div>
<div class="form-field"><label for="mileage">Przebieg (km)</label><input type="number" name="mileage" id="mileage" value="<?php echo $log_to_edit ? esc_attr( $log_to_edit->mileage ) : round( (float) $total_mileage ); ?>"><p class="description">Przebieg sprzętu w momencie serwisu. Domyślnie aktualny całkowity przebieg.</p></div>
<?php submit_button( $log_to_edit ? 'Zaktualizuj wpis' : 'Dodaj wpis', 'primary', 'submit_log' ); ?>
<?php if ( $log_to_edit ) : ?><a href="<?php echo esc_url( remove_query_arg( array( 'action', 'log_id' ) ) ); ?>" class="button">Anuluj edycję</a><?php endif; ?>
</form>
</div>
</div>
</div>
<div id="col-right">
<div class="col-wrap">
<table class="wp-list-table widefat fixed striped">
<tfoot>
<tr>
<th colspan="3" style="text-align:right;">Suma kosztów:</th>
<th colspan="3"><strong><?php echo number_format( (float) $total_service_cost, 2, ',', ' ' ); ?> zł</strong></th>
</tr>
</tfoot>
<thead><tr><th>Data</th><th>Typ</th><th>Opis</th><th>Koszt</th><th>Przebieg</th><th style="width: 100px;">Akcje</th></tr></thead>
<tbody>
<?php if ( empty( $service_log ) ) : ?>
<tr><td colspan="6">Brak wpisów w dzienniku.</td></tr>
<?php else : ?>
<?php foreach ( $service_log as $log ) : ?>
<tr>
<td><?php echo esc_html( $log->log_date ); ?></td>
<td><?php echo esc_html( $log->log_type ); ?></td>
<td><?php echo nl2br( esc_html( $log->description ) ); ?></td>
<td><?php echo $log->cost ? number_format( $log->cost, 2, ',', ' ' ) . ' zł' : '-'; ?></td>
<td><?php echo $log->mileage ? number_format( $log->mileage, 0, '', ' ' ) . ' km' : '-'; ?></td>
<td>
<a href="<?php echo esc_url( add_query_arg( array( 'action' => 'edit_log', 'log_id' => $log->id ) ) ); ?>">Edytuj</a> |
<a href="<?php echo esc_url( wp_nonce_url( add_query_arg( array( 'action' => 'delete_log', 'log_id' => $log->id ) ), 'statpress_delete_log_' . $log->id ) ); ?>" onclick="return confirm('Czy na pewno chcesz usunąć ten wpis?')" style="color: #a00;">Usuń</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<?php
}
+87 -87
View File
@@ -1,88 +1,88 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
function statpress_event_types_page() {
global $wpdb;
$table_event_types = $wpdb->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" );
?>
<div class="wrap">
<h1>Typy Wydarzeń</h1>
<?php if ( ! empty( $message ) ) : ?>
<div class="notice <?php echo esc_attr( $notice_class ); ?> is-dismissible"><p><?php echo esc_html( $message ); ?></p></div>
<?php endif; ?>
<div id="col-container" class="wp-clearfix">
<div id="col-left">
<div class="col-wrap">
<div class="form-wrap">
<h2><?php echo $item_to_edit ? 'Edytuj typ wydarzenia' : 'Dodaj nowy typ wydarzenia'; ?></h2>
<form method="post">
<input type="hidden" name="event_type_id" value="<?php echo $item_to_edit ? esc_attr( $item_to_edit->id ) : '0'; ?>">
<?php wp_nonce_field( 'statpress_manage_event_type' ); ?>
<div class="form-field">
<label for="event_type_name">Nazwa</label>
<input type="text" name="event_type_name" id="event_type_name" value="<?php echo $item_to_edit ? esc_attr( $item_to_edit->name ) : ''; ?>" required>
</div>
<?php submit_button( $item_to_edit ? 'Zaktualizuj' : 'Dodaj' ); ?>
</form>
</div>
</div>
</div>
<div id="col-right">
<div class="col-wrap">
<table class="wp-list-table widefat fixed striped">
<thead><tr><th>Nazwa</th><th style="width: 100px;">Akcje</th></tr></thead>
<tbody>
<?php foreach ( $event_types as $type ) : ?>
<tr><td><?php echo esc_html( $type->name ); ?></td><td><a href="<?php echo esc_url( add_query_arg( array( 'action' => 'edit', 'id' => $type->id ) ) ); ?>">Edytuj</a> | <a href="<?php echo esc_url( wp_nonce_url( add_query_arg( array( 'action' => 'delete', 'id' => $type->id ) ), 'statpress_delete_event_type_' . $type->id ) ); ?>" onclick="return confirm('Czy na pewno chcesz usunąć ten typ?')" style="color: #a00;">Usuń</a></td></tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<?php
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
function statpress_event_types_page() {
global $wpdb;
$table_event_types = $wpdb->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" );
?>
<div class="wrap">
<h1>Typy Wydarzeń</h1>
<?php if ( ! empty( $message ) ) : ?>
<div class="notice <?php echo esc_attr( $notice_class ); ?> is-dismissible"><p><?php echo esc_html( $message ); ?></p></div>
<?php endif; ?>
<div id="col-container" class="wp-clearfix">
<div id="col-left">
<div class="col-wrap">
<div class="form-wrap">
<h2><?php echo $item_to_edit ? 'Edytuj typ wydarzenia' : 'Dodaj nowy typ wydarzenia'; ?></h2>
<form method="post">
<input type="hidden" name="event_type_id" value="<?php echo $item_to_edit ? esc_attr( $item_to_edit->id ) : '0'; ?>">
<?php wp_nonce_field( 'statpress_manage_event_type' ); ?>
<div class="form-field">
<label for="event_type_name">Nazwa</label>
<input type="text" name="event_type_name" id="event_type_name" value="<?php echo $item_to_edit ? esc_attr( $item_to_edit->name ) : ''; ?>" required>
</div>
<?php submit_button( $item_to_edit ? 'Zaktualizuj' : 'Dodaj' ); ?>
</form>
</div>
</div>
</div>
<div id="col-right">
<div class="col-wrap">
<table class="wp-list-table widefat fixed striped">
<thead><tr><th>Nazwa</th><th style="width: 100px;">Akcje</th></tr></thead>
<tbody>
<?php foreach ( $event_types as $type ) : ?>
<tr><td><?php echo esc_html( $type->name ); ?></td><td><a href="<?php echo esc_url( add_query_arg( array( 'action' => 'edit', 'id' => $type->id ) ) ); ?>">Edytuj</a> | <a href="<?php echo esc_url( wp_nonce_url( add_query_arg( array( 'action' => 'delete', 'id' => $type->id ) ), 'statpress_delete_event_type_' . $type->id ) ); ?>" onclick="return confirm('Czy na pewno chcesz usunąć ten typ?')" style="color: #a00;">Usuń</a></td></tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<?php
}
+198 -198
View File
@@ -1,199 +1,199 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Calculates the current progress for a given goal.
*
* @param object $goal The goal object from the database.
* @return array An array containing 'current_value' and 'percentage'.
*/
function statpress_get_goal_progress( $goal ) {
global $wpdb;
$table_activities = $wpdb->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" );
?>
<div class="wrap">
<h1>Zarządzaj Celami</h1>
<?php if ( ! empty( $message ) ) : ?>
<div class="notice <?php echo esc_attr( $notice_class ); ?> is-dismissible"><p><?php echo esc_html( $message ); ?></p></div>
<?php endif; ?>
<div id="col-container" class="wp-clearfix">
<div id="col-left">
<div class="col-wrap">
<div class="form-wrap">
<h2><?php echo $item_to_edit ? 'Edytuj cel' : 'Dodaj nowy cel'; ?></h2>
<form method="post">
<input type="hidden" name="goal_id" value="<?php echo $item_to_edit ? esc_attr( $item_to_edit->id ) : '0'; ?>">
<?php wp_nonce_field( 'statpress_manage_goal' ); ?>
<div class="form-field form-required"><label for="goal_name">Nazwa celu</label><input type="text" name="goal_name" id="goal_name" value="<?php echo $item_to_edit ? esc_attr( $item_to_edit->name ) : ''; ?>" required></div>
<div class="form-field form-required"><label for="goal_type">Typ celu</label><select name="goal_type" id="goal_type" required><option value="distance" <?php if ( $item_to_edit ) { selected( $item_to_edit->goal_type, 'distance' ); } ?>>Dystans (km)</option><option value="duration_sec" <?php if ( $item_to_edit ) { selected( $item_to_edit->goal_type, 'duration_sec' ); } ?>>Czas (w godzinach)</option><option value="count" <?php if ( $item_to_edit ) { selected( $item_to_edit->goal_type, 'count' ); } ?>>Liczba aktywności</option></select></div>
<div class="form-field form-required"><label for="target_value">Wartość docelowa</label><input type="text" name="target_value" id="target_value" value="<?php echo $item_to_edit ? esc_attr( 'duration_sec' === $item_to_edit->goal_type ? $item_to_edit->target_value / 3600 : $item_to_edit->target_value ) : ''; ?>" required><p>Dla czasu podaj wartość w godzinach (np. 100.5).</p></div>
<div class="form-field form-required"><label for="year">Rok</label><input type="number" name="year" id="year" value="<?php echo $item_to_edit ? esc_attr( $item_to_edit->year ) : current_time( 'Y' ); ?>" required></div>
<div class="form-field"><label for="month">Miesiąc</label><select name="month" id="month"><option value="all">Cały rok</option><?php for ( $i = 1; $i <= 12; $i++ ) : ?><option value="<?php echo $i; ?>" <?php if ( $item_to_edit ) { selected( $item_to_edit->month, $i ); } ?>><?php echo date_i18n( 'F', mktime( 0, 0, 0, $i, 10 ) ); ?></option><?php endfor; ?></select></div>
<div class="form-field"><label for="category_id">Kategoria</label><select name="category_id" id="category_id"><option value="all">Wszystkie</option><?php foreach ( $categories as $cat ) : ?><option value="<?php echo esc_attr( $cat->id ); ?>" <?php if ( $item_to_edit ) { selected( $item_to_edit->category_id, $cat->id ); } ?>><?php echo esc_html( $cat->name ); ?></option><?php endforeach; ?></select></div>
<?php submit_button( $item_to_edit ? 'Zaktualizuj' : 'Dodaj' ); ?>
</form>
</div>
</div>
</div>
<div id="col-right">
<div class="col-wrap">
<table class="wp-list-table widefat fixed striped">
<thead><tr><th>Cel</th><th>Postęp</th><th style="width: 100px;">Akcje</th></tr></thead>
<tbody>
<?php if ( empty( $goals ) ) : ?>
<tr><td colspan="3">Brak zdefiniowanych celów.</td></tr>
<?php else : ?>
<?php foreach ( $goals as $goal ) : ?>
<?php
$progress = statpress_get_goal_progress( $goal );
$percentage = min( 100, $progress['percentage'] );
$target_formatted = '';
$current_formatted = '';
if ( 'duration_sec' === $goal->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'];
}
?>
<tr>
<td>
<strong><?php echo esc_html( $goal->name ); ?></strong><br>
<small>
<?php echo esc_html( $goal->year ); ?>
<?php if ( $goal->month ) { echo ' / ' . date_i18n( 'F', mktime( 0, 0, 0, $goal->month, 10 ) ); } ?>
<?php if ( $goal->category_name ) { echo ' / ' . esc_html( $goal->category_name ); } ?>
</small>
</td>
<td>
<div class="statpress-progress-bar-container">
<div class="statpress-progress-bar" style="width: <?php echo esc_attr( $percentage ); ?>%;"></div>
</div>
<small><?php echo $current_formatted; ?> z <?php echo $target_formatted; ?> (<?php echo round( $percentage, 1 ); ?>%)</small>
</td>
<td>
<a href="<?php echo esc_url( add_query_arg( array( 'action' => 'edit', 'id' => $goal->id ) ) ); ?>">Edytuj</a> |
<a href="<?php echo esc_url( wp_nonce_url( add_query_arg( array( 'action' => 'delete', 'id' => $goal->id ) ), 'statpress_delete_goal_' . $goal->id ) ); ?>" onclick="return confirm('Czy na pewno chcesz usunąć ten cel?')" style="color: #a00;">Usuń</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<style>
.statpress-progress-bar-container { background: #eee; border-radius: 4px; height: 12px; width: 100%; overflow: hidden; margin-bottom: 4px; }
.statpress-progress-bar { background: #3498db; height: 100%; border-radius: 4px; transition: width 0.5s ease-in-out; }
</style>
<?php
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Calculates the current progress for a given goal.
*
* @param object $goal The goal object from the database.
* @return array An array containing 'current_value' and 'percentage'.
*/
function statpress_get_goal_progress( $goal ) {
global $wpdb;
$table_activities = $wpdb->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" );
?>
<div class="wrap">
<h1>Zarządzaj Celami</h1>
<?php if ( ! empty( $message ) ) : ?>
<div class="notice <?php echo esc_attr( $notice_class ); ?> is-dismissible"><p><?php echo esc_html( $message ); ?></p></div>
<?php endif; ?>
<div id="col-container" class="wp-clearfix">
<div id="col-left">
<div class="col-wrap">
<div class="form-wrap">
<h2><?php echo $item_to_edit ? 'Edytuj cel' : 'Dodaj nowy cel'; ?></h2>
<form method="post">
<input type="hidden" name="goal_id" value="<?php echo $item_to_edit ? esc_attr( $item_to_edit->id ) : '0'; ?>">
<?php wp_nonce_field( 'statpress_manage_goal' ); ?>
<div class="form-field form-required"><label for="goal_name">Nazwa celu</label><input type="text" name="goal_name" id="goal_name" value="<?php echo $item_to_edit ? esc_attr( $item_to_edit->name ) : ''; ?>" required></div>
<div class="form-field form-required"><label for="goal_type">Typ celu</label><select name="goal_type" id="goal_type" required><option value="distance" <?php if ( $item_to_edit ) { selected( $item_to_edit->goal_type, 'distance' ); } ?>>Dystans (km)</option><option value="duration_sec" <?php if ( $item_to_edit ) { selected( $item_to_edit->goal_type, 'duration_sec' ); } ?>>Czas (w godzinach)</option><option value="count" <?php if ( $item_to_edit ) { selected( $item_to_edit->goal_type, 'count' ); } ?>>Liczba aktywności</option></select></div>
<div class="form-field form-required"><label for="target_value">Wartość docelowa</label><input type="text" name="target_value" id="target_value" value="<?php echo $item_to_edit ? esc_attr( 'duration_sec' === $item_to_edit->goal_type ? $item_to_edit->target_value / 3600 : $item_to_edit->target_value ) : ''; ?>" required><p>Dla czasu podaj wartość w godzinach (np. 100.5).</p></div>
<div class="form-field form-required"><label for="year">Rok</label><input type="number" name="year" id="year" value="<?php echo $item_to_edit ? esc_attr( $item_to_edit->year ) : current_time( 'Y' ); ?>" required></div>
<div class="form-field"><label for="month">Miesiąc</label><select name="month" id="month"><option value="all">Cały rok</option><?php for ( $i = 1; $i <= 12; $i++ ) : ?><option value="<?php echo $i; ?>" <?php if ( $item_to_edit ) { selected( $item_to_edit->month, $i ); } ?>><?php echo date_i18n( 'F', mktime( 0, 0, 0, $i, 10 ) ); ?></option><?php endfor; ?></select></div>
<div class="form-field"><label for="category_id">Kategoria</label><select name="category_id" id="category_id"><option value="all">Wszystkie</option><?php foreach ( $categories as $cat ) : ?><option value="<?php echo esc_attr( $cat->id ); ?>" <?php if ( $item_to_edit ) { selected( $item_to_edit->category_id, $cat->id ); } ?>><?php echo esc_html( $cat->name ); ?></option><?php endforeach; ?></select></div>
<?php submit_button( $item_to_edit ? 'Zaktualizuj' : 'Dodaj' ); ?>
</form>
</div>
</div>
</div>
<div id="col-right">
<div class="col-wrap">
<table class="wp-list-table widefat fixed striped">
<thead><tr><th>Cel</th><th>Postęp</th><th style="width: 100px;">Akcje</th></tr></thead>
<tbody>
<?php if ( empty( $goals ) ) : ?>
<tr><td colspan="3">Brak zdefiniowanych celów.</td></tr>
<?php else : ?>
<?php foreach ( $goals as $goal ) : ?>
<?php
$progress = statpress_get_goal_progress( $goal );
$percentage = min( 100, $progress['percentage'] );
$target_formatted = '';
$current_formatted = '';
if ( 'duration_sec' === $goal->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'];
}
?>
<tr>
<td>
<strong><?php echo esc_html( $goal->name ); ?></strong><br>
<small>
<?php echo esc_html( $goal->year ); ?>
<?php if ( $goal->month ) { echo ' / ' . date_i18n( 'F', mktime( 0, 0, 0, $goal->month, 10 ) ); } ?>
<?php if ( $goal->category_name ) { echo ' / ' . esc_html( $goal->category_name ); } ?>
</small>
</td>
<td>
<div class="statpress-progress-bar-container">
<div class="statpress-progress-bar" style="width: <?php echo esc_attr( $percentage ); ?>%;"></div>
</div>
<small><?php echo $current_formatted; ?> z <?php echo $target_formatted; ?> (<?php echo round( $percentage, 1 ); ?>%)</small>
</td>
<td>
<a href="<?php echo esc_url( add_query_arg( array( 'action' => 'edit', 'id' => $goal->id ) ) ); ?>">Edytuj</a> |
<a href="<?php echo esc_url( wp_nonce_url( add_query_arg( array( 'action' => 'delete', 'id' => $goal->id ) ), 'statpress_delete_goal_' . $goal->id ) ); ?>" onclick="return confirm('Czy na pewno chcesz usunąć ten cel?')" style="color: #a00;">Usuń</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<style>
.statpress-progress-bar-container { background: #eee; border-radius: 4px; height: 12px; width: 100%; overflow: hidden; margin-bottom: 4px; }
.statpress-progress-bar { background: #3498db; height: 100%; border-radius: 4px; transition: width 0.5s ease-in-out; }
</style>
<?php
}
+308 -308
View File
@@ -1,309 +1,309 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
function statpress_import_csv_page() {
echo '<div class="wrap"><h1>Importuj aktywności z pliku CSV</h1>';
// Handle the form submission
if ( 'POST' === $_SERVER['REQUEST_METHOD'] && isset( $_POST['statpress_csv_import_nonce_field'] ) ) {
statpress_handle_csv_import();
}
// Display the form
?>
<div class="postbox">
<div class="postbox-header"><h2 class="hndle">Instrukcje i formularz importu</h2></div>
<div class="inside">
<p>Aby zaimportować dane, możesz wgrać plik CSV <strong>LUB</strong> wkleić dane bezpośrednio w pole tekstowe poniżej. Ta druga opcja jest zalecana, jeśli napotykasz błędy z plikiem.</p>
<p><strong>Wymagane kolumny:</strong> <code>Data</code>, <code>Tytuł</code>, <code>Dystans</code> oraz <code>Typ aktywności</code> (lub <code>Kategoria</code>).</p>
<p><strong>Opcjonalne kolumny:</strong> <code>Czas</code> (format HH:MM:SS), <code>Kalorie</code>, <code>Średnie tętno</code>, <code>Maksymalne tętno</code>, <code>Średnia prędkość</code>, <code>Maksymalna prędkość</code>, <code>Średni rytm pedałowania</code>, <code>Maksymalny rytm pedałowania</code>, <code>Całkowity wznios</code>, <code>Całkowity spadek</code>, <code>Minimalna wysokość</code>, <code>Maksymalna wysokość</code>, <code>Sprzęt</code>, <code>Typ wydarzenia</code>.</p>
<p><strong>Ważne:</strong>
<ul>
<li>Data musi być w formacie <code>YYYY-MM-DD</code>.</li>
<li>Dystans i prędkość: użyj kropki jako separatora dziesiętnego (np. <code>10.5</code>).</li>
<li>Nazwy w kolumnach <code>Typ aktywności</code>, <code>Sprzęt</code>, <code>Typ wydarzenia</code> muszą dokładnie odpowiadać nazwom zdefiniowanym w ustawieniach wtyczki. Jeśli nazwa nie zostanie znaleziona, wiersz zostanie pominięty.</li>
</ul>
</p>
<hr>
<form method="post" enctype="multipart/form-data">
<?php wp_nonce_field( 'statpress_csv_import_nonce', 'statpress_csv_import_nonce_field' ); ?>
<table class="form-table">
<tr valign="top">
<th scope="row"><label for="statpress_csv_file">Opcja 1: Wgraj plik CSV</label></th>
<td><input type="file" id="statpress_csv_file" name="statpress_csv_file" accept=".csv,text/csv" /></td>
</tr>
<tr valign="top">
<th scope="row"><label for="statpress_csv_data">Opcja 2: Wklej dane CSV</label></th>
<td><textarea name="statpress_csv_data" id="statpress_csv_data" rows="15" class="large-text" placeholder="Wklej tutaj zawartość swojego pliku CSV...&#10;Typ aktywności,Data,Tytuł,Dystans,...&#10;Rower,2025-01-01,Nowy Rok,10.5,..."></textarea></td>
</tr>
</table>
<?php submit_button( 'Importuj' ); ?>
</form>
</div>
</div>
<?php
echo '</div>';
}
function statpress_handle_csv_import() {
global $wpdb;
if ( ! isset( $_POST['statpress_csv_import_nonce_field'] ) || ! wp_verify_nonce( $_POST['statpress_csv_import_nonce_field'], 'statpress_csv_import_nonce' ) ) {
echo '<div class="notice notice-error"><p>Błąd weryfikacji bezpieczeństwa.</p></div>';
return;
}
if ( ! current_user_can( 'manage_options' ) ) {
echo '<div class="notice notice-error"><p>Nie masz wystarczających uprawnień.</p></div>';
return;
}
// Unify input source: prefer textarea, fall back to file upload.
$csv_content = '';
if ( ! empty( $_POST['statpress_csv_data'] ) ) {
$csv_content = stripslashes( $_POST['statpress_csv_data'] );
} elseif ( ! empty( $_FILES['statpress_csv_file']['tmp_name'] ) && UPLOAD_ERR_OK === $_FILES['statpress_csv_file']['error'] ) {
$csv_content = file_get_contents( $_FILES['statpress_csv_file']['tmp_name'] );
}
if ( empty( trim( $csv_content ) ) ) {
echo '<div class="notice notice-error"><p>Nie podano danych do importu. Wgraj plik lub wklej dane w pole tekstowe.</p></div>';
return;
}
// Mapowanie polskich i angielskich nazw kolumn na wewnętrzne klucze
$column_map = array(
// Polish => English
'typ aktywności' => 'category_name',
'data' => 'date',
'tytuł' => 'title',
'dystans' => 'distance',
'kalorie' => 'calories',
'czas' => 'duration',
'średnie tętno' => 'avg_heart_rate',
'maksymalne tętno' => 'max_heart_rate',
'średnia prędkość' => 'avg_speed',
'maksymalna prędkość' => 'max_speed',
'średni rytm pedałowania' => 'avg_cadence',
'maksymalny rytm pedałowania' => 'max_cadence',
'całkowity wznios' => 'total_elevation_gain',
'całkowity spadek' => 'total_elevation_loss',
'minimalna wysokość' => 'min_altitude',
'maksymalna wysokość' => 'max_altitude',
'sprzęt' => 'equipment_name',
'typ wydarzenia' => 'event_type_name',
'komentarz' => 'comment',
'link do strava' => 'strava_url',
// English keys for compatibility
'category_name' => 'category_name',
'date' => 'date',
'title' => 'title',
'distance' => 'distance',
'calories' => 'calories',
'duration' => 'duration',
'avg_heart_rate' => 'avg_heart_rate',
'max_heart_rate' => 'max_heart_rate',
'avg_speed' => 'avg_speed',
'max_speed' => 'max_speed',
'avg_cadence' => 'avg_cadence',
'max_cadence' => 'max_cadence',
'total_elevation_gain' => 'total_elevation_gain',
'total_elevation_loss' => 'total_elevation_loss',
'min_altitude' => 'min_altitude',
'max_altitude' => 'max_altitude',
'equipment_name' => 'equipment_name',
'event_type_name' => 'event_type_name',
'comment' => 'comment',
'strava_url' => 'strava_url',
);
// --- START: Robust, case-insensitive lookup ---
$table_categories = $wpdb->prefix . 'statpress_categories';
$table_event_types = $wpdb->prefix . 'statpress_event_types';
$table_equipment = $wpdb->prefix . 'statpress_equipment';
$create_lookup = function( $table_name ) use ( $wpdb ) {
$items = $wpdb->get_results( "SELECT id, name FROM {$table_name}" );
$lookup = array();
if ( is_array( $items ) ) {
foreach ( $items as $item ) {
// Use a robust trim to handle various whitespace characters and make it case-insensitive
$clean_name = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $item->name );
$lookup[ mb_strtolower( $clean_name, 'UTF-8' ) ] = $item->id;
}
}
return $lookup;
};
$categories_lookup = $create_lookup( $table_categories );
$event_types_lookup = $create_lookup( $table_event_types );
$equipment_lookup = $create_lookup( $table_equipment );
// --- END: Robust, case-insensitive lookup ---
// Process the CSV file
$table_activities = $wpdb->prefix . 'statpress_activities';
$imported_count = 0;
$skipped_details = array();
$row_number = 1; // Header is row 1
// Normalize line endings and split into lines
$lines = str_replace( array( "\r\n", "\r" ), "\n", $csv_content );
$lines = explode( "\n", $lines );
if ( empty( $lines ) || empty( trim( $lines[0] ) ) ) {
echo '<div class="notice notice-info"><p>Podane dane CSV są puste.</p></div>';
return;
}
// --- Delimiter and BOM detection ---
$first_line = $lines[0];
$delimiter = ( substr_count( $first_line, ';' ) > substr_count( $first_line, ',' ) ) ? ';' : ',';
// --- BOM removal from first header element ---
$bom = "\xEF\xBB\xBF";
if ( substr( $first_line, 0, 3 ) === $bom ) {
$lines[0] = substr( $first_line, 3 );
}
$header_raw = str_getcsv( array_shift( $lines ), $delimiter );
$header_raw = array_map( 'trim', $header_raw );
// Translate header from Polish/English to internal keys
$header = array();
foreach ( $header_raw as $col ) {
$header[] = $column_map[ strtolower( $col ) ] ?? 'ignored_' . uniqid();
}
$required_internal_keys = array( 'date', 'title', 'category_name', 'distance' );
$missing_keys = array_diff( $required_internal_keys, $header );
if ( ! empty( $missing_keys ) ) {
$key_to_polish_map = array(
'date' => 'Data',
'title' => 'Tytuł',
'category_name' => 'Kategoria / Typ aktywności',
'distance' => 'Dystans',
);
$missing_polish_names = array_map( fn( $key ) => $key_to_polish_map[ $key ] ?? $key, $missing_keys );
echo '<div class="notice notice-error"><p>Brak wymaganych kolumn w danych CSV: ' . esc_html( implode( ', ', $missing_polish_names ) ) . '</p></div>';
return;
}
$parse_and_round_int = fn( $val ) => round( floatval( str_replace( ',', '.', $val ) ) );
$null_if_empty = fn( $value ) => '' !== $value ? $value : null;
foreach ( $lines as $line ) {
$row_number++;
if ( empty( trim( $line ) ) ) {
continue; // Skip empty lines
}
$data = str_getcsv( $line, $delimiter );
if ( count( $data ) !== count( $header ) ) {
$skipped_details[] = array(
'row' => $row_number,
'reason' => 'Nieprawidłowa liczba kolumn (oczekiwano ' . count( $header ) . ', otrzymano ' . count( $data ) . ').',
'data' => $line,
);
continue;
}
$row_data = array_combine( $header, $data );
// Detailed validation
$validation_errors = array();
if ( empty( $row_data['date'] ) ) {
$validation_errors[] = 'brak daty';
}
if ( empty( $row_data['title'] ) ) {
$validation_errors[] = 'brak tytułu';
}
if ( ! isset( $row_data['distance'] ) || '' === $row_data['distance'] ) {
$validation_errors[] = 'brak dystansu';
}
$category_name = $row_data['category_name'] ?? '';
$clean_category_name = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $category_name );
$category_id = $categories_lookup[ mb_strtolower( $clean_category_name, 'UTF-8' ) ] ?? null;
if ( empty( $clean_category_name ) ) {
$validation_errors[] = 'brak nazwy kategorii';
} elseif ( is_null( $category_id ) ) {
$available_categories_from_db = $wpdb->get_col( "SELECT name FROM $table_categories ORDER BY name" );
$validation_errors[] = 'nieznana kategoria: "' . esc_html( $category_name ) . '". Sprawdź, czy nazwa jest poprawna. Dostępne w bazie: "' . esc_html( implode( '", "', $available_categories_from_db ) ) . '".';
}
if ( ! empty( $validation_errors ) ) {
$skipped_details[] = array(
'row' => $row_number,
'reason' => ucfirst( implode( ', ', $validation_errors ) ) . '.',
'data' => $line,
);
continue;
}
// Get IDs for optional fields using the same robust method
$get_id = function( $name, $lookup_table ) {
$clean_name = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $name );
return $lookup_table[ mb_strtolower( $clean_name, 'UTF-8' ) ] ?? null;
};
$equipment_id = $get_id( $row_data['equipment_name'] ?? '', $equipment_lookup );
$event_type_id = $get_id( $row_data['event_type_name'] ?? '', $event_types_lookup );
$insert_data = array(
'date' => sanitize_text_field( $row_data['date'] ),
'title' => sanitize_text_field( $row_data['title'] ),
'category_id' => $category_id,
'distance' => floatval( str_replace( ',', '.', $row_data['distance'] ) ),
'duration' => isset( $row_data['duration'] ) ? sanitize_text_field( $row_data['duration'] ) : '00:00:00',
'calories' => isset( $row_data['calories'] ) ? intval( str_replace( ',', '.', $row_data['calories'] ) ) : 0,
'comment' => isset( $row_data['comment'] ) ? sanitize_textarea_field( $row_data['comment'] ) : null,
'strava_url' => isset( $row_data['strava_url'] ) ? $null_if_empty( esc_url_raw( $row_data['strava_url'] ) ) : null,
'avg_heart_rate' => isset( $row_data['avg_heart_rate'] ) ? $null_if_empty( $parse_and_round_int( $row_data['avg_heart_rate'] ) ) : null,
'max_heart_rate' => isset( $row_data['max_heart_rate'] ) ? $null_if_empty( $parse_and_round_int( $row_data['max_heart_rate'] ) ) : null,
'avg_speed' => isset( $row_data['avg_speed'] ) ? $null_if_empty( floatval( str_replace( ',', '.', $row_data['avg_speed'] ) ) ) : null,
'max_speed' => isset( $row_data['max_speed'] ) ? $null_if_empty( floatval( str_replace( ',', '.', $row_data['max_speed'] ) ) ) : null,
'avg_cadence' => isset( $row_data['avg_cadence'] ) ? $null_if_empty( $parse_and_round_int( $row_data['avg_cadence'] ) ) : null,
'max_cadence' => isset( $row_data['max_cadence'] ) ? $null_if_empty( $parse_and_round_int( $row_data['max_cadence'] ) ) : null,
'total_elevation_gain' => isset( $row_data['total_elevation_gain'] ) ? $null_if_empty( $parse_and_round_int( $row_data['total_elevation_gain'] ) ) : null,
'total_elevation_loss' => isset( $row_data['total_elevation_loss'] ) ? $null_if_empty( $parse_and_round_int( $row_data['total_elevation_loss'] ) ) : null,
'min_altitude' => isset( $row_data['min_altitude'] ) ? $null_if_empty( $parse_and_round_int( $row_data['min_altitude'] ) ) : null,
'max_altitude' => isset( $row_data['max_altitude'] ) ? $null_if_empty( $parse_and_round_int( $row_data['max_altitude'] ) ) : null,
'equipment_id' => $equipment_id,
'event_type_id' => $event_type_id,
);
if ( $wpdb->insert( $table_activities, $insert_data ) ) {
$imported_count++;
} else {
$skipped_details[] = array(
'row' => $row_number,
'reason' => 'Błąd zapisu do bazy danych. (' . esc_html( $wpdb->last_error ) . ')',
'data' => $line,
);
}
}
if ( $imported_count > 0 ) {
echo '<div class="notice notice-success is-dismissible"><p>Pomyślnie zaimportowano ' . esc_html( $imported_count ) . ' aktywności.</p></div>';}
if ( ! empty( $skipped_details ) ) {
echo '<div class="notice notice-warning">';
echo '<h4>Pominięto ' . count( $skipped_details ) . ' wierszy z powodu błędów</h4>';
echo '<div style="max-height: 300px; overflow-y: auto; border: 1px solid #c3c4c7; padding: 10px; background: #fff; margin-top: 10px; font-size: 12px;">';
echo '<table class="wp-list-table widefat striped" style="margin:0;"><thead><tr><th style="width:80px">Wiersz</th><th>Powód pominięcia</th><th>Dane wiersza</th></tr></thead><tbody>';
foreach ( $skipped_details as $error ) {
echo '<tr>';
echo '<td>' . esc_html( $error['row'] ) . '</td>';
echo '<td>' . esc_html( $error['reason'] ) . '</td>';
echo '<td><small>' . esc_html( wp_trim_words( $error['data'], 25, '...' ) ) . '</small></td>';
echo '</tr>';
}
echo '</tbody></table>';
echo '</div></div>';
}
if ( 0 === $imported_count && empty( $skipped_details ) && $row_number > 1 ) {
echo '<div class="notice notice-info"><p>Dane CSV nie zawierały żadnych poprawnych wierszy do importu.</p></div>';
} elseif ( 1 === $row_number ) {
echo '<div class="notice notice-info"><p>Dane CSV były puste lub zawierały tylko nagłówek.</p></div>';}
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
function statpress_import_csv_page() {
echo '<div class="wrap"><h1>Importuj aktywności z pliku CSV</h1>';
// Handle the form submission
if ( 'POST' === $_SERVER['REQUEST_METHOD'] && isset( $_POST['statpress_csv_import_nonce_field'] ) ) {
statpress_handle_csv_import();
}
// Display the form
?>
<div class="postbox">
<div class="postbox-header"><h2 class="hndle">Instrukcje i formularz importu</h2></div>
<div class="inside">
<p>Aby zaimportować dane, możesz wgrać plik CSV <strong>LUB</strong> wkleić dane bezpośrednio w pole tekstowe poniżej. Ta druga opcja jest zalecana, jeśli napotykasz błędy z plikiem.</p>
<p><strong>Wymagane kolumny:</strong> <code>Data</code>, <code>Tytuł</code>, <code>Dystans</code> oraz <code>Typ aktywności</code> (lub <code>Kategoria</code>).</p>
<p><strong>Opcjonalne kolumny:</strong> <code>Czas</code> (format HH:MM:SS), <code>Kalorie</code>, <code>Średnie tętno</code>, <code>Maksymalne tętno</code>, <code>Średnia prędkość</code>, <code>Maksymalna prędkość</code>, <code>Średni rytm pedałowania</code>, <code>Maksymalny rytm pedałowania</code>, <code>Całkowity wznios</code>, <code>Całkowity spadek</code>, <code>Minimalna wysokość</code>, <code>Maksymalna wysokość</code>, <code>Sprzęt</code>, <code>Typ wydarzenia</code>.</p>
<p><strong>Ważne:</strong>
<ul>
<li>Data musi być w formacie <code>YYYY-MM-DD</code>.</li>
<li>Dystans i prędkość: użyj kropki jako separatora dziesiętnego (np. <code>10.5</code>).</li>
<li>Nazwy w kolumnach <code>Typ aktywności</code>, <code>Sprzęt</code>, <code>Typ wydarzenia</code> muszą dokładnie odpowiadać nazwom zdefiniowanym w ustawieniach wtyczki. Jeśli nazwa nie zostanie znaleziona, wiersz zostanie pominięty.</li>
</ul>
</p>
<hr>
<form method="post" enctype="multipart/form-data">
<?php wp_nonce_field( 'statpress_csv_import_nonce', 'statpress_csv_import_nonce_field' ); ?>
<table class="form-table">
<tr valign="top">
<th scope="row"><label for="statpress_csv_file">Opcja 1: Wgraj plik CSV</label></th>
<td><input type="file" id="statpress_csv_file" name="statpress_csv_file" accept=".csv,text/csv" /></td>
</tr>
<tr valign="top">
<th scope="row"><label for="statpress_csv_data">Opcja 2: Wklej dane CSV</label></th>
<td><textarea name="statpress_csv_data" id="statpress_csv_data" rows="15" class="large-text" placeholder="Wklej tutaj zawartość swojego pliku CSV...&#10;Typ aktywności,Data,Tytuł,Dystans,...&#10;Rower,2025-01-01,Nowy Rok,10.5,..."></textarea></td>
</tr>
</table>
<?php submit_button( 'Importuj' ); ?>
</form>
</div>
</div>
<?php
echo '</div>';
}
function statpress_handle_csv_import() {
global $wpdb;
if ( ! isset( $_POST['statpress_csv_import_nonce_field'] ) || ! wp_verify_nonce( $_POST['statpress_csv_import_nonce_field'], 'statpress_csv_import_nonce' ) ) {
echo '<div class="notice notice-error"><p>Błąd weryfikacji bezpieczeństwa.</p></div>';
return;
}
if ( ! current_user_can( 'manage_options' ) ) {
echo '<div class="notice notice-error"><p>Nie masz wystarczających uprawnień.</p></div>';
return;
}
// Unify input source: prefer textarea, fall back to file upload.
$csv_content = '';
if ( ! empty( $_POST['statpress_csv_data'] ) ) {
$csv_content = stripslashes( $_POST['statpress_csv_data'] );
} elseif ( ! empty( $_FILES['statpress_csv_file']['tmp_name'] ) && UPLOAD_ERR_OK === $_FILES['statpress_csv_file']['error'] ) {
$csv_content = file_get_contents( $_FILES['statpress_csv_file']['tmp_name'] );
}
if ( empty( trim( $csv_content ) ) ) {
echo '<div class="notice notice-error"><p>Nie podano danych do importu. Wgraj plik lub wklej dane w pole tekstowe.</p></div>';
return;
}
// Mapowanie polskich i angielskich nazw kolumn na wewnętrzne klucze
$column_map = array(
// Polish => English
'typ aktywności' => 'category_name',
'data' => 'date',
'tytuł' => 'title',
'dystans' => 'distance',
'kalorie' => 'calories',
'czas' => 'duration',
'średnie tętno' => 'avg_heart_rate',
'maksymalne tętno' => 'max_heart_rate',
'średnia prędkość' => 'avg_speed',
'maksymalna prędkość' => 'max_speed',
'średni rytm pedałowania' => 'avg_cadence',
'maksymalny rytm pedałowania' => 'max_cadence',
'całkowity wznios' => 'total_elevation_gain',
'całkowity spadek' => 'total_elevation_loss',
'minimalna wysokość' => 'min_altitude',
'maksymalna wysokość' => 'max_altitude',
'sprzęt' => 'equipment_name',
'typ wydarzenia' => 'event_type_name',
'komentarz' => 'comment',
'link do strava' => 'strava_url',
// English keys for compatibility
'category_name' => 'category_name',
'date' => 'date',
'title' => 'title',
'distance' => 'distance',
'calories' => 'calories',
'duration' => 'duration',
'avg_heart_rate' => 'avg_heart_rate',
'max_heart_rate' => 'max_heart_rate',
'avg_speed' => 'avg_speed',
'max_speed' => 'max_speed',
'avg_cadence' => 'avg_cadence',
'max_cadence' => 'max_cadence',
'total_elevation_gain' => 'total_elevation_gain',
'total_elevation_loss' => 'total_elevation_loss',
'min_altitude' => 'min_altitude',
'max_altitude' => 'max_altitude',
'equipment_name' => 'equipment_name',
'event_type_name' => 'event_type_name',
'comment' => 'comment',
'strava_url' => 'strava_url',
);
// --- START: Robust, case-insensitive lookup ---
$table_categories = $wpdb->prefix . 'statpress_categories';
$table_event_types = $wpdb->prefix . 'statpress_event_types';
$table_equipment = $wpdb->prefix . 'statpress_equipment';
$create_lookup = function( $table_name ) use ( $wpdb ) {
$items = $wpdb->get_results( "SELECT id, name FROM {$table_name}" );
$lookup = array();
if ( is_array( $items ) ) {
foreach ( $items as $item ) {
// Use a robust trim to handle various whitespace characters and make it case-insensitive
$clean_name = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $item->name );
$lookup[ mb_strtolower( $clean_name, 'UTF-8' ) ] = $item->id;
}
}
return $lookup;
};
$categories_lookup = $create_lookup( $table_categories );
$event_types_lookup = $create_lookup( $table_event_types );
$equipment_lookup = $create_lookup( $table_equipment );
// --- END: Robust, case-insensitive lookup ---
// Process the CSV file
$table_activities = $wpdb->prefix . 'statpress_activities';
$imported_count = 0;
$skipped_details = array();
$row_number = 1; // Header is row 1
// Normalize line endings and split into lines
$lines = str_replace( array( "\r\n", "\r" ), "\n", $csv_content );
$lines = explode( "\n", $lines );
if ( empty( $lines ) || empty( trim( $lines[0] ) ) ) {
echo '<div class="notice notice-info"><p>Podane dane CSV są puste.</p></div>';
return;
}
// --- Delimiter and BOM detection ---
$first_line = $lines[0];
$delimiter = ( substr_count( $first_line, ';' ) > substr_count( $first_line, ',' ) ) ? ';' : ',';
// --- BOM removal from first header element ---
$bom = "\xEF\xBB\xBF";
if ( substr( $first_line, 0, 3 ) === $bom ) {
$lines[0] = substr( $first_line, 3 );
}
$header_raw = str_getcsv( array_shift( $lines ), $delimiter );
$header_raw = array_map( 'trim', $header_raw );
// Translate header from Polish/English to internal keys
$header = array();
foreach ( $header_raw as $col ) {
$header[] = $column_map[ strtolower( $col ) ] ?? 'ignored_' . uniqid();
}
$required_internal_keys = array( 'date', 'title', 'category_name', 'distance' );
$missing_keys = array_diff( $required_internal_keys, $header );
if ( ! empty( $missing_keys ) ) {
$key_to_polish_map = array(
'date' => 'Data',
'title' => 'Tytuł',
'category_name' => 'Kategoria / Typ aktywności',
'distance' => 'Dystans',
);
$missing_polish_names = array_map( fn( $key ) => $key_to_polish_map[ $key ] ?? $key, $missing_keys );
echo '<div class="notice notice-error"><p>Brak wymaganych kolumn w danych CSV: ' . esc_html( implode( ', ', $missing_polish_names ) ) . '</p></div>';
return;
}
$parse_and_round_int = fn( $val ) => round( floatval( str_replace( ',', '.', $val ) ) );
$null_if_empty = fn( $value ) => '' !== $value ? $value : null;
foreach ( $lines as $line ) {
$row_number++;
if ( empty( trim( $line ) ) ) {
continue; // Skip empty lines
}
$data = str_getcsv( $line, $delimiter );
if ( count( $data ) !== count( $header ) ) {
$skipped_details[] = array(
'row' => $row_number,
'reason' => 'Nieprawidłowa liczba kolumn (oczekiwano ' . count( $header ) . ', otrzymano ' . count( $data ) . ').',
'data' => $line,
);
continue;
}
$row_data = array_combine( $header, $data );
// Detailed validation
$validation_errors = array();
if ( empty( $row_data['date'] ) ) {
$validation_errors[] = 'brak daty';
}
if ( empty( $row_data['title'] ) ) {
$validation_errors[] = 'brak tytułu';
}
if ( ! isset( $row_data['distance'] ) || '' === $row_data['distance'] ) {
$validation_errors[] = 'brak dystansu';
}
$category_name = $row_data['category_name'] ?? '';
$clean_category_name = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $category_name );
$category_id = $categories_lookup[ mb_strtolower( $clean_category_name, 'UTF-8' ) ] ?? null;
if ( empty( $clean_category_name ) ) {
$validation_errors[] = 'brak nazwy kategorii';
} elseif ( is_null( $category_id ) ) {
$available_categories_from_db = $wpdb->get_col( "SELECT name FROM $table_categories ORDER BY name" );
$validation_errors[] = 'nieznana kategoria: "' . esc_html( $category_name ) . '". Sprawdź, czy nazwa jest poprawna. Dostępne w bazie: "' . esc_html( implode( '", "', $available_categories_from_db ) ) . '".';
}
if ( ! empty( $validation_errors ) ) {
$skipped_details[] = array(
'row' => $row_number,
'reason' => ucfirst( implode( ', ', $validation_errors ) ) . '.',
'data' => $line,
);
continue;
}
// Get IDs for optional fields using the same robust method
$get_id = function( $name, $lookup_table ) {
$clean_name = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $name );
return $lookup_table[ mb_strtolower( $clean_name, 'UTF-8' ) ] ?? null;
};
$equipment_id = $get_id( $row_data['equipment_name'] ?? '', $equipment_lookup );
$event_type_id = $get_id( $row_data['event_type_name'] ?? '', $event_types_lookup );
$insert_data = array(
'date' => sanitize_text_field( $row_data['date'] ),
'title' => sanitize_text_field( $row_data['title'] ),
'category_id' => $category_id,
'distance' => floatval( str_replace( ',', '.', $row_data['distance'] ) ),
'duration' => isset( $row_data['duration'] ) ? sanitize_text_field( $row_data['duration'] ) : '00:00:00',
'calories' => isset( $row_data['calories'] ) ? intval( str_replace( ',', '.', $row_data['calories'] ) ) : 0,
'comment' => isset( $row_data['comment'] ) ? sanitize_textarea_field( $row_data['comment'] ) : null,
'strava_url' => isset( $row_data['strava_url'] ) ? $null_if_empty( esc_url_raw( $row_data['strava_url'] ) ) : null,
'avg_heart_rate' => isset( $row_data['avg_heart_rate'] ) ? $null_if_empty( $parse_and_round_int( $row_data['avg_heart_rate'] ) ) : null,
'max_heart_rate' => isset( $row_data['max_heart_rate'] ) ? $null_if_empty( $parse_and_round_int( $row_data['max_heart_rate'] ) ) : null,
'avg_speed' => isset( $row_data['avg_speed'] ) ? $null_if_empty( floatval( str_replace( ',', '.', $row_data['avg_speed'] ) ) ) : null,
'max_speed' => isset( $row_data['max_speed'] ) ? $null_if_empty( floatval( str_replace( ',', '.', $row_data['max_speed'] ) ) ) : null,
'avg_cadence' => isset( $row_data['avg_cadence'] ) ? $null_if_empty( $parse_and_round_int( $row_data['avg_cadence'] ) ) : null,
'max_cadence' => isset( $row_data['max_cadence'] ) ? $null_if_empty( $parse_and_round_int( $row_data['max_cadence'] ) ) : null,
'total_elevation_gain' => isset( $row_data['total_elevation_gain'] ) ? $null_if_empty( $parse_and_round_int( $row_data['total_elevation_gain'] ) ) : null,
'total_elevation_loss' => isset( $row_data['total_elevation_loss'] ) ? $null_if_empty( $parse_and_round_int( $row_data['total_elevation_loss'] ) ) : null,
'min_altitude' => isset( $row_data['min_altitude'] ) ? $null_if_empty( $parse_and_round_int( $row_data['min_altitude'] ) ) : null,
'max_altitude' => isset( $row_data['max_altitude'] ) ? $null_if_empty( $parse_and_round_int( $row_data['max_altitude'] ) ) : null,
'equipment_id' => $equipment_id,
'event_type_id' => $event_type_id,
);
if ( $wpdb->insert( $table_activities, $insert_data ) ) {
$imported_count++;
} else {
$skipped_details[] = array(
'row' => $row_number,
'reason' => 'Błąd zapisu do bazy danych. (' . esc_html( $wpdb->last_error ) . ')',
'data' => $line,
);
}
}
if ( $imported_count > 0 ) {
echo '<div class="notice notice-success is-dismissible"><p>Pomyślnie zaimportowano ' . esc_html( $imported_count ) . ' aktywności.</p></div>';}
if ( ! empty( $skipped_details ) ) {
echo '<div class="notice notice-warning">';
echo '<h4>Pominięto ' . count( $skipped_details ) . ' wierszy z powodu błędów</h4>';
echo '<div style="max-height: 300px; overflow-y: auto; border: 1px solid #c3c4c7; padding: 10px; background: #fff; margin-top: 10px; font-size: 12px;">';
echo '<table class="wp-list-table widefat striped" style="margin:0;"><thead><tr><th style="width:80px">Wiersz</th><th>Powód pominięcia</th><th>Dane wiersza</th></tr></thead><tbody>';
foreach ( $skipped_details as $error ) {
echo '<tr>';
echo '<td>' . esc_html( $error['row'] ) . '</td>';
echo '<td>' . esc_html( $error['reason'] ) . '</td>';
echo '<td><small>' . esc_html( wp_trim_words( $error['data'], 25, '...' ) ) . '</small></td>';
echo '</tr>';
}
echo '</tbody></table>';
echo '</div></div>';
}
if ( 0 === $imported_count && empty( $skipped_details ) && $row_number > 1 ) {
echo '<div class="notice notice-info"><p>Dane CSV nie zawierały żadnych poprawnych wierszy do importu.</p></div>';
} elseif ( 1 === $row_number ) {
echo '<div class="notice notice-info"><p>Dane CSV były puste lub zawierały tylko nagłówek.</p></div>';}
}
+158 -158
View File
@@ -1,159 +1,159 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
function statpress_infographic_page() {
global $wpdb;
$table_activities = $wpdb->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 ) . '"
}
}
}
});
});
'
);
?>
<div class="wrap">
<h1>Infografika Statystyk Sportowych</h1>
<div class="tablenav top">
<div class="alignleft actions">
<form method="get" style="display: flex; gap: 5px; align-items: center;">
<input type="hidden" name="page" value="statpress-infographic">
<label for="filter-by-year" class="screen-reader-text">Filtruj według roku</label>
<select name="year" id="filter-by-year">
<?php foreach ( $available_years as $year_option ) : ?>
<option value="<?php echo esc_attr( $year_option ); ?>" <?php selected( $current_year, $year_option ); ?>><?php echo esc_html( $year_option ); ?></option>
<?php endforeach; ?>
</select>
<?php submit_button( 'Filtruj', 'secondary', 'filter_action', false ); ?>
</form>
</div>
</div>
<div class="postbox" style="margin-bottom: 20px;">
<div class="postbox-header"><h2 class="hndle">Statystyki Ogólne (wszystkie lata)</h2></div>
<div class="inside statpress-infographic-grid">
<div class="statpress-infographic-card"><h3>Dystans</h3><p><?php echo number_format( $overall_stats->total_distance, 2, ',', ' ' ); ?> km</p></div>
<div class="statpress-infographic-card"><h3>Czas</h3><p><?php echo esc_html( $overall_stats->total_duration ); ?></p></div>
<div class="statpress-infographic-card"><h3>Wznios</h3><p><?php echo number_format( $overall_stats->total_elevation_gain, 0, ',', ' ' ); ?> m</p></div>
<div class="statpress-infographic-card"><h3>Aktywności</h3><p><?php echo number_format( $overall_stats->total_activities, 0, ',', ' ' ); ?></p></div>
</div>
</div>
<div class="postbox" style="margin-bottom: 20px;">
<div class="postbox-header"><h2 class="hndle">Statystyki dla <?php echo esc_html( $current_year ); ?></h2></div>
<div class="inside statpress-infographic-grid">
<div class="statpress-infographic-card"><h3>Dystans</h3><p><?php echo number_format( $yearly_stats->total_distance, 2, ',', ' ' ); ?> km</p></div>
<div class="statpress-infographic-card"><h3>Czas</h3><p><?php echo esc_html( $yearly_stats->total_duration ); ?></p></div>
<div class="statpress-infographic-card"><h3>Wznios</h3><p><?php echo number_format( $yearly_stats->total_elevation_gain, 0, ',', ' ' ); ?> m</p></div>
<div class="statpress-infographic-card"><h3>Aktywności</h3><p><?php echo number_format( $yearly_stats->total_activities, 0, ',', ' ' ); ?></p></div>
</div>
</div>
<div class="postbox" style="margin-bottom: 20px;">
<div class="postbox-header"><h2 class="hndle">Rozkład Dystansu wg Kategorii w <?php echo esc_html( $current_year ); ?></h2></div>
<div class="inside">
<div style="position: relative; height:40vh; width:100%; max-width: 600px; margin: 0 auto;">
<canvas id="statpressCategoryPieChart"></canvas>
</div>
</div>
</div>
</div>
<?php
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
function statpress_infographic_page() {
global $wpdb;
$table_activities = $wpdb->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 ) . '"
}
}
}
});
});
'
);
?>
<div class="wrap">
<h1>Infografika Statystyk Sportowych</h1>
<div class="tablenav top">
<div class="alignleft actions">
<form method="get" style="display: flex; gap: 5px; align-items: center;">
<input type="hidden" name="page" value="statpress-infographic">
<label for="filter-by-year" class="screen-reader-text">Filtruj według roku</label>
<select name="year" id="filter-by-year">
<?php foreach ( $available_years as $year_option ) : ?>
<option value="<?php echo esc_attr( $year_option ); ?>" <?php selected( $current_year, $year_option ); ?>><?php echo esc_html( $year_option ); ?></option>
<?php endforeach; ?>
</select>
<?php submit_button( 'Filtruj', 'secondary', 'filter_action', false ); ?>
</form>
</div>
</div>
<div class="postbox" style="margin-bottom: 20px;">
<div class="postbox-header"><h2 class="hndle">Statystyki Ogólne (wszystkie lata)</h2></div>
<div class="inside statpress-infographic-grid">
<div class="statpress-infographic-card"><h3>Dystans</h3><p><?php echo number_format( $overall_stats->total_distance, 2, ',', ' ' ); ?> km</p></div>
<div class="statpress-infographic-card"><h3>Czas</h3><p><?php echo esc_html( $overall_stats->total_duration ); ?></p></div>
<div class="statpress-infographic-card"><h3>Wznios</h3><p><?php echo number_format( $overall_stats->total_elevation_gain, 0, ',', ' ' ); ?> m</p></div>
<div class="statpress-infographic-card"><h3>Aktywności</h3><p><?php echo number_format( $overall_stats->total_activities, 0, ',', ' ' ); ?></p></div>
</div>
</div>
<div class="postbox" style="margin-bottom: 20px;">
<div class="postbox-header"><h2 class="hndle">Statystyki dla <?php echo esc_html( $current_year ); ?></h2></div>
<div class="inside statpress-infographic-grid">
<div class="statpress-infographic-card"><h3>Dystans</h3><p><?php echo number_format( $yearly_stats->total_distance, 2, ',', ' ' ); ?> km</p></div>
<div class="statpress-infographic-card"><h3>Czas</h3><p><?php echo esc_html( $yearly_stats->total_duration ); ?></p></div>
<div class="statpress-infographic-card"><h3>Wznios</h3><p><?php echo number_format( $yearly_stats->total_elevation_gain, 0, ',', ' ' ); ?> m</p></div>
<div class="statpress-infographic-card"><h3>Aktywności</h3><p><?php echo number_format( $yearly_stats->total_activities, 0, ',', ' ' ); ?></p></div>
</div>
</div>
<div class="postbox" style="margin-bottom: 20px;">
<div class="postbox-header"><h2 class="hndle">Rozkład Dystansu wg Kategorii w <?php echo esc_html( $current_year ); ?></h2></div>
<div class="inside">
<div style="position: relative; height:40vh; width:100%; max-width: 600px; margin: 0 auto;">
<canvas id="statpressCategoryPieChart"></canvas>
</div>
</div>
</div>
</div>
<?php
}
+160 -137
View File
@@ -1,138 +1,161 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
function statpress_settings_page() {
?>
<div class="wrap">
<h1>Ustawienia Wtyczki Statystyk</h1>
<form method="post" action="options.php">
<?php
settings_fields( 'statpress_privacy_settings' );
do_settings_sections( 'statpress-privacy-section' );
settings_fields( 'statpress_api_settings' );
do_settings_sections( 'statpress-api-section' );
submit_button();
?>
</form>
</div>
<?php
}
function statpress_register_settings() {
register_setting(
'statpress_privacy_settings',
'statpress_privacy_options',
'statpress_sanitize_privacy_options'
);
add_settings_section(
'statpress_privacy_zone_section',
'Strefa Prywatności GPX',
'statpress_privacy_section_callback',
'statpress-privacy-section'
);
add_settings_field(
'statpress_privacy_latitude',
'Szerokość geograficzna (Latitude)',
'statpress_render_lat_field',
'statpress-privacy-section',
'statpress_privacy_zone_section'
);
add_settings_field(
'statpress_privacy_longitude',
'Długość geograficzna (Longitude)',
'statpress_render_lon_field',
'statpress-privacy-section',
'statpress_privacy_zone_section'
);
add_settings_field(
'statpress_privacy_radius',
'Promień strefy (w metrach)',
'statpress_render_radius_field',
'statpress-privacy-section',
'statpress_privacy_zone_section'
);
// API Settings
register_setting(
'statpress_api_settings',
'statpress_api_options',
'statpress_sanitize_api_options'
);
add_settings_section(
'statpress_api_section',
'Ustawienia API',
'statpress_api_section_callback',
'statpress-api-section'
);
add_settings_field(
'statpress_enable_api',
'REST API',
'statpress_render_enable_api_field',
'statpress-privacy-section',
'statpress_privacy_zone_section'
);
}
function statpress_privacy_section_callback() {
echo '<p>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.</p>';
echo '<p>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.</p>';
}
function statpress_render_lat_field() {
$options = get_option( 'statpress_privacy_options' );
$latitude = isset( $options['latitude'] ) ? esc_attr( $options['latitude'] ) : '';
echo "<input type='text' name='statpress_privacy_options[latitude]' value='{$latitude}' placeholder='np. 52.2297' class='regular-text' />";
}
function statpress_render_lon_field() {
$options = get_option( 'statpress_privacy_options' );
$longitude = isset( $options['longitude'] ) ? esc_attr( $options['longitude'] ) : '';
echo "<input type='text' name='statpress_privacy_options[longitude]' value='{$longitude}' placeholder='np. 21.0122' class='regular-text' />";
}
function statpress_render_radius_field() {
$options = get_option( 'statpress_privacy_options' );
$radius = isset( $options['radius'] ) ? esc_attr( $options['radius'] ) : '500';
echo "<input type='number' name='statpress_privacy_options[radius]' value='{$radius}' class='small-text' /> 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 '<p>Ustawienia związane z integracją wtyczki z zewnętrznymi aplikacjami, np. mobilnymi.</p>';
}
function statpress_render_enable_api_field() {
$options = get_option( 'statpress_api_options' );
$checked = isset( $options['enable_api'] ) && $options['enable_api'] ? 'checked' : '';
echo "<label><input type='checkbox' name='statpress_api_options[enable_api]' value='1' {$checked} /> Włącz REST API</label>";
echo '<p class="description">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.</p>';
}
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;
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
function statpress_settings_page() {
?>
<div class="wrap">
<?php
if ( get_transient( 'statpress_migration_reset_notice' ) ) {
echo '<div class="notice notice-success is-dismissible"><p>Status migracji został zresetowany. Wróć do <a href="' . esc_url( admin_url( 'admin.php?page=statpress-dashboard' ) ) . '">głównego panelu</a>, aby ponownie uruchomić migrację.</p></div>';
delete_transient( 'statpress_migration_reset_notice' );
}
?>
<h1>Ustawienia Wtyczki Statystyk</h1>
<form method="post" action="options.php">
<?php
settings_fields( 'statpress_settings_group' );
do_settings_sections( 'statpress_settings_page' );
submit_button();
?>
</form>
<hr>
<h2>Narzędzia deweloperskie</h2>
<div class="postbox">
<div class="postbox-header"><h3 class="hndle">Resetowanie migracji</h3></div>
<div class="inside">
<p>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.</p>
<form method="post">
<input type="hidden" name="statpress_action" value="reset_migration"><?php wp_nonce_field( 'statpress_reset_migration_nonce', '_wpnonce' ); ?><?php submit_button( 'Zresetuj status migracji', 'delete' ); ?></form>
</div>
</div>
</div>
<?php
}
function statpress_register_settings() {
// Define a single group and page for all settings on this form.
$option_group = 'statpress_settings_group';
$page_slug = 'statpress_settings_page';
// Register Privacy settings under the main group.
register_setting(
$option_group,
'statpress_privacy_options',
'statpress_sanitize_privacy_options'
);
// Register API settings under the same main group.
register_setting(
$option_group,
'statpress_api_options',
'statpress_sanitize_api_options'
);
// Add the Privacy section to the main page.
add_settings_section(
'statpress_privacy_zone_section',
'Strefa Prywatności GPX',
'statpress_privacy_section_callback',
$page_slug
);
add_settings_field(
'statpress_privacy_latitude',
'Szerokość geograficzna (Latitude)',
'statpress_render_lat_field',
$page_slug,
'statpress_privacy_zone_section'
);
add_settings_field(
'statpress_privacy_longitude',
'Długość geograficzna (Longitude)',
'statpress_render_lon_field',
$page_slug,
'statpress_privacy_zone_section'
);
add_settings_field(
'statpress_privacy_radius',
'Promień strefy (w metrach)',
'statpress_render_radius_field',
$page_slug,
'statpress_privacy_zone_section'
);
// Add the API section to the same main page.
add_settings_section(
'statpress_api_section',
'Ustawienia API',
'statpress_api_section_callback',
$page_slug
);
add_settings_field(
'statpress_enable_api',
'REST API',
'statpress_render_enable_api_field',
$page_slug,
'statpress_api_section'
);
}
function statpress_privacy_section_callback() {
echo '<p>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.</p>';
echo '<p>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.</p>';
}
function statpress_render_lat_field() {
$options = get_option( 'statpress_privacy_options' );
$latitude = isset( $options['latitude'] ) ? esc_attr( $options['latitude'] ) : '';
echo "<input type='text' name='statpress_privacy_options[latitude]' value='{$latitude}' placeholder='np. 52.2297' class='regular-text' />";
}
function statpress_render_lon_field() {
$options = get_option( 'statpress_privacy_options' );
$longitude = isset( $options['longitude'] ) ? esc_attr( $options['longitude'] ) : '';
echo "<input type='text' name='statpress_privacy_options[longitude]' value='{$longitude}' placeholder='np. 21.0122' class='regular-text' />";
}
function statpress_render_radius_field() {
$options = get_option( 'statpress_privacy_options' );
$radius = isset( $options['radius'] ) ? esc_attr( $options['radius'] ) : '500';
echo "<input type='number' name='statpress_privacy_options[radius]' value='{$radius}' class='small-text' /> 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 '<p>Ustawienia związane z integracją wtyczki z zewnętrznymi aplikacjami, np. mobilnymi.</p>';
}
function statpress_render_enable_api_field() {
$options = get_option( 'statpress_api_options' );
$checked = isset( $options['enable_api'] ) && $options['enable_api'] ? 'checked' : '';
echo "<label><input type='checkbox' name='statpress_api_options[enable_api]' value='1' {$checked} /> Włącz REST API</label>";
echo '<p class="description">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.</p>';
}
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;
}
+291 -291
View File
@@ -1,292 +1,292 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
function statpress_yearly_summary_page() {
global $wpdb;
$table_activities = $wpdb->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));
}
});
'
);
?>
<div class="wrap">
<h1>Podsumowanie Roczne</h1>
<div class="tablenav top">
<div class="alignleft actions">
<label for="filter-by-year" class="screen-reader-text">Filtruj według roku</label>
<form method="get" style="display: flex; gap: 5px; align-items: center;">
<input type="hidden" name="page" value="statpress-yearly-summary">
<select name="year" id="filter-by-year">
<?php foreach ( $available_years as $year_option ) : ?>
<option value="<?php echo esc_attr( $year_option ); ?>" <?php selected( $current_year, $year_option ); ?>><?php echo esc_html( $year_option ); ?></option>
<?php endforeach; ?>
</select>
<?php submit_button( 'Filtruj', 'secondary', 'filter_action', false ); ?>
</form>
</div>
</div>
<?php if ( ! empty( $goals_for_year ) ) : ?>
<div class="postbox" style="margin-bottom: 20px;">
<div class="postbox-header"><h2 class="hndle">Cele na <?php echo esc_html( $current_year ); ?></h2></div>
<div class="inside">
<ul class="statpress-goals-list">
<?php
foreach ( $goals_for_year as $goal ) :
$progress = statpress_get_goal_progress( $goal );
$percentage = min( 100, $progress['percentage'] );
$target_formatted = '';
$current_formatted = '';
if ( 'duration_sec' === $goal->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'];
}
?>
<li class="statpress-goal-item">
<div class="statpress-goal-info">
<strong><?php echo esc_html( $goal->name ); ?></strong>
<small><?php echo $current_formatted; ?> / <?php echo $target_formatted; ?> (<?php echo round( $percentage, 1 ); ?>%)</small>
</div>
<div class="statpress-progress-bar-container">
<div class="statpress-progress-bar" style="width: <?php echo esc_attr( $percentage ); ?>%;"></div>
</div>
</li>
<?php endforeach; ?>
</ul>
</div>
</div>
<style>
.statpress-goals-list { list-style: none; margin: 0; padding: 0; } .statpress-goal-item { margin-bottom: 15px; } .statpress-goal-info { display: flex; justify-content: space-between; margin-bottom: 5px; }
.statpress-progress-bar-container { background: #eee; border-radius: 4px; height: 18px; width: 100%; overflow: hidden; } .statpress-progress-bar { background: #3498db; height: 100%; border-radius: 4px; transition: width 0.5s ease-in-out; text-align: center; color: white; font-size: 11px; line-height: 18px; }
</style>
<?php endif; ?>
<div class="postbox" style="margin-bottom: 20px;">
<div class="postbox-header"><h2 class="hndle">Wykresy dla <?php echo esc_html( $current_year ); ?></h2></div>
<div class="inside">
<nav class="nav-tab-wrapper" style="margin-bottom: 20px;">
<a href="#distance" class="nav-tab nav-tab-active">Dystans</a>
<a href="#duration" class="nav-tab">Czas</a>
<a href="#calories" class="nav-tab">Kalorie</a>
<a href="#activities" class="nav-tab">Aktywności</a>
</nav>
<div style="position: relative; height:40vh; width:100%;">
<canvas id="statpressYearlyChart"></canvas>
</div>
</div>
</div>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr><th>Miesiąc</th><th>Dystans (km)</th><th>Kalorie (kcal)</th><th>Czas</th></tr>
</thead>
<tbody>
<?php foreach ( $full_year_summary as $month_data ) : ?>
<tr>
<td><?php echo esc_html( $month_data->month_name ); ?></td>
<td><?php echo number_format( $month_data->total_distance, 2, ',', ' ' ); ?></td>
<td><?php echo number_format( $month_data->total_calories, 0, ',', ' ' ); ?></td>
<td><?php echo esc_html( gmdate( 'H:i:s', $month_data->total_seconds ) ); ?></td>
</tr>
<?php endforeach; ?>
<tr class="alternate">
<th>SUMA ROCZNA</th>
<th><?php echo number_format( $total_year_distance, 2, ',', ' ' ); ?></th>
<th><?php echo number_format( $total_year_calories, 0, ',', ' ' ); ?></th>
<th><?php echo esc_html( $total_year_duration_formatted ); ?></th>
</tr>
</tbody>
</table>
</div>
<?php
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
function statpress_yearly_summary_page() {
global $wpdb;
$table_activities = $wpdb->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));
}
});
'
);
?>
<div class="wrap">
<h1>Podsumowanie Roczne</h1>
<div class="tablenav top">
<div class="alignleft actions">
<label for="filter-by-year" class="screen-reader-text">Filtruj według roku</label>
<form method="get" style="display: flex; gap: 5px; align-items: center;">
<input type="hidden" name="page" value="statpress-yearly-summary">
<select name="year" id="filter-by-year">
<?php foreach ( $available_years as $year_option ) : ?>
<option value="<?php echo esc_attr( $year_option ); ?>" <?php selected( $current_year, $year_option ); ?>><?php echo esc_html( $year_option ); ?></option>
<?php endforeach; ?>
</select>
<?php submit_button( 'Filtruj', 'secondary', 'filter_action', false ); ?>
</form>
</div>
</div>
<?php if ( ! empty( $goals_for_year ) ) : ?>
<div class="postbox" style="margin-bottom: 20px;">
<div class="postbox-header"><h2 class="hndle">Cele na <?php echo esc_html( $current_year ); ?></h2></div>
<div class="inside">
<ul class="statpress-goals-list">
<?php
foreach ( $goals_for_year as $goal ) :
$progress = statpress_get_goal_progress( $goal );
$percentage = min( 100, $progress['percentage'] );
$target_formatted = '';
$current_formatted = '';
if ( 'duration_sec' === $goal->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'];
}
?>
<li class="statpress-goal-item">
<div class="statpress-goal-info">
<strong><?php echo esc_html( $goal->name ); ?></strong>
<small><?php echo $current_formatted; ?> / <?php echo $target_formatted; ?> (<?php echo round( $percentage, 1 ); ?>%)</small>
</div>
<div class="statpress-progress-bar-container">
<div class="statpress-progress-bar" style="width: <?php echo esc_attr( $percentage ); ?>%;"></div>
</div>
</li>
<?php endforeach; ?>
</ul>
</div>
</div>
<style>
.statpress-goals-list { list-style: none; margin: 0; padding: 0; } .statpress-goal-item { margin-bottom: 15px; } .statpress-goal-info { display: flex; justify-content: space-between; margin-bottom: 5px; }
.statpress-progress-bar-container { background: #eee; border-radius: 4px; height: 18px; width: 100%; overflow: hidden; } .statpress-progress-bar { background: #3498db; height: 100%; border-radius: 4px; transition: width 0.5s ease-in-out; text-align: center; color: white; font-size: 11px; line-height: 18px; }
</style>
<?php endif; ?>
<div class="postbox" style="margin-bottom: 20px;">
<div class="postbox-header"><h2 class="hndle">Wykresy dla <?php echo esc_html( $current_year ); ?></h2></div>
<div class="inside">
<nav class="nav-tab-wrapper" style="margin-bottom: 20px;">
<a href="#distance" class="nav-tab nav-tab-active">Dystans</a>
<a href="#duration" class="nav-tab">Czas</a>
<a href="#calories" class="nav-tab">Kalorie</a>
<a href="#activities" class="nav-tab">Aktywności</a>
</nav>
<div style="position: relative; height:40vh; width:100%;">
<canvas id="statpressYearlyChart"></canvas>
</div>
</div>
</div>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr><th>Miesiąc</th><th>Dystans (km)</th><th>Kalorie (kcal)</th><th>Czas</th></tr>
</thead>
<tbody>
<?php foreach ( $full_year_summary as $month_data ) : ?>
<tr>
<td><?php echo esc_html( $month_data->month_name ); ?></td>
<td><?php echo number_format( $month_data->total_distance, 2, ',', ' ' ); ?></td>
<td><?php echo number_format( $month_data->total_calories, 0, ',', ' ' ); ?></td>
<td><?php echo esc_html( gmdate( 'H:i:s', $month_data->total_seconds ) ); ?></td>
</tr>
<?php endforeach; ?>
<tr class="alternate">
<th>SUMA ROCZNA</th>
<th><?php echo number_format( $total_year_distance, 2, ',', ' ' ); ?></th>
<th><?php echo number_format( $total_year_calories, 0, ',', ' ' ); ?></th>
<th><?php echo esc_html( $total_year_duration_formatted ); ?></th>
</tr>
</tbody>
</table>
</div>
<?php
}
+242 -181
View File
@@ -1,182 +1,243 @@
<?php
/**
* REST API routes for the plugin.
*
* @package WordPress Activity Stats
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Register REST API routes.
*/
function statpress_register_rest_routes() {
$namespace = 'statpress/v1';
// Route for getting a collection of activities
register_rest_route(
$namespace,
'/activities',
array(
array(
'methods' => 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<id>[\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 );
<?php
/**
* REST API routes for the plugin.
*
* @package WordPress Activity Stats
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Register REST API routes.
*/
function statpress_register_rest_routes() {
$namespace = 'statpress/v1';
// Route for getting a collection of activities
register_rest_route(
$namespace,
'/activities',
array(
array(
'methods' => 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<id>[\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 );
}
+94 -94
View File
@@ -1,95 +1,95 @@
<?php
/**
* CRUD functions for Activities.
*
* @package WordPress Activity Stats
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Saves or updates an activity in the database.
*
* @param array $data Associative array of data to save.
* @param int $activity_id The ID of the activity to update. 0 for new activity.
* @return int|false The ID of the saved/updated activity, or false on failure.
*/
function statpress_save_activity_data( array $data, int $activity_id = 0 ) {
global $wpdb;
$table_activities = $wpdb->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;
<?php
/**
* CRUD functions for Activities.
*
* @package WordPress Activity Stats
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Saves or updates an activity in the database.
*
* @param array $data Associative array of data to save.
* @param int $activity_id The ID of the activity to update. 0 for new activity.
* @return int|false The ID of the saved/updated activity, or false on failure.
*/
function statpress_save_activity_data( array $data, int $activity_id = 0 ) {
global $wpdb;
$table_activities = $wpdb->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;
}
+296 -138
View File
@@ -1,139 +1,297 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Fetches and parses a GPX file from a URL to extract track points and elevation profile.
*
* @param string $gpx_url The URL of the GPX file.
* @return array An array containing 'points' for the map and 'elevation_profile'.
*/
function statpress_parse_gpx_data( $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();
}
// --- 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,
);
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Fetches and parses a GPX file from a URL to extract track points and elevation profile.
*
* @param string $gpx_url The URL of the GPX file.
* @return array An array containing 'points' for the map and 'elevation_profile'.
*/
function statpress_parse_gpx_data( $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();
}
// --- 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'],
);
}
+32 -32
View File
@@ -1,33 +1,33 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Add GPX support to WordPress Media Library.
*
* @param array $mimes Allowed mime types.
* @return array Modified mime types.
*/
function statpress_add_gpx_mime_type( $mimes ) {
$mimes['gpx'] = 'application/gpx+xml';
return $mimes;
}
/**
* Bypasses WordPress's strict file type check for GPX files.
* This is needed because WordPress can be overly cautious with XML-based files.
*
* @param array $data File data.
* @param string $file Full path to the file.
* @param string $filename The filename.
* @param array $mimes Mime types.
* @return array Modified file data.
*/
function statpress_fix_gpx_upload_permission( $data, $file, $filename, $mimes ) {
if ( strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) ) === 'gpx' ) {
$data['ext'] = 'gpx';
$data['type'] = 'application/gpx+xml';
}
return $data;
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Add GPX support to WordPress Media Library.
*
* @param array $mimes Allowed mime types.
* @return array Modified mime types.
*/
function statpress_add_gpx_mime_type( $mimes ) {
$mimes['gpx'] = 'application/gpx+xml';
return $mimes;
}
/**
* Bypasses WordPress's strict file type check for GPX files.
* This is needed because WordPress can be overly cautious with XML-based files.
*
* @param array $data File data.
* @param string $file Full path to the file.
* @param string $filename The filename.
* @param array $mimes Mime types.
* @return array Modified file data.
*/
function statpress_fix_gpx_upload_permission( $data, $file, $filename, $mimes ) {
if ( strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) ) === 'gpx' ) {
$data['ext'] = 'gpx';
$data['type'] = 'application/gpx+xml';
}
return $data;
}
+17 -17
View File
@@ -1,18 +1,18 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Registers styles for frontend and enqueues them when shortcodes are used.
*/
function mystat_enqueue_frontend_assets() {
// Register the stylesheet. It will be enqueued by the shortcodes when needed.
$plugin_version = '1.0';
wp_register_style(
'mystat-frontend-styles',
MYSTAT_PLUGIN_URL . 'assets/css/frontend.css',
array(),
$plugin_version
);
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Registers styles for frontend and enqueues them when shortcodes are used.
*/
function statpress_enqueue_frontend_assets() {
// Register the stylesheet. It will be enqueued by the shortcodes when needed.
$plugin_version = '1.0';
wp_register_style(
'statpress-frontend-styles',
STATPRESS_PLUGIN_URL . 'assets/css/frontend.css',
array(),
$plugin_version
);
}
+392 -392
View File
@@ -1,393 +1,393 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Rejestruje shortcode [moje_statystyki].
*/
function mystat_register_shortcode() {
add_shortcode( 'moje_statystyki', 'mystat_shortcode_handler' );
add_shortcode( 'moje_statystyki_wpis', 'mystat_single_activity_shortcode_handler' );
}
/**
* Funkcja obsługująca shortcode.
* @param array $atts Atrybuty shortcode'u (np. year, month).
* @return string HTML do wyświetlenia.
*/
function mystat_shortcode_handler( $atts ) {
wp_enqueue_style( 'mystat-frontend-styles' );
global $wpdb;
// Ustawienie domyślnych atrybutów (bieżący rok i miesiąc)
$atts = shortcode_atts(
array(
'year' => 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();
?>
<div class="mystats-shortcode-container">
<h3>Podsumowanie miesiąca</h3>
<table class="mystats-summary-table">
<thead>
<tr>
<th>Całkowity dystans</th>
<th>Całkowity czas</th>
</tr>
</thead>
<tbody>
<tr>
<td><?php echo number_format( $total_distance, 2, ',', ' ' ); ?> km</td>
<td><?php echo esc_html( $total_duration_formatted ); ?></td>
</tr>
</tbody>
</table>
<h3>Lista aktywności</h3>
<table class="mystats-activity-table">
<thead>
<tr>
<th>Data</th>
<th>Tytuł</th>
<th>Kategoria</th>
<th>Dystans</th>
<th>Czas</th>
<th>Sprzęt</th>
</tr>
</thead>
<tbody>
<?php if ( ! empty( $activities ) ) : ?>
<?php foreach ( $activities as $row ) : ?>
<tr>
<td><?php echo esc_html( date_i18n( 'd.m.Y', strtotime( $row->date ) ) ); ?></td>
<td><?php echo esc_html( $row->title ); ?></td>
<td><?php echo esc_html( $row->category_name ); ?></td>
<td><?php echo number_format( $row->distance, 2, ',', ' ' ); ?> km</td>
<td><?php echo esc_html( $row->duration ); ?></td>
<td><?php echo esc_html( $row->equipment_name ); ?></td>
</tr>
<?php if ( $row->avg_speed || $row->avg_heart_rate || $row->avg_cadence ) : ?>
<tr class="mystats-activity-details-row">
<td colspan="6" class="mystats-activity-details-cell">
<table class="mystats-nested-details-table">
<thead>
<tr>
<?php if ( $row->avg_speed ) { echo '<th>Śr. prędkość</th>'; } ?>
<?php if ( $row->avg_heart_rate ) { echo '<th>Śr. tętno</th>'; } ?>
<?php if ( $row->avg_cadence ) { echo '<th>Śr. kadencja</th>'; } ?>
</tr>
</thead>
<tbody>
<tr>
<?php if ( $row->avg_speed ) { echo '<td>' . number_format( $row->avg_speed, 1, ',', ' ' ) . ' km/h</td>'; } ?>
<?php if ( $row->avg_heart_rate ) { echo '<td>' . $row->avg_heart_rate . ' bpm</td>'; } ?>
<?php if ( $row->avg_cadence ) { echo '<td>' . $row->avg_cadence . ' rpm</td>'; } ?>
</tr>
</tbody>
</table>
</td>
</tr>
<?php endif; ?>
<?php endforeach; ?>
<?php else : ?>
<tr>
<td colspan="6">Brak aktywności w tym miesiącu.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<?php
// Zwrócenie zawartości bufora
return ob_get_clean();
}
function mystat_single_activity_shortcode_handler( $atts ) {
wp_enqueue_style( 'mystat-frontend-styles' );
global $wpdb;
$atts = shortcode_atts(
array(
'id' => 0,
),
$atts,
'moje_statystyki_wpis'
);
$activity_id = intval( $atts['id'] );
if ( 0 === $activity_id ) {
return '<p><strong>Błąd:</strong> Nie podano ID wpisu w shortcode.</p>';
}
// 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 '<p><strong>Błąd:</strong> Nie znaleziono wpisu o ID ' . esc_html( $activity_id ) . '.</p>';
}
// Funkcja pomocnicza do wyświetlania wiersza
$render_row = function( $label, $value, $unit = '' ) {
if ( ! is_null( $value ) && '' !== $value && 0 != $value ) {
echo '<tr>';
echo '<th>' . esc_html( $label ) . '</th>';
echo '<td>' . esc_html( $value ) . ( $unit ? ' ' . esc_html( $unit ) : '' ) . '</td>';
echo '</tr>';
}
};
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: \'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>\' }).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<yData.length; i++) {
if (yData[i] !== null) {
filteredY.push(yData[i]);
filteredX.push(xData[i]);
}
}
}
const ctx = chartEl.getContext("2d");
activeChart = new Chart(ctx, {
type: "line",
data: {
labels: filteredX,
datasets: [{
label: chartConfigs[chartType].label, data: filteredY,
borderColor: chartConfigs[chartType].color, backgroundColor: chartConfigs[chartType].color + "20",
fill: true, pointRadius: 0, tension: 0.1
}]
},
options: {
responsive: true, maintainAspectRatio: false,
scales: {
x: { title: { display: true, text: xAxisConfigs[xAxisType].label }, ticks: { callback: xAxisType === "time" ? xAxisConfigs.time.formatter : (v) => 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 );
}
?>
<div class="mystat-single-activity-shortcode" id="<?php echo esc_attr( $unique_id ); ?>">
<h4><?php echo esc_html( $activity->title ); ?></h4>
<p><em><?php echo esc_html( date_i18n( 'j F Y', strtotime( $activity->date ) ) ); ?></em></p>
<div class="mystat-single-columns-container">
<div class="mystat-single-col">
<table class="mystat-single-summary-table">
<tbody>
<?php $render_row( 'Dystans', number_format( $activity->distance, 2, ',', ' ' ), 'km' ); ?>
<?php $render_row( 'Czas trwania', $activity->duration ); ?>
<?php $render_row( 'Średnia prędkość', number_format( $activity->avg_speed, 1, ',', ' ' ), 'km/h' ); ?>
<?php $render_row( 'Suma wzniosów', $activity->total_elevation_gain, 'm' ); ?>
</tbody>
</table>
</div>
<div class="mystat-single-col">
<table class="mystat-single-summary-table">
<tbody>
<?php $render_row( 'Kategoria', $activity->category_name ); ?>
<?php $render_row( 'Sprzęt', $activity->equipment_name ); ?>
<?php if ( ! empty( $activity->strava_url ) ) : ?>
<tr><th>Strava</th><td><a href="<?php echo esc_url( $activity->strava_url ); ?>" target="_blank" rel="noopener noreferrer">Zobacz aktywność</a></td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php if ( $has_gpx_data ) : ?>
<div id="<?php echo esc_attr( $map_id ); ?>" class="mystat-single-map" style="height: 350px; width: 100%; margin-top: 15px; border-radius: 5px; margin-bottom: 20px;"></div>
<?php if ( ! empty( $available_profiles ) ) : ?>
<div class="mystat-charts-container">
<div class="mystat-chart-controls" style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; margin-bottom: 15px; border-bottom: 1px solid #eee; padding-bottom: 10px; gap: 10px;">
<div class="mystat-chart-tabs" style="display: flex; flex-wrap: wrap; gap: 5px;">
<?php
$is_first = true;
foreach ( $available_profiles as $key => $label ) :
?>
<button data-type="<?php echo esc_attr( $key ); ?>" class="mystat-chart-tab <?php
if ( $is_first ) {
echo 'active';
$is_first = false; }
?>
"><?php echo esc_html( $label ); ?></button>
<?php endforeach; ?>
</div>
<?php if ( $has_time_data ) : ?>
<div class="mystat-xaxis-switcher" style="padding-top: 5px; font-size: 0.9em; white-space: nowrap;">
<strong>Oś X:</strong>&nbsp;
<label><input type="radio" name="xaxis-<?php echo esc_attr( $unique_id ); ?>" value="distance" checked> Dystans</label>
&nbsp;
<label><input type="radio" name="xaxis-<?php echo esc_attr( $unique_id ); ?>" value="time"> Czas</label>
</div>
<?php else : ?>
<input type="hidden" name="xaxis-<?php echo esc_attr( $unique_id ); ?>" value="distance" checked>
<?php endif; ?>
</div>
<div class="mystat-chart-wrapper" style="position: relative; height:250px; width:100%;">
<canvas id="<?php echo esc_attr( $chart_id ); ?>"></canvas>
</div>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
<?php
return ob_get_clean();
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Rejestruje shortcode [statpress_summary] and [statpress_activity].
*/
function statpress_register_shortcode() {
add_shortcode( 'statpress_summary', 'statpress_shortcode_handler' );
add_shortcode( 'statpress_activity', 'statpress_single_activity_shortcode_handler' );
}
/**
* Funkcja obsługująca shortcode.
* @param array $atts Atrybuty shortcode'u (np. year, month).
* @return string HTML do wyświetlenia.
*/
function statpress_shortcode_handler( $atts ) {
wp_enqueue_style( 'statpress-frontend-styles' );
global $wpdb;
// Ustawienie domyślnych atrybutów (bieżący rok i miesiąc)
$atts = shortcode_atts(
array(
'year' => 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();
?>
<div class="statpress-shortcode-container">
<h3>Podsumowanie miesiąca</h3>
<table class="statpress-summary-table">
<thead>
<tr>
<th>Całkowity dystans</th>
<th>Całkowity czas</th>
</tr>
</thead>
<tbody>
<tr>
<td><?php echo number_format( $total_distance, 2, ',', ' ' ); ?> km</td>
<td><?php echo esc_html( $total_duration_formatted ); ?></td>
</tr>
</tbody>
</table>
<h3>Lista aktywności</h3>
<table class="statpress-activity-table">
<thead>
<tr>
<th>Data</th>
<th>Tytuł</th>
<th>Kategoria</th>
<th>Dystans</th>
<th>Czas</th>
<th>Sprzęt</th>
</tr>
</thead>
<tbody>
<?php if ( ! empty( $activities ) ) : ?>
<?php foreach ( $activities as $row ) : ?>
<tr>
<td><?php echo esc_html( date_i18n( 'd.m.Y', strtotime( $row->date ) ) ); ?></td>
<td><?php echo esc_html( $row->title ); ?></td>
<td><?php echo esc_html( $row->category_name ); ?></td>
<td><?php echo number_format( $row->distance, 2, ',', ' ' ); ?> km</td>
<td><?php echo esc_html( $row->duration ); ?></td>
<td><?php echo esc_html( $row->equipment_name ); ?></td>
</tr>
<?php if ( $row->avg_speed || $row->avg_heart_rate || $row->avg_cadence ) : ?>
<tr class="statpress-activity-details-row">
<td colspan="6" class="statpress-activity-details-cell">
<table class="statpress-nested-details-table">
<thead>
<tr>
<?php if ( $row->avg_speed ) { echo '<th>Śr. prędkość</th>'; } ?>
<?php if ( $row->avg_heart_rate ) { echo '<th>Śr. tętno</th>'; } ?>
<?php if ( $row->avg_cadence ) { echo '<th>Śr. kadencja</th>'; } ?>
</tr>
</thead>
<tbody>
<tr>
<?php if ( $row->avg_speed ) { echo '<td>' . number_format( $row->avg_speed, 1, ',', ' ' ) . ' km/h</td>'; } ?>
<?php if ( $row->avg_heart_rate ) { echo '<td>' . $row->avg_heart_rate . ' bpm</td>'; } ?>
<?php if ( $row->avg_cadence ) { echo '<td>' . $row->avg_cadence . ' rpm</td>'; } ?>
</tr>
</tbody>
</table>
</td>
</tr>
<?php endif; ?>
<?php endforeach; ?>
<?php else : ?>
<tr>
<td colspan="6">Brak aktywności w tym miesiącu.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<?php
// Zwrócenie zawartości bufora
return ob_get_clean();
}
function statpress_single_activity_shortcode_handler( $atts ) {
wp_enqueue_style( 'statpress-frontend-styles' );
global $wpdb;
$atts = shortcode_atts(
array(
'id' => 0,
),
$atts,
'statpress_activity'
);
$activity_id = intval( $atts['id'] );
if ( 0 === $activity_id ) {
return '<p><strong>Błąd:</strong> Nie podano ID wpisu w shortcode.</p>';
}
// 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 '<p><strong>Błąd:</strong> Nie znaleziono wpisu o ID ' . esc_html( $activity_id ) . '.</p>';
}
// Funkcja pomocnicza do wyświetlania wiersza
$render_row = function( $label, $value, $unit = '' ) {
if ( ! is_null( $value ) && '' !== $value && 0 != $value ) {
echo '<tr>';
echo '<th>' . esc_html( $label ) . '</th>';
echo '<td>' . esc_html( $value ) . ( $unit ? ' ' . esc_html( $unit ) : '' ) . '</td>';
echo '</tr>';
}
};
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: \'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>\' }).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<yData.length; i++) {
if (yData[i] !== null) {
filteredY.push(yData[i]);
filteredX.push(xData[i]);
}
}
}
const ctx = chartEl.getContext("2d");
activeChart = new Chart(ctx, {
type: "line",
data: {
labels: filteredX,
datasets: [{
label: chartConfigs[chartType].label, data: filteredY,
borderColor: chartConfigs[chartType].color, backgroundColor: chartConfigs[chartType].color + "20",
fill: true, pointRadius: 0, tension: 0.1
}]
},
options: {
responsive: true, maintainAspectRatio: false,
scales: {
x: { title: { display: true, text: xAxisConfigs[xAxisType].label }, ticks: { callback: xAxisType === "time" ? xAxisConfigs.time.formatter : (v) => 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 );
}
?>
<div class="statpress-single-activity-shortcode" id="<?php echo esc_attr( $unique_id ); ?>">
<h4><?php echo esc_html( $activity->title ); ?></h4>
<p><em><?php echo esc_html( date_i18n( 'j F Y', strtotime( $activity->date ) ) ); ?></em></p>
<div class="statpress-single-columns-container">
<div class="statpress-single-col">
<table class="statpress-single-summary-table">
<tbody>
<?php $render_row( 'Dystans', number_format( $activity->distance, 2, ',', ' ' ), 'km' ); ?>
<?php $render_row( 'Czas trwania', $activity->duration ); ?>
<?php $render_row( 'Średnia prędkość', number_format( $activity->avg_speed, 1, ',', ' ' ), 'km/h' ); ?>
<?php $render_row( 'Suma wzniosów', $activity->total_elevation_gain, 'm' ); ?>
</tbody>
</table>
</div>
<div class="statpress-single-col">
<table class="statpress-single-summary-table">
<tbody>
<?php $render_row( 'Kategoria', $activity->category_name ); ?>
<?php $render_row( 'Sprzęt', $activity->equipment_name ); ?>
<?php if ( ! empty( $activity->strava_url ) ) : ?>
<tr><th>Strava</th><td><a href="<?php echo esc_url( $activity->strava_url ); ?>" target="_blank" rel="noopener noreferrer">Zobacz aktywność</a></td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php if ( $has_gpx_data ) : ?>
<div id="<?php echo esc_attr( $map_id ); ?>" class="statpress-single-map" style="height: 350px; width: 100%; margin-top: 15px; border-radius: 5px; margin-bottom: 20px;"></div>
<?php if ( ! empty( $available_profiles ) ) : ?>
<div class="statpress-charts-container">
<div class="statpress-chart-controls" style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; margin-bottom: 15px; border-bottom: 1px solid #eee; padding-bottom: 10px; gap: 10px;">
<div class="statpress-chart-tabs" style="display: flex; flex-wrap: wrap; gap: 5px;">
<?php
$is_first = true;
foreach ( $available_profiles as $key => $label ) :
?>
<button data-type="<?php echo esc_attr( $key ); ?>" class="statpress-chart-tab <?php
if ( $is_first ) {
echo 'active';
$is_first = false; }
?>
"><?php echo esc_html( $label ); ?></button>
<?php endforeach; ?>
</div>
<?php if ( $has_time_data ) : ?>
<div class="statpress-xaxis-switcher" style="padding-top: 5px; font-size: 0.9em; white-space: nowrap;">
<strong>Oś X:</strong>&nbsp;
<label><input type="radio" name="xaxis-<?php echo esc_attr( $unique_id ); ?>" value="distance" checked> Dystans</label>
&nbsp;
<label><input type="radio" name="xaxis-<?php echo esc_attr( $unique_id ); ?>" value="time"> Czas</label>
</div>
<?php else : ?>
<input type="hidden" name="xaxis-<?php echo esc_attr( $unique_id ); ?>" value="distance" checked>
<?php endif; ?>
</div>
<div class="statpress-chart-wrapper" style="position: relative; height:250px; width:100%;">
<canvas id="<?php echo esc_attr( $chart_id ); ?>"></canvas>
</div>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
<?php
return ob_get_clean();
}
+112
View File
@@ -36,6 +36,9 @@ require_once STATPRESS_PLUGIN_DIR . 'includes/admin/pages/page-yearly-summary.ph
require_once STATPRESS_PLUGIN_DIR . 'includes/admin/pages/page-infographic.php';
require_once STATPRESS_PLUGIN_DIR . 'includes/admin/pages/page-import-csv.php';
global $statpress_plugin_hooks;
$statpress_plugin_hooks = array();
add_action( 'admin_menu', 'statpress_add_admin_menu' );
add_action( 'admin_init', 'statpress_admin_init_setup' );
add_action( 'admin_enqueue_scripts', 'statpress_enqueue_admin_styles' );
@@ -53,3 +56,112 @@ require_once STATPRESS_PLUGIN_DIR . 'includes/frontend/shortcodes.php';
add_action( 'wp_enqueue_scripts', 'statpress_enqueue_frontend_assets' );
add_action( 'init', 'statpress_register_shortcode' );
// --- 5. MIGRACJA DANYCH (jednorazowa) ---
add_action( 'admin_init', 'statpress_handle_data_migration' );
add_action( 'admin_init', 'statpress_handle_admin_tools' );
/**
* Handles the one-time data migration from 'mystat_' tables to 'statpress_' tables.
*/
function statpress_handle_data_migration() {
// Check if the migration action is triggered, nonce is valid, and user has permissions.
if ( ! isset( $_GET['action'] ) || 'statpress_migrate_data' !== $_GET['action'] ) {
return;
}
if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( $_GET['_wpnonce'], 'statpress_migration_nonce' ) ) {
return;
}
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
global $wpdb;
// Define table columns to ensure robust migration, even if schemas differ.
$table_columns = array(
'activities' => '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;
}
}