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:
2026-01-21 23:50:57 +01:00
parent ff9b27e811
commit 45531f86d6
11 changed files with 864 additions and 11 deletions

View File

@@ -1253,6 +1253,7 @@ final class AdminController
<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('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('Actions', 'wc-licensed-product'); ?></th>
</tr>
@@ -1260,7 +1261,7 @@ final class AdminController
<tbody>
<?php if (empty($enrichedLicenses)): ?>
<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>
<?php else: ?>
<?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>
</div>
</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()); ?>">
<?php $expiresAt = $item['license']->getExpiresAt(); ?>
<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('Domain', '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('Actions', 'wc-licensed-product'); ?></th>
</tr>

View 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
}
}

View File

@@ -10,6 +10,7 @@ declare(strict_types=1);
namespace Jeremias\WcLicensedProduct;
use Jeremias\WcLicensedProduct\Admin\AdminController;
use Jeremias\WcLicensedProduct\Admin\AnalyticsController;
use Jeremias\WcLicensedProduct\Admin\OrderLicenseController;
use Jeremias\WcLicensedProduct\Admin\SettingsController;
use Jeremias\WcLicensedProduct\Admin\VersionAdminController;
@@ -68,6 +69,14 @@ final class Plugin
return self::$instance;
}
/**
* Get singleton instance (alias for instance())
*/
public static function getInstance(): Plugin
{
return self::instance();
}
/**
* Private constructor for singleton
*/
@@ -125,6 +134,7 @@ final class Plugin
new VersionAdminController($this->versionManager);
new OrderLicenseController($this->licenseManager);
new SettingsController();
(new AnalyticsController($this->licenseManager))->init();
}
}