You've already forked wc-licensed-product
Implement version 0.0.11 features
- Add Created date column to admin license overview - Add License Statistics page under WooCommerce menu - Add REST API endpoints for analytics data with time-series support - WooCommerce Analytics integration via submenu page New files: - src/Admin/AnalyticsController.php - templates/admin/statistics.html.twig REST API endpoints: - GET /wc-licensed-product/v1/analytics/stats - GET /wc-licensed-product/v1/analytics/products Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
25
CHANGELOG.md
25
CHANGELOG.md
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [0.0.10] - 2026-01-21
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -264,7 +283,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- WordPress REST API integration
|
- WordPress REST API integration
|
||||||
- Custom WooCommerce product type extending WC_Product
|
- 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.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.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
|
[0.0.5]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.0.4...v0.0.5
|
||||||
|
|||||||
37
CLAUDE.md
37
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 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
|
## Technical Stack
|
||||||
|
|
||||||
- **Language:** PHP 8.3.x
|
- **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)
|
- Created release package: `releases/wc-licensed-product-0.0.10.zip` (472 KB)
|
||||||
- SHA256: `3f4a093f6d4d02389082c3a88c00542f477ab3ad4d4a0c65079e524ef0739620`
|
- SHA256: `3f4a093f6d4d02389082c3a88c00542f477ab3ad4d4a0c65079e524ef0739620`
|
||||||
- Tagged as `v0.0.10` and pushed to `main` branch
|
- 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
|
||||||
|
|||||||
Binary file not shown.
@@ -3,7 +3,7 @@
|
|||||||
# This file is distributed under the GPL-2.0-or-later.
|
# This file is distributed under the GPL-2.0-or-later.
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
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"
|
"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"
|
"POT-Creation-Date: 2026-01-21T00:00:00+00:00\n"
|
||||||
"PO-Revision-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."
|
msgid "Failed to update license domain."
|
||||||
msgstr "Lizenz-Domain konnte nicht aktualisiert werden."
|
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"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# This file is distributed under the GPL-2.0-or-later.
|
# This file is distributed under the GPL-2.0-or-later.
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
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"
|
"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"
|
"POT-Creation-Date: 2026-01-21T00:00:00+00:00\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
@@ -939,3 +939,34 @@ msgstr ""
|
|||||||
|
|
||||||
msgid "Failed to update license domain."
|
msgid "Failed to update license domain."
|
||||||
msgstr ""
|
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 ""
|
||||||
|
|||||||
@@ -1253,6 +1253,7 @@ final class AdminController
|
|||||||
<th><?php esc_html_e('Customer', 'wc-licensed-product'); ?></th>
|
<th><?php esc_html_e('Customer', 'wc-licensed-product'); ?></th>
|
||||||
<th><?php esc_html_e('Domain', 'wc-licensed-product'); ?></th>
|
<th><?php esc_html_e('Domain', 'wc-licensed-product'); ?></th>
|
||||||
<th><?php esc_html_e('Status', 'wc-licensed-product'); ?></th>
|
<th><?php esc_html_e('Status', 'wc-licensed-product'); ?></th>
|
||||||
|
<th><?php esc_html_e('Created', 'wc-licensed-product'); ?></th>
|
||||||
<th><?php esc_html_e('Expires', 'wc-licensed-product'); ?></th>
|
<th><?php esc_html_e('Expires', 'wc-licensed-product'); ?></th>
|
||||||
<th><?php esc_html_e('Actions', 'wc-licensed-product'); ?></th>
|
<th><?php esc_html_e('Actions', 'wc-licensed-product'); ?></th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1260,7 +1261,7 @@ final class AdminController
|
|||||||
<tbody>
|
<tbody>
|
||||||
<?php if (empty($enrichedLicenses)): ?>
|
<?php if (empty($enrichedLicenses)): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="8"><?php esc_html_e('No licenses found.', 'wc-licensed-product'); ?></td>
|
<td colspan="9"><?php esc_html_e('No licenses found.', 'wc-licensed-product'); ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?php foreach ($enrichedLicenses as $item): ?>
|
<?php foreach ($enrichedLicenses as $item): ?>
|
||||||
@@ -1320,6 +1321,9 @@ final class AdminController
|
|||||||
<button type="button" class="wclp-cancel-btn button button-small"><?php esc_html_e('Cancel', 'wc-licensed-product'); ?></button>
|
<button type="button" class="wclp-cancel-btn button button-small"><?php esc_html_e('Cancel', 'wc-licensed-product'); ?></button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="wclp-created-cell">
|
||||||
|
<?php echo esc_html($item['license']->getCreatedAt()->format(get_option('date_format'))); ?>
|
||||||
|
</td>
|
||||||
<td class="wclp-editable-cell" data-field="expiry" data-license-id="<?php echo esc_attr($item['license']->getId()); ?>">
|
<td class="wclp-editable-cell" data-field="expiry" data-license-id="<?php echo esc_attr($item['license']->getId()); ?>">
|
||||||
<?php $expiresAt = $item['license']->getExpiresAt(); ?>
|
<?php $expiresAt = $item['license']->getExpiresAt(); ?>
|
||||||
<span class="wclp-display-value">
|
<span class="wclp-display-value">
|
||||||
@@ -1387,6 +1391,7 @@ final class AdminController
|
|||||||
<th><?php esc_html_e('Customer', 'wc-licensed-product'); ?></th>
|
<th><?php esc_html_e('Customer', 'wc-licensed-product'); ?></th>
|
||||||
<th><?php esc_html_e('Domain', 'wc-licensed-product'); ?></th>
|
<th><?php esc_html_e('Domain', 'wc-licensed-product'); ?></th>
|
||||||
<th><?php esc_html_e('Status', 'wc-licensed-product'); ?></th>
|
<th><?php esc_html_e('Status', 'wc-licensed-product'); ?></th>
|
||||||
|
<th><?php esc_html_e('Created', 'wc-licensed-product'); ?></th>
|
||||||
<th><?php esc_html_e('Expires', 'wc-licensed-product'); ?></th>
|
<th><?php esc_html_e('Expires', 'wc-licensed-product'); ?></th>
|
||||||
<th><?php esc_html_e('Actions', 'wc-licensed-product'); ?></th>
|
<th><?php esc_html_e('Actions', 'wc-licensed-product'); ?></th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
523
src/Admin/AnalyticsController.php
Normal file
523
src/Admin/AnalyticsController.php
Normal file
@@ -0,0 +1,523 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* WooCommerce Analytics Integration Controller
|
||||||
|
*
|
||||||
|
* @package Jeremias\WcLicensedProduct\Admin
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Jeremias\WcLicensedProduct\Admin;
|
||||||
|
|
||||||
|
use Jeremias\WcLicensedProduct\License\LicenseManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integrates license statistics with WooCommerce Analytics
|
||||||
|
*/
|
||||||
|
class AnalyticsController
|
||||||
|
{
|
||||||
|
private LicenseManager $licenseManager;
|
||||||
|
|
||||||
|
public function __construct(LicenseManager $licenseManager)
|
||||||
|
{
|
||||||
|
$this->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
|
||||||
|
{
|
||||||
|
?>
|
||||||
|
<div class="wrap wclp-statistics">
|
||||||
|
<h1><?php esc_html_e('License Statistics', 'wc-licensed-product'); ?></h1>
|
||||||
|
|
||||||
|
<div class="wclp-stats-overview">
|
||||||
|
<div class="wclp-stat-cards">
|
||||||
|
<div class="wclp-stat-card">
|
||||||
|
<h3><?php esc_html_e('Total Licenses', 'wc-licensed-product'); ?></h3>
|
||||||
|
<span class="wclp-stat-number"><?php echo esc_html($stats['total']); ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="wclp-stat-card wclp-stat-active">
|
||||||
|
<h3><?php esc_html_e('Active', 'wc-licensed-product'); ?></h3>
|
||||||
|
<span class="wclp-stat-number"><?php echo esc_html($stats['by_status']['active'] ?? 0); ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="wclp-stat-card wclp-stat-inactive">
|
||||||
|
<h3><?php esc_html_e('Inactive', 'wc-licensed-product'); ?></h3>
|
||||||
|
<span class="wclp-stat-number"><?php echo esc_html($stats['by_status']['inactive'] ?? 0); ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="wclp-stat-card wclp-stat-expired">
|
||||||
|
<h3><?php esc_html_e('Expired', 'wc-licensed-product'); ?></h3>
|
||||||
|
<span class="wclp-stat-number"><?php echo esc_html($stats['by_status']['expired'] ?? 0); ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="wclp-stat-card wclp-stat-revoked">
|
||||||
|
<h3><?php esc_html_e('Revoked', 'wc-licensed-product'); ?></h3>
|
||||||
|
<span class="wclp-stat-number"><?php echo esc_html($stats['by_status']['revoked'] ?? 0); ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($stats['expiring_soon'] > 0): ?>
|
||||||
|
<div class="notice notice-warning">
|
||||||
|
<p>
|
||||||
|
<strong><?php esc_html_e('Attention:', 'wc-licensed-product'); ?></strong>
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %d: number of licenses */
|
||||||
|
_n(
|
||||||
|
'%d license is expiring within the next 30 days.',
|
||||||
|
'%d licenses are expiring within the next 30 days.',
|
||||||
|
$stats['expiring_soon'],
|
||||||
|
'wc-licensed-product'
|
||||||
|
),
|
||||||
|
$stats['expiring_soon']
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
<a href="<?php echo esc_url(admin_url('admin.php?page=wc-licenses')); ?>">
|
||||||
|
<?php esc_html_e('View Licenses', 'wc-licensed-product'); ?>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="wclp-stats-details">
|
||||||
|
<div class="wclp-stat-box">
|
||||||
|
<h3><?php esc_html_e('License Types', 'wc-licensed-product'); ?></h3>
|
||||||
|
<table class="widefat striped">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><?php esc_html_e('Lifetime Licenses', 'wc-licensed-product'); ?></td>
|
||||||
|
<td class="wclp-stat-value"><?php echo esc_html($stats['lifetime'] ?? 0); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><?php esc_html_e('Time-limited Licenses', 'wc-licensed-product'); ?></td>
|
||||||
|
<td class="wclp-stat-value"><?php echo esc_html($stats['expiring'] ?? 0); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><?php esc_html_e('Expiring Soon (30 days)', 'wc-licensed-product'); ?></td>
|
||||||
|
<td class="wclp-stat-value"><?php echo esc_html($stats['expiring_soon'] ?? 0); ?></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wclp-stat-box">
|
||||||
|
<h3><?php esc_html_e('Top Products by Licenses', 'wc-licensed-product'); ?></h3>
|
||||||
|
<?php if (empty($stats['by_product'])): ?>
|
||||||
|
<p class="description"><?php esc_html_e('No license data available yet.', 'wc-licensed-product'); ?></p>
|
||||||
|
<?php else: ?>
|
||||||
|
<table class="widefat striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?php esc_html_e('Product', 'wc-licensed-product'); ?></th>
|
||||||
|
<th class="wclp-stat-value"><?php esc_html_e('Licenses', 'wc-licensed-product'); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($stats['by_product'] as $product): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?php echo esc_html($product['product_name']); ?></td>
|
||||||
|
<td class="wclp-stat-value"><?php echo esc_html($product['count']); ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wclp-stat-box">
|
||||||
|
<h3><?php esc_html_e('Top Domains', 'wc-licensed-product'); ?></h3>
|
||||||
|
<?php if (empty($stats['top_domains'])): ?>
|
||||||
|
<p class="description"><?php esc_html_e('No license data available yet.', 'wc-licensed-product'); ?></p>
|
||||||
|
<?php else: ?>
|
||||||
|
<table class="widefat striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?php esc_html_e('Domain', 'wc-licensed-product'); ?></th>
|
||||||
|
<th class="wclp-stat-value"><?php esc_html_e('Licenses', 'wc-licensed-product'); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($stats['top_domains'] as $domain): ?>
|
||||||
|
<tr>
|
||||||
|
<td><code><?php echo esc_html($domain['domain']); ?></code></td>
|
||||||
|
<td class="wclp-stat-value"><?php echo esc_html($domain['count']); ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wclp-chart-section">
|
||||||
|
<h3><?php esc_html_e('Licenses Created (Last 12 Months)', 'wc-licensed-product'); ?></h3>
|
||||||
|
<?php if (empty($stats['monthly'])): ?>
|
||||||
|
<p class="description"><?php esc_html_e('No license data available yet.', 'wc-licensed-product'); ?></p>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="wclp-chart-container">
|
||||||
|
<div class="wclp-bar-chart">
|
||||||
|
<?php
|
||||||
|
$maxValue = max(1, max($stats['monthly']));
|
||||||
|
foreach ($stats['monthly'] as $month => $count):
|
||||||
|
$height = ($count / $maxValue * 100);
|
||||||
|
?>
|
||||||
|
<div class="wclp-bar-wrapper">
|
||||||
|
<div class="wclp-bar" style="height: <?php echo esc_attr($height); ?>%;" title="<?php echo esc_attr($count); ?> <?php esc_attr_e('licenses', 'wc-licensed-product'); ?>">
|
||||||
|
<span class="wclp-bar-value"><?php echo esc_html($count); ?></span>
|
||||||
|
</div>
|
||||||
|
<span class="wclp-bar-label"><?php echo esc_html(date_i18n('M Y', strtotime($month . '-01'))); ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wclp-stats-actions">
|
||||||
|
<h2><?php esc_html_e('Quick Actions', 'wc-licensed-product'); ?></h2>
|
||||||
|
<div class="wclp-action-buttons">
|
||||||
|
<a href="<?php echo esc_url(admin_url('admin.php?page=wc-licenses')); ?>" class="button button-primary">
|
||||||
|
<span class="dashicons dashicons-admin-network"></span>
|
||||||
|
<?php esc_html_e('Manage Licenses', 'wc-licensed-product'); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo esc_url(admin_url('admin.php?page=wc-licenses&action=export_csv')); ?>" class="button">
|
||||||
|
<span class="dashicons dashicons-download"></span>
|
||||||
|
<?php esc_html_e('Export to CSV', 'wc-licensed-product'); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo esc_url(admin_url('admin.php?page=wc-settings&tab=licensed_product')); ?>" class="button">
|
||||||
|
<span class="dashicons dashicons-admin-generic"></span>
|
||||||
|
<?php esc_html_e('Settings', 'wc-licensed-product'); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wclp-api-info">
|
||||||
|
<h3><?php esc_html_e('REST API Endpoints', 'wc-licensed-product'); ?></h3>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e('The following REST API endpoints are available for retrieving license statistics:', 'wc-licensed-product'); ?>
|
||||||
|
</p>
|
||||||
|
<table class="widefat striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?php esc_html_e('Endpoint', 'wc-licensed-product'); ?></th>
|
||||||
|
<th><?php esc_html_e('Description', 'wc-licensed-product'); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>GET /wc-licensed-product/v1/analytics/stats</code></td>
|
||||||
|
<td><?php esc_html_e('Get license statistics with time-series data', 'wc-licensed-product'); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>GET /wc-licensed-product/v1/analytics/products</code></td>
|
||||||
|
<td><?php esc_html_e('Get license counts by product', 'wc-licensed-product'); ?></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ declare(strict_types=1);
|
|||||||
namespace Jeremias\WcLicensedProduct;
|
namespace Jeremias\WcLicensedProduct;
|
||||||
|
|
||||||
use Jeremias\WcLicensedProduct\Admin\AdminController;
|
use Jeremias\WcLicensedProduct\Admin\AdminController;
|
||||||
|
use Jeremias\WcLicensedProduct\Admin\AnalyticsController;
|
||||||
use Jeremias\WcLicensedProduct\Admin\OrderLicenseController;
|
use Jeremias\WcLicensedProduct\Admin\OrderLicenseController;
|
||||||
use Jeremias\WcLicensedProduct\Admin\SettingsController;
|
use Jeremias\WcLicensedProduct\Admin\SettingsController;
|
||||||
use Jeremias\WcLicensedProduct\Admin\VersionAdminController;
|
use Jeremias\WcLicensedProduct\Admin\VersionAdminController;
|
||||||
@@ -68,6 +69,14 @@ final class Plugin
|
|||||||
return self::$instance;
|
return self::$instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get singleton instance (alias for instance())
|
||||||
|
*/
|
||||||
|
public static function getInstance(): Plugin
|
||||||
|
{
|
||||||
|
return self::instance();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Private constructor for singleton
|
* Private constructor for singleton
|
||||||
*/
|
*/
|
||||||
@@ -125,6 +134,7 @@ final class Plugin
|
|||||||
new VersionAdminController($this->versionManager);
|
new VersionAdminController($this->versionManager);
|
||||||
new OrderLicenseController($this->licenseManager);
|
new OrderLicenseController($this->licenseManager);
|
||||||
new SettingsController();
|
new SettingsController();
|
||||||
|
(new AnalyticsController($this->licenseManager))->init();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -95,6 +95,7 @@
|
|||||||
<th>{{ __('Customer') }}</th>
|
<th>{{ __('Customer') }}</th>
|
||||||
<th>{{ __('Domain') }}</th>
|
<th>{{ __('Domain') }}</th>
|
||||||
<th>{{ __('Status') }}</th>
|
<th>{{ __('Status') }}</th>
|
||||||
|
<th>{{ __('Created') }}</th>
|
||||||
<th>{{ __('Expires') }}</th>
|
<th>{{ __('Expires') }}</th>
|
||||||
<th>{{ __('Actions') }}</th>
|
<th>{{ __('Actions') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -102,7 +103,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% if licenses is empty %}
|
{% if licenses is empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="8">{{ __('No licenses found.') }}</td>
|
<td colspan="9">{{ __('No licenses found.') }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% for item in licenses %}
|
{% for item in licenses %}
|
||||||
@@ -160,6 +161,9 @@
|
|||||||
<button type="button" class="wclp-cancel-btn button button-small">{{ __('Cancel') }}</button>
|
<button type="button" class="wclp-cancel-btn button button-small">{{ __('Cancel') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="wclp-created-cell">
|
||||||
|
{{ item.license.createdAt|date('Y-m-d') }}
|
||||||
|
</td>
|
||||||
<td class="wclp-editable-cell" data-field="expiry" data-license-id="{{ item.license.id }}">
|
<td class="wclp-editable-cell" data-field="expiry" data-license-id="{{ item.license.id }}">
|
||||||
<span class="wclp-display-value">
|
<span class="wclp-display-value">
|
||||||
{% if item.license.expiresAt %}
|
{% if item.license.expiresAt %}
|
||||||
@@ -219,6 +223,7 @@
|
|||||||
<th>{{ __('Customer') }}</th>
|
<th>{{ __('Customer') }}</th>
|
||||||
<th>{{ __('Domain') }}</th>
|
<th>{{ __('Domain') }}</th>
|
||||||
<th>{{ __('Status') }}</th>
|
<th>{{ __('Status') }}</th>
|
||||||
|
<th>{{ __('Created') }}</th>
|
||||||
<th>{{ __('Expires') }}</th>
|
<th>{{ __('Expires') }}</th>
|
||||||
<th>{{ __('Actions') }}</th>
|
<th>{{ __('Actions') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
196
templates/admin/statistics.html.twig
Normal file
196
templates/admin/statistics.html.twig
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
<div class="wrap wclp-statistics">
|
||||||
|
<h1>{{ __('License Statistics') }}</h1>
|
||||||
|
|
||||||
|
<div class="wclp-stats-overview">
|
||||||
|
<div class="wclp-stat-cards">
|
||||||
|
<div class="wclp-stat-card wclp-stat-total">
|
||||||
|
<div class="wclp-stat-icon"><span class="dashicons dashicons-admin-network"></span></div>
|
||||||
|
<div class="wclp-stat-content">
|
||||||
|
<span class="wclp-stat-number">{{ stats.total }}</span>
|
||||||
|
<span class="wclp-stat-label">{{ __('Total Licenses') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="wclp-stat-card wclp-stat-active">
|
||||||
|
<div class="wclp-stat-icon"><span class="dashicons dashicons-yes-alt"></span></div>
|
||||||
|
<div class="wclp-stat-content">
|
||||||
|
<span class="wclp-stat-number">{{ stats.by_status.active }}</span>
|
||||||
|
<span class="wclp-stat-label">{{ __('Active') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="wclp-stat-card wclp-stat-inactive">
|
||||||
|
<div class="wclp-stat-icon"><span class="dashicons dashicons-marker"></span></div>
|
||||||
|
<div class="wclp-stat-content">
|
||||||
|
<span class="wclp-stat-number">{{ stats.by_status.inactive }}</span>
|
||||||
|
<span class="wclp-stat-label">{{ __('Inactive') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="wclp-stat-card wclp-stat-expired">
|
||||||
|
<div class="wclp-stat-icon"><span class="dashicons dashicons-calendar-alt"></span></div>
|
||||||
|
<div class="wclp-stat-content">
|
||||||
|
<span class="wclp-stat-number">{{ stats.by_status.expired }}</span>
|
||||||
|
<span class="wclp-stat-label">{{ __('Expired') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="wclp-stat-card wclp-stat-revoked">
|
||||||
|
<div class="wclp-stat-icon"><span class="dashicons dashicons-dismiss"></span></div>
|
||||||
|
<div class="wclp-stat-content">
|
||||||
|
<span class="wclp-stat-number">{{ stats.by_status.revoked }}</span>
|
||||||
|
<span class="wclp-stat-label">{{ __('Revoked') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if stats.expiring_soon > 0 %}
|
||||||
|
<div class="notice notice-warning">
|
||||||
|
<p>
|
||||||
|
<span class="dashicons dashicons-warning"></span>
|
||||||
|
<strong>{{ __('Attention:') }}</strong>
|
||||||
|
{{ stats.expiring_soon }} {{ stats.expiring_soon == 1 ? __('license is') : __('licenses are') }}
|
||||||
|
{{ __('expiring within the next 30 days.') }}
|
||||||
|
<a href="{{ admin_url }}?page=wc-licenses">{{ __('View Licenses') }}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="wclp-stats-details">
|
||||||
|
<div class="wclp-stat-box">
|
||||||
|
<h3>{{ __('License Types') }}</h3>
|
||||||
|
<table class="widefat striped">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>{{ __('Lifetime Licenses') }}</td>
|
||||||
|
<td class="wclp-stat-value">{{ stats.lifetime }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ __('Time-limited Licenses') }}</td>
|
||||||
|
<td class="wclp-stat-value">{{ stats.expiring }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ __('Expiring Soon (30 days)') }}</td>
|
||||||
|
<td class="wclp-stat-value {% if stats.expiring_soon > 0 %}wclp-warning{% endif %}">
|
||||||
|
{{ stats.expiring_soon }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wclp-stat-box">
|
||||||
|
<h3>{{ __('Top Products by Licenses') }}</h3>
|
||||||
|
{% if stats.by_product is empty %}
|
||||||
|
<p class="description">{{ __('No license data available yet.') }}</p>
|
||||||
|
{% else %}
|
||||||
|
<table class="widefat striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ __('Product') }}</th>
|
||||||
|
<th class="wclp-stat-value">{{ __('Licenses') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for product in stats.by_product %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ esc_html(product.product_name) }}</td>
|
||||||
|
<td class="wclp-stat-value">{{ product.count }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wclp-stat-box">
|
||||||
|
<h3>{{ __('Top Domains') }}</h3>
|
||||||
|
{% if stats.top_domains is empty %}
|
||||||
|
<p class="description">{{ __('No license data available yet.') }}</p>
|
||||||
|
{% else %}
|
||||||
|
<table class="widefat striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ __('Domain') }}</th>
|
||||||
|
<th class="wclp-stat-value">{{ __('Licenses') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for domain in stats.top_domains %}
|
||||||
|
<tr>
|
||||||
|
<td><code>{{ esc_html(domain.domain) }}</code></td>
|
||||||
|
<td class="wclp-stat-value">{{ domain.count }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wclp-chart-section">
|
||||||
|
<h3>{{ __('Licenses Created (Last 12 Months)') }}</h3>
|
||||||
|
{% if stats.monthly is empty %}
|
||||||
|
<p class="description">{{ __('No license data available yet.') }}</p>
|
||||||
|
{% else %}
|
||||||
|
<div class="wclp-chart-container">
|
||||||
|
<div class="wclp-bar-chart" id="wclp-monthly-chart">
|
||||||
|
{% 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 %}
|
||||||
|
<div class="wclp-bar-wrapper">
|
||||||
|
<div class="wclp-bar" style="height: {{ (count / max_value * 100)|round }}%;" title="{{ count }} {{ __('licenses') }}">
|
||||||
|
<span class="wclp-bar-value">{{ count }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="wclp-bar-label">{{ month|date('M Y') }}</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wclp-stats-actions">
|
||||||
|
<h2>{{ __('Quick Actions') }}</h2>
|
||||||
|
<div class="wclp-action-buttons">
|
||||||
|
<a href="{{ admin_url }}?page=wc-licenses" class="button button-primary">
|
||||||
|
<span class="dashicons dashicons-admin-network"></span>
|
||||||
|
{{ __('Manage Licenses') }}
|
||||||
|
</a>
|
||||||
|
<a href="{{ admin_url }}?page=wc-licenses&action=export_csv" class="button">
|
||||||
|
<span class="dashicons dashicons-download"></span>
|
||||||
|
{{ __('Export to CSV') }}
|
||||||
|
</a>
|
||||||
|
<a href="{{ admin_url }}?page=wc-settings&tab=licensed_product" class="button">
|
||||||
|
<span class="dashicons dashicons-admin-generic"></span>
|
||||||
|
{{ __('Settings') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wclp-api-info">
|
||||||
|
<h3>{{ __('REST API Endpoints') }}</h3>
|
||||||
|
<p class="description">
|
||||||
|
{{ __('The following REST API endpoints are available for retrieving license statistics:') }}
|
||||||
|
</p>
|
||||||
|
<table class="widefat striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ __('Endpoint') }}</th>
|
||||||
|
<th>{{ __('Description') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>GET {{ rest_url }}stats</code></td>
|
||||||
|
<td>{{ __('Get license statistics with time-series data') }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>GET {{ rest_url }}products</code></td>
|
||||||
|
<td>{{ __('Get license counts by product') }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
* Plugin Name: WooCommerce Licensed Product
|
* Plugin Name: WooCommerce Licensed Product
|
||||||
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-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.
|
* 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: Marco Graetsch
|
||||||
* Author URI: https://src.bundespruefstelle.ch/magdev
|
* Author URI: https://src.bundespruefstelle.ch/magdev
|
||||||
* License: GPL-2.0-or-later
|
* License: GPL-2.0-or-later
|
||||||
@@ -28,7 +28,7 @@ if (!defined('ABSPATH')) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Plugin constants
|
// 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_FILE', __FILE__);
|
||||||
define('WC_LICENSED_PRODUCT_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
define('WC_LICENSED_PRODUCT_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
||||||
define('WC_LICENSED_PRODUCT_PLUGIN_URL', plugin_dir_url(__FILE__));
|
define('WC_LICENSED_PRODUCT_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||||
|
|||||||
Reference in New Issue
Block a user