From 45531f86d6470515d098fae62bf00bf5a89873fb Mon Sep 17 00:00:00 2001 From: magdev Date: Wed, 21 Jan 2026 23:50:57 +0100 Subject: [PATCH] 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 --- CHANGELOG.md | 25 +- CLAUDE.md | 37 +- languages/wc-licensed-product-de_CH.mo | Bin 25501 -> 26362 bytes languages/wc-licensed-product-de_CH.po | 33 +- languages/wc-licensed-product.pot | 33 +- src/Admin/AdminController.php | 7 +- src/Admin/AnalyticsController.php | 523 +++++++++++++++++++++++++ src/Plugin.php | 10 + templates/admin/licenses.html.twig | 7 +- templates/admin/statistics.html.twig | 196 +++++++++ wc-licensed-product.php | 4 +- 11 files changed, 864 insertions(+), 11 deletions(-) create mode 100644 src/Admin/AnalyticsController.php create mode 100644 templates/admin/statistics.html.twig 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 3d6cb54ceac26833aa34b1c979b348ab2f79827c..e111388f54b614a704f0e40de1d09959dea9c2dd 100644 GIT binary patch delta 7172 zcmZYE3w+P@9>?+T?80UkYs~#;EHlPNVl2#cQtpum|IOe0ng8a$|2XLMuMR48LOYyF zNkylNB>$a4D$1n`xzur_+sbjP!$CTokk0G9-*=ro9_Rb$v*-8w{awEI-{zdB_C{RZ z9})VhQPfI9%8D?i1+I%V=5g|!5>;!=@}|br#ua!SK8~GmBWB{~n1=Dqj7i7t)~V>B z{1EE;S!{xho4a<#ZpMVnU@~f80oK9$Fa{sO2KXq}#b>b*Zp3)pY5f@UD1VL)wqq3S za1ci0Ow7O%>lz$Jc^}?M`zE=iG4-gJi*dLJHTY#nr_FPifNx_G9zb>A1ggUqQO{jL zbv&k(F{xM&)q!r-Y+OXSFKWQMurBSJePkNqQG4M#tWWtOs)2~s#`M5AycPSS8eERF zX;x!0ZbkkxhxwtYzKGcv=NK~q2ckN-1~sGWF{Fyu$moGBs23hYX2+aE^|Wpqw?m1j znMy%5n2Aw1)YcD2b!-yScTC{@v){aa$k7M$r8&Q8V2G)lMJO-nt`1Mh}*urluUD zaR+h|%^npQZN3!b z6opJ@GHSRt>U`gVTC*vrO*tF2M;4>jc0Fn&dvPkBKy9|c>FzoAB2}gwHPW|G@BI|@ z+$m%-O>{>Mi1qJCMlT+SU2r<;##N|}RakeUMtTJ6;Azxz7i>9^`PB6msF~<&?Srh6 z8ICt$8RlRZqiNrqCliPN##$JW;VwxOssqW$CDQ>l(s8H}m7zxX0P4P{F%vgo41R;! zq^EH(cI@mfSpe0+1sKvumXgs6%dsmyj~dx$sPDiPTc6DIr~|{04Q^&ym!bATg)MJK zb?6(^`!8ZWyn<>cmR_pdB9r;o4e3;<$K9;GP%q3y_MaJwdTmDy)1Vs(^1DI7xm(CsO$Nt7lcq9UWV%6W2la; zMV;#~ssjgb2p&N_-->C~681*^Gc))x3Rj27aF02QCD@v?r3aRyrur4s@!4lRiELnF zZg7uNN7Vg;QST{0{gPUVx8cjE`y;x$1L}nOKJ>);7#c-JBb$mESqUcKd{hUPB41ha zB5JekLyi16>bdVx$L$JwF!e@$lj8lTHNJ*AZt?sEQ@JbZ`F_Zz51BkNO{f@yn!;JA z<99!1;CgI?pV<17sGdjh3qUVSM0L0g4#cjQgY!@$ufT@*HAdnE%*7uuOXt5wHfNOz zpY<_h*34dPidRuDXxz&f24m7t_fJ7q!#slO=)0&TIFBPRrMElf0n}-_8#TjgQJeQg ztUCW2$!JO{kSQ_SQ8(^I?b3s&@4}b1z7jV_7;^^o+>?A&l5idJEi&7%3!b#)#(ms! ze^f^UI1ATeNEPvYjo}xa>4oa)WYjO4dr%$OgX-7;)G<4T$ykYr_%mLI_4>I>l7c!t znK%}6usyCoCdX_=z4uZ-=AYR$zfhqIJy;l>f&ebTzlz3~m|{GUg4{IV@a4{-l(Xn@)~c>_Z34dbYIi3$&@p?G#u6HLNpn1QO#L+$!e zsG0Df_DUg6!6#8mb`@J=EYsK$J75Y9L3Mm4vN~o-h>X@^7pj5G!R`yPQ6G}QsPla* zw!)d{;De~Oe+kv0&8U&Ti@NU+YUJlpGjzq48w_#(9B+ndFEoJ6WHPs)M)ES!c~glT zX_K1AU#vJ0wZ^MaQ@sh*z+Tj;IEeMjq4vroOvN%(hgTyV3z?V6=zPE9 zR+z)qlcnpK?&VTGJ z?o@U|txa!ii{r5kE<`=}6h`5G)aE&iQ?U|9V(zW_2L=CNK`)+1jda{_cjl&}mU1E1 z#>cQJ?VGh^)X+B6X8Qtl8h%3U_O2t`8OlSgBnqB{Nya$rnTdJ}^aQKu&# z)!|a>y;z^}G7M>Vt|6nf-Hr)(2=&GL*4AG_Em173*2Na6FJl_&`TnTgKGl}*!8<6g zvGqS=H_A;%yI;s#(V-j~&HQVP*HWQf`v#`t5nGNJfI&6*I z@CMZRo`Kr!520pc9crd_A)glW6~RA#y zNXMlWYGl2!DNaL;;BM5?EyE;y6Sd0^p+@|*tv`j@3%{UFMcaw))Tg7P^IuA)Cl~&N z-EluMizafCTb_syQeJ`jE;Q$mYovWq9h{8n*h0+0mDn8jqJzg#?~R)5*7riH%s9-Z zeN#b3Q*;iS;bqiPBu#PGJ`Hs}2UVYk+H4cCH{OkEa2smskD&HUC1&F#T#B8iGDzHt zde3kV^M3;w4;fA2D%8|%KyAJXY>Dq+8~hSm;jefTw#s*ZOHM*v529YY3biL*wAbH6 z_OJN_b1=QY{VTYzfcaO$Pf$?{H(?%zu@_!NZL%KIT>Yr}^{9?iqMrX1HN{DMchhkg z_QScDfp4G&dICpdv+3@P&Ym7}ADB;trfwZ-1~%IA>lj0M2Wm$4qBiGY)Mon}Bk>Gs zcb~-wRHxSwort%H&Z;D7qn{wO{#A+gO?`V&6&I^&-5<4V{n;9IKh(&7Q6tX;7>9U{ z*iN)2^kGt3=wc3FfOwIZW9ve{vzd-HI-wC&r}Jbq#Yztl+CWbbnZ(b;!$cdNy$zQT zuMkO;%W)f_Wnn*?HwmStTUMd`9j)gLxP$^V0>%QH)L{T0f*F+T8mf_=s(hIb2CJ}v!O@ua=(qCPw z{`bByHlNPJew#mpeTX#bwqP-_lTgw|RJx1k#C3h%yP%SXSVZhmffPF8Y6`I-@s%yN zL7&Z!z(;JpCcjWr zoFPsT`o6a%l=KBmA@r&Jk*LQrN;>a#$j`zE%vB}nl1tU^l=|cwaP31|SItXW|9dID zPCQMt;HK*IHkoFWGi}{|%2$aFL?WSdJCR3l?5jR7Q1vNpYoAHR1XYqoxm5l9i_H(w z`YX*QQn|TnRf)e=t@^&dOvKq2ZYE!w{A2d|UTb6O{$=wbwMIUf{CuL3eaFrAS|bd- z&yS~U#TC4tm`j|tbs6@dPi^^CYdvhvwZoWi>;8bXh!(aS&Gl;<{{#y8dtZH?@auY$RhR;N*Tmrq8%?yBvz@C)SkRjCDD@nPuP$6o=_T3 z3@3&WGl?gO9O7^N_>sZ`^RR%>d`uu>i0bqindga~gqOIU`={a{qA0vSIW{8FSL&4C zm7S81I?XBa6?pxn-crXC@K#lZKWcg{GCJV(1ij%sEnkm_^OhC_d?i6&vA=vl>!UH# zN{W5{V0eUcI&!EtSpB?HP+aB@mOA*qhG$Wu1m>n~3!NDZ&a$%(ErC{P;o78M1&)lnzQ{Ii1Lw+CiMk8ZCWXiU t{|=|r=bz?Ge{Xxh(G9ukk2AN}Z%6*WCb&4)8{o#md`}>J_4a4t{tMZ!k(B@d delta 6496 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 + @@ -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__));