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';
// SQL dla Kategorii
$sql_cat = "CREATE TABLE $table_categories (
id mediumint(9) NOT NULL AUTO_INCREMENT,
name varchar(50) NOT NULL,
icon varchar(50) NOT NULL,
color varchar(20) NOT NULL,
PRIMARY KEY (id)
) $charset_collate;";
// SQL dla Typów Wydarzeń
$sql_event_types = "CREATE TABLE $table_event_types (
id mediumint(9) NOT NULL AUTO_INCREMENT,
name varchar(100) NOT NULL,
PRIMARY KEY (id)
) $charset_collate;";
// SQL dla Sprzętu
$sql_equipment = "CREATE TABLE $table_equipment (
id mediumint(9) NOT NULL AUTO_INCREMENT,
name varchar(100) NOT NULL,
PRIMARY KEY (id)
) $charset_collate;";
// SQL dla 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_equipment );
dbDelta( $sql_cat );
dbDelta( $sql_event_types );
dbDelta( $sql_act );
// Dodanie domyślnych kategorii, jeśli tabela jest pusta
if ( $wpdb->get_var( "SELECT COUNT(*) FROM $table_categories" ) == 0 ) {
$wpdb->insert( $table_categories, array( 'name' => 'Rower', 'icon' => 'dashicons-buddicons-groups', 'color' => '#3498db' ) );
$wpdb->insert( $table_categories, array( 'name' => 'Bieganie', 'icon' => 'dashicons-businessman', 'color' => '#e74c3c' ) );
}
// Dodanie domyślnych typów wydarzeń, jeśli tabela jest pusta
if ( $wpdb->get_var( "SELECT COUNT(*) FROM $table_event_types" ) == 0 ) {
$default_event_types = ['Bez kategorii', 'Fitness', 'Geocaching', 'Podróżowanie', 'Rekreacyjny', 'Specjalne zdarzenie', 'Transport', 'Trening', 'Wyścig'];
foreach ($default_event_types as $type_name) {
$wpdb->insert( $table_event_types, array( 'name' => $type_name ) );
}
// Ustawienie domyślnego typu "Trening" dla istniejących aktywności, które go nie mają
$training_id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $table_event_types WHERE name = %s", 'Trening' ) );
if ($training_id) {
$wpdb->query( $wpdb->prepare( "UPDATE $table_activities SET event_type_id = %d WHERE event_type_id IS NULL OR event_type_id = 0", $training_id ) );
}
}
// Dodanie domyślnego sprzętu, jeśli tabela jest pusta
if ( $wpdb->get_var( "SELECT COUNT(*) FROM $table_equipment" ) == 0 ) {
$default_equipment = ['Giant Revolt', 'Cube LTD', 'Author Agang', 'Liv Tempt 4', 'Cube Acid 24', 'Mongoose BMX', 'Nextbike - Miejski'];
foreach ($default_equipment as $eq_name) {
$wpdb->insert( $table_equipment, array( 'name' => $eq_name ) );
}
}
}
// --- 2. MENU ADMINA I DASHBOARD ---
$mystat_plugin_hooks = [];
add_action( 'admin_menu', 'mystat_add_admin_menu' );
/**
* Set up admin-specific hooks.
*/
function mystat_admin_init_setup() {
add_filter( 'upload_mimes', 'mystat_add_gpx_mime_type' );
add_filter( 'wp_check_filetype_and_ext', 'mystat_fix_gpx_upload_permission', 10, 4 );
mystat_register_settings();
}
add_action( 'admin_init', 'mystat_admin_init_setup' );
/**
* Enqueue admin-specific CSS.
*
* @param string $hook The current admin page hook.
*/
function mystat_enqueue_admin_styles( $hook ) {
global $mystat_plugin_hooks;
if ( in_array( $hook, $mystat_plugin_hooks, true ) ) {
$plugin_version = '1.0'; // You can use filemtime() for cache-busting in development
wp_enqueue_style( 'mystat-admin-styles', plugin_dir_url( __FILE__ ) . 'assets/css/admin.css', [], $plugin_version );
}
}
add_action( 'admin_enqueue_scripts', 'mystat_enqueue_admin_styles' );
/**
* Add GPX support to WordPress Media Library.
*
* @param array $mimes Allowed mime types.
* @return array Modified mime types.
*/
function mystat_add_gpx_mime_type( $mimes ) {
$mimes['gpx'] = 'application/gpx+xml';
return $mimes;
}
/**
* Bypasses WordPress's strict file type check for GPX files.
* This is needed because WordPress can be overly cautious with XML-based files.
*
* @param array $data File data.
* @param string $file Full path to the file.
* @param string $filename The filename.
* @param array $mimes Mime types.
* @return array Modified file data.
*/
function mystat_fix_gpx_upload_permission( $data, $file, $filename, $mimes ) {
if ( strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) ) === 'gpx' ) {
$data['ext'] = 'gpx';
$data['type'] = 'application/gpx+xml';
}
return $data;
}
function mystat_add_admin_menu() {
global $mystat_plugin_hooks;
$mystat_plugin_hooks[] = add_menu_page(
'Moje Statystyki', // Tytuł strony
'Statystyki', // Tytuł w menu
'manage_options', // Wymagane uprawnienia
'moje-statystyki', // Slug menu
'mystat_dashboard_page', // Funkcja renderująca stronę główną (dashboard)
'dashicons-chart-line', // Ikona
6 // Pozycja
);
$mystat_plugin_hooks[] = add_submenu_page(
'moje-statystyki', // Slug rodzica
'Dodaj Nowy Trening', // Tytuł strony
'Nowy trening', // Tytuł w podmenu
'manage_options', // Wymagane uprawnienia
'mystat-nowy-trening', // Slug podmenu
'mystat_add_new_page' // Funkcja renderująca stronę dodawania
);
$mystat_plugin_hooks[] = add_submenu_page(
'moje-statystyki',
'Typy Wydarzeń',
'Typy wydarzeń',
'manage_options',
'mystat-event-types',
'mystat_event_types_page'
);
$mystat_plugin_hooks[] = add_submenu_page(
'moje-statystyki',
'Sprzęt',
'Sprzęt',
'manage_options',
'mystat-equipment',
'mystat_equipment_page'
);
$mystat_plugin_hooks[] = add_submenu_page(
null, // Ukryta strona, nie pojawia się w menu
'Szczegóły Treningu', // Tytuł strony
'Szczegóły Treningu', // Tytuł w menu (nieistotny)
'manage_options', // Wymagane uprawnienia
'mystat-view-activity', // Slug podmenu
'mystat_view_activity_page' // Funkcja renderująca
);
$mystat_plugin_hooks[] = add_submenu_page(
null, // Ukryta strona
'Edytuj Trening', // Tytuł strony
'Edytuj Trening', // Tytuł w menu (nieistotny)
'manage_options', // Wymagane uprawnienia
'mystat-edit-activity', // Slug podmenu
'mystat_edit_activity_page' // Funkcja renderująca
);
$mystat_plugin_hooks[] = add_submenu_page(
'moje-statystyki', // Slug rodzica
'Podsumowanie Roczne', // Tytuł strony
'Podsumowanie Roczne', // Tytuł w podmenu
'manage_options', // Wymagane uprawnienia
'mystat-yearly-summary', // Slug podmenu
'mystat_yearly_summary_page'// Funkcja renderująca
);
$mystat_plugin_hooks[] = add_submenu_page(
'moje-statystyki', // Slug rodzica
'Infografika', // Tytuł strony
'Infografika', // Tytuł w podmenu
'manage_options', // Wymagane uprawnienia
'mystat-infographic', // Slug podmenu
'mystat_infographic_page' // Funkcja renderująca
);
$mystat_plugin_hooks[] = add_submenu_page(
'moje-statystyki', // Slug rodzica
'Import CSV', // Tytuł strony
'Import CSV', // Tytuł w podmenu
'manage_options', // Wymagane uprawnienia
'mystat-import-csv', // Slug podmenu
'mystat_import_csv_page' // Funkcja renderująca
);
$mystat_plugin_hooks[] = add_submenu_page(
'moje-statystyki', // Slug rodzica
'Ustawienia', // Tytuł strony
'Ustawienia', // Tytuł w podmenu
'manage_options', // Wymagane uprawnienia
'mystat-settings', // Slug podmenu
'mystat_settings_page' // Funkcja renderująca
);
}
function mystat_dashboard_page() {
echo '
Moje Statystyki Sportowe ';
mystat_render_history_table();
echo '';
}
function mystat_add_new_page() {
echo '
Dodaj Nowy Trening ';
// Obsługa zapisu formularza (musi być przed renderowaniem, aby wyświetlić komunikat)
mystat_handle_activity_form_submission();
// Formularz dodawania
mystat_render_add_form();
echo '';
}
function mystat_event_types_page() {
global $wpdb;
$table_event_types = $wpdb->prefix . 'mystat_event_types';
$message = '';
$notice_class = '';
// Handle POST requests (add/update)
if ( isset( $_POST['submit'] ) && check_admin_referer( 'mystat_manage_event_type' ) ) {
$name = sanitize_text_field( $_POST['event_type_name'] );
$type_id = isset( $_POST['event_type_id'] ) ? intval( $_POST['event_type_id'] ) : 0;
if ( ! empty( $name ) ) {
if ( $type_id > 0 ) { // Update
$wpdb->update( $table_event_types, [ 'name' => $name ], [ 'id' => $type_id ] );
$message = 'Typ wydarzenia zaktualizowany.';
$notice_class = 'notice-success';
} else { // Insert
$wpdb->insert( $table_event_types, [ 'name' => $name ] );
$message = 'Typ wydarzenia dodany.';
$notice_class = 'notice-success';
}
} else {
$message = 'Nazwa typu wydarzenia nie może być pusta.';
$notice_class = 'notice-error';
}
}
// Handle GET requests (delete)
if ( isset( $_GET['action'], $_GET['id'], $_GET['_wpnonce'] ) && $_GET['action'] === 'delete' ) {
if ( wp_verify_nonce( $_GET['_wpnonce'], 'mystat_delete_event_type_' . $_GET['id'] ) ) {
$wpdb->delete( $table_event_types, [ 'id' => intval( $_GET['id'] ) ] );
$message = 'Typ wydarzenia usunięty.';
$notice_class = 'notice-success';
}
}
// Prepare for form (for editing)
$item_to_edit = null;
if ( isset( $_GET['action'], $_GET['id'] ) && $_GET['action'] === 'edit' ) {
$item_to_edit = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_event_types WHERE id = %d", intval( $_GET['id'] ) ) );
}
$event_types = $wpdb->get_results( "SELECT * FROM $table_event_types ORDER BY name ASC" );
?>
prefix . 'mystat_equipment';
$message = '';
$notice_class = '';
// Handle POST requests (add/update)
if ( isset( $_POST['submit'] ) && check_admin_referer( 'mystat_manage_equipment' ) ) {
$name = sanitize_text_field( $_POST['equipment_name'] );
$item_id = isset( $_POST['equipment_id'] ) ? intval( $_POST['equipment_id'] ) : 0;
if ( ! empty( $name ) ) {
if ( $item_id > 0 ) { // Update
$wpdb->update( $table_equipment, [ 'name' => $name ], [ 'id' => $item_id ] );
$message = 'Sprzęt zaktualizowany.';
$notice_class = 'notice-success';
} else { // Insert
$wpdb->insert( $table_equipment, [ 'name' => $name ] );
$message = 'Sprzęt dodany.';
$notice_class = 'notice-success';
}
} else {
$message = 'Nazwa sprzętu nie może być pusta.';
$notice_class = 'notice-error';
}
}
// Handle GET requests (delete)
if ( isset( $_GET['action'], $_GET['id'], $_GET['_wpnonce'] ) && $_GET['action'] === 'delete' ) {
if ( wp_verify_nonce( $_GET['_wpnonce'], 'mystat_delete_equipment_' . $_GET['id'] ) ) {
$wpdb->delete( $table_equipment, [ 'id' => intval( $_GET['id'] ) ] );
$message = 'Sprzęt usunięty.';
$notice_class = 'notice-success';
}
}
// Prepare for form (for editing)
$item_to_edit = null;
if ( isset( $_GET['action'], $_GET['id'] ) && $_GET['action'] === 'edit' ) {
$item_to_edit = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_equipment WHERE id = %d", intval( $_GET['id'] ) ) );
}
$equipment_list = $wpdb->get_results( "SELECT * FROM $table_equipment ORDER BY name ASC" );
?>
Ustawienia Wtyczki Statystyk
Zdefiniuj strefę prywatności, aby ukryć początek i koniec swoich tras GPX. Punkty wewnątrz tego okręgu nie będą wyświetlane na mapie.';
echo 'Aby znaleźć swoje współrzędne, kliknij prawym przyciskiem myszy na mapie Google w wybranym miejscu - współrzędne pojawią się jako pierwsza pozycja w menu.
';
}
function mystat_render_lat_field() {
$options = get_option( 'mystat_privacy_options' );
$latitude = isset( $options['latitude'] ) ? esc_attr( $options['latitude'] ) : '';
echo " ";
}
function mystat_render_lon_field() {
$options = get_option( 'mystat_privacy_options' );
$longitude = isset( $options['longitude'] ) ? esc_attr( $options['longitude'] ) : '';
echo " ";
}
function mystat_render_radius_field() {
$options = get_option( 'mystat_privacy_options' );
$radius = isset( $options['radius'] ) ? esc_attr( $options['radius'] ) : '500';
echo " metrów";
}
function mystat_sanitize_privacy_options( $input ) {
$sanitized_input = [];
if ( isset( $input['latitude'] ) ) {
$sanitized_input['latitude'] = floatval( str_replace( ',', '.', $input['latitude'] ) );
}
if ( isset( $input['longitude'] ) ) {
$sanitized_input['longitude'] = floatval( str_replace( ',', '.', $input['longitude'] ) );
}
if ( isset( $input['radius'] ) ) {
$sanitized_input['radius'] = abs( intval( $input['radius'] ) );
}
return $sanitized_input;
}
function mystat_yearly_summary_page() {
global $wpdb;
$table_activities = $wpdb->prefix . 'mystat_activities';
$current_year = isset( $_GET['year'] ) ? intval( $_GET['year'] ) : current_time( 'Y' );
// Pobierz dostępne lata z bazy danych
$available_years = $wpdb->get_col( "SELECT DISTINCT YEAR(date) FROM $table_activities ORDER BY YEAR(date) DESC" );
if ( empty( $available_years ) ) {
$available_years = [current_time('Y')]; // Domyślny rok, jeśli brak danych
}
// Zapytanie SQL do grupowania danych miesięcznie
$sql = $wpdb->prepare("
SELECT
MONTH(date) as month_num,
SUM(distance) as total_distance,
SUM(calories) as total_calories,
SUM(TIME_TO_SEC(duration)) as total_seconds,
COUNT(id) as activity_count
FROM $table_activities
WHERE YEAR(date) = %d
GROUP BY month_num
ORDER BY month_num ASC
", $current_year);
$monthly_summary = $wpdb->get_results( $sql, OBJECT_K ); // OBJECT_K zwróci tablicę z kluczami będącymi numerami miesięcy
// Przygotowanie danych dla wszystkich 12 miesięcy
$full_year_summary = [];
$total_year_distance = 0;
$total_year_calories = 0;
$total_year_seconds = 0;
// Określ, ile miesięcy pokazać, aby uniknąć zer dla przyszłych miesięcy
$this_year = (int) current_time('Y');
$this_month = (int) current_time('n');
$loop_until_month = 12; // Domyślnie dla lat ubiegłych
if ( $current_year == $this_year ) {
// Dla bieżącego roku, pokaż miesiące do bieżącego miesiąca
$loop_until_month = $this_month;
} elseif ( $current_year > $this_year ) {
// Dla przyszłych lat, pokaż miesiące tylko do ostatniego, w którym są dane
$last_month_with_data = $wpdb->get_var( $wpdb->prepare( "SELECT MAX(MONTH(date)) FROM $table_activities WHERE YEAR(date) = %d", $current_year ) );
$loop_until_month = $last_month_with_data ? (int) $last_month_with_data : 0;
}
for ( $i = 1; $i <= $loop_until_month; $i++ ) {
$month_name = date_i18n( 'F', mktime( 0, 0, 0, $i, 10 ) ); // Nazwa miesiąca
$data = isset( $monthly_summary[$i] ) ? $monthly_summary[$i] : null;
$full_year_summary[$i] = (object) [
'month_name' => $month_name,
'total_distance' => $data ? $data->total_distance : 0,
'total_calories' => $data ? (int)$data->total_calories : 0,
'total_seconds' => $data ? (int)$data->total_seconds : 0,
'activity_count' => $data ? (int)$data->activity_count : 0,
];
$total_year_distance += $full_year_summary[$i]->total_distance;
$total_year_seconds += $full_year_summary[$i]->total_seconds;
$total_year_calories += $full_year_summary[$i]->total_calories;
}
$total_year_hours = floor($total_year_seconds / 3600);
$total_year_minutes = floor(($total_year_seconds % 3600) / 60);
$total_year_duration_formatted = sprintf('%d godz. %d min.', $total_year_hours, $total_year_minutes);
// Przygotowanie danych dla wykresu
$chart_labels_js = [];
$chart_datasets = [
'distance' => [],
'duration' => [],
'calories' => [],
'activities' => [],
];
foreach ($full_year_summary as $month_data) {
$chart_labels_js[] = $month_data->month_name;
$chart_datasets['distance'][] = round((float)$month_data->total_distance, 2);
$chart_datasets['duration'][] = round($month_data->total_seconds / 3600, 2); // w godzinach
$chart_datasets['calories'][] = $month_data->total_calories;
$chart_datasets['activities'][] = $month_data->activity_count;
}
// Włączenie skryptów dla Chart.js
wp_enqueue_script('chart-js', 'https://cdn.jsdelivr.net/npm/chart.js', [], null, true);
wp_register_script('mystat-chart-loader', false);
wp_enqueue_script('mystat-chart-loader');
$chart_configs = [
'distance' => [
'label' => 'Dystans (km)',
'data' => $chart_datasets['distance'],
'backgroundColor' => 'rgba(52, 152, 219, 0.5)',
'borderColor' => 'rgba(52, 152, 219, 1)',
'yAxisLabel' => 'Kilometry'
],
'duration' => [
'label' => 'Czas trwania (godz.)',
'data' => $chart_datasets['duration'],
'backgroundColor' => 'rgba(26, 188, 156, 0.5)',
'borderColor' => 'rgba(26, 188, 156, 1)',
'yAxisLabel' => 'Godziny'
],
'calories' => [
'label' => 'Kalorie (kcal)',
'data' => $chart_datasets['calories'],
'backgroundColor' => 'rgba(231, 76, 60, 0.5)',
'borderColor' => 'rgba(231, 76, 60, 1)',
'yAxisLabel' => 'kcal'
],
'activities' => [
'label' => 'Liczba aktywności',
'data' => $chart_datasets['activities'],
'backgroundColor' => 'rgba(241, 196, 15, 0.5)',
'borderColor' => 'rgba(241, 196, 15, 1)',
'yAxisLabel' => 'Ilość'
],
];
wp_add_inline_script('mystat-chart-loader', '
document.addEventListener("DOMContentLoaded", function() {
const chartLabels = ' . json_encode($chart_labels_js) . ';
const chartConfigs = ' . json_encode($chart_configs) . ';
let activeChart = null;
const tabs = document.querySelectorAll(".nav-tab");
tabs.forEach(tab => {
tab.addEventListener("click", function(e) {
e.preventDefault();
tabs.forEach(t => t.classList.remove("nav-tab-active"));
this.classList.add("nav-tab-active");
const chartType = this.getAttribute("href").substring(1);
renderChart(chartType);
});
});
function renderChart(type) {
if (activeChart) {
activeChart.destroy();
}
const config = chartConfigs[type];
const ctx = document.getElementById("mystatYearlyChart").getContext("2d");
activeChart = new Chart(ctx, {
type: "bar",
data: {
labels: chartLabels,
datasets: [{
label: config.label,
data: config.data,
backgroundColor: config.backgroundColor,
borderColor: config.borderColor,
borderWidth: 1
}]
},
options: {
responsive: true, maintainAspectRatio: false,
scales: { y: { beginAtZero: true, title: { display: true, text: config.yAxisLabel } } }
}
});
}
// Render initial chart
if (tabs.length > 0) {
renderChart(tabs[0].getAttribute("href").substring(1));
}
});
');
?>
Podsumowanie Roczne
Miesiąc Dystans (km) Kalorie (kcal) Czas
month_name ); ?>
total_distance, 2, ',', ' ' ); ?>
total_calories, 0, ',', ' ' ); ?>
total_seconds) ); ?>
SUMA ROCZNA
20 ] );
if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) {
return [];
}
$gpx_content = wp_remote_retrieve_body( $response );
if ( empty( $gpx_content ) ) {
return [];
}
// --- Privacy Zone ---
$privacy_options = get_option( 'mystat_privacy_options' );
$privacy_enabled = ! empty( $privacy_options['latitude'] ) && ! empty( $privacy_options['longitude'] ) && ! empty( $privacy_options['radius'] );
$privacy_center_lat = $privacy_enabled ? (float) $privacy_options['latitude'] : 0;
$privacy_center_lon = $privacy_enabled ? (float) $privacy_options['longitude'] : 0;
$privacy_radius_km = $privacy_enabled ? ( (int) $privacy_options['radius'] ) / 1000 : 0; // Convert meters to km
libxml_use_internal_errors( true );
$gpx = simplexml_load_string( $gpx_content );
libxml_clear_errors();
if ( $gpx === false ) {
return [];
}
// Use XPath to be more robust against different GPX structures/namespaces
$gpx->registerXPathNamespace( 'gpx', 'http://www.topografix.com/GPX/1/1' );
$trackpoints = $gpx->xpath( '//gpx:trkpt' );
if ( empty( $trackpoints ) ) {
$trackpoints = $gpx->xpath( '//trkpt' ); // Fallback for files without namespace
}
$raw_points = [];
$start_time = null;
foreach ( $trackpoints as $trkpt ) {
if ( isset( $trkpt['lat'], $trkpt['lon'] ) ) {
$extensions = $trkpt->extensions ? $trkpt->extensions->children( 'gpxtpx', true ) : null;
$time = isset( $trkpt->time ) ? strtotime( (string) $trkpt->time ) : null;
if ( $time && is_null( $start_time ) ) {
$start_time = $time;
}
$hr_val = ( $extensions && isset( $extensions->TrackPointExtension->hr ) ) ? (int) $extensions->TrackPointExtension->hr : null;
$cad_val = ( $extensions && isset( $extensions->TrackPointExtension->cad ) ) ? (int) $extensions->TrackPointExtension->cad : null;
$raw_points[] = [
'lat' => (float) $trkpt['lat'],
'lon' => (float) $trkpt['lon'],
'ele' => isset( $trkpt->ele ) ? (float) $trkpt->ele : null,
'time_offset' => $start_time && $time ? $time - $start_time : null,
'hr' => $hr_val,
'cad' => $cad_val,
];
}
}
if ( empty( $raw_points ) ) {
return [];
}
// Process raw points to calculate profiles
$map_points = [];
$profiles = [ 'distance' => [], 'time' => [], 'elevation' => [], 'speed' => [], 'hr' => [], 'cadence' => [] ];
$cumulative_distance = 0;
$haversine = function( $lat1, $lon1, $lat2, $lon2 ) {
$earth_radius = 6371; // in km
$dLat = deg2rad( $lat2 - $lat1 );
$dLon = deg2rad( $lon2 - $lon1 );
$a = sin( $dLat / 2 ) * sin( $dLat / 2 ) + cos( deg2rad( $lat1 ) ) * cos( deg2rad( $lat2 ) ) * sin( $dLon / 2 ) * sin( $dLon / 2 );
$c = 2 * atan2( sqrt( $a ), sqrt( 1 - $a ) );
return $earth_radius * $c;
};
foreach ( $raw_points as $i => $point ) {
$is_in_privacy_zone = false;
if ( $privacy_enabled ) {
$distance_from_center = $haversine( $privacy_center_lat, $privacy_center_lon, $point['lat'], $point['lon'] );
if ( $distance_from_center <= $privacy_radius_km ) {
$is_in_privacy_zone = true;
}
}
if ( ! $is_in_privacy_zone ) {
$map_points[] = [ $point['lat'], $point['lon'] ];
$speed = null;
if ( $i > 0 ) {
$prev_point = $raw_points[ $i - 1 ];
$distance_delta = $haversine( $prev_point['lat'], $prev_point['lon'], $point['lat'], $point['lon'] ); // km
$cumulative_distance += $distance_delta;
if ( ! is_null( $point['time_offset'] ) && ! is_null( $prev_point['time_offset'] ) ) {
$time_delta = $point['time_offset'] - $prev_point['time_offset']; // seconds
if ( $time_delta > 0 ) {
$speed = ( $distance_delta * 3600 ) / $time_delta; // km/h
}
}
}
$profiles['distance'][] = round( $cumulative_distance, 3 );
$profiles['time'][] = $point['time_offset'];
$profiles['elevation'][] = ! is_null( $point['ele'] ) ? round( $point['ele'], 2 ) : null;
$profiles['speed'][] = ! is_null( $speed ) ? round( $speed, 1 ) : null;
$profiles['hr'][] = $point['hr'];
$profiles['cadence'][] = $point['cad'];
}
}
return [
'points' => $map_points,
'profiles' => $profiles,
];
}
function mystat_infographic_page() {
global $wpdb;
$table_activities = $wpdb->prefix . 'mystat_activities';
$table_categories = $wpdb->prefix . 'mystat_categories';
$current_year = isset( $_GET['year'] ) ? intval( $_GET['year'] ) : current_time( 'Y' );
// Pobierz dostępne lata z bazy danych
$available_years = $wpdb->get_col( "SELECT DISTINCT YEAR(date) FROM $table_activities ORDER BY YEAR(date) DESC" );
if ( empty( $available_years ) ) {
$available_years = [current_time('Y')]; // Domyślny rok, jeśli brak danych
}
// --- 1. Statystyki ogólne (wszystkie lata) ---
$overall_stats = $wpdb->get_row("
SELECT
SUM(distance) as total_distance,
SEC_TO_TIME(SUM(TIME_TO_SEC(duration))) as total_duration,
SUM(total_elevation_gain) as total_elevation_gain,
COUNT(id) as total_activities
FROM $table_activities
");
// --- 2. Statystyki dla wybranego roku ---
$yearly_stats = $wpdb->get_row($wpdb->prepare("
SELECT
SUM(distance) as total_distance,
SEC_TO_TIME(SUM(TIME_TO_SEC(duration))) as total_duration,
SUM(total_elevation_gain) as total_elevation_gain,
COUNT(id) as total_activities
FROM $table_activities
WHERE YEAR(date) = %d
", $current_year));
// --- 3. Dane dla wykresu kołowego (dystans per kategoria dla wybranego roku) ---
$category_distance_data = $wpdb->get_results($wpdb->prepare("
SELECT c.name as category_name, c.color, SUM(a.distance) as total_distance
FROM $table_activities a
LEFT JOIN $table_categories c ON a.category_id = c.id
WHERE YEAR(a.date) = %d
GROUP BY c.name, c.color
HAVING SUM(a.distance) > 0
ORDER BY total_distance DESC
", $current_year));
$chart_labels = [];
$chart_data = [];
$chart_colors = [];
foreach ($category_distance_data as $data) {
$chart_labels[] = $data->category_name;
$chart_data[] = round((float)$data->total_distance, 2);
$chart_colors[] = $data->color;
}
// Włączenie skryptów dla Chart.js
wp_enqueue_script('chart-js', 'https://cdn.jsdelivr.net/npm/chart.js', [], null, true);
wp_register_script('mystat-infographic-chart-loader', false);
wp_enqueue_script('mystat-infographic-chart-loader');
wp_add_inline_script('mystat-infographic-chart-loader', '
document.addEventListener("DOMContentLoaded", function() {
const ctx = document.getElementById("mystatCategoryPieChart");
if (!ctx) return;
new Chart(ctx, {
type: "pie",
data: {
labels: ' . json_encode($chart_labels) . ',
datasets: [{
data: ' . json_encode($chart_data) . ',
backgroundColor: ' . json_encode($chart_colors) . ',
hoverOffset: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: "right",
},
title: {
display: true,
text: "Dystans wg kategorii w ' . esc_js($current_year) . '"
}
}
}
});
});
');
?>
Infografika Statystyk Sportowych
Dystans total_distance, 2, ',', ' '); ?> km
Wznios total_elevation_gain, 0, ',', ' '); ?> m
Aktywności total_activities, 0, ',', ' '); ?>
Dystans total_distance, 2, ',', ' '); ?> km
Wznios total_elevation_gain, 0, ',', ' '); ?> m
Aktywności total_activities, 0, ',', ' '); ?>
Importuj aktywności z pliku CSV ';
// Handle the form submission
if ( 'POST' === $_SERVER['REQUEST_METHOD'] && isset( $_POST['mystat_csv_import_nonce_field'] ) ) {
mystat_handle_csv_import();
}
// Display the form
?>
Aby zaimportować dane, możesz wgrać plik CSV LUB wkleić dane bezpośrednio w pole tekstowe poniżej. Ta druga opcja jest zalecana, jeśli napotykasz błędy z plikiem.
Wymagane kolumny: Data, Tytuł, Dystans oraz Typ aktywności (lub Kategoria).
Opcjonalne kolumny: Czas (format HH:MM:SS), Kalorie, Średnie tętno, Maksymalne tętno, Średnia prędkość, Maksymalna prędkość, Średni rytm pedałowania, Maksymalny rytm pedałowania, Całkowity wznios, Całkowity spadek, Minimalna wysokość, Maksymalna wysokość, Sprzęt, Typ wydarzenia.
Ważne:
Data musi być w formacie YYYY-MM-DD.
Dystans i prędkość: użyj kropki jako separatora dziesiętnego (np. 10.5).
Nazwy w kolumnach Typ aktywności, Sprzęt, Typ wydarzenia muszą dokładnie odpowiadać nazwom zdefiniowanym w ustawieniach wtyczki. Jeśli nazwa nie zostanie znaleziona, wiersz zostanie pominięty.
';
}
function mystat_handle_csv_import() {
global $wpdb;
if ( ! isset( $_POST['mystat_csv_import_nonce_field'] ) || ! wp_verify_nonce( $_POST['mystat_csv_import_nonce_field'], 'mystat_csv_import_nonce' ) ) {
echo 'Błąd weryfikacji bezpieczeństwa.
';
return;
}
if ( ! current_user_can( 'manage_options' ) ) {
echo 'Nie masz wystarczających uprawnień.
';
return;
}
// Unify input source: prefer textarea, fall back to file upload.
$csv_content = '';
if ( ! empty( $_POST['mystat_csv_data'] ) ) {
$csv_content = stripslashes( $_POST['mystat_csv_data'] );
} elseif ( ! empty( $_FILES['mystat_csv_file']['tmp_name'] ) && UPLOAD_ERR_OK === $_FILES['mystat_csv_file']['error'] ) {
$csv_content = file_get_contents( $_FILES['mystat_csv_file']['tmp_name'] );
}
if ( empty( trim( $csv_content ) ) ) {
echo 'Nie podano danych do importu. Wgraj plik lub wklej dane w pole tekstowe.
';
return;
}
// Mapowanie polskich i angielskich nazw kolumn na wewnętrzne klucze
$column_map = [
// Polish => English
'typ aktywności' => 'category_name',
'data' => 'date',
'tytuł' => 'title',
'dystans' => 'distance',
'kalorie' => 'calories',
'czas' => 'duration',
'średnie tętno' => 'avg_heart_rate',
'maksymalne tętno' => 'max_heart_rate',
'średnia prędkość' => 'avg_speed',
'maksymalna prędkość' => 'max_speed',
'średni rytm pedałowania' => 'avg_cadence',
'maksymalny rytm pedałowania' => 'max_cadence',
'całkowity wznios' => 'total_elevation_gain',
'całkowity spadek' => 'total_elevation_loss',
'minimalna wysokość' => 'min_altitude',
'maksymalna wysokość' => 'max_altitude',
'sprzęt' => 'equipment_name',
'typ wydarzenia' => 'event_type_name',
'komentarz' => 'comment',
'link do strava' => 'strava_url',
// English keys for compatibility
'category_name' => 'category_name', 'date' => 'date', 'title' => 'title', 'distance' => 'distance',
'calories' => 'calories', 'duration' => 'duration', 'avg_heart_rate' => 'avg_heart_rate',
'max_heart_rate' => 'max_heart_rate', 'avg_speed' => 'avg_speed', 'max_speed' => 'max_speed',
'avg_cadence' => 'avg_cadence', 'max_cadence' => 'max_cadence', 'total_elevation_gain' => 'total_elevation_gain',
'total_elevation_loss' => 'total_elevation_loss', 'min_altitude' => 'min_altitude', 'max_altitude' => 'max_altitude',
'equipment_name' => 'equipment_name', 'event_type_name' => 'event_type_name', 'comment' => 'comment', 'strava_url' => 'strava_url',
];
// --- START: Robust, case-insensitive lookup ---
$table_categories = $wpdb->prefix . 'mystat_categories';
$table_event_types = $wpdb->prefix . 'mystat_event_types';
$table_equipment = $wpdb->prefix . 'mystat_equipment';
$create_lookup = function( $table_name ) use ( $wpdb ) {
$items = $wpdb->get_results( "SELECT id, name FROM {$table_name}" );
$lookup = [];
if ( is_array( $items ) ) {
foreach ( $items as $item ) {
// Use a robust trim to handle various whitespace characters and make it case-insensitive
$clean_name = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $item->name );
$lookup[ mb_strtolower( $clean_name, 'UTF-8' ) ] = $item->id;
}
}
return $lookup;
};
$categories_lookup = $create_lookup( $table_categories );
$event_types_lookup = $create_lookup( $table_event_types );
$equipment_lookup = $create_lookup( $table_equipment );
// --- END: Robust, case-insensitive lookup ---
// Process the CSV file
$table_activities = $wpdb->prefix . 'mystat_activities';
$imported_count = 0;
$skipped_details = [];
$row_number = 1; // Header is row 1
// Normalize line endings and split into lines
$lines = str_replace( ["\r\n", "\r"], "\n", $csv_content );
$lines = explode( "\n", $lines );
if ( empty($lines) || empty(trim($lines[0])) ) {
echo 'Podane dane CSV są puste.
';
return;
}
// --- Delimiter and BOM detection ---
$first_line = $lines[0];
$delimiter = (substr_count($first_line, ';') > substr_count($first_line, ',')) ? ';' : ',';
// --- BOM removal from first header element ---
$bom = "\xEF\xBB\xBF";
if (substr($first_line, 0, 3) === $bom) {
$lines[0] = substr($first_line, 3);
}
$header_raw = str_getcsv( array_shift( $lines ), $delimiter );
$header_raw = array_map('trim', $header_raw);
// Translate header from Polish/English to internal keys
$header = [];
foreach ($header_raw as $col) {
$header[] = $column_map[strtolower($col)] ?? 'ignored_' . uniqid();
}
$required_internal_keys = ['date', 'title', 'category_name', 'distance'];
$missing_keys = array_diff($required_internal_keys, $header);
if (!empty($missing_keys)) {
$key_to_polish_map = [
'date' => 'Data', 'title' => 'Tytuł',
'category_name' => 'Kategoria / Typ aktywności', 'distance' => 'Dystans',
];
$missing_polish_names = array_map(fn($key) => $key_to_polish_map[$key] ?? $key, $missing_keys);
echo 'Brak wymaganych kolumn w danych CSV: ' . esc_html(implode(', ', $missing_polish_names)) . '
';
return;
}
$parse_and_round_int = fn($val) => round(floatval(str_replace(',', '.', $val)));
$null_if_empty = fn($value) => $value !== '' ? $value : null;
foreach ( $lines as $line ) { // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.Found
$row_number++;
if ( empty( trim( $line ) ) ) {
continue; // Skip empty lines
}
$data = str_getcsv( $line, $delimiter );
if (count($data) !== count($header)) {
$skipped_details[] = [
'row' => $row_number,
'reason' => 'Nieprawidłowa liczba kolumn (oczekiwano ' . count($header) . ', otrzymano ' . count($data) . ').',
'data' => $line,
];
continue;
}
$row_data = array_combine( $header, $data );
// Detailed validation
$validation_errors = [];
if ( empty( $row_data['date'] ) ) {
$validation_errors[] = 'brak daty';
}
if ( empty( $row_data['title'] ) ) {
$validation_errors[] = 'brak tytułu';
}
if ( ! isset( $row_data['distance'] ) || $row_data['distance'] === '' ) {
$validation_errors[] = 'brak dystansu';
}
$category_name = $row_data['category_name'] ?? '';
// Use a robust trim to handle various whitespace characters and make it case-insensitive
$clean_category_name = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $category_name );
$category_id = $categories_lookup[ mb_strtolower( $clean_category_name, 'UTF-8' ) ] ?? null;
if ( empty( $clean_category_name ) ) {
$validation_errors[] = 'brak nazwy kategorii';
} elseif ( is_null( $category_id ) ) {
$available_categories_from_db = $wpdb->get_col( "SELECT name FROM $table_categories ORDER BY name" );
$validation_errors[] = 'nieznana kategoria: "' . esc_html( $category_name ) . '". Sprawdź, czy nazwa jest poprawna. Dostępne w bazie: "' . esc_html( implode( '", "', $available_categories_from_db ) ) . '".';
}
if ( ! empty( $validation_errors ) ) {
$skipped_details[] = [
'row' => $row_number,
'reason' => ucfirst( implode( ', ', $validation_errors ) ) . '.',
'data' => $line,
];
continue;
}
// Get IDs for optional fields using the same robust method
$get_id = function( $name, $lookup_table ) {
$clean_name = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $name );
return $lookup_table[ mb_strtolower( $clean_name, 'UTF-8' ) ] ?? null;
};
$equipment_id = $get_id( $row_data['equipment_name'] ?? '', $equipment_lookup );
$event_type_id = $get_id( $row_data['event_type_name'] ?? '', $event_types_lookup );
$insert_data = [
'date' => sanitize_text_field( $row_data['date'] ),
'title' => sanitize_text_field( $row_data['title'] ),
'category_id' => $category_id,
'distance' => floatval( str_replace( ',', '.', $row_data['distance'] ) ),
'duration' => isset( $row_data['duration'] ) ? sanitize_text_field( $row_data['duration'] ) : '00:00:00',
'calories' => isset( $row_data['calories'] ) ? intval( str_replace( ',', '.', $row_data['calories'] ) ) : 0,
'comment' => isset( $row_data['comment'] ) ? sanitize_textarea_field( $row_data['comment'] ) : null,
'strava_url' => isset( $row_data['strava_url'] ) ? $null_if_empty( esc_url_raw( $row_data['strava_url'] ) ) : null,
'avg_heart_rate' => isset( $row_data['avg_heart_rate'] ) ? $null_if_empty( $parse_and_round_int( $row_data['avg_heart_rate'] ) ) : null,
'max_heart_rate' => isset( $row_data['max_heart_rate'] ) ? $null_if_empty( $parse_and_round_int( $row_data['max_heart_rate'] ) ) : null,
'avg_speed' => isset( $row_data['avg_speed'] ) ? $null_if_empty( floatval( str_replace( ',', '.', $row_data['avg_speed'] ) ) ) : null,
'max_speed' => isset( $row_data['max_speed'] ) ? $null_if_empty( floatval( str_replace( ',', '.', $row_data['max_speed'] ) ) ) : null,
'avg_cadence' => isset( $row_data['avg_cadence'] ) ? $null_if_empty( $parse_and_round_int( $row_data['avg_cadence'] ) ) : null,
'max_cadence' => isset( $row_data['max_cadence'] ) ? $null_if_empty( $parse_and_round_int( $row_data['max_cadence'] ) ) : null,
'total_elevation_gain' => isset( $row_data['total_elevation_gain'] ) ? $null_if_empty( $parse_and_round_int( $row_data['total_elevation_gain'] ) ) : null,
'total_elevation_loss' => isset( $row_data['total_elevation_loss'] ) ? $null_if_empty( $parse_and_round_int( $row_data['total_elevation_loss'] ) ) : null,
'min_altitude' => isset( $row_data['min_altitude'] ) ? $null_if_empty( $parse_and_round_int( $row_data['min_altitude'] ) ) : null,
'max_altitude' => isset( $row_data['max_altitude'] ) ? $null_if_empty( $parse_and_round_int( $row_data['max_altitude'] ) ) : null,
'equipment_id' => $equipment_id,
'event_type_id' => $event_type_id,
];
if ( $wpdb->insert( $table_activities, $insert_data ) ) {
$imported_count++;
} else {
$skipped_details[] = [
'row' => $row_number,
'reason' => 'Błąd zapisu do bazy danych. (' . esc_html( $wpdb->last_error ) . ')',
'data' => $line,
];
}
}
if ( $imported_count > 0 ) echo 'Pomyślnie zaimportowano ' . esc_html( $imported_count ) . ' aktywności.
';
if ( ! empty( $skipped_details ) ) {
echo '';
echo '
Pominięto ' . count( $skipped_details ) . ' wierszy z powodu błędów ';
echo '
';
echo '
Wiersz Powód pominięcia Dane wiersza ';
foreach ( $skipped_details as $error ) {
echo '';
echo '' . esc_html( $error['row'] ) . ' ';
echo '' . esc_html( $error['reason'] ) . ' ';
echo '' . esc_html( wp_trim_words( $error['data'], 25, '...' ) ) . ' ';
echo ' ';
}
echo '
';
echo '
';
}
if ( $imported_count === 0 && empty($skipped_details) && $row_number > 1) echo 'Dane CSV nie zawierały żadnych poprawnych wierszy do importu.
';
elseif ($row_number === 1) echo 'Dane CSV były puste lub zawierały tylko nagłówek.
';
}
function mystat_edit_activity_page() {
global $wpdb;
$activity_id = isset( $_GET['id'] ) ? intval( $_GET['id'] ) : 0;
if ( $activity_id === 0 ) {
echo 'Błąd Nie podano ID aktywności do edycji.
';
return;
}
// Handle form submission for update
mystat_handle_activity_form_submission();
$table_activities = $wpdb->prefix . 'mystat_activities';
$activity = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_activities WHERE id = %d", $activity_id ) );
if ( ! $activity ) {
echo 'Błąd Nie znaleziono aktywności o podanym ID.
';
return;
}
echo '
Edytuj Trening ';
mystat_render_add_form( $activity );
echo '';
}
function mystat_view_activity_page() {
global $wpdb;
$activity_id = isset( $_GET['id'] ) ? intval( $_GET['id'] ) : 0;
if ( $activity_id === 0 ) {
echo 'Błąd Nie podano ID aktywności.
';
return;
}
$table_activities = $wpdb->prefix . 'mystat_activities';
$table_categories = $wpdb->prefix . 'mystat_categories';
$table_event_types = $wpdb->prefix . 'mystat_event_types';
$table_equipment = $wpdb->prefix . 'mystat_equipment';
$sql = $wpdb->prepare("
SELECT a.*, c.name as category_name, c.icon, c.color, et.name as event_type_name, eq.name as equipment_name
FROM $table_activities a
LEFT JOIN $table_categories c ON a.category_id = c.id
LEFT JOIN $table_event_types et ON a.event_type_id = et.id
LEFT JOIN $table_equipment eq ON a.equipment_id = eq.id
WHERE a.id = %d
", $activity_id);
$activity = $wpdb->get_row( $sql );
if ( ! $activity ) {
echo 'Błąd Nie znaleziono aktywności o podanym ID.
';
return;
}
// Funkcja pomocnicza do wyświetlania wiersza, jeśli wartość istnieje
$render_row = function($label, $value, $unit = '') {
if ( ! is_null($value) && $value !== '' ) {
echo '';
echo '' . esc_html($label) . ' ';
echo '' . esc_html($value) . ($unit ? ' ' . esc_html($unit) : '') . ' ';
echo ' ';
}
};
// Prepare map and chart data if GPX exists
$gpx_data = [];
$has_gpx_data = false;
if ( ! empty( $activity->gpx_url ) ) {
$gpx_data = mystat_parse_gpx_data( $activity->gpx_url );
$has_gpx_data = !empty($gpx_data['points']);
}
if ($has_gpx_data) {
wp_enqueue_style('leaflet-css', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css');
wp_enqueue_script('leaflet-js', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js', [], '1.9.4', true);
wp_enqueue_script('chart-js', 'https://cdn.jsdelivr.net/npm/chart.js', [], null, true);
wp_register_script('mystat-details-loader', false);
wp_enqueue_script('mystat-details-loader');
// Check which profiles have data
$available_profiles = [];
if (!empty(array_filter($gpx_data['profiles']['elevation']))) $available_profiles['elevation'] = 'Wysokość';
if (!empty(array_filter($gpx_data['profiles']['speed']))) $available_profiles['speed'] = 'Prędkość';
if (!empty(array_filter($gpx_data['profiles']['hr']))) $available_profiles['hr'] = 'Tętno';
if (!empty(array_filter($gpx_data['profiles']['cadence']))) $available_profiles['cadence'] = 'Kadencja';
$has_time_data = !empty(array_filter($gpx_data['profiles']['time'], fn($t) => !is_null($t)));
$chart_js = '
const track_points = ' . json_encode($gpx_data['points']) . ';
const profiles = ' . json_encode($gpx_data['profiles']) . ';
let activeChart = null;
if (typeof L !== "undefined" && track_points.length > 0) {
const map = L.map("mystat-activity-map");
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: \'© OpenStreetMap \' }).addTo(map);
const polyline = L.polyline(track_points, {color: "' . esc_js( $activity->color ) . '"}).addTo(map);
map.fitBounds(polyline.getBounds().pad(0.1));
L.marker(track_points[0]).addTo(map).bindPopup("Start");
L.marker(track_points[track_points.length - 1]).addTo(map).bindPopup("Koniec");
}
const chartConfigs = {
elevation: { label: "Wysokość", unit: "m n.p.m.", color: "#8e44ad" },
speed: { label: "Prędkość", unit: "km/h", color: "#2980b9" },
hr: { label: "Tętno", unit: "bpm", color: "#c0392b" },
cadence: { label: "Kadencja", unit: "rpm", color: "#27ae60" }
};
const xAxisConfigs = {
distance: { label: "Dystans (km)", data: profiles.distance },
time: {
label: "Czas", data: profiles.time,
formatter: (s) => s === null ? "" : new Date(s * 1000).toISOString().substr(11, 8)
}
};
function renderChart() {
if (activeChart) activeChart.destroy();
const chartType = document.querySelector(".mystat-chart-tabs .nav-tab-active").getAttribute("href").substring(1);
const xAxisType = document.querySelector(\'input[name="mystat_xaxis"]:checked\').value;
const yData = profiles[chartType], xData = xAxisConfigs[xAxisType].data;
const filteredY = [], filteredX = [];
if(yData) {
for(let i=0; i v, maxRotation: 0, autoSkip: true, maxTicksLimit: 10 } },
y: { title: { display: true, text: chartConfigs[chartType].unit } }
},
plugins: { legend: { display: false } },
interaction: { intersect: false, mode: "index" },
}
});
}
document.querySelectorAll(".mystat-chart-tabs .nav-tab").forEach(t => t.addEventListener("click", e => {
e.preventDefault();
document.querySelector(".mystat-chart-tabs .nav-tab-active").classList.remove("nav-tab-active");
e.target.classList.add("nav-tab-active");
renderChart();
}));
document.querySelectorAll(\'input[name="mystat_xaxis"]\').forEach(r => r.addEventListener("change", renderChart));
if (document.querySelector(".mystat-chart-tabs .nav-tab")) renderChart();
';
wp_add_inline_script('mystat-details-loader', 'document.addEventListener("DOMContentLoaded", function() {' . $chart_js . '});');
}
?>
Szczegóły treningu: title ); ?>
Edytuj
← Powrót do listy aktywności
Notatki i linki
Mapa Trasy
Wykresy
gpx_url ) ) : ?>
Nie udało się wczytać danych z pliku GPX lub plik jest uszkodzony/pusty. Brak danych do wyświetlenia mapy i wykresów.
0 ? 'mystat_edit_entry_' . $activity_id : 'mystat_add_entry';
// Weryfikacja bezpieczeństwa (Nonce)
if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], $nonce_action ) ) {
echo 'Błąd weryfikacji bezpieczeństwa formularza.
';
return;
}
$table_activities = $wpdb->prefix . 'mystat_activities';
// Przygotowanie danych (zamiana przecinka na kropkę w dystansie)
$distance = isset($_POST['distance']) ? floatval( str_replace( ',', '.', $_POST['distance'] ) ) : 0;
// Funkcja pomocnicza do zamiany pustych wartości na NULL, aby poprawnie zapisać je w bazie
$null_if_empty = function($value) {
return $value !== '' ? $value : null;
};
$data = array(
'category_id' => intval( $_POST['category_id'] ),
'date' => sanitize_text_field( $_POST['date'] ),
'title' => sanitize_text_field( $_POST['title'] ),
'distance' => $distance,
'duration' => sanitize_text_field( $_POST['duration'] ),
'calories' => intval( $_POST['calories'] ),
'comment' => sanitize_textarea_field( $_POST['comment'] ),
'strava_url' => $null_if_empty( esc_url_raw( $_POST['strava_url'] ) ),
'avg_heart_rate' => $null_if_empty( intval( $_POST['avg_heart_rate'] ) ),
'max_heart_rate' => $null_if_empty( intval( $_POST['max_heart_rate'] ) ),
'avg_speed' => $null_if_empty( floatval( str_replace( ',', '.', $_POST['avg_speed'] ) ) ),
'max_speed' => $null_if_empty( floatval( str_replace( ',', '.', $_POST['max_speed'] ) ) ),
'avg_cadence' => $null_if_empty( intval( $_POST['avg_cadence'] ) ),
'max_cadence' => $null_if_empty( intval( $_POST['max_cadence'] ) ),
'total_elevation_gain' => $null_if_empty( intval( $_POST['total_elevation_gain'] ) ),
'total_elevation_loss' => $null_if_empty( intval( $_POST['total_elevation_loss'] ) ),
'min_altitude' => $null_if_empty( intval( $_POST['min_altitude'] ) ),
'max_altitude' => $null_if_empty( intval( $_POST['max_altitude'] ) ),
'equipment_id' => $null_if_empty( intval( $_POST['equipment_id'] ) ),
'gpx_url' => $null_if_empty( esc_url_raw( $_POST['gpx_url'] ) ),
'event_type_id' => $null_if_empty( intval( $_POST['event_type_id'] ) ),
);
// Format danych dla $wpdb->insert
$format = array(
'%d', '%s', '%s', '%f', '%s', '%d', '%s', // Pola podstawowe
'%s', '%d', '%d', '%f', '%f', '%d', '%d', // Tętno, prędkość, kadencja
'%d', '%d', '%d', '%d', '%d', '%s', '%d' // Wysokość, sprzęt, linki, typ wydarzenia
);
if ( $activity_id > 0 ) {
// UPDATE
$result = $wpdb->update( $table_activities, $data, [ 'id' => $activity_id ], $format, [ '%d' ] );
$message = 'Trening zaktualizowany pomyślnie!';
} else {
// INSERT
$result = $wpdb->insert( $table_activities, $data, $format );
$message = 'Trening dodany pomyślnie!';
}
if ( $result !== false ) {
echo '' . esc_html( $message ) . '
';
} else {
echo 'Wystąpił błąd podczas zapisu do bazy.
';
}
}
/**
* Renderowanie formularza HTML
*/
function mystat_render_add_form( $activity = null ) {
// Enqueue media scripts for the uploader
wp_enqueue_media();
global $wpdb;
$table_categories = $wpdb->prefix . 'mystat_categories';
$table_event_types = $wpdb->prefix . 'mystat_event_types';
$table_equipment = $wpdb->prefix . 'mystat_equipment';
$categories = $wpdb->get_results( "SELECT * FROM $table_categories ORDER BY name ASC" );
$event_types = $wpdb->get_results( "SELECT * FROM $table_event_types ORDER BY name ASC" );
$equipment_list = $wpdb->get_results( "SELECT * FROM $table_equipment ORDER BY name ASC" );
$is_edit_mode = ! is_null( $activity );
$nonce_action = $is_edit_mode ? 'mystat_edit_entry_' . $activity->id : 'mystat_add_entry';
$form_title = $is_edit_mode ? 'Edytuj Aktywność' : 'Dodaj Nową Aktywność';
$button_text = $is_edit_mode ? 'Zaktualizuj Trening' : 'Zapisz Trening';
?>
prefix
// Jeśli tabele są "sztywne" (bez prefixu wp_), usuń $wpdb->prefix.
$table_activities = $wpdb->prefix . 'mystat_activities';
$table_categories = $wpdb->prefix . 'mystat_categories';
// --- 1. OBSŁUGA USUWANIA (DELETE) ---
if ( isset( $_GET['action'], $_GET['id'], $_GET['_wpnonce'] ) && $_GET['action'] === 'mystat_delete' ) {
$activity_id = intval( $_GET['id'] );
// Weryfikacja bezpieczeństwa (Nonce)
if ( wp_verify_nonce( $_GET['_wpnonce'], 'mystat_delete_' . $activity_id ) ) {
$result = $wpdb->delete(
$table_activities,
array( 'id' => $activity_id ),
array( '%d' )
);
if ( $result ) {
echo 'Aktywność została usunięta.
';
} else {
echo 'Wystąpił błąd podczas usuwania.
';
}
} else {
echo 'Błąd weryfikacji bezpieczeństwa (Nonce).
';
}
}
// --- 2. USTAWIENIA PAGINACJI ---
$items_per_page = 20; // Ile wpisów na stronę
$current_page = isset( $_GET['paged'] ) ? max( 1, intval( $_GET['paged'] ) ) : 1;
$offset = ( $current_page - 1 ) * $items_per_page;
// --- 3. POBIERANIE DANYCH (SELECT) ---
// Pobranie całkowitej liczby wpisów do paginacji
$total_items = $wpdb->get_var( "SELECT COUNT(id) FROM $table_activities" );
$total_pages = ceil( $total_items / $items_per_page );
// Pobieramy wpisy dla bieżącej strony
$sql = $wpdb->prepare("
SELECT a.*, c.name as category_name, c.icon, c.color, et.name as event_type_name, eq.name as equipment_name
FROM $table_activities a
LEFT JOIN $table_categories c ON a.category_id = c.id
LEFT JOIN {$wpdb->prefix}mystat_event_types et ON a.event_type_id = et.id
LEFT JOIN {$wpdb->prefix}mystat_equipment eq ON a.equipment_id = eq.id
ORDER BY a.date DESC, a.id DESC
LIMIT %d OFFSET %d
", $items_per_page, $offset);
$activities = $wpdb->get_results( $sql );
// --- 4. WIDOK TABELI (HTML) ---
?>
Historia Aktywności
1 ) : ?>
aktywności
add_query_arg( 'paged', '%#%' ),
'format' => '',
'total' => $total_pages,
'current' => $current_page,
'prev_text' => '« Poprzednia',
'next_text' => 'Następna »',
) );
?>
Ikona
Data
Tytuł
Kategoria
Typ
Sprzęt
Dystans (km)
Czas
Śr. prędkość
Akcja
'mystat_delete',
'id' => $row->id,
) ), 'mystat_delete_' . $row->id );
$edit_url = add_query_arg( array(
'page' => 'mystat-edit-activity',
'id' => $row->id
), admin_url( 'admin.php' ) );
$details_url = add_query_arg( array(
'page' => 'mystat-view-activity',
'id' => $row->id
), admin_url( 'admin.php' ) );
?>
icon ) ) : ?>
date ); ?>
title ? wp_trim_words( $row->title, 6 ) : '(bez tytułu)' ); ?>
category_name ); ?>
event_type_name ); ?>
equipment_name ); ?>
distance, 2, ',', ' ' ); ?>
duration ); ?>
avg_speed ? number_format( $row->avg_speed, 1, ',', ' ' ) . ' km/h' : '-'; ?>
Edytuj
Szczegóły
Usuń
Brak zarejestrowanych aktywności. Dodaj pierwszy trening powyżej!
1 ) : ?>
aktywności
add_query_arg( 'paged', '%#%' ),
'format' => '',
'total' => $total_pages,
'current' => $current_page,
'prev_text' => '« Poprzednia',
'next_text' => 'Następna »',
) );
?>
current_time( 'Y' ),
'month' => current_time( 'n' ),
), $atts, 'moje_statystyki' );
$year = intval( $atts['year'] );
$month = intval( $atts['month'] );
// Pobieranie danych z bazy
$table_activities = $wpdb->prefix . 'mystat_activities';
$sql = $wpdb->prepare("
SELECT a.*, c.name as category_name, eq.name as equipment_name
FROM $table_activities a
LEFT JOIN {$wpdb->prefix}mystat_categories c ON a.category_id = c.id
LEFT JOIN {$wpdb->prefix}mystat_equipment eq ON a.equipment_id = eq.id
WHERE YEAR(a.date) = %d AND MONTH(a.date) = %d
ORDER BY a.date ASC
", $year, $month);
$activities = $wpdb->get_results( $sql );
// Obliczanie podsumowań
$total_distance = 0;
$total_seconds = 0;
foreach ($activities as $activity) {
$total_distance += $activity->distance;
if ( ! empty( $activity->duration ) ) {
list($h, $m, $s) = explode(':', $activity->duration);
$total_seconds += $h * 3600 + $m * 60 + $s;
}
}
$hours = floor($total_seconds / 3600);
$minutes = floor(($total_seconds % 3600) / 60);
$total_duration_formatted = sprintf('%d godz. %d min.', $hours, $minutes);
// Rozpoczęcie buforowania wyjścia
ob_start();
?>
Podsumowanie miesiąca
Całkowity dystans
Całkowity czas
km
Lista aktywności
Data
Tytuł
Kategoria
Dystans
Czas
Sprzęt
date ) ) ); ?>
title ); ?>
category_name ); ?>
distance, 2, ',', ' ' ); ?> km
duration ); ?>
equipment_name ); ?>
avg_speed || $row->avg_heart_rate || $row->avg_cadence ) : ?>
avg_speed) echo 'Śr. prędkość '; ?>
avg_heart_rate) echo 'Śr. tętno '; ?>
avg_cadence) echo 'Śr. kadencja '; ?>
avg_speed) echo '' . number_format( $row->avg_speed, 1, ',', ' ' ) . ' km/h '; ?>
avg_heart_rate) echo '' . $row->avg_heart_rate . ' bpm '; ?>
avg_cadence) echo '' . $row->avg_cadence . ' rpm '; ?>
Brak aktywności w tym miesiącu.
0,
), $atts, 'moje_statystyki_wpis' );
$activity_id = intval( $atts['id'] );
if ( $activity_id === 0 ) {
return 'Błąd: Nie podano ID wpisu w shortcode.
';
}
// Pobieranie danych z bazy
$table_activities = $wpdb->prefix . 'mystat_activities';
$sql = $wpdb->prepare("
SELECT a.*, c.name as category_name, c.color as category_color, et.name as event_type_name, eq.name as equipment_name
FROM $table_activities a
LEFT JOIN {$wpdb->prefix}mystat_categories c ON a.category_id = c.id
LEFT JOIN {$wpdb->prefix}mystat_event_types et ON a.event_type_id = et.id
LEFT JOIN {$wpdb->prefix}mystat_equipment eq ON a.equipment_id = eq.id
WHERE a.id = %d
", $activity_id);
$activity = $wpdb->get_row( $sql );
if ( ! $activity ) {
return 'Błąd: Nie znaleziono wpisu o ID ' . esc_html($activity_id) . '.
';
}
// Funkcja pomocnicza do wyświetlania wiersza
$render_row = function($label, $value, $unit = '') {
if ( ! is_null($value) && $value !== '' && $value != 0) {
echo '';
echo '' . esc_html($label) . ' ';
echo '' . esc_html($value) . ($unit ? ' ' . esc_html($unit) : '') . ' ';
echo ' ';
}
};
ob_start();
// Prepare map and chart data if GPX exists
$gpx_data = [];
$has_gpx_data = false;
if ( ! empty( $activity->gpx_url ) ) {
$gpx_data = mystat_parse_gpx_data( $activity->gpx_url );
$has_gpx_data = !empty($gpx_data['points']);
}
$unique_id = 'mystat-activity-' . esc_attr( $activity->id );
if ($has_gpx_data) {
wp_enqueue_style('leaflet-css', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css');
wp_enqueue_script('leaflet-js', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js', [], '1.9.4', true);
wp_enqueue_script('chart-js', 'https://cdn.jsdelivr.net/npm/chart.js', [], null, true);
$map_id = 'map-' . $unique_id;
$chart_id = 'chart-' . $unique_id;
$available_profiles = [];
if (!empty(array_filter($gpx_data['profiles']['elevation']))) $available_profiles['elevation'] = 'Wysokość';
if (!empty(array_filter($gpx_data['profiles']['speed']))) $available_profiles['speed'] = 'Prędkość';
if (!empty(array_filter($gpx_data['profiles']['hr']))) $available_profiles['hr'] = 'Tętno';
if (!empty(array_filter($gpx_data['profiles']['cadence']))) $available_profiles['cadence'] = 'Kadencja';
$has_time_data = !empty(array_filter($gpx_data['profiles']['time'], fn($t) => !is_null($t)));
wp_register_script('mystat-shortcode-loader-' . $activity->id, false);
wp_enqueue_script('mystat-shortcode-loader-' . $activity->id);
$js_script = '
(function() {
document.addEventListener("DOMContentLoaded", function() {
const uniqueId = "' . esc_js($unique_id) . '";
const containerEl = document.getElementById(uniqueId);
if (!containerEl || typeof L === "undefined" || typeof Chart === "undefined") return;
const trackPoints = ' . json_encode( $gpx_data['points'] ) . ';
const profiles = ' . json_encode( $gpx_data['profiles'] ) . ';
let activeChart = null;
const mapId = "' . esc_js($map_id) . '";
const mapEl = document.getElementById(mapId);
if (mapEl && trackPoints.length > 0) {
if (mapEl._leaflet_id) mapEl._leaflet_id = null;
const map = L.map(mapId);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: \'© OpenStreetMap \' }).addTo(map);
const polyline = L.polyline(trackPoints, {color: "' . esc_js( $activity->category_color ) . '"}).addTo(map);
map.fitBounds(polyline.getBounds().pad(0.1));
}
const chartId = "' . esc_js($chart_id) . '";
const chartEl = document.getElementById(chartId);
if (!chartEl) return;
const chartConfigs = {
elevation: { label: "Wysokość", unit: "m n.p.m.", color: "#8e44ad" },
speed: { label: "Prędkość", unit: "km/h", color: "#2980b9" },
hr: { label: "Tętno", unit: "bpm", color: "#c0392b" },
cadence: { label: "Kadencja", unit: "rpm", color: "#27ae60" }
};
const xAxisConfigs = {
distance: { label: "Dystans (km)", data: profiles.distance },
time: { label: "Czas", data: profiles.time, formatter: (s) => s === null ? "" : new Date(s * 1000).toISOString().substr(11, 8) }
};
function renderChart() {
if (activeChart) activeChart.destroy();
const activeTab = containerEl.querySelector(".mystat-chart-tab.active");
if (!activeTab) return;
const chartType = activeTab.dataset.type;
const xAxisRadio = containerEl.querySelector(\'input[name="xaxis-\' + uniqueId + \'"]:checked\');
if (!xAxisRadio) return;
const xAxisType = xAxisRadio.value;
const yData = profiles[chartType], xData = xAxisConfigs[xAxisType].data;
const filteredY = [], filteredX = [];
if(yData) {
for(let i=0; i v, maxRotation: 0, autoSkip: true, maxTicksLimit: 7 } },
y: { title: { display: true, text: chartConfigs[chartType].unit } }
},
plugins: { legend: { display: false } },
interaction: { intersect: false, mode: "index" },
}
});
}
containerEl.querySelectorAll(".mystat-chart-tab").forEach(t => t.addEventListener("click", e => {
e.preventDefault();
containerEl.querySelector(".mystat-chart-tab.active").classList.remove("active");
e.currentTarget.classList.add("active");
renderChart();
}));
containerEl.querySelectorAll(\'input[name="xaxis-\' + uniqueId + \'"]\').forEach(r => r.addEventListener("change", renderChart));
if (containerEl.querySelector(".mystat-chart-tab")) renderChart();
});
})();';
wp_add_inline_script('mystat-shortcode-loader-' . $activity->id, $js_script);
}
?>
title ); ?>
date ) ) ); ?>
distance, 2, ',', ' '), 'km'); ?>
duration); ?>
avg_speed, 1, ',', ' '), 'km/h'); ?>
total_elevation_gain, 'm'); ?>
category_name); ?>
equipment_name); ?>
strava_url ) ) : ?>
Strava Zobacz aktywność