瀏覽代碼

studiou-wc-custom-reports: Initial version, all tested. Except:

2. In WooCommerce/Custom Reports grid shows data for property values generate columns that are 0, therefore there are product variants with this propertz values
Dalibor Votruba 1 年之前
父節點
當前提交
7b4a8d7a4a

+ 19 - 0
studiou-wc-custom-reports/directory_structure.txt

@@ -0,0 +1,19 @@
+Project Directory Structure 
+Generated on: 16.06.2025  9:42:33,86 
+Root: d:\Projects\quadarax\qdr.app\qdr.app.woo.plugins\studiou-wc-custom-reports 
+ 
+directory_structure.txt 
+gen_dirstruct.cmd 
+instructions.txt 
+readme.md 
+studiou-wc-custom-reports.php 
+includes/ 
+  class-admin-view.php 
+  class-db-manager.php 
+  class-export-handler.php 
+  class-main.php 
+  class-product-categories-report.php 
+  class-report-data.php 
+  class-report-factory.php 
+  class-settings-manager.php 
+  class-utils.php 

+ 73 - 0
studiou-wc-custom-reports/gen_dirstruct.cmd

@@ -0,0 +1,73 @@
+@echo off
+setlocal enabledelayedexpansion
+
+rem Generate directory structure file for Visual Studio extension project
+rem Excludes: .vs, bin, obj directories and *.user files
+
+set OUTPUT_FILE=directory_structure.txt
+set CURRENT_DIR=%CD%
+
+echo Generating directory structure...
+echo Project Directory Structure > "%OUTPUT_FILE%"
+echo Generated on: %DATE% %TIME% >> "%OUTPUT_FILE%"
+echo Root: %CURRENT_DIR% >> "%OUTPUT_FILE%"
+echo. >> "%OUTPUT_FILE%"
+
+rem Function to process directory recursively
+call :ProcessDirectory "." ""
+
+echo.
+echo Directory structure generated successfully in: %OUTPUT_FILE%
+pause
+goto :eof
+
+:ProcessDirectory
+set "dir_path=%~1"
+set "indent=%~2"
+
+rem Get directory name for display
+for %%F in ("%dir_path%") do set "dir_name=%%~nxF"
+
+rem Skip excluded directories
+if /i "%dir_name%"==".vs" goto :eof
+if /i "%dir_name%"=="bin" goto :eof
+if /i "%dir_name%"=="obj" goto :eof
+if /i "%dir_name%"=="packages" goto :eof
+if /i "%dir_name%"==".git" goto :eof
+if /i "%dir_name%"=="node_modules" goto :eof
+
+rem Print current directory (except root)
+if not "%dir_path%"=="." (
+    echo %indent%%dir_name%/ >> "%OUTPUT_FILE%"
+    set "new_indent=%indent%  "
+) else (
+    set "new_indent="
+)
+
+rem Process files in current directory
+for %%F in ("%dir_path%\*") do (
+    set "file_name=%%~nxF"
+    set "file_ext=%%~xF"
+    
+    rem Skip user-specific and temporary files
+    if /i not "!file_ext!"==".user" (
+        if /i not "!file_ext!"==".suo" (
+            if /i not "!file_ext!"==".cache" (
+                if /i not "!file_name!"==".gitignore" (
+                    if /i not "!file_name!"=="Thumbs.db" (
+                        if /i not "!file_name!"=="Desktop.ini" (
+                            echo %new_indent%!file_name! >> "%OUTPUT_FILE%"
+                        )
+                    )
+                )
+            )
+        )
+    )
+)
+
+rem Process subdirectories
+for /d %%D in ("%dir_path%\*") do (
+    call :ProcessDirectory "%%D" "%new_indent%"
+)
+
+goto :eof

+ 310 - 0
studiou-wc-custom-reports/includes/class-admin-view.php

@@ -0,0 +1,310 @@
+<?php
+/**
+ * Admin View Class - Handles admin interface rendering
+ *
+ * @package StudiouWCCustomReports
+ */
+
+if (!defined('ABSPATH')) {
+    exit;
+}
+
+class StudiouWC_Admin_View {
+    
+    private $db_manager;
+    private $settings_manager;
+    
+    public function __construct($db_manager, $settings_manager) {
+        $this->db_manager = $db_manager;
+        $this->settings_manager = $settings_manager;
+    }
+    
+    public function add_admin_menu() {
+        // Add Custom Reports page under WooCommerce > Reports
+        add_submenu_page(
+            'woocommerce',
+            __('Custom Reports', StudiouWC_Main::get_text_domain()),
+            __('Custom Reports', StudiouWC_Main::get_text_domain()),
+            'manage_woocommerce',
+            'studiou-custom-reports',
+            array($this, 'display_custom_reports_page')
+        );
+        
+        // Add Settings page under WooCommerce > Reports Settings
+        add_submenu_page(
+            'woocommerce',
+            __('Custom Report Settings', StudiouWC_Main::get_text_domain()),
+            __('Custom Report Settings', StudiouWC_Main::get_text_domain()),
+            'manage_woocommerce',
+            'studiou-reports-settings',
+            array($this, 'display_settings_page')
+        );
+    }
+    
+    public function enqueue_admin_scripts($hook) {
+        if (strpos($hook, 'studiou-') !== false) {
+            wp_enqueue_script('jquery');
+            wp_enqueue_script('jquery-ui-datepicker');
+            wp_enqueue_style('jquery-ui-datepicker', 'https://code.jquery.com/ui/1.12.1/themes/ui-lightness/jquery-ui.css');
+        }
+    }
+    
+    public function display_custom_reports_page() {
+        $settings = $this->settings_manager->get_settings();
+        
+        // Handle form submission
+        $view = isset($_GET['view']) ? sanitize_text_field($_GET['view']) : 'product_categories_summary';
+        $date_from = isset($_GET['date_from']) ? sanitize_text_field($_GET['date_from']) : $this->settings_manager->get_default_date_from();
+        $date_to = isset($_GET['date_to']) ? sanitize_text_field($_GET['date_to']) : date('Y-m-d');
+        $statuses = isset($_GET['statuses']) ? array_map('sanitize_text_field', $_GET['statuses']) : ($settings['default_statuses'] ?? array());
+        $per_page = isset($_GET['per_page']) ? intval($_GET['per_page']) : 20;
+        $paged = isset($_GET['paged']) ? intval($_GET['paged']) : 1;
+        $orderby = isset($_GET['orderby']) ? sanitize_text_field($_GET['orderby']) : 'category';
+        $order = isset($_GET['order']) ? sanitize_text_field($_GET['order']) : 'ASC';
+        
+        // Get report data
+        $report_data = $this->db_manager->get_product_categories_summary($date_from, $date_to, $statuses, $per_page, $paged, $orderby, $order);
+        $total_items = $this->db_manager->get_total_items($date_from, $date_to, $statuses);
+        $total_pages = ceil($total_items / $per_page);
+        
+        $this->render_reports_page($view, $date_from, $date_to, $statuses, $per_page, $paged, $orderby, $order, $report_data, $total_items, $total_pages, $settings);
+    }
+    
+    public function display_settings_page() {
+        $settings = $this->settings_manager->get_settings();
+        $this->render_settings_page($settings);
+    }
+    
+    private function render_reports_page($view, $date_from, $date_to, $statuses, $per_page, $paged, $orderby, $order, $report_data, $total_items, $total_pages, $settings) {
+        ?>
+        <div class="wrap">
+            <h1><?php _e('Custom Reports', StudiouWC_Main::get_text_domain()); ?></h1>
+            
+            <?php $this->render_filter_form($view, $date_from, $date_to, $statuses, $per_page); ?>
+            
+            <?php if (!empty($report_data)): ?>
+                <?php $this->render_pagination_top($total_items, $total_pages, $paged); ?>
+                <?php $this->render_data_table($report_data, $orderby, $order, $settings); ?>
+            <?php else: ?>
+                <p><?php _e('No data found for the selected criteria.', StudiouWC_Main::get_text_domain()); ?></p>
+            <?php endif; ?>
+        </div>
+        
+        <?php $this->render_reports_javascript(); ?>
+        <?php
+    }
+    
+    private function render_filter_form($view, $date_from, $date_to, $statuses, $per_page) {
+        ?>
+        <form method="get" id="studiou-reports-form">
+            <input type="hidden" name="page" value="studiou-custom-reports">
+            
+            <table class="form-table">
+                <tr>
+                    <th><label for="view"><?php _e('View', StudiouWC_Main::get_text_domain()); ?></label></th>
+                    <td>
+                        <select name="view" id="view">
+                            <option value="product_categories_summary" <?php selected($view, 'product_categories_summary'); ?>>
+                                <?php _e('Product Categories and Variants Summary', StudiouWC_Main::get_text_domain()); ?>
+                            </option>
+                        </select>
+                    </td>
+                </tr>
+                <tr>
+                    <th><label for="date_from"><?php _e('Date From', StudiouWC_Main::get_text_domain()); ?></label></th>
+                    <td><input type="date" name="date_from" id="date_from" value="<?php echo esc_attr($date_from); ?>"></td>
+                </tr>
+                <tr>
+                    <th><label for="date_to"><?php _e('Date To', StudiouWC_Main::get_text_domain()); ?></label></th>
+                    <td><input type="date" name="date_to" id="date_to" value="<?php echo esc_attr($date_to); ?>"></td>
+                </tr>
+                <tr>
+                    <th><label for="statuses"><?php _e('Included Statuses', StudiouWC_Main::get_text_domain()); ?></label></th>
+                    <td>
+                        <?php $this->render_status_checkboxes($statuses); ?>
+                    </td>
+                </tr>
+                <tr>
+                    <th><label for="per_page"><?php _e('Items per page', StudiouWC_Main::get_text_domain()); ?></label></th>
+                    <td>
+                        <select name="per_page" id="per_page">
+                            <option value="10" <?php selected($per_page, 10); ?>>10</option>
+                            <option value="20" <?php selected($per_page, 20); ?>>20</option>
+                            <option value="50" <?php selected($per_page, 50); ?>>50</option>
+                            <option value="100" <?php selected($per_page, 100); ?>>100</option>
+                        </select>
+                    </td>
+                </tr>
+            </table>
+            
+            <p class="submit">
+                <input type="submit" name="filter" class="button-primary" value="<?php _e('Filter', StudiouWC_Main::get_text_domain()); ?>">
+                <button type="button" id="export-csv" class="button"><?php _e('Export', StudiouWC_Main::get_text_domain()); ?></button>
+            </p>
+        </form>
+        <?php
+    }
+    
+    private function render_status_checkboxes($selected_statuses) {
+        $order_statuses = wc_get_order_statuses();
+        foreach ($order_statuses as $status_key => $status_label) {
+            $checked = in_array($status_key, $selected_statuses) ? 'checked' : '';
+            echo '<label><input type="checkbox" name="statuses[]" value="' . esc_attr($status_key) . '" ' . $checked . '> ' . esc_html($status_label) . '</label><br>';
+        }
+    }
+    
+    private function render_pagination_top($total_items, $total_pages, $paged) {
+        ?>
+        <div class="tablenav top">
+            <div class="tablenav-pages">
+                <span class="displaying-num"><?php printf(__('%d items', StudiouWC_Main::get_text_domain()), $total_items); ?></span>
+                <?php if ($total_pages > 1): ?>
+                <span class="pagination-links">
+                    <?php if ($paged > 1): ?>
+                    <a class="prev-page button" href="<?php echo esc_url(add_query_arg('paged', $paged - 1)); ?>"><?php _e('‹', StudiouWC_Main::get_text_domain()); ?></a>
+                    <?php endif; ?>
+                    <span class="paging-input">
+                        <span class="tablenav-paging-text"><?php printf(__('%1$d of %2$d', StudiouWC_Main::get_text_domain()), $paged, $total_pages); ?></span>
+                    </span>
+                    <?php if ($paged < $total_pages): ?>
+                    <a class="next-page button" href="<?php echo esc_url(add_query_arg('paged', $paged + 1)); ?>"><?php _e('›', StudiouWC_Main::get_text_domain()); ?></a>
+                    <?php endif; ?>
+                </span>
+                <?php endif; ?>
+            </div>
+        </div>
+        <?php
+    }
+    
+    private function render_data_table($report_data, $orderby, $order, $settings) {
+        $default_property = $settings['default_property'] ?? '';
+        $property_values = array();
+        
+        if (!empty($default_property)) {
+            $property_values = $this->db_manager->get_property_values($default_property);
+        }
+        
+        ?>
+        <table class="wp-list-table widefat fixed striped">
+            <thead>
+                <tr>
+                    <th><a href="<?php echo esc_url(add_query_arg(array('orderby' => 'category', 'order' => $order === 'ASC' ? 'DESC' : 'ASC'))); ?>"><?php _e('Product Category', StudiouWC_Main::get_text_domain()); ?></a></th>
+                    <th><a href="<?php echo esc_url(add_query_arg(array('orderby' => 'orders_count', 'order' => $order === 'ASC' ? 'DESC' : 'ASC'))); ?>"><?php _e('Orders Count', StudiouWC_Main::get_text_domain()); ?></a></th>
+                    <?php foreach ($property_values as $slug => $name): ?>
+                        <th><?php echo esc_html($name . ' Count'); ?></th>
+                    <?php endforeach; ?>
+                    <th><a href="<?php echo esc_url(add_query_arg(array('orderby' => 'total_price', 'order' => $order === 'ASC' ? 'DESC' : 'ASC'))); ?>"><?php _e('Total Price', StudiouWC_Main::get_text_domain()); ?></a></th>
+                </tr>
+            </thead>
+            <tbody>
+                <?php foreach ($report_data as $row): ?>
+                <tr>
+                    <td><?php echo esc_html($row['category']); ?></td>
+                    <td><?php echo esc_html($row['orders_count']); ?></td>
+                    <?php foreach ($property_values as $slug => $name): ?>
+                    <td><?php echo esc_html($row['property_counts'][$slug] ?? 0); ?></td>
+                    <?php endforeach; ?>
+                    <td><?php echo wc_price($row['total_price']); ?></td>
+                </tr>
+                <?php endforeach; ?>
+            </tbody>
+        </table>
+        <?php
+    }
+    
+    private function render_reports_javascript() {
+        ?>
+        <script>
+        jQuery(document).ready(function($) {
+            $('#export-csv').click(function() {
+                var formData = $('#studiou-reports-form').serialize();
+                formData += '&action=studiou_export_csv&export=1';
+                
+                var form = $('<form method="post" action="' + ajaxurl + '"></form>');
+                $.each(formData.split('&'), function(i, field) {
+                    var parts = field.split('=');
+                    if (parts.length === 2) {
+                        form.append('<input type="hidden" name="' + decodeURIComponent(parts[0]) + '" value="' + decodeURIComponent(parts[1]) + '">');
+                    }
+                });
+                $('body').append(form);
+                form.submit();
+                form.remove();
+            });
+        });
+        </script>
+        <?php
+    }
+    
+    private function render_settings_page($settings) {
+        ?>
+        <div class="wrap">
+            <h1><?php _e('Custom Report Settings', StudiouWC_Main::get_text_domain()); ?></h1>
+            
+            <form method="post" id="studiou-settings-form">
+                <table class="form-table">
+                    <tr>
+                        <th><label for="default_interval"><?php _e('Default shown interval', StudiouWC_Main::get_text_domain()); ?></label></th>
+                        <td>
+                            <select name="default_interval" id="default_interval">
+                                <option value="last_day" <?php selected($settings['default_interval'] ?? '', 'last_day'); ?>><?php _e('Last day', StudiouWC_Main::get_text_domain()); ?></option>
+                                <option value="last_week" <?php selected($settings['default_interval'] ?? '', 'last_week'); ?>><?php _e('Last week', StudiouWC_Main::get_text_domain()); ?></option>
+                                <option value="last_month" <?php selected($settings['default_interval'] ?? '', 'last_month'); ?>><?php _e('Last month', StudiouWC_Main::get_text_domain()); ?></option>
+                            </select>
+                        </td>
+                    </tr>
+                    <tr>
+                        <th><label for="default_property"><?php _e('Default property', StudiouWC_Main::get_text_domain()); ?></label></th>
+                        <td>
+                            <select name="default_property" id="default_property">
+                                <option value=""><?php _e('Select property', StudiouWC_Main::get_text_domain()); ?></option>
+                                <?php
+                                $properties = $this->db_manager->get_product_properties();
+                                foreach ($properties as $slug => $name) {
+                                    $selected = selected($settings['default_property'] ?? '', $slug, false);
+                                    echo '<option value="' . esc_attr($slug) . '" ' . $selected . '>' . esc_html($name) . '</option>';
+                                }
+                                ?>
+                            </select>
+                        </td>
+                    </tr>
+                    <tr>
+                        <th><label for="default_statuses"><?php _e('Default Included statuses', StudiouWC_Main::get_text_domain()); ?></label></th>
+                        <td>
+                            <?php
+                            $order_statuses = wc_get_order_statuses();
+                            $default_statuses = $settings['default_statuses'] ?? array();
+                            foreach ($order_statuses as $status_key => $status_label) {
+                                $checked = in_array($status_key, $default_statuses) ? 'checked' : '';
+                                echo '<label><input type="checkbox" name="default_statuses[]" value="' . esc_attr($status_key) . '" ' . $checked . '> ' . esc_html($status_label) . '</label><br>';
+                            }
+                            ?>
+                        </td>
+                    </tr>
+                </table>
+                
+                <p class="submit">
+                    <button type="button" id="save-settings" class="button-primary"><?php _e('Save Settings', StudiouWC_Main::get_text_domain()); ?></button>
+                </p>
+            </form>
+        </div>
+        
+        <script>
+        jQuery(document).ready(function($) {
+            $('#save-settings').click(function() {
+                var formData = $('#studiou-settings-form').serialize();
+                formData += '&action=studiou_save_settings';
+                
+                $.post(ajaxurl, formData, function(response) {
+                    if (response.success) {
+                        alert('<?php _e('Settings saved successfully!', StudiouWC_Main::get_text_domain()); ?>');
+                    } else {
+                        alert('<?php _e('Error saving settings.', StudiouWC_Main::get_text_domain()); ?>');
+                    }
+                });
+            });
+        });
+        </script>
+        <?php
+    }
+}

+ 409 - 0
studiou-wc-custom-reports/includes/class-db-manager.php

@@ -0,0 +1,409 @@
+<?php
+/**
+ * Database Manager Class - Handles all database operations (HPOS Compatible)
+ *
+ * @package StudiouWCCustomReports
+ */
+
+if (!defined('ABSPATH')) {
+    exit;
+}
+
+class StudiouWC_DB_Manager {
+    
+    public function get_product_categories_summary($date_from, $date_to, $statuses, $per_page, $paged, $orderby, $order) {
+        global $wpdb;
+        
+        if (empty($statuses)) {
+            return array();
+        }
+        
+        $settings_manager = new StudiouWC_Settings_Manager();
+        $settings = $settings_manager->get_settings();
+        $default_property = $settings['default_property'] ?? '';
+        
+        $statuses_placeholder = implode(',', array_fill(0, count($statuses), '%s'));
+        $offset = ($paged - 1) * $per_page;
+        
+        // Check if HPOS is enabled
+        if ($this->is_hpos_enabled()) {
+            $sql = $this->get_hpos_categories_query($statuses_placeholder);
+        } else {
+            $sql = $this->get_legacy_categories_query($statuses_placeholder);
+        }
+        
+        // Add ordering
+        $sql .= $this->get_order_clause($orderby, $order);
+        $sql .= " LIMIT %d OFFSET %d";
+        
+        $query_params = array_merge(
+            array($date_from, $date_to),
+            $statuses,
+            array($per_page, $offset)
+        );
+        
+        $results = $wpdb->get_results($wpdb->prepare($sql, $query_params), ARRAY_A);
+        
+        // Get property counts for each category if property is set
+        foreach ($results as &$result) {
+            $result['property_counts'] = array();
+            if (!empty($default_property)) {
+                $result['property_counts'] = $this->get_property_counts_for_category(
+                    $result['category'], 
+                    $date_from, 
+                    $date_to, 
+                    $statuses, 
+                    $default_property
+                );
+            }
+        }
+        
+        return $results;
+    }
+    
+    public function get_property_counts_for_category($category, $date_from, $date_to, $statuses, $property) {
+        global $wpdb;
+        
+        if (empty($statuses)) {
+            return array();
+        }
+        
+        $statuses_placeholder = implode(',', array_fill(0, count($statuses), '%s'));
+        $taxonomy = 'pa_' . $property; // Add the pa_ prefix for WooCommerce attributes
+        
+        if ($this->is_hpos_enabled()) {
+            $sql = $this->get_hpos_property_counts_query($statuses_placeholder);
+        } else {
+            $sql = $this->get_legacy_property_counts_query($statuses_placeholder);
+        }
+        
+        $query_params = array_merge(
+            array($date_from, $date_to),
+            $statuses,
+            array($category, $taxonomy)
+        );
+        
+        $results = $wpdb->get_results($wpdb->prepare($sql, $query_params), ARRAY_A);
+        
+        $property_counts = array();
+        foreach ($results as $result) {
+            if (!empty($result['property_value'])) {
+                $property_counts[$result['property_value']] = $result['count'];
+            }
+        }
+        
+        return $property_counts;
+    }
+    
+    public function get_total_items($date_from, $date_to, $statuses) {
+        global $wpdb;
+        
+        if (empty($statuses)) {
+            return 0;
+        }
+        
+        $statuses_placeholder = implode(',', array_fill(0, count($statuses), '%s'));
+        
+        if ($this->is_hpos_enabled()) {
+            $sql = $this->get_hpos_total_items_query($statuses_placeholder);
+        } else {
+            $sql = $this->get_legacy_total_items_query($statuses_placeholder);
+        }
+        
+        $query_params = array_merge(
+            array($date_from, $date_to),
+            $statuses
+        );
+        
+        return $wpdb->get_var($wpdb->prepare($sql, $query_params));
+    }
+    
+    /**
+     * Get product attributes (not meta keys) for dropdown
+     */
+    public function get_product_properties() {
+        global $wpdb;
+        
+        // Get WooCommerce product attributes
+        $attributes = $wpdb->get_results("
+            SELECT attribute_name, attribute_label 
+            FROM {$wpdb->prefix}woocommerce_attribute_taxonomies 
+            ORDER BY attribute_label
+        ", ARRAY_A);
+        
+        $properties = array();
+        foreach ($attributes as $attribute) {
+            $properties[$attribute['attribute_name']] = $attribute['attribute_label'];
+        }
+        
+        return $properties;
+    }
+    
+    /**
+     * Get property values for a specific attribute
+     */
+    public function get_property_values($property) {
+        global $wpdb;
+        
+        if (empty($property)) {
+            return array();
+        }
+        
+        // Build taxonomy name for WooCommerce attributes
+        $taxonomy = 'pa_' . $property;
+        
+        // Get terms for this attribute taxonomy
+        $sql = "
+            SELECT t.name, t.slug
+            FROM {$wpdb->terms} t
+            INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
+            WHERE tt.taxonomy = %s
+            ORDER BY t.name
+        ";
+        
+        $results = $wpdb->get_results($wpdb->prepare($sql, $taxonomy), ARRAY_A);
+        
+        $values = array();
+        foreach ($results as $result) {
+            $values[$result['slug']] = $result['name'];
+        }
+        
+        return $values;
+    }
+    
+    /**
+     * Get attribute name from attribute slug
+     */
+    public function get_attribute_name($attribute_slug) {
+        global $wpdb;
+        
+        $result = $wpdb->get_var($wpdb->prepare("
+            SELECT attribute_label 
+            FROM {$wpdb->prefix}woocommerce_attribute_taxonomies 
+            WHERE attribute_name = %s
+        ", $attribute_slug));
+        
+        return $result ? $result : $attribute_slug;
+    }
+    
+    /**
+     * Check if HPOS (High-Performance Order Storage) is enabled
+     */
+    private function is_hpos_enabled() {
+        return class_exists('Automattic\WooCommerce\Utilities\OrderUtil') && 
+               \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
+    }
+    
+    /**
+     * Get HPOS compatible query for categories summary
+     */
+    private function get_hpos_categories_query($statuses_placeholder) {
+        global $wpdb;
+        
+        return "
+            SELECT 
+                t.name as category,
+                COUNT(DISTINCT o.id) as orders_count,
+                SUM(oi.product_net_revenue) as total_price
+            FROM {$wpdb->prefix}wc_orders o
+            INNER JOIN {$wpdb->prefix}wc_order_product_lookup oi ON o.id = oi.order_id
+            INNER JOIN {$wpdb->posts} p ON oi.product_id = p.ID OR oi.variation_id = p.ID
+            INNER JOIN {$wpdb->term_relationships} tr ON (
+                CASE 
+                    WHEN oi.variation_id > 0 THEN 
+                        (SELECT post_parent FROM {$wpdb->posts} WHERE ID = oi.variation_id)
+                    ELSE oi.product_id 
+                END
+            ) = tr.object_id
+            INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
+            INNER JOIN {$wpdb->terms} t ON tt.term_id = t.term_id
+            WHERE o.date_created_gmt >= %s 
+            AND o.date_created_gmt <= %s 
+            AND o.status IN ($statuses_placeholder)
+            AND tt.taxonomy = 'product_cat'
+            GROUP BY t.term_id, t.name
+        ";
+    }
+    
+    /**
+     * Get legacy query for categories summary (pre-HPOS)
+     */
+    private function get_legacy_categories_query($statuses_placeholder) {
+        global $wpdb;
+        
+        return "
+            SELECT 
+                t.name as category,
+                COUNT(DISTINCT p_order.ID) as orders_count,
+                SUM(oi.product_net_revenue) as total_price
+            FROM {$wpdb->posts} p_order
+            INNER JOIN {$wpdb->prefix}wc_order_product_lookup oi ON p_order.ID = oi.order_id
+            INNER JOIN {$wpdb->posts} p ON oi.product_id = p.ID OR oi.variation_id = p.ID
+            INNER JOIN {$wpdb->term_relationships} tr ON (
+                CASE 
+                    WHEN oi.variation_id > 0 THEN 
+                        (SELECT post_parent FROM {$wpdb->posts} WHERE ID = oi.variation_id)
+                    ELSE oi.product_id 
+                END
+            ) = tr.object_id
+            INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
+            INNER JOIN {$wpdb->terms} t ON tt.term_id = t.term_id
+            WHERE p_order.post_date >= %s 
+            AND p_order.post_date <= %s 
+            AND p_order.post_status IN ($statuses_placeholder)
+            AND p_order.post_type = 'shop_order'
+            AND tt.taxonomy = 'product_cat'
+            GROUP BY t.term_id, t.name
+        ";
+    }
+    
+    /**
+     * Get HPOS compatible query for property counts
+     */
+    private function get_hpos_property_counts_query($statuses_placeholder) {
+        global $wpdb;
+        
+        return "
+            SELECT 
+                t_attr.slug as property_value,
+                COUNT(DISTINCT oi.order_item_id) as count
+            FROM {$wpdb->prefix}wc_orders o
+            INNER JOIN {$wpdb->prefix}wc_order_product_lookup oi ON o.id = oi.order_id
+            INNER JOIN {$wpdb->posts} p ON oi.product_id = p.ID OR oi.variation_id = p.ID
+            INNER JOIN {$wpdb->term_relationships} tr_cat ON (
+                CASE 
+                    WHEN oi.variation_id > 0 THEN 
+                        (SELECT post_parent FROM {$wpdb->posts} WHERE ID = oi.variation_id)
+                    ELSE oi.product_id 
+                END
+            ) = tr_cat.object_id
+            INNER JOIN {$wpdb->term_taxonomy} tt_cat ON tr_cat.term_taxonomy_id = tt_cat.term_taxonomy_id
+            INNER JOIN {$wpdb->terms} t_cat ON tt_cat.term_id = t_cat.term_id
+            INNER JOIN {$wpdb->term_relationships} tr_attr ON (
+                CASE 
+                    WHEN oi.variation_id > 0 THEN oi.variation_id
+                    ELSE oi.product_id 
+                END
+            ) = tr_attr.object_id
+            INNER JOIN {$wpdb->term_taxonomy} tt_attr ON tr_attr.term_taxonomy_id = tt_attr.term_taxonomy_id
+            INNER JOIN {$wpdb->terms} t_attr ON tt_attr.term_id = t_attr.term_id
+            WHERE o.date_created_gmt >= %s 
+            AND o.date_created_gmt <= %s 
+            AND o.status IN ($statuses_placeholder)
+            AND tt_cat.taxonomy = 'product_cat'
+            AND t_cat.name = %s
+            AND tt_attr.taxonomy = %s
+            GROUP BY t_attr.term_id, t_attr.slug
+        ";
+    }
+    
+    /**
+     * Get legacy query for property counts (pre-HPOS)
+     */
+    private function get_legacy_property_counts_query($statuses_placeholder) {
+        global $wpdb;
+        
+        return "
+            SELECT 
+                t_attr.slug as property_value,
+                COUNT(DISTINCT oi.order_item_id) as count
+            FROM {$wpdb->posts} p_order
+            INNER JOIN {$wpdb->prefix}wc_order_product_lookup oi ON p_order.ID = oi.order_id
+            INNER JOIN {$wpdb->posts} p ON oi.product_id = p.ID OR oi.variation_id = p.ID
+            INNER JOIN {$wpdb->term_relationships} tr_cat ON (
+                CASE 
+                    WHEN oi.variation_id > 0 THEN 
+                        (SELECT post_parent FROM {$wpdb->posts} WHERE ID = oi.variation_id)
+                    ELSE oi.product_id 
+                END
+            ) = tr_cat.object_id
+            INNER JOIN {$wpdb->term_taxonomy} tt_cat ON tr_cat.term_taxonomy_id = tt_cat.term_taxonomy_id
+            INNER JOIN {$wpdb->terms} t_cat ON tt_cat.term_id = t_cat.term_id
+            INNER JOIN {$wpdb->term_relationships} tr_attr ON (
+                CASE 
+                    WHEN oi.variation_id > 0 THEN oi.variation_id
+                    ELSE oi.product_id 
+                END
+            ) = tr_attr.object_id
+            INNER JOIN {$wpdb->term_taxonomy} tt_attr ON tr_attr.term_taxonomy_id = tt_attr.term_taxonomy_id
+            INNER JOIN {$wpdb->terms} t_attr ON tt_attr.term_id = t_attr.term_id
+            WHERE p_order.post_date >= %s 
+            AND p_order.post_date <= %s 
+            AND p_order.post_status IN ($statuses_placeholder)
+            AND p_order.post_type = 'shop_order'
+            AND tt_cat.taxonomy = 'product_cat'
+            AND t_cat.name = %s
+            AND tt_attr.taxonomy = %s
+            GROUP BY t_attr.term_id, t_attr.slug
+        ";
+    }
+    
+    /**
+     * Get HPOS compatible query for total items
+     */
+    private function get_hpos_total_items_query($statuses_placeholder) {
+        global $wpdb;
+        
+        return "
+            SELECT COUNT(DISTINCT t.term_id)
+            FROM {$wpdb->prefix}wc_orders o
+            INNER JOIN {$wpdb->prefix}wc_order_product_lookup oi ON o.id = oi.order_id
+            INNER JOIN {$wpdb->posts} p ON oi.product_id = p.ID OR oi.variation_id = p.ID
+            INNER JOIN {$wpdb->term_relationships} tr ON (
+                CASE 
+                    WHEN oi.variation_id > 0 THEN 
+                        (SELECT post_parent FROM {$wpdb->posts} WHERE ID = oi.variation_id)
+                    ELSE oi.product_id 
+                END
+            ) = tr.object_id
+            INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
+            INNER JOIN {$wpdb->terms} t ON tt.term_id = t.term_id
+            WHERE o.date_created_gmt >= %s 
+            AND o.date_created_gmt <= %s 
+            AND o.status IN ($statuses_placeholder)
+            AND tt.taxonomy = 'product_cat'
+        ";
+    }
+    
+    /**
+     * Get legacy query for total items (pre-HPOS)
+     */
+    private function get_legacy_total_items_query($statuses_placeholder) {
+        global $wpdb;
+        
+        return "
+            SELECT COUNT(DISTINCT t.term_id)
+            FROM {$wpdb->posts} p_order
+            INNER JOIN {$wpdb->prefix}wc_order_product_lookup oi ON p_order.ID = oi.order_id
+            INNER JOIN {$wpdb->posts} p ON oi.product_id = p.ID OR oi.variation_id = p.ID
+            INNER JOIN {$wpdb->term_relationships} tr ON (
+                CASE 
+                    WHEN oi.variation_id > 0 THEN 
+                        (SELECT post_parent FROM {$wpdb->posts} WHERE ID = oi.variation_id)
+                    ELSE oi.product_id 
+                END
+            ) = tr.object_id
+            INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
+            INNER JOIN {$wpdb->terms} t ON tt.term_id = t.term_id
+            WHERE p_order.post_date >= %s 
+            AND p_order.post_date <= %s 
+            AND p_order.post_status IN ($statuses_placeholder)
+            AND p_order.post_type = 'shop_order'
+            AND tt.taxonomy = 'product_cat'
+        ";
+    }
+    
+    private function get_order_clause($orderby, $order) {
+        $order = ($order === 'DESC') ? 'DESC' : 'ASC';
+        
+        switch ($orderby) {
+            case 'orders_count':
+                return " ORDER BY orders_count $order";
+            case 'total_price':
+                return " ORDER BY total_price $order";
+            case 'category':
+            default:
+                return " ORDER BY t.name $order";
+        }
+    }
+}

+ 97 - 0
studiou-wc-custom-reports/includes/class-export-handler.php

@@ -0,0 +1,97 @@
+<?php
+/**
+ * Export Handler Class - Handles CSV export functionality
+ *
+ * @package StudiouWCCustomReports
+ */
+
+if (!defined('ABSPATH')) {
+    exit;
+}
+
+class StudiouWC_Export_Handler {
+    
+    private $db_manager;
+    private $settings_manager;
+    
+    public function __construct($db_manager, $settings_manager) {
+        $this->db_manager = $db_manager;
+        $this->settings_manager = $settings_manager;
+    }
+    
+    public function export_csv() {
+        if (!current_user_can('manage_woocommerce')) {
+            wp_die(__('You do not have sufficient permissions to access this page.', StudiouWC_Main::get_text_domain()));
+        }
+        
+        $date_from = isset($_POST['date_from']) ? sanitize_text_field($_POST['date_from']) : '';
+        $date_to = isset($_POST['date_to']) ? sanitize_text_field($_POST['date_to']) : '';
+        $statuses = isset($_POST['statuses']) ? array_map('sanitize_text_field', $_POST['statuses']) : array();
+        
+        // Get all data without pagination for export
+        $report_data = $this->db_manager->get_product_categories_summary($date_from, $date_to, $statuses, 999999, 1, 'category', 'ASC');
+        
+        $settings = $this->settings_manager->get_settings();
+        $default_property = $settings['default_property'] ?? '';
+        $property_values = array();
+        
+        if (!empty($default_property)) {
+            $property_values = $this->db_manager->get_property_values($default_property);
+        }
+        
+        $this->generate_csv_output($report_data, $property_values);
+    }
+    
+    private function generate_csv_output($report_data, $property_values) {
+        // Set headers for CSV download
+        header('Content-Type: text/csv');
+        header('Content-Disposition: attachment; filename="custom-reports-' . date('Y-m-d') . '.csv"');
+        header('Pragma: no-cache');
+        header('Expires: 0');
+        
+        $output = fopen('php://output', 'w');
+        
+        // CSV headers
+        $headers = $this->get_csv_headers($property_values);
+        fputcsv($output, $headers);
+        
+        // CSV data
+        foreach ($report_data as $row) {
+            $csv_row = $this->format_csv_row($row, $property_values);
+            fputcsv($output, $csv_row);
+        }
+        
+        fclose($output);
+        exit;
+    }
+    
+    private function get_csv_headers($property_values) {
+        $headers = array(
+            __('Product Category', StudiouWC_Main::get_text_domain()),
+            __('Orders Count', StudiouWC_Main::get_text_domain())
+        );
+        
+        foreach ($property_values as $slug => $name) {
+            $headers[] = $name . ' Count';
+        }
+        
+        $headers[] = __('Total Price', StudiouWC_Main::get_text_domain());
+        
+        return $headers;
+    }
+    
+    private function format_csv_row($row, $property_values) {
+        $csv_row = array(
+            $row['category'],
+            $row['orders_count']
+        );
+        
+        foreach ($property_values as $slug => $name) {
+            $csv_row[] = $row['property_counts'][$slug] ?? 0;
+        }
+        
+        $csv_row[] = $row['total_price'];
+        
+        return $csv_row;
+    }
+}

+ 69 - 0
studiou-wc-custom-reports/includes/class-main.php

@@ -0,0 +1,69 @@
+<?php
+/**
+ * Main Plugin Class
+ *
+ * @package StudiouWCCustomReports
+ */
+
+if (!defined('ABSPATH')) {
+    exit;
+}
+
+class StudiouWC_Main {
+    
+    private $db_manager;
+    private $settings_manager;
+    private $admin_view;
+    private $export_handler;
+    
+    public function __construct() {
+        $this->init_classes();
+        $this->init_hooks();
+    }
+    
+    private function init_classes() {
+        $this->db_manager = new StudiouWC_DB_Manager();
+        $this->settings_manager = new StudiouWC_Settings_Manager();
+        $this->admin_view = new StudiouWC_Admin_View($this->db_manager, $this->settings_manager);
+        $this->export_handler = new StudiouWC_Export_Handler($this->db_manager, $this->settings_manager);
+    }
+    
+    private function init_hooks() {
+        add_action('init', array($this, 'init'));
+        add_action('admin_menu', array($this->admin_view, 'add_admin_menu'));
+        add_action('admin_enqueue_scripts', array($this->admin_view, 'enqueue_admin_scripts'));
+        add_action('wp_ajax_studiou_export_csv', array($this->export_handler, 'export_csv'));
+        add_action('wp_ajax_studiou_save_settings', array($this->settings_manager, 'save_settings'));
+        
+        register_activation_hook(STUDIOU_WC_CUSTOM_REPORTS_PLUGIN_FILE, array($this->settings_manager, 'activate'));
+        register_deactivation_hook(STUDIOU_WC_CUSTOM_REPORTS_PLUGIN_FILE, array($this, 'deactivate'));
+    }
+    
+    public function init() {
+        load_plugin_textdomain(
+            STUDIOU_WC_CUSTOM_REPORTS_TEXT_DOMAIN, 
+            false, 
+            dirname(plugin_basename(STUDIOU_WC_CUSTOM_REPORTS_PLUGIN_FILE)) . '/languages'
+        );
+    }
+    
+    public function deactivate() {
+        // Cleanup if needed
+    }
+    
+    public static function get_text_domain() {
+        return STUDIOU_WC_CUSTOM_REPORTS_TEXT_DOMAIN;
+    }
+    
+    public static function get_version() {
+        return STUDIOU_WC_CUSTOM_REPORTS_VERSION;
+    }
+    
+    public static function get_plugin_dir() {
+        return STUDIOU_WC_CUSTOM_REPORTS_PLUGIN_DIR;
+    }
+    
+    public static function get_plugin_url() {
+        return STUDIOU_WC_CUSTOM_REPORTS_PLUGIN_URL;
+    }
+}

+ 93 - 0
studiou-wc-custom-reports/includes/class-product-categories-report.php

@@ -0,0 +1,93 @@
+<?php
+/**
+ * Product Categories Report Class - Specific implementation for product categories report
+ *
+ * @package StudiouWCCustomReports
+ */
+
+if (!defined('ABSPATH')) {
+    exit;
+}
+
+class StudiouWC_Product_Categories_Report {
+    
+    private $db_manager;
+    private $data;
+    
+    public function __construct($db_manager, $data) {
+        $this->db_manager = $db_manager;
+        $this->data = $data;
+    }
+    
+    public function get_report_data() {
+        return $this->db_manager->get_product_categories_summary(
+            $this->data->get_date_from(),
+            $this->data->get_date_to(),
+            $this->data->get_statuses(),
+            $this->data->get_per_page(),
+            $this->data->get_paged(),
+            $this->data->get_orderby(),
+            $this->data->get_order()
+        );
+    }
+    
+    public function get_total_items() {
+        return $this->db_manager->get_total_items(
+            $this->data->get_date_from(),
+            $this->data->get_date_to(),
+            $this->data->get_statuses()
+        );
+    }
+    
+    public function get_report_summary() {
+        $data = $this->get_report_data();
+        
+        $summary = array(
+            'total_categories' => count($data),
+            'total_orders' => 0,
+            'total_revenue' => 0,
+            'average_orders_per_category' => 0,
+            'average_revenue_per_category' => 0
+        );
+        
+        foreach ($data as $row) {
+            $summary['total_orders'] += $row['orders_count'];
+            $summary['total_revenue'] += $row['total_price'];
+        }
+        
+        if ($summary['total_categories'] > 0) {
+            $summary['average_orders_per_category'] = $summary['total_orders'] / $summary['total_categories'];
+            $summary['average_revenue_per_category'] = $summary['total_revenue'] / $summary['total_categories'];
+        }
+        
+        return $summary;
+    }
+    
+    public function get_top_categories($limit = 5, $order_by = 'total_price') {
+        $data = $this->get_report_data();
+        
+        // Sort data based on the specified field
+        usort($data, function($a, $b) use ($order_by) {
+            if ($order_by === 'orders_count') {
+                return $b['orders_count'] - $a['orders_count'];
+            } else {
+                return $b['total_price'] - $a['total_price'];
+            }
+        });
+        
+        return array_slice($data, 0, $limit);
+    }
+    
+    public function get_export_data() {
+        // Get all data without pagination for export
+        return $this->db_manager->get_product_categories_summary(
+            $this->data->get_date_from(),
+            $this->data->get_date_to(),
+            $this->data->get_statuses(),
+            999999, // Large number to get all records
+            1,       // First page
+            $this->data->get_orderby(),
+            $this->data->get_order()
+        );
+    }
+}

+ 118 - 0
studiou-wc-custom-reports/includes/class-report-data.php

@@ -0,0 +1,118 @@
+<?php
+/**
+ * Report Data Model Class - Handles data structure and validation
+ *
+ * @package StudiouWCCustomReports
+ */
+
+if (!defined('ABSPATH')) {
+    exit;
+}
+
+class StudiouWC_Report_Data {
+    
+    private $view;
+    private $date_from;
+    private $date_to;
+    private $statuses;
+    private $per_page;
+    private $paged;
+    private $orderby;
+    private $order;
+    
+    public function __construct($params = array()) {
+        $this->view = $params['view'] ?? 'product_categories_summary';
+        $this->date_from = $params['date_from'] ?? '';
+        $this->date_to = $params['date_to'] ?? date('Y-m-d');
+        $this->statuses = $params['statuses'] ?? array();
+        $this->per_page = intval($params['per_page'] ?? 20);
+        $this->paged = intval($params['paged'] ?? 1);
+        $this->orderby = $params['orderby'] ?? 'category';
+        $this->order = $params['order'] ?? 'ASC';
+        
+        $this->validate_params();
+    }
+    
+    private function validate_params() {
+        // Validate date format
+        if (!empty($this->date_from) && !$this->is_valid_date($this->date_from)) {
+            $this->date_from = date('Y-m-d', strtotime('-1 month'));
+        }
+        
+        if (!empty($this->date_to) && !$this->is_valid_date($this->date_to)) {
+            $this->date_to = date('Y-m-d');
+        }
+        
+        // Validate order direction
+        if (!in_array(strtoupper($this->order), array('ASC', 'DESC'))) {
+            $this->order = 'ASC';
+        }
+        
+        // Validate orderby column
+        $valid_orderby = array('category', 'orders_count', 'total_price');
+        if (!in_array($this->orderby, $valid_orderby)) {
+            $this->orderby = 'category';
+        }
+        
+        // Validate per_page limits
+        if ($this->per_page < 1 || $this->per_page > 1000) {
+            $this->per_page = 20;
+        }
+        
+        // Validate paged
+        if ($this->paged < 1) {
+            $this->paged = 1;
+        }
+    }
+    
+    private function is_valid_date($date) {
+        $d = DateTime::createFromFormat('Y-m-d', $date);
+        return $d && $d->format('Y-m-d') === $date;
+    }
+    
+    // Getters
+    public function get_view() { 
+        return $this->view; 
+    }
+    
+    public function get_date_from() { 
+        return $this->date_from; 
+    }
+    
+    public function get_date_to() { 
+        return $this->date_to; 
+    }
+    
+    public function get_statuses() { 
+        return $this->statuses; 
+    }
+    
+    public function get_per_page() { 
+        return $this->per_page; 
+    }
+    
+    public function get_paged() { 
+        return $this->paged; 
+    }
+    
+    public function get_orderby() { 
+        return $this->orderby; 
+    }
+    
+    public function get_order() { 
+        return $this->order; 
+    }
+    
+    public function to_array() {
+        return array(
+            'view' => $this->view,
+            'date_from' => $this->date_from,
+            'date_to' => $this->date_to,
+            'statuses' => $this->statuses,
+            'per_page' => $this->per_page,
+            'paged' => $this->paged,
+            'orderby' => $this->orderby,
+            'order' => $this->order
+        );
+    }
+}

+ 39 - 0
studiou-wc-custom-reports/includes/class-report-factory.php

@@ -0,0 +1,39 @@
+<?php
+/**
+ * Report Factory Class - Creates different types of reports
+ *
+ * @package StudiouWCCustomReports
+ */
+
+if (!defined('ABSPATH')) {
+    exit;
+}
+
+class StudiouWC_Report_Factory {
+    
+    private $db_manager;
+    
+    public function __construct($db_manager) {
+        $this->db_manager = $db_manager;
+    }
+    
+    public function create_report($type, $data) {
+        switch ($type) {
+            case 'product_categories_summary':
+                return new StudiouWC_Product_Categories_Report($this->db_manager, $data);
+            default:
+                throw new InvalidArgumentException("Unknown report type: $type");
+        }
+    }
+    
+    public function get_available_report_types() {
+        return array(
+            'product_categories_summary' => __('Product Categories and Variants Summary', StudiouWC_Main::get_text_domain())
+        );
+    }
+    
+    public function is_valid_report_type($type) {
+        $available_types = array_keys($this->get_available_report_types());
+        return in_array($type, $available_types);
+    }
+}

+ 63 - 0
studiou-wc-custom-reports/includes/class-settings-manager.php

@@ -0,0 +1,63 @@
+<?php
+/**
+ * Settings Manager Class - Handles plugin settings
+ *
+ * @package StudiouWCCustomReports
+ */
+
+if (!defined('ABSPATH')) {
+    exit;
+}
+
+class StudiouWC_Settings_Manager {
+    
+    private const OPTION_NAME = 'studiou_wc_custom_reports_settings';
+    
+    public function activate() {
+        $default_settings = array(
+            'default_interval' => 'last_month',
+            'default_property' => '',
+            'default_statuses' => array('wc-completed', 'wc-processing', 'wc-on-hold')
+        );
+        add_option(self::OPTION_NAME, $default_settings);
+    }
+    
+    public function get_settings() {
+        return get_option(self::OPTION_NAME, array());
+    }
+    
+    public function update_settings($settings) {
+        return update_option(self::OPTION_NAME, $settings);
+    }
+    
+    public function get_default_date_from() {
+        $settings = $this->get_settings();
+        $interval = $settings['default_interval'] ?? 'last_month';
+        
+        switch ($interval) {
+            case 'last_day':
+                return date('Y-m-d', strtotime('-1 day'));
+            case 'last_week':
+                return date('Y-m-d', strtotime('-1 week'));
+            case 'last_month':
+            default:
+                return date('Y-m-d', strtotime('-1 month'));
+        }
+    }
+    
+    public function save_settings() {
+        if (!current_user_can('manage_woocommerce')) {
+            wp_send_json_error(__('You do not have sufficient permissions.', StudiouWC_Main::get_text_domain()));
+        }
+        
+        $settings = array(
+            'default_interval' => isset($_POST['default_interval']) ? sanitize_text_field($_POST['default_interval']) : 'last_month',
+            'default_property' => isset($_POST['default_property']) ? sanitize_text_field($_POST['default_property']) : '',
+            'default_statuses' => isset($_POST['default_statuses']) ? array_map('sanitize_text_field', $_POST['default_statuses']) : array()
+        );
+        
+        $this->update_settings($settings);
+        
+        wp_send_json_success(__('Settings saved successfully!', StudiouWC_Main::get_text_domain()));
+    }
+}

+ 187 - 0
studiou-wc-custom-reports/includes/class-utils.php

@@ -0,0 +1,187 @@
+<?php
+/**
+ * Utility Helper Class - Common utility functions
+ *
+ * @package StudiouWCCustomReports
+ */
+
+if (!defined('ABSPATH')) {
+    exit;
+}
+
+class StudiouWC_Utils {
+    
+    /**
+     * Recursively sanitize request data
+     */
+    public static function sanitize_request_data($data) {
+        if (is_array($data)) {
+            return array_map(array(self::class, 'sanitize_request_data'), $data);
+        }
+        
+        return sanitize_text_field($data);
+    }
+    
+    /**
+     * Format price using WooCommerce formatting
+     */
+    public static function format_price($price) {
+        return wc_price($price);
+    }
+    
+    /**
+     * Get WooCommerce order statuses
+     */
+    public static function get_order_statuses() {
+        return wc_get_order_statuses();
+    }
+    
+    /**
+     * Validate date format
+     */
+    public static function is_valid_date_format($date, $format = 'Y-m-d') {
+        $d = DateTime::createFromFormat($format, $date);
+        return $d && $d->format($format) === $date;
+    }
+    
+    /**
+     * Generate pagination links
+     */
+    public static function get_pagination_links($current_page, $total_pages, $base_url) {
+        $links = array();
+        
+        if ($current_page > 1) {
+            $links['prev'] = add_query_arg('paged', $current_page - 1, $base_url);
+        }
+        
+        if ($current_page < $total_pages) {
+            $links['next'] = add_query_arg('paged', $current_page + 1, $base_url);
+        }
+        
+        return $links;
+    }
+    
+    /**
+     * Get file extension from filename
+     */
+    public static function get_file_extension($filename) {
+        return pathinfo($filename, PATHINFO_EXTENSION);
+    }
+    
+    /**
+     * Generate secure filename for exports
+     */
+    public static function generate_export_filename($prefix = 'custom-reports', $extension = 'csv') {
+        $timestamp = date('Y-m-d-H-i-s');
+        return sanitize_file_name($prefix . '-' . $timestamp . '.' . $extension);
+    }
+    
+    /**
+     * Convert array to CSV row
+     */
+    public static function array_to_csv_row($data, $delimiter = ',') {
+        $output = fopen('php://temp', 'r+');
+        fputcsv($output, $data, $delimiter);
+        rewind($output);
+        $csv_row = stream_get_contents($output);
+        fclose($output);
+        
+        return rtrim($csv_row, "\n");
+    }
+    
+    /**
+     * Validate and sanitize URL parameters
+     */
+    public static function sanitize_url_params($params) {
+        $sanitized = array();
+        
+        foreach ($params as $key => $value) {
+            switch ($key) {
+                case 'date_from':
+                case 'date_to':
+                    if (self::is_valid_date_format($value)) {
+                        $sanitized[$key] = $value;
+                    }
+                    break;
+                    
+                case 'per_page':
+                case 'paged':
+                    $sanitized[$key] = max(1, intval($value));
+                    break;
+                    
+                case 'orderby':
+                    $valid_orderby = array('category', 'orders_count', 'total_price');
+                    if (in_array($value, $valid_orderby)) {
+                        $sanitized[$key] = $value;
+                    }
+                    break;
+                    
+                case 'order':
+                    if (in_array(strtoupper($value), array('ASC', 'DESC'))) {
+                        $sanitized[$key] = strtoupper($value);
+                    }
+                    break;
+                    
+                case 'statuses':
+                    if (is_array($value)) {
+                        $sanitized[$key] = array_map('sanitize_text_field', $value);
+                    }
+                    break;
+                    
+                default:
+                    $sanitized[$key] = sanitize_text_field($value);
+                    break;
+            }
+        }
+        
+        return $sanitized;
+    }
+    
+    /**
+     * Format bytes to human readable format
+     */
+    public static function format_bytes($bytes, $precision = 2) {
+        $units = array('B', 'KB', 'MB', 'GB', 'TB');
+        
+        for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
+            $bytes /= 1024;
+        }
+        
+        return round($bytes, $precision) . ' ' . $units[$i];
+    }
+    
+    /**
+     * Log debug information
+     */
+    public static function debug_log($message, $data = null) {
+        if (defined('WP_DEBUG') && WP_DEBUG) {
+            $log_message = '[Studiou WC Custom Reports] ' . $message;
+            
+            if ($data !== null) {
+                $log_message .= ' | Data: ' . print_r($data, true);
+            }
+            
+            error_log($log_message);
+        }
+    }
+    
+    /**
+     * Check if current user can manage reports
+     */
+    public static function current_user_can_manage_reports() {
+        return current_user_can('manage_woocommerce');
+    }
+    
+    /**
+     * Get plugin information
+     */
+    public static function get_plugin_info() {
+        return array(
+            'name' => 'QDR - Studiou WC Custom Reports',
+            'version' => StudiouWC_Main::get_version(),
+            'text_domain' => StudiouWC_Main::get_text_domain(),
+            'plugin_dir' => StudiouWC_Main::get_plugin_dir(),
+            'plugin_url' => StudiouWC_Main::get_plugin_url()
+        );
+    }
+}

+ 54 - 0
studiou-wc-custom-reports/instructions.txt

@@ -0,0 +1,54 @@
+Write WordPress plugin called "studiou-wc-custom-reports" in PHP, that extends WooCommerce plugin.
+Plugin will have following features:
+
+1. Add new page to menu WooCommerce/Reports that shows custom overviews according to order, product, product categories
+2. On new created page named "Custom Reports" will following main controls, that manage current view:
+	- View - current type of view (data to be shown)	
+	- Date From (date from interval) - initialy set from default
+	- Date To (date to interval) - initialy set from default
+	- Included statuses (defines set of statuses include all)
+	- Items per page
+	- Paged grid with sortable columns
+	- Button called "Export" to export shown data to .csv file
+3. Add new page to menu WooCommerce/Reports Settings to show studiou-wc-custom-reports settings
+4. On new created page "Custom Report settings" will be following controls:
+	- Combo box called "Default shown interval" (shows default interval calculated from now for initial filter) with following values: "Last day", "Last week", "Last month"
+	- Combo box called "Default property" (shows list of properties to use in filter) contailns list of properties
+	- Default Included statuses (defines set of statuses include all)
+	- Button called "Save Settings"
+5. View "Product Categories and Variants Summary"
+	
+	Query:
+		- from orders in filter interval (Date From,Date To), that has status defined in filter (Included statuses)
+		- all products from previous
+		- all variants from previous that has property defined in Default property
+		- select aggregate property, sum(price)
+		
+	Columns:
+	Product category
+	Orders count
+	(dynamic columns by Property values) <value> Count
+	Total price
+	
+ 
+6. Plugin will have following header:
+
+/**
+ * Plugin Name: QDR - Studiou WC Custom Reports
+ * Plugin URI: https://www.quadarax.com/plugins/studiou-wc-custom-reports
+ * Description: Allows view WooCommerce/Reports that shows custom overviews according to order, product, product categories, properties
+ * Version: 1.0.0
+ * Requires at least: 6.8.1
+ * Requires PHP: 8.2
+ * Author: Dalibor Votruba
+ * Author URI: https://www.quadarax.com
+ * License: GPL v2 or later
+ * License URI: https://www.gnu.org/licenses/gpl-2.0.html
+ * Text Domain: studiou-wc-custom-reports
+ * Domain Path: /languages
+ * WC requires at least: 9.0
+ * WC tested up to: 9.9
+ * Woo: 12345:342928dfsfhsf8429842374wdf4234sfd
+ */
+
+

+ 179 - 0
studiou-wc-custom-reports/readme.md

@@ -0,0 +1,179 @@
+# QDR - Studiou WC Custom Reports
+
+A WordPress plugin that extends WooCommerce with custom reporting functionality for product categories and variants analysis.
+
+## Description
+
+This plugin adds advanced reporting capabilities to WooCommerce, allowing you to analyze sales data by product categories and product properties/variants. It provides detailed insights into order counts, revenue, and property-based breakdowns.
+
+## Features
+
+- **Custom Reports Page**: Advanced filtering and data visualization
+- **Product Categories Summary**: Analyze sales by product categories
+- **Dynamic Property Columns**: Show counts by product properties (size, color, etc.)
+- **Flexible Date Filtering**: Custom date ranges with preset options
+- **Order Status Filtering**: Include/exclude specific order statuses
+- **Sortable Columns**: Sort by category, orders count, or total price
+- **Pagination**: Handle large datasets efficiently
+- **CSV Export**: Export filtered data to CSV files
+- **Settings Management**: Configure default values and preferences
+
+## Installation
+
+1. Upload the plugin files to `/wp-content/plugins/studiou-wc-custom-reports/`
+2. Activate the plugin through the 'Plugins' screen in WordPress
+3. Navigate to WooCommerce > Custom Reports to start using the plugin
+
+## Requirements
+
+- WordPress 6.8.1 or higher
+- PHP 8.2 or higher
+- WooCommerce 9.0 or higher
+
+## File Structure
+
+```
+studiou-wc-custom-reports/
+├── studiou-wc-custom-reports.php     # Main plugin file
+├── includes/
+│   ├── class-main.php                # Main plugin class
+│   ├── class-db-manager.php          # Database operations
+│   ├── class-settings-manager.php    # Settings management
+│   ├── class-admin-view.php          # Admin interface
+│   ├── class-export-handler.php      # CSV export functionality
+│   ├── class-report-data.php         # Data model and validation
+│   ├── class-report-factory.php      # Report factory pattern
+│   ├── class-product-categories-report.php  # Specific report implementation
+│   └── class-utils.php               # Utility functions
+├── languages/                        # Translation files
+└── README.md                         # This file
+```
+
+## Class Structure
+
+### Main Classes
+
+- **StudiouWC_Main**: Main plugin orchestrator
+- **StudiouWC_DB_Manager**: Handles all database queries and operations
+- **StudiouWC_Settings_Manager**: Manages plugin settings and configuration
+- **StudiouWC_Admin_View**: Renders admin interface and handles user interactions
+- **StudiouWC_Export_Handler**: Manages CSV export functionality
+
+### Supporting Classes
+
+- **StudiouWC_Report_Data**: Data model with validation
+- **StudiouWC_Report_Factory**: Factory for creating different report types
+- **StudiouWC_Product_Categories_Report**: Specific report implementation
+- **StudiouWC_Utils**: Common utility functions
+
+## Usage
+
+### Accessing Reports
+
+1. Go to **WooCommerce > Custom Reports**
+2. Configure your filters:
+   - **View**: Select report type
+   - **Date Range**: Set from/to dates
+   - **Order Statuses**: Choose which statuses to include
+   - **Items per page**: Control pagination
+3. Click **Filter** to update results
+4. Use **Export** to download CSV
+
+### Configuring Settings
+
+1. Go to **WooCommerce > Custom Report Settings**
+2. Set your preferences:
+   - **Default Interval**: Choose default date range
+   - **Default Property**: Select product property for variant analysis
+   - **Default Statuses**: Choose default order statuses
+3. Click **Save Settings**
+
+## Extending the Plugin
+
+### Adding New Report Types
+
+1. Create a new report class in `includes/`
+2. Update `StudiouWC_Report_Factory` to handle the new type
+3. Add corresponding database methods in `StudiouWC_DB_Manager`
+4. Update the view dropdown in `StudiouWC_Admin_View`
+
+### Adding Custom Properties
+
+The plugin automatically detects product meta fields as properties. To add custom properties:
+
+1. Add meta fields to your products
+2. The plugin will automatically include them in the property dropdown
+3. Configure the default property in settings
+
+## Hooks and Filters
+
+The plugin is designed to be extensible through WordPress hooks and filters (to be implemented in future versions).
+
+## Support
+
+For support and feature requests, please contact the plugin author or visit the plugin homepage.
+
+## License
+
+This plugin is licensed under GPL v2 or later.
+
+## Changelog
+
+### 1.0.0
+- Initial release
+- Product Categories and Variants Summary report
+- CSV export functionality
+- Settings management
+- Admin interface with filtering and pagination
+- Object-oriented architecture with separated concerns
+- Full WordPress and WooCommerce integration
+
+## Developer Notes
+
+### Architecture
+
+The plugin follows object-oriented principles with clear separation of concerns:
+
+- **Single Responsibility**: Each class has one primary purpose
+- **Dependency Injection**: Classes receive dependencies through constructors
+- **Factory Pattern**: Used for creating different report types
+- **Data Validation**: Input sanitization and validation throughout
+- **Security**: Proper capability checks and nonce verification
+
+### Database Queries
+
+All database operations are handled through the `StudiouWC_DB_Manager` class using WordPress's `$wpdb` with prepared statements for security.
+
+### Coding Standards
+
+- Follows WordPress coding standards
+- PSR-4 autoloading compatibility
+- Proper sanitization and escaping
+- Internationalization ready
+
+### Performance Considerations
+
+- Efficient database queries with proper indexing
+- Pagination for large datasets
+- Limited property value queries
+- Caching opportunities for future enhancement
+
+## Contributing
+
+When contributing to this plugin:
+
+1. Follow WordPress coding standards
+2. Add proper docblocks to all methods
+3. Include unit tests for new functionality
+4. Ensure backward compatibility
+5. Test with different WooCommerce versions
+
+## Security
+
+The plugin implements several security measures:
+
+- Capability checks for all admin functions
+- Input sanitization and validation
+- Prepared SQL statements
+- Proper data escaping in output
+- CSRF protection through WordPress nonces

+ 66 - 0
studiou-wc-custom-reports/studiou-wc-custom-reports.php

@@ -0,0 +1,66 @@
+<?php
+/**
+ * Plugin Name: QDR - Studiou WC Custom Reports
+ * Plugin URI: https://www.quadarax.com/plugins/studiou-wc-custom-reports
+ * Description: Allows view WooCommerce/Reports that shows custom overviews according to order, product, product categories, properties
+ * Version: 1.0.0
+ * Requires at least: 6.8.1
+ * Requires PHP: 8.2
+ * Author: Dalibor Votruba
+ * Author URI: https://www.quadarax.com
+ * License: GPL v2 or later
+ * License URI: https://www.gnu.org/licenses/gpl-2.0.html
+ * Text Domain: studiou-wc-custom-reports
+ * Domain Path: /languages
+ * WC requires at least: 9.0
+ * WC tested up to: 9.9
+ * Woo: 12345:342928dfsfhsf8429842374wdf4234sfd
+ */
+
+// Prevent direct access
+if (!defined('ABSPATH')) {
+    exit;
+}
+
+// Define plugin constants
+define('STUDIOU_WC_CUSTOM_REPORTS_PLUGIN_FILE', __FILE__);
+define('STUDIOU_WC_CUSTOM_REPORTS_PLUGIN_DIR', plugin_dir_path(__FILE__));
+define('STUDIOU_WC_CUSTOM_REPORTS_PLUGIN_URL', plugin_dir_url(__FILE__));
+define('STUDIOU_WC_CUSTOM_REPORTS_VERSION', '1.0.0');
+define('STUDIOU_WC_CUSTOM_REPORTS_TEXT_DOMAIN', 'studiou-wc-custom-reports');
+
+// Check if WooCommerce is active
+if (!in_array('woocommerce/woocommerce.php', apply_filters('active_plugins', get_option('active_plugins')))) {
+    add_action('admin_notices', function() {
+        echo '<div class="notice notice-error"><p>' . __('Studiou WC Custom Reports requires WooCommerce to be installed and active.', STUDIOU_WC_CUSTOM_REPORTS_TEXT_DOMAIN) . '</p></div>';
+    });
+    return;
+}
+
+// Declare HPOS compatibility
+add_action('before_woocommerce_init', function() {
+    if (class_exists('\Automattic\WooCommerce\Utilities\FeaturesUtil')) {
+        \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility('custom_order_tables', __FILE__, true);
+    }
+});
+
+// Autoload classes
+spl_autoload_register(function ($class_name) {
+    if (strpos($class_name, 'StudiouWC_') === 0) {
+        $file_name = 'class-' . strtolower(str_replace('_', '-', substr($class_name, 10))) . '.php';
+        $file_path = STUDIOU_WC_CUSTOM_REPORTS_PLUGIN_DIR . 'includes/' . $file_name;
+        
+        if (file_exists($file_path)) {
+            require_once $file_path;
+        }
+    }
+});
+
+// Include main plugin class
+require_once STUDIOU_WC_CUSTOM_REPORTS_PLUGIN_DIR . 'includes/class-main.php';
+
+// Initialize the plugin
+function studiou_wc_custom_reports_init() {
+    new StudiouWC_Main();
+}
+add_action('plugins_loaded', 'studiou_wc_custom_reports_init');