You've already forked wc-licensed-product
- 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>
524 lines
22 KiB
PHP
524 lines
22 KiB
PHP
<?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
|
|
}
|
|
}
|