2118 lines
99 KiB
PHP
2118 lines
99 KiB
PHP
<?php
|
|
/**
|
|
* Plugin Name: WordPress Activity Stats
|
|
* Description: Wtyczka do śledzenia statystyk sportowych (Rower, Bieganie, itp.).
|
|
* Version: 1.0
|
|
* Author: Jacek Fefliński
|
|
*/
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
// --- 1. INSTALACJA BAZY DANYCH (AKTYWACJA) ---
|
|
register_activation_hook( __FILE__, 'mystat_activate' );
|
|
|
|
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';
|
|
|
|
// 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 );
|
|
}
|
|
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
|
|
);
|
|
}
|
|
|
|
function mystat_dashboard_page() {
|
|
echo '<div class="wrap"><h1>Moje Statystyki Sportowe</h1>';
|
|
mystat_render_history_table();
|
|
echo '</div>';
|
|
}
|
|
|
|
function mystat_add_new_page() {
|
|
echo '<div class="wrap"><h1>Dodaj Nowy Trening</h1>';
|
|
// Obsługa zapisu formularza (musi być przed renderowaniem, aby wyświetlić komunikat)
|
|
mystat_handle_activity_form_submission();
|
|
// Formularz dodawania
|
|
mystat_render_add_form();
|
|
echo '</div>';
|
|
}
|
|
|
|
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" );
|
|
?>
|
|
<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( 'mystat_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( [ 'action' => 'edit', 'id' => $type->id ] ) ); ?>">Edytuj</a> | <a href="<?php echo esc_url( wp_nonce_url( add_query_arg( [ 'action' => 'delete', 'id' => $type->id ] ), 'mystat_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
|
|
}
|
|
|
|
function mystat_equipment_page() {
|
|
global $wpdb;
|
|
$table_equipment = $wpdb->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" );
|
|
?>
|
|
<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 sprzęt' : '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( 'mystat_manage_equipment' ); ?>
|
|
<div class="form-field">
|
|
<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>
|
|
<?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 ( $equipment_list as $item ) : ?>
|
|
<tr><td><?php echo esc_html( $item->name ); ?></td><td><a href="<?php echo esc_url( add_query_arg( [ 'action' => 'edit', 'id' => $item->id ] ) ); ?>">Edytuj</a> | <a href="<?php echo esc_url( wp_nonce_url( add_query_arg( [ 'action' => 'delete', 'id' => $item->id ] ), 'mystat_delete_equipment_' . $item->id ) ); ?>" onclick="return confirm('Czy na pewno chcesz usunąć ten sprzęt?')" style="color: #a00;">Usuń</a></td></tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
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));
|
|
}
|
|
});
|
|
');
|
|
|
|
?>
|
|
<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="mystat-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>
|
|
|
|
<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="mystatYearlyChart"></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
|
|
}
|
|
|
|
/**
|
|
* 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 mystat_parse_gpx_data( $gpx_url ) {
|
|
if ( empty( $gpx_url ) ) {
|
|
return [];
|
|
}
|
|
|
|
$response = wp_remote_get( $gpx_url, [ 'timeout' => 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 [];
|
|
}
|
|
|
|
libxml_use_internal_errors( true );
|
|
$gpx = simplexml_load_string( $gpx_content );
|
|
libxml_clear_errors();
|
|
|
|
if ( $gpx === false ) {
|
|
return [];
|
|
}
|
|
|
|
$track_data = [
|
|
'points' => [],
|
|
'elevation_profile' => [],
|
|
];
|
|
$raw_points = [];
|
|
|
|
// Extract raw points with lat, lon, ele
|
|
if ( isset( $gpx->trk ) ) {
|
|
foreach ( $gpx->trk->trkseg as $trkseg ) {
|
|
foreach ( $trkseg->trkpt as $trkpt ) {
|
|
if ( isset( $trkpt['lat'], $trkpt['lon'] ) ) {
|
|
$raw_points[] = [
|
|
'lat' => (float) $trkpt['lat'],
|
|
'lon' => (float) $trkpt['lon'],
|
|
'ele' => isset( $trkpt->ele ) ? (float) $trkpt->ele : null,
|
|
];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( empty( $raw_points ) ) {
|
|
return $track_data;
|
|
}
|
|
|
|
// Process raw points to calculate profile
|
|
$cumulative_distance = 0;
|
|
$elevation_profile = [];
|
|
$map_points = [];
|
|
|
|
$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 ) {
|
|
$map_points[] = [ $point['lat'], $point['lon'] ];
|
|
|
|
if ( $i > 0 ) {
|
|
$prev_point = $raw_points[ $i - 1 ];
|
|
$distance_increment = $haversine( $prev_point['lat'], $prev_point['lon'], $point['lat'], $point['lon'] );
|
|
$cumulative_distance += $distance_increment;
|
|
}
|
|
|
|
if ( ! is_null( $point['ele'] ) ) {
|
|
$elevation_profile[] = [
|
|
'distance' => round( $cumulative_distance, 3 ), // in km
|
|
'elevation' => round( $point['ele'], 2 ), // in m
|
|
];
|
|
}
|
|
}
|
|
|
|
$track_data['points'] = $map_points;
|
|
$track_data['elevation_profile'] = $elevation_profile;
|
|
|
|
return $track_data;
|
|
}
|
|
|
|
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) . '"
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
');
|
|
|
|
?>
|
|
<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="mystat-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 mystat-infographic-grid">
|
|
<div class="mystat-infographic-card"><h3>Dystans</h3><p><?php echo number_format($overall_stats->total_distance, 2, ',', ' '); ?> km</p></div>
|
|
<div class="mystat-infographic-card"><h3>Czas</h3><p><?php echo esc_html($overall_stats->total_duration); ?></p></div>
|
|
<div class="mystat-infographic-card"><h3>Wznios</h3><p><?php echo number_format($overall_stats->total_elevation_gain, 0, ',', ' '); ?> m</p></div>
|
|
<div class="mystat-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 mystat-infographic-grid">
|
|
<div class="mystat-infographic-card"><h3>Dystans</h3><p><?php echo number_format($yearly_stats->total_distance, 2, ',', ' '); ?> km</p></div>
|
|
<div class="mystat-infographic-card"><h3>Czas</h3><p><?php echo esc_html($yearly_stats->total_duration); ?></p></div>
|
|
<div class="mystat-infographic-card"><h3>Wznios</h3><p><?php echo number_format($yearly_stats->total_elevation_gain, 0, ',', ' '); ?> m</p></div>
|
|
<div class="mystat-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="mystatCategoryPieChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
function mystat_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['mystat_csv_import_nonce_field'] ) ) {
|
|
mystat_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( 'mystat_csv_import_nonce', 'mystat_csv_import_nonce_field' ); ?>
|
|
<table class="form-table">
|
|
<tr valign="top">
|
|
<th scope="row"><label for="mystat_csv_file">Opcja 1: Wgraj plik CSV</label></th>
|
|
<td><input type="file" id="mystat_csv_file" name="mystat_csv_file" accept=".csv,text/csv" /></td>
|
|
</tr>
|
|
<tr valign="top">
|
|
<th scope="row"><label for="mystat_csv_data">Opcja 2: Wklej dane CSV</label></th>
|
|
<td><textarea name="mystat_csv_data" id="mystat_csv_data" rows="15" class="large-text" placeholder="Wklej tutaj zawartość swojego pliku CSV... Typ aktywności,Data,Tytuł,Dystans,... Rower,2025-01-01,Nowy Rok,10.5,..."></textarea></td>
|
|
</tr>
|
|
</table>
|
|
<?php submit_button( 'Importuj' ); ?>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
echo '</div>';
|
|
}
|
|
|
|
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 '<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['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 '<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 = [
|
|
// 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 '<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 = [];
|
|
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 '<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 ) { // 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 '<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 ( $imported_count === 0 && 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 ($row_number === 1) echo '<div class="notice notice-info"><p>Dane CSV były puste lub zawierały tylko nagłówek.</p></div>';
|
|
}
|
|
|
|
function mystat_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
|
|
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 '<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>';
|
|
mystat_render_add_form( $activity );
|
|
echo '</div>';
|
|
}
|
|
|
|
function mystat_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 . '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 '<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 = [];
|
|
if ( ! empty( $activity->gpx_url ) ) {
|
|
$gpx_data = mystat_parse_gpx_data( $activity->gpx_url );
|
|
|
|
if ( ! empty( $gpx_data['points'] ) ) {
|
|
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');
|
|
|
|
$map_script = '
|
|
const track_points = ' . json_encode($gpx_data['points']) . ';
|
|
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: \'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors\'
|
|
}).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");
|
|
}
|
|
';
|
|
|
|
$elevation_chart_script = '';
|
|
if ( ! empty( $gpx_data['elevation_profile'] ) ) {
|
|
$elevation_chart_script = '
|
|
const elevation_data = ' . json_encode( $gpx_data['elevation_profile'] ) . ';
|
|
if (typeof Chart !== "undefined" && elevation_data.length > 0) {
|
|
const ctx = document.getElementById("mystat-elevation-chart").getContext("2d");
|
|
new Chart(ctx, {
|
|
type: "line",
|
|
data: {
|
|
labels: elevation_data.map(p => p.distance),
|
|
datasets: [{
|
|
label: "Wysokość (m)",
|
|
data: elevation_data.map(p => p.elevation),
|
|
borderColor: "' . esc_js( $activity->color ) . '",
|
|
backgroundColor: "' . esc_js( $activity->color ) . '20",
|
|
fill: true,
|
|
pointRadius: 0,
|
|
tension: 0.1
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true, maintainAspectRatio: false,
|
|
scales: {
|
|
x: { title: { display: true, text: "Dystans (km)" } },
|
|
y: { title: { display: true, text: "Wysokość (m n.p.m.)" } }
|
|
},
|
|
plugins: { legend: { display: false } }
|
|
}
|
|
});
|
|
}
|
|
';
|
|
}
|
|
|
|
wp_add_inline_script('mystat-details-loader',
|
|
'document.addEventListener("DOMContentLoaded", function() {' . $map_script . $elevation_chart_script . '});'
|
|
);
|
|
}
|
|
}
|
|
|
|
?>
|
|
<div class="wrap">
|
|
<h1>
|
|
Szczegóły treningu: <?php echo esc_html( $activity->title ); ?>
|
|
<a href="<?php echo esc_url( add_query_arg( ['page' => 'mystat-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=moje-statystyki') ); ?>">← 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="mystat-details-container">
|
|
<div class="mystat-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="mystat-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 ( ! empty( $gpx_data['points'] ) ) : ?>
|
|
<hr>
|
|
<h3>Mapa Trasy</h3>
|
|
<div id="mystat-activity-map" style="height: 450px; width: 100%; border: 1px solid #ddd; margin-bottom: 20px;"></div>
|
|
|
|
<?php if ( ! empty( $gpx_data['elevation_profile'] ) ) : ?>
|
|
<h3>Profil Wysokości</h3>
|
|
<div style="position: relative; height:250px; width:100%;">
|
|
<canvas id="mystat-elevation-chart"></canvas>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php elseif ( ! empty( $activity->gpx_url ) ) : ?>
|
|
<hr>
|
|
<h3>Mapa Trasy</h3>
|
|
<div class="notice notice-warning inline">
|
|
<p>Nie udało się wczytać danych z pliku GPX lub plik jest uszkodzony/pusty.</p>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Obsługa zapisu nowego lub edytowanego wpisu do bazy danych
|
|
*/
|
|
function mystat_handle_activity_form_submission() {
|
|
global $wpdb;
|
|
|
|
// Sprawdź czy formularz został wysłany
|
|
if ( ! isset( $_POST['mystat_submit_activity'] ) ) {
|
|
return;
|
|
}
|
|
|
|
$activity_id = isset( $_POST['activity_id'] ) ? intval( $_POST['activity_id'] ) : 0;
|
|
$nonce_action = $activity_id > 0 ? 'mystat_edit_entry_' . $activity_id : 'mystat_add_entry';
|
|
|
|
// Weryfikacja bezpieczeństwa (Nonce)
|
|
if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], $nonce_action ) ) {
|
|
echo '<div class="notice notice-error"><p>Błąd weryfikacji bezpieczeństwa formularza.</p></div>';
|
|
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 '<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 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';
|
|
|
|
?>
|
|
<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="mystat_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
|
|
}
|
|
|
|
function mystat_render_history_table() {
|
|
global $wpdb;
|
|
|
|
// Definicje nazw tabel (z uwzględnieniem prefixu WP, jeśli był użyty przy tworzeniu)
|
|
// Zakładam, że tabele nazywają się dokładnie tak jak w dokumentacji, ale dobrą praktyką jest $wpdb->prefix
|
|
// Jeśli tabele są "sztywne" (bez prefixu wp_), usuń $wpdb->prefix.
|
|
$table_activities = $wpdb->prefix . 'mystat_activities';
|
|
$table_categories = $wpdb->prefix . 'mystat_categories';
|
|
|
|
// --- 1. OBSŁUGA USUWANIA (DELETE) ---
|
|
if ( isset( $_GET['action'], $_GET['id'], $_GET['_wpnonce'] ) && $_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 '<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}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) ---
|
|
?>
|
|
<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' => '« Poprzednia',
|
|
'next_text' => 'Następna »',
|
|
) );
|
|
?>
|
|
</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' => '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' ) );
|
|
?>
|
|
<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' => '« Poprzednia',
|
|
'next_text' => 'Następna »',
|
|
) );
|
|
?>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php
|
|
}
|
|
?>
|
|
|
|
<?php
|
|
// --- 3. SHORTCODE DO WYŚWIETLANIA NA FRONCIE ---
|
|
|
|
/**
|
|
* 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',
|
|
plugin_dir_url( __FILE__ ) . 'assets/css/frontend.css',
|
|
[],
|
|
$plugin_version
|
|
);
|
|
}
|
|
add_action( 'wp_enqueue_scripts', 'mystat_enqueue_frontend_assets' );
|
|
|
|
/**
|
|
* 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' );
|
|
}
|
|
add_action( 'init', 'mystat_register_shortcode' );
|
|
|
|
/**
|
|
* 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 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();
|
|
}
|
|
?>
|
|
|
|
<?php
|
|
/**
|
|
* Funkcja obsługująca shortcode [moje_statystyki_wpis].
|
|
* @param array $atts Atrybuty shortcode'u (np. id).
|
|
* @return string HTML do wyświetlenia.
|
|
*/
|
|
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 ( $activity_id === 0 ) {
|
|
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 !== '' && $value != 0) {
|
|
echo '<tr>';
|
|
echo '<th>' . esc_html($label) . '</th>';
|
|
echo '<td>' . esc_html($value) . ($unit ? ' ' . esc_html($unit) : '') . '</td>';
|
|
echo '</tr>';
|
|
}
|
|
};
|
|
|
|
ob_start();
|
|
|
|
// Prepare map data if GPX exists
|
|
$gpx_data = [];
|
|
if ( ! empty( $activity->gpx_url ) ) {
|
|
$gpx_data = mystat_parse_gpx_data( $activity->gpx_url );
|
|
|
|
if ( ! empty( $gpx_data['points'] ) ) {
|
|
// Enqueue scripts for the frontend
|
|
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 = 'mystat-map-' . esc_attr( $activity->id );
|
|
$chart_id = 'mystat-chart-' . esc_attr( $activity->id );
|
|
|
|
// Pass track data to a script
|
|
wp_register_script('mystat-shortcode-loader-' . $activity->id, false);
|
|
wp_enqueue_script('mystat-shortcode-loader-' . $activity->id);
|
|
|
|
$map_script = '
|
|
const trackPoints = ' . json_encode( $gpx_data['points'] ) . ';
|
|
const mapId = "' . esc_js($map_id) . '";
|
|
|
|
var container = L.DomUtil.get(mapId);
|
|
if(container != null) { container._leaflet_id = null; }
|
|
|
|
const map = L.map(mapId);
|
|
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
|
attribution: \'© <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));
|
|
';
|
|
|
|
$elevation_chart_script = '';
|
|
if ( ! empty( $gpx_data['elevation_profile'] ) ) {
|
|
$elevation_chart_script = '
|
|
const elevationData = ' . json_encode( $gpx_data['elevation_profile'] ) . ';
|
|
const chartId = "' . esc_js($chart_id) . '";
|
|
const ctx = document.getElementById(chartId).getContext("2d");
|
|
new Chart(ctx, {
|
|
type: "line",
|
|
data: {
|
|
labels: elevationData.map(p => p.distance),
|
|
datasets: [{
|
|
label: "Wysokość (m)",
|
|
data: elevationData.map(p => p.elevation),
|
|
borderColor: "' . esc_js( $activity->category_color ) . '",
|
|
backgroundColor: "' . esc_js( $activity->category_color ) . '20",
|
|
fill: true, pointRadius: 0, tension: 0.1
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true, maintainAspectRatio: false,
|
|
scales: {
|
|
x: { title: { display: true, text: "Dystans (km)" } },
|
|
y: { title: { display: true, text: "Wysokość (m n.p.m.)" } }
|
|
},
|
|
plugins: { legend: { display: false } }
|
|
}
|
|
});
|
|
';
|
|
}
|
|
|
|
wp_add_inline_script('mystat-shortcode-loader-' . $activity->id,
|
|
'(function() {
|
|
document.addEventListener("DOMContentLoaded", function() {
|
|
if (typeof L === "undefined" || typeof Chart === "undefined") return;
|
|
' . $map_script . '
|
|
' . $elevation_chart_script . '
|
|
});
|
|
})();'
|
|
);
|
|
}
|
|
}
|
|
|
|
?>
|
|
<div class="mystat-single-activity-shortcode">
|
|
<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 ( ! empty( $gpx_data['points'] ) ) : ?>
|
|
<div id="<?php echo $map_id; ?>" style="height: 350px; width: 100%; margin-top: 15px; border-radius: 5px; margin-bottom: 20px;"></div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ( ! empty( $gpx_data['elevation_profile'] ) ) : ?>
|
|
<div style="position: relative; height:250px; width:100%;">
|
|
<canvas id="<?php echo $chart_id; ?>"></canvas>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php
|
|
return ob_get_clean();
|
|
}
|
|
?>
|