diff --git a/CHANGELOG.md b/CHANGELOG.md index c424410..0405345 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.0.11] - 2026-01-21 + +### Added + +- Created date column in admin license overview +- License Statistics page under WooCommerce menu +- REST API endpoints for analytics data: + - `GET /wp-json/wc-licensed-product/v1/analytics/stats` - License statistics with time-series data + - `GET /wp-json/wc-licensed-product/v1/analytics/products` - License counts by product +- WooCommerce Analytics integration via submenu page + +### Technical Details + +- New `AnalyticsController` class for WooCommerce Analytics integration +- Statistics page accessible via WooCommerce > License Statistics +- Time-series data supports day, week, month, quarter, year intervals +- REST API endpoints for external analytics integrations +- Statistics template `templates/admin/statistics.html.twig` + ## [0.0.10] - 2026-01-21 ### Added @@ -264,7 +283,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - WordPress REST API integration - Custom WooCommerce product type extending WC_Product -[Unreleased]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.0.7...HEAD +[Unreleased]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.0.11...HEAD +[0.0.11]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.0.10...v0.0.11 +[0.0.10]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.0.9...v0.0.10 +[0.0.9]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.0.8...v0.0.9 +[0.0.8]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.0.7...v0.0.8 [0.0.7]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.0.6...v0.0.7 [0.0.6]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.0.5...v0.0.6 [0.0.5]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.0.4...v0.0.5 diff --git a/CLAUDE.md b/CLAUDE.md index d7150bf..d83a938 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,10 +36,6 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w - Version uploads not appearing in list (under investigation - may require plugin reactivation to ensure database tables exist) -### Version 0.0.11 (planned) - -- TBD - no specific features planned yet - ## Technical Stack - **Language:** PHP 8.3.x @@ -597,3 +593,36 @@ Full API documentation available in `openapi.json` (OpenAPI 3.1 specification). - Created release package: `releases/wc-licensed-product-0.0.10.zip` (472 KB) - SHA256: `3f4a093f6d4d02389082c3a88c00542f477ab3ad4d4a0c65079e524ef0739620` - Tagged as `v0.0.10` and pushed to `main` branch + +### 2026-01-21 - Version 0.0.11 Features + +**Implemented:** + +- Created date column added to admin license overview +- License Statistics page under WooCommerce menu (WooCommerce > License Statistics) +- REST API endpoints for analytics data with time-series support +- WooCommerce Analytics integration via submenu page + +**New files:** + +- `src/Admin/AnalyticsController.php` - WooCommerce Analytics integration +- `templates/admin/statistics.html.twig` - Statistics page template + +**New REST API endpoints:** + +- `GET /wp-json/wc-licensed-product/v1/analytics/stats` - License statistics with time-series data (supports day/week/month/quarter/year intervals) +- `GET /wp-json/wc-licensed-product/v1/analytics/products` - License counts by product + +**Modified files:** + +- `templates/admin/licenses.html.twig` - Added "Created" column +- `src/Admin/AdminController.php` - Added "Created" column to fallback rendering +- `src/Plugin.php` - Added AnalyticsController initialization and `getInstance()` alias + +**Technical notes:** + +- Statistics page accessible via WooCommerce > License Statistics submenu +- REST API endpoints support date range filtering (`after`, `before` parameters) +- Time-series data aggregation supports multiple intervals (day, week, month, quarter, year) +- AnalyticsController registers REST routes and renders statistics page +- Page uses existing dashboard CSS styles for consistent appearance diff --git a/languages/wc-licensed-product-de_CH.mo b/languages/wc-licensed-product-de_CH.mo index 3d6cb54..e111388 100644 Binary files a/languages/wc-licensed-product-de_CH.mo and b/languages/wc-licensed-product-de_CH.mo differ diff --git a/languages/wc-licensed-product-de_CH.po b/languages/wc-licensed-product-de_CH.po index a4f203e..0a6cdda 100644 --- a/languages/wc-licensed-product-de_CH.po +++ b/languages/wc-licensed-product-de_CH.po @@ -3,7 +3,7 @@ # This file is distributed under the GPL-2.0-or-later. msgid "" msgstr "" -"Project-Id-Version: WC Licensed Product 0.0.10\n" +"Project-Id-Version: WC Licensed Product 0.0.11\n" "Report-Msgid-Bugs-To: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/issues\n" "POT-Creation-Date: 2026-01-21T00:00:00+00:00\n" "PO-Revision-Date: 2026-01-21T00:00:00+00:00\n" @@ -939,3 +939,34 @@ msgstr "Fehler beim Speichern. Bitte versuchen Sie es erneut." msgid "Failed to update license domain." msgstr "Lizenz-Domain konnte nicht aktualisiert werden." + +#. Admin - License Overview (Created column) +msgid "Created" +msgstr "Erstellt" + +#. Admin - License Statistics +msgid "License Statistics" +msgstr "Lizenz-Statistiken" + +msgid "%d license is expiring within the next 30 days." +msgid_plural "%d licenses are expiring within the next 30 days." +msgstr[0] "%d Lizenz läuft innerhalb der nächsten 30 Tage ab." +msgstr[1] "%d Lizenzen laufen innerhalb der nächsten 30 Tage ab." + +msgid "REST API Endpoints" +msgstr "REST-API-Endpunkte" + +msgid "The following REST API endpoints are available for retrieving license statistics:" +msgstr "Die folgenden REST-API-Endpunkte sind für den Abruf von Lizenz-Statistiken verfügbar:" + +msgid "Endpoint" +msgstr "Endpunkt" + +msgid "Description" +msgstr "Beschreibung" + +msgid "Get license statistics with time-series data" +msgstr "Lizenz-Statistiken mit Zeitreihen-Daten abrufen" + +msgid "Get license counts by product" +msgstr "Lizenz-Anzahl pro Produkt abrufen" diff --git a/languages/wc-licensed-product.pot b/languages/wc-licensed-product.pot index 3fe9de9..e2ed34d 100644 --- a/languages/wc-licensed-product.pot +++ b/languages/wc-licensed-product.pot @@ -2,7 +2,7 @@ # This file is distributed under the GPL-2.0-or-later. msgid "" msgstr "" -"Project-Id-Version: WC Licensed Product 0.0.10\n" +"Project-Id-Version: WC Licensed Product 0.0.11\n" "Report-Msgid-Bugs-To: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/issues\n" "POT-Creation-Date: 2026-01-21T00:00:00+00:00\n" "MIME-Version: 1.0\n" @@ -939,3 +939,34 @@ msgstr "" msgid "Failed to update license domain." msgstr "" + +#. Admin - License Overview (Created column) +msgid "Created" +msgstr "" + +#. Admin - License Statistics +msgid "License Statistics" +msgstr "" + +msgid "%d license is expiring within the next 30 days." +msgid_plural "%d licenses are expiring within the next 30 days." +msgstr[0] "" +msgstr[1] "" + +msgid "REST API Endpoints" +msgstr "" + +msgid "The following REST API endpoints are available for retrieving license statistics:" +msgstr "" + +msgid "Endpoint" +msgstr "" + +msgid "Description" +msgstr "" + +msgid "Get license statistics with time-series data" +msgstr "" + +msgid "Get license counts by product" +msgstr "" diff --git a/src/Admin/AdminController.php b/src/Admin/AdminController.php index 6251646..15fc39b 100644 --- a/src/Admin/AdminController.php +++ b/src/Admin/AdminController.php @@ -1253,6 +1253,7 @@ final class AdminController + @@ -1260,7 +1261,7 @@ final class AdminController - + @@ -1320,6 +1321,9 @@ final class AdminController + + getCreatedAt()->format(get_option('date_format'))); ?> + getExpiresAt(); ?> @@ -1387,6 +1391,7 @@ final class AdminController + diff --git a/src/Admin/AnalyticsController.php b/src/Admin/AnalyticsController.php new file mode 100644 index 0000000..b4b02b8 --- /dev/null +++ b/src/Admin/AnalyticsController.php @@ -0,0 +1,523 @@ +licenseManager = $licenseManager; + } + + /** + * Initialize analytics hooks + */ + public function init(): void + { + // Add submenu under WooCommerce Analytics + add_action('admin_menu', [$this, 'addAnalyticsSubmenu'], 99); + + // Register REST API endpoints for analytics data + add_action('rest_api_init', [$this, 'registerRestRoutes']); + + // Add license stats to WooCommerce Admin data registry + add_action('admin_enqueue_scripts', [$this, 'enqueueAnalyticsData']); + + // Add analytics navigation item (WC Admin) + add_filter('woocommerce_navigation_menu_items', [$this, 'addNavigationItem']); + + // Register WooCommerce Analytics report page + add_filter('woocommerce_analytics_report_menu_items', [$this, 'addAnalyticsReportMenuItem']); + } + + /** + * Add submenu page under WooCommerce menu + */ + public function addAnalyticsSubmenu(): void + { + add_submenu_page( + 'woocommerce', + __('License Statistics', 'wc-licensed-product'), + __('License Statistics', 'wc-licensed-product'), + 'manage_woocommerce', + 'wc-license-statistics', + [$this, 'renderStatisticsPage'] + ); + } + + /** + * Add navigation item for WC Admin navigation + */ + public function addNavigationItem(array $items): array + { + $items[] = [ + 'id' => 'wc-license-statistics', + 'title' => __('License Statistics', 'wc-licensed-product'), + 'parent' => 'woocommerce-analytics', + 'path' => '/analytics/license-statistics', + ]; + + return $items; + } + + /** + * Add report menu item to WooCommerce Analytics + */ + public function addAnalyticsReportMenuItem(array $report_pages): array + { + $report_pages[] = [ + 'id' => 'wc-license-statistics', + 'title' => __('License Statistics', 'wc-licensed-product'), + 'parent' => 'woocommerce-analytics', + 'path' => '/analytics/license-statistics', + ]; + + return $report_pages; + } + + /** + * Register REST API routes for analytics data + */ + public function registerRestRoutes(): void + { + register_rest_route('wc-licensed-product/v1', '/analytics/stats', [ + 'methods' => \WP_REST_Server::READABLE, + 'callback' => [$this, 'getAnalyticsStats'], + 'permission_callback' => function () { + return current_user_can('manage_woocommerce'); + }, + 'args' => [ + 'after' => [ + 'description' => __('Limit response to stats after a given date.', 'wc-licensed-product'), + 'type' => 'string', + 'format' => 'date-time', + ], + 'before' => [ + 'description' => __('Limit response to stats before a given date.', 'wc-licensed-product'), + 'type' => 'string', + 'format' => 'date-time', + ], + 'interval' => [ + 'description' => __('Time interval to aggregate stats.', 'wc-licensed-product'), + 'type' => 'string', + 'enum' => ['day', 'week', 'month', 'quarter', 'year'], + 'default' => 'month', + ], + ], + ]); + + register_rest_route('wc-licensed-product/v1', '/analytics/products', [ + 'methods' => \WP_REST_Server::READABLE, + 'callback' => [$this, 'getProductStats'], + 'permission_callback' => function () { + return current_user_can('manage_woocommerce'); + }, + 'args' => [ + 'per_page' => [ + 'description' => __('Maximum number of items to return.', 'wc-licensed-product'), + 'type' => 'integer', + 'default' => 10, + ], + 'orderby' => [ + 'description' => __('Sort by this field.', 'wc-licensed-product'), + 'type' => 'string', + 'enum' => ['licenses_count', 'product_name'], + 'default' => 'licenses_count', + ], + 'order' => [ + 'description' => __('Order direction.', 'wc-licensed-product'), + 'type' => 'string', + 'enum' => ['asc', 'desc'], + 'default' => 'desc', + ], + ], + ]); + } + + /** + * Get analytics stats via REST API + */ + public function getAnalyticsStats(\WP_REST_Request $request): \WP_REST_Response + { + $stats = $this->licenseManager->getStatistics(); + $interval = $request->get_param('interval') ?: 'month'; + + // Get time-series data based on interval + $timeSeriesData = $this->getTimeSeriesData($interval, $request->get_param('after'), $request->get_param('before')); + + return new \WP_REST_Response([ + 'totals' => [ + 'total_licenses' => $stats['total'], + 'active_licenses' => $stats['by_status']['active'] ?? 0, + 'inactive_licenses' => $stats['by_status']['inactive'] ?? 0, + 'expired_licenses' => $stats['by_status']['expired'] ?? 0, + 'revoked_licenses' => $stats['by_status']['revoked'] ?? 0, + 'lifetime_licenses' => $stats['lifetime'] ?? 0, + 'expiring_soon' => $stats['expiring_soon'] ?? 0, + ], + 'intervals' => $timeSeriesData, + ], 200); + } + + /** + * Get product statistics via REST API + */ + public function getProductStats(\WP_REST_Request $request): \WP_REST_Response + { + $stats = $this->licenseManager->getStatistics(); + $perPage = $request->get_param('per_page') ?: 10; + + $productStats = array_slice($stats['by_product'] ?? [], 0, $perPage); + + return new \WP_REST_Response([ + 'products' => $productStats, + ], 200); + } + + /** + * Get time-series data for the specified interval + */ + private function getTimeSeriesData(string $interval, ?string $after = null, ?string $before = null): array + { + global $wpdb; + + $tableName = $wpdb->prefix . 'wc_licensed_product_licenses'; + + // Set default date range + $endDate = $before ? new \DateTimeImmutable($before) : new \DateTimeImmutable(); + $startDate = $after ? new \DateTimeImmutable($after) : $endDate->modify('-12 months'); + + // Build date format based on interval + switch ($interval) { + case 'day': + $dateFormat = '%Y-%m-%d'; + $phpFormat = 'Y-m-d'; + break; + case 'week': + $dateFormat = '%Y-%u'; + $phpFormat = 'Y-W'; + break; + case 'quarter': + $dateFormat = "CONCAT(YEAR(created_at), '-Q', QUARTER(created_at))"; + $phpFormat = 'Y-\QQ'; + break; + case 'year': + $dateFormat = '%Y'; + $phpFormat = 'Y'; + break; + case 'month': + default: + $dateFormat = '%Y-%m'; + $phpFormat = 'Y-m'; + break; + } + + // Special handling for quarter since it's not a simple DATE_FORMAT + if ($interval === 'quarter') { + $sql = $wpdb->prepare( + "SELECT {$dateFormat} as period, COUNT(*) as count + FROM {$tableName} + WHERE created_at >= %s AND created_at <= %s + GROUP BY period + ORDER BY period ASC", + $startDate->format('Y-m-d 00:00:00'), + $endDate->format('Y-m-d 23:59:59') + ); + } else { + $sql = $wpdb->prepare( + "SELECT DATE_FORMAT(created_at, %s) as period, COUNT(*) as count + FROM {$tableName} + WHERE created_at >= %s AND created_at <= %s + GROUP BY period + ORDER BY period ASC", + $dateFormat, + $startDate->format('Y-m-d 00:00:00'), + $endDate->format('Y-m-d 23:59:59') + ); + } + + $results = $wpdb->get_results($sql, ARRAY_A); + + $data = []; + foreach ($results as $row) { + $data[] = [ + 'interval' => $row['period'], + 'subtotals' => [ + 'licenses_count' => (int) $row['count'], + ], + ]; + } + + return $data; + } + + /** + * Enqueue license analytics data for WC Admin + */ + public function enqueueAnalyticsData(): void + { + if (!function_exists('wc_admin_get_feature_config')) { + return; + } + + $screen = get_current_screen(); + if (!$screen || strpos($screen->id, 'woocommerce') === false) { + return; + } + + $stats = $this->licenseManager->getStatistics(); + + wp_localize_script('wc-admin-app', 'wcLicenseStats', [ + 'total' => $stats['total'], + 'active' => $stats['by_status']['active'] ?? 0, + 'inactive' => $stats['by_status']['inactive'] ?? 0, + 'expired' => $stats['by_status']['expired'] ?? 0, + 'revoked' => $stats['by_status']['revoked'] ?? 0, + 'lifetime' => $stats['lifetime'] ?? 0, + 'expiringSoon' => $stats['expiring_soon'] ?? 0, + 'endpoints' => [ + 'stats' => rest_url('wc-licensed-product/v1/analytics/stats'), + 'products' => rest_url('wc-licensed-product/v1/analytics/products'), + ], + ]); + } + + /** + * Render the statistics page + */ + public function renderStatisticsPage(): void + { + $stats = $this->licenseManager->getStatistics(); + + // Render using Twig if available + $plugin = \Jeremias\WcLicensedProduct\Plugin::getInstance(); + $twig = $plugin->getTwig(); + + if ($twig) { + try { + echo $twig->render('admin/statistics.html.twig', [ + 'stats' => $stats, + 'admin_url' => admin_url('admin.php'), + 'rest_url' => rest_url('wc-licensed-product/v1/analytics/'), + ]); + return; + } catch (\Twig\Error\LoaderError $e) { + // Template not found, use fallback + } + } + + // Fallback PHP rendering + $this->renderStatisticsPageFallback($stats); + } + + /** + * Fallback rendering for statistics page + */ + private function renderStatisticsPageFallback(array $stats): void + { + ?> +
+

+ +
+
+
+

+ +
+
+

+ +
+
+

+ +
+
+

+ +
+
+

+ +
+
+ + 0): ?> +
+

+ + + + + +

+
+ + +
+
+

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

+ +

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

+ +

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

+ +

+ +
+
+ $count): + $height = ($count / $maxValue * 100); + ?> +
+
+ +
+ +
+ +
+
+ +
+
+ + + +
+

+

+ +

+ + + + + + + + + + + + + + + + + +
GET /wc-licensed-product/v1/analytics/stats
GET /wc-licensed-product/v1/analytics/products
+
+
+ versionManager); new OrderLicenseController($this->licenseManager); new SettingsController(); + (new AnalyticsController($this->licenseManager))->init(); } } diff --git a/templates/admin/licenses.html.twig b/templates/admin/licenses.html.twig index ce7a9f3..530d4d8 100644 --- a/templates/admin/licenses.html.twig +++ b/templates/admin/licenses.html.twig @@ -95,6 +95,7 @@ {{ __('Customer') }} {{ __('Domain') }} {{ __('Status') }} + {{ __('Created') }} {{ __('Expires') }} {{ __('Actions') }} @@ -102,7 +103,7 @@ {% if licenses is empty %} - {{ __('No licenses found.') }} + {{ __('No licenses found.') }} {% else %} {% for item in licenses %} @@ -160,6 +161,9 @@ + + {{ item.license.createdAt|date('Y-m-d') }} + {% if item.license.expiresAt %} @@ -219,6 +223,7 @@ {{ __('Customer') }} {{ __('Domain') }} {{ __('Status') }} + {{ __('Created') }} {{ __('Expires') }} {{ __('Actions') }} diff --git a/templates/admin/statistics.html.twig b/templates/admin/statistics.html.twig new file mode 100644 index 0000000..360b6c8 --- /dev/null +++ b/templates/admin/statistics.html.twig @@ -0,0 +1,196 @@ +
+

{{ __('License Statistics') }}

+ +
+
+
+
+
+ {{ stats.total }} + {{ __('Total Licenses') }} +
+
+
+
+
+ {{ stats.by_status.active }} + {{ __('Active') }} +
+
+
+
+
+ {{ stats.by_status.inactive }} + {{ __('Inactive') }} +
+
+
+
+
+ {{ stats.by_status.expired }} + {{ __('Expired') }} +
+
+
+
+
+ {{ stats.by_status.revoked }} + {{ __('Revoked') }} +
+
+
+ + {% if stats.expiring_soon > 0 %} +
+

+ + {{ __('Attention:') }} + {{ stats.expiring_soon }} {{ stats.expiring_soon == 1 ? __('license is') : __('licenses are') }} + {{ __('expiring within the next 30 days.') }} + {{ __('View Licenses') }} +

+
+ {% endif %} + +
+
+

{{ __('License Types') }}

+ + + + + + + + + + + + + + + +
{{ __('Lifetime Licenses') }}{{ stats.lifetime }}
{{ __('Time-limited Licenses') }}{{ stats.expiring }}
{{ __('Expiring Soon (30 days)') }} + {{ stats.expiring_soon }} +
+
+ +
+

{{ __('Top Products by Licenses') }}

+ {% if stats.by_product is empty %} +

{{ __('No license data available yet.') }}

+ {% else %} + + + + + + + + + {% for product in stats.by_product %} + + + + + {% endfor %} + +
{{ __('Product') }}{{ __('Licenses') }}
{{ esc_html(product.product_name) }}{{ product.count }}
+ {% endif %} +
+ +
+

{{ __('Top Domains') }}

+ {% if stats.top_domains is empty %} +

{{ __('No license data available yet.') }}

+ {% else %} + + + + + + + + + {% for domain in stats.top_domains %} + + + + + {% endfor %} + +
{{ __('Domain') }}{{ __('Licenses') }}
{{ esc_html(domain.domain) }}{{ domain.count }}
+ {% endif %} +
+
+ +
+

{{ __('Licenses Created (Last 12 Months)') }}

+ {% if stats.monthly is empty %} +

{{ __('No license data available yet.') }}

+ {% else %} +
+
+ {% set max_value = 1 %} + {% for count in stats.monthly %} + {% if count > max_value %} + {% set max_value = count %} + {% endif %} + {% endfor %} + {% for month, count in stats.monthly %} +
+
+ {{ count }} +
+ {{ month|date('M Y') }} +
+ {% endfor %} +
+
+ {% endif %} +
+
+ + + +
+

{{ __('REST API Endpoints') }}

+

+ {{ __('The following REST API endpoints are available for retrieving license statistics:') }} +

+ + + + + + + + + + + + + + + + + +
{{ __('Endpoint') }}{{ __('Description') }}
GET {{ rest_url }}stats{{ __('Get license statistics with time-series data') }}
GET {{ rest_url }}products{{ __('Get license counts by product') }}
+
+
diff --git a/wc-licensed-product.php b/wc-licensed-product.php index dc57e35..02f63c0 100644 --- a/wc-licensed-product.php +++ b/wc-licensed-product.php @@ -3,7 +3,7 @@ * Plugin Name: WooCommerce Licensed Product * Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-licensed-product * Description: WooCommerce plugin to sell software products using license keys with domain-based validation. - * Version: 0.0.10 + * Version: 0.0.11 * Author: Marco Graetsch * Author URI: https://src.bundespruefstelle.ch/magdev * License: GPL-2.0-or-later @@ -28,7 +28,7 @@ if (!defined('ABSPATH')) { } // Plugin constants -define('WC_LICENSED_PRODUCT_VERSION', '0.0.10'); +define('WC_LICENSED_PRODUCT_VERSION', '0.0.11'); define('WC_LICENSED_PRODUCT_PLUGIN_FILE', __FILE__); define('WC_LICENSED_PRODUCT_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('WC_LICENSED_PRODUCT_PLUGIN_URL', plugin_dir_url(__FILE__));