feat: Complete Dashboard API Integration with Analytics Controller

 Features:
- Implemented API integration for all 7 dashboard pages
- Added Analytics REST API controller with 7 endpoints
- Full loading and error states with retry functionality
- Seamless dummy data toggle for development

📊 Dashboard Pages:
- Customers Analytics (complete)
- Revenue Analytics (complete)
- Orders Analytics (complete)
- Products Analytics (complete)
- Coupons Analytics (complete)
- Taxes Analytics (complete)
- Dashboard Overview (complete)

🔌 Backend:
- Created AnalyticsController.php with REST endpoints
- All endpoints return 501 (Not Implemented) for now
- Ready for HPOS-based implementation
- Proper permission checks

🎨 Frontend:
- useAnalytics hook for data fetching
- React Query caching
- ErrorCard with retry functionality
- TypeScript type safety
- Zero build errors

📝 Documentation:
- DASHBOARD_API_IMPLEMENTATION.md guide
- Backend implementation roadmap
- Testing strategy

🔧 Build:
- All pages compile successfully
- Production-ready with dummy data fallback
- Zero TypeScript errors
This commit is contained in:
dwindown
2025-11-04 11:19:00 +07:00
commit 232059e928
148 changed files with 28984 additions and 0 deletions

View File

@@ -0,0 +1,220 @@
<?php
namespace WooNooW\Compat;
if ( ! defined('ABSPATH') ) exit;
/**
* Addon Registry
*
* Central registry for WooNooW addons. Collects metadata, validates dependencies,
* and exposes addon information to the SPA.
*
* @since 1.0.0
*/
class AddonRegistry {
const REGISTRY_OPTION = 'wnw_addon_registry';
const REGISTRY_VERSION = '1.0.0';
/**
* Initialize hooks
*/
public static function init() {
add_action('plugins_loaded', [__CLASS__, 'collect_addons'], 20);
add_action('activated_plugin', [__CLASS__, 'flush']);
add_action('deactivated_plugin', [__CLASS__, 'flush']);
}
/**
* Collect and validate addons
*/
public static function collect_addons() {
// Start with empty registry
$addons = [];
/**
* Filter: woonoow/addon_registry
*
* Allows addons to register themselves with WooNooW.
*
* @param array $addons Array of addon configurations
*
* Example:
* add_filter('woonoow/addon_registry', function($addons) {
* $addons['my-addon'] = [
* 'id' => 'my-addon',
* 'name' => 'My Addon',
* 'version' => '1.0.0',
* 'author' => 'Author Name',
* 'description' => 'Addon description',
* 'spa_bundle' => plugin_dir_url(__FILE__) . 'dist/addon.js',
* 'dependencies' => ['woocommerce' => '8.0'],
* 'routes' => [],
* 'nav_items' => [],
* 'widgets' => [],
* ];
* return $addons;
* });
*/
$addons = apply_filters('woonoow/addon_registry', $addons);
// Validate and normalize each addon
$validated = [];
foreach ($addons as $id => $addon) {
$validated[$id] = self::validate_addon($id, $addon);
}
// Store in option
update_option(self::REGISTRY_OPTION, [
'version' => self::REGISTRY_VERSION,
'addons' => $validated,
'updated' => time(),
], false);
}
/**
* Validate and normalize addon configuration
*
* @param string $id Addon ID
* @param array $addon Addon configuration
* @return array Validated addon configuration
*/
private static function validate_addon(string $id, array $addon): array {
$defaults = [
'id' => $id,
'name' => $id,
'version' => '1.0.0',
'author' => '',
'description' => '',
'spa_bundle' => '',
'dependencies' => [],
'routes' => [],
'nav_items' => [],
'widgets' => [],
'enabled' => true,
];
$validated = wp_parse_args($addon, $defaults);
// Ensure ID matches
$validated['id'] = $id;
// Validate dependencies
$validated['dependencies_met'] = self::check_dependencies($validated['dependencies']);
// If dependencies not met, disable addon
if (!$validated['dependencies_met']) {
$validated['enabled'] = false;
}
return $validated;
}
/**
* Check if addon dependencies are met
*
* @param array $dependencies Array of plugin => version requirements
* @return bool True if all dependencies met
*/
private static function check_dependencies(array $dependencies): bool {
foreach ($dependencies as $plugin => $required_version) {
// Check WooCommerce
if ($plugin === 'woocommerce') {
if (!defined('WC_VERSION')) {
return false;
}
if (version_compare(WC_VERSION, $required_version, '<')) {
return false;
}
}
// Check WordPress
if ($plugin === 'wordpress') {
global $wp_version;
if (version_compare($wp_version, $required_version, '<')) {
return false;
}
}
// Check other plugins (basic check)
if (!in_array($plugin, ['woocommerce', 'wordpress'])) {
if (!is_plugin_active($plugin)) {
return false;
}
}
}
return true;
}
/**
* Get all registered addons
*
* @param bool $enabled_only Return only enabled addons
* @return array Array of addon configurations
*/
public static function get_addons(bool $enabled_only = false): array {
$data = get_option(self::REGISTRY_OPTION, []);
$addons = $data['addons'] ?? [];
if ($enabled_only) {
$addons = array_filter($addons, function($addon) {
return !empty($addon['enabled']);
});
}
return $addons;
}
/**
* Get a specific addon
*
* @param string $id Addon ID
* @return array|null Addon configuration or null if not found
*/
public static function get_addon(string $id): ?array {
$addons = self::get_addons();
return $addons[$id] ?? null;
}
/**
* Check if an addon is registered and enabled
*
* @param string $id Addon ID
* @return bool True if addon is registered and enabled
*/
public static function is_addon_enabled(string $id): bool {
$addon = self::get_addon($id);
return $addon && !empty($addon['enabled']);
}
/**
* Flush the registry cache
*/
public static function flush() {
delete_option(self::REGISTRY_OPTION);
}
/**
* Get registry for frontend (sanitized)
*
* @return array Array suitable for JSON encoding
*/
public static function get_frontend_registry(): array {
$addons = self::get_addons(true); // Only enabled
$frontend = [];
foreach ($addons as $id => $addon) {
$frontend[$id] = [
'id' => $addon['id'],
'name' => $addon['name'],
'version' => $addon['version'],
'author' => $addon['author'],
'description' => $addon['description'],
'spa_bundle' => $addon['spa_bundle'],
// Don't expose internal data
];
}
return $frontend;
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace WooNooW\Compat;
class HideWooMenus {
public static function init() {
add_action('admin_menu', [__CLASS__, 'hide'], 999);
}
public static function hide() {
// remove_menu_page('woocommerce');
foreach (['wc-admin','wc-reports','wc-settings','wc-status','wc-addons'] as $slug) {
// remove_submenu_page('woocommerce', $slug);
// Extra Woo top-level entries
// remove_menu_page('edit.php?post_type=product'); // Products
// remove_menu_page('wc-admin&path=/marketing'); // Marketing
// remove_menu_page('wc-admin&path=/payments/overview'); // Payments
// remove_menu_page('wc-admin&path=/analytics/overview'); // Analytics (if present)
}
}
}

View File

@@ -0,0 +1,6 @@
<?php
namespace WooNooW\Compat;
class HooksShim {
// placeholder untuk hook-mirror/helper di kemudian hari
}

View File

@@ -0,0 +1,105 @@
<?php
namespace WooNooW\Compat;
if ( ! defined('ABSPATH') ) exit;
class MenuProvider {
const SNAPSHOT_OPTION = 'wnw_wc_menu_snapshot';
const SNAPSHOT_TTL = 60; // seconds (dev: keep small)
public static function init() {
add_action('admin_menu', [__CLASS__, 'flag_menus_ready'], 9999);
add_action('admin_head', [__CLASS__, 'snapshot_menus']);
add_action('switch_theme', [__CLASS__, 'flush']);
add_action('activated_plugin', [__CLASS__, 'flush']);
add_action('deactivated_plugin', [__CLASS__, 'flush']);
}
public static function flag_menus_ready() {
// no-op; ensures our priority ordering
}
public static function snapshot_menus() {
if ( defined('WNW_DEV') && WNW_DEV ) {
delete_transient(self::SNAPSHOT_OPTION);
}
$cached = get_transient(self::SNAPSHOT_OPTION);
if ( $cached && ! empty($cached['items']) ) return;
global $menu, $submenu;
$items = [];
$push = static function($parent, $entry) use (&$items) {
$title = isset($entry[0]) ? wp_strip_all_tags($entry[0]) : '';
$cap = $entry[1] ?? '';
$slug = $entry[2] ?? '';
$href = (strpos($slug, 'http') === 0) ? $slug : admin_url($slug);
$items[] = [
'title' => $title,
'capability' => $cap,
'slug' => (string) $slug,
'href' => $href,
'parent_slug' => $parent,
];
};
foreach ((array)$menu as $m) {
if (empty($m[2])) continue;
$pslug = $m[2];
$push(null, $m);
if ( isset($submenu[$pslug]) ) {
foreach ($submenu[$pslug] as $sub) $push($pslug, $sub);
}
}
// classify
foreach ($items as &$it) {
$it['area'] = self::classify($it);
$it['mode'] = 'bridge'; // default; SPA may override per known routes
$it['bridge_url'] = $it['href'];
}
// filter out separators and empty titles
$items = array_values(array_filter($items, function($it) {
$title = trim($it['title'] ?? '');
$slug = $it['slug'] ?? '';
$href = $it['href'] ?? '';
if ($title === '') return false;
if (stripos($slug, 'separator') === 0) return false;
if (preg_match('/separator-woocommerce/i', $slug)) return false;
if (preg_match('/separator/i', $href)) return false;
return true;
}));
set_transient(self::SNAPSHOT_OPTION, ['items' => $items, 'ts' => time()], self::SNAPSHOT_TTL);
update_option(self::SNAPSHOT_OPTION, ['items' => $items, 'ts' => time()], false); // backup for inspection
}
private static function classify(array $it): string {
$slug = (string) ($it['slug'] ?? '');
$href = (string) ($it['href'] ?? '');
$hay = $slug . ' ' . $href;
// Main areas
if (preg_match('~shop_order|wc-orders|orders~i', $hay)) return 'orders';
if (preg_match('~post_type=product|products~i', $hay)) return 'products';
if (preg_match('~wc-admin|analytics|marketing~i', $hay)) return 'dashboard';
// Settings family
if (preg_match('~wc-settings|woocommerce|status|addons~i', $hay)) return 'settings';
// Unknowns fall under Settings → Add-ons by default
return 'addons';
}
public static function get_snapshot(): array {
$data = get_transient(self::SNAPSHOT_OPTION);
if ( ! $data ) $data = get_option(self::SNAPSHOT_OPTION, []);
return $data['items'] ?? [];
}
public static function flush() {
delete_transient(self::SNAPSHOT_OPTION);
delete_option(self::SNAPSHOT_OPTION);
}
}

View File

@@ -0,0 +1,205 @@
<?php
namespace WooNooW\Compat;
if ( ! defined('ABSPATH') ) exit;
/**
* Navigation Registry
*
* Manages dynamic navigation tree building. Allows addons to inject
* menu items into the main navigation or existing sections.
*
* @since 1.0.0
*/
class NavigationRegistry {
const NAV_OPTION = 'wnw_nav_tree';
const NAV_VERSION = '1.0.0';
/**
* Initialize hooks
*/
public static function init() {
// Use 'init' hook instead of 'plugins_loaded' to avoid translation loading warnings (WP 6.7+)
add_action('init', [__CLASS__, 'build_nav_tree'], 10);
add_action('activated_plugin', [__CLASS__, 'flush']);
add_action('deactivated_plugin', [__CLASS__, 'flush']);
}
/**
* Build the complete navigation tree
*/
public static function build_nav_tree() {
// Base navigation tree (core WooNooW sections)
$tree = self::get_base_tree();
/**
* Filter: woonoow/nav_tree
*
* Allows addons to modify the entire navigation tree.
*
* @param array $tree Array of main navigation nodes
*
* Example:
* add_filter('woonoow/nav_tree', function($tree) {
* $tree[] = [
* 'key' => 'subscriptions',
* 'label' => 'Subscriptions',
* 'path' => '/subscriptions',
* 'icon' => 'repeat', // lucide icon name
* 'children' => [
* ['label' => 'All Subscriptions', 'mode' => 'spa', 'path' => '/subscriptions'],
* ['label' => 'New', 'mode' => 'spa', 'path' => '/subscriptions/new'],
* ],
* ];
* return $tree;
* });
*/
$tree = apply_filters('woonoow/nav_tree', $tree);
// Allow per-section modification
foreach ($tree as &$section) {
$key = $section['key'] ?? '';
if ($key) {
/**
* Filter: woonoow/nav_tree/{key}/children
*
* Allows addons to inject items into specific sections.
*
* Example:
* add_filter('woonoow/nav_tree/products/children', function($children) {
* $children[] = [
* 'label' => 'Bundles',
* 'mode' => 'spa',
* 'path' => '/products/bundles',
* ];
* return $children;
* });
*/
$section['children'] = apply_filters(
"woonoow/nav_tree/{$key}/children",
$section['children'] ?? []
);
}
}
// Store in option
update_option(self::NAV_OPTION, [
'version' => self::NAV_VERSION,
'tree' => $tree,
'updated' => time(),
], false);
}
/**
* Get base navigation tree (core sections)
*
* @return array Base navigation tree
*/
private static function get_base_tree(): array {
return [
[
'key' => 'dashboard',
'label' => __('Dashboard', 'woonoow'),
'path' => '/',
'icon' => 'layout-dashboard',
'children' => [
['label' => __('Overview', 'woonoow'), 'mode' => 'spa', 'path' => '/', 'exact' => true],
['label' => __('Revenue', 'woonoow'), 'mode' => 'spa', 'path' => '/dashboard/revenue'],
['label' => __('Orders', 'woonoow'), 'mode' => 'spa', 'path' => '/dashboard/orders'],
['label' => __('Products', 'woonoow'), 'mode' => 'spa', 'path' => '/dashboard/products'],
['label' => __('Customers', 'woonoow'), 'mode' => 'spa', 'path' => '/dashboard/customers'],
['label' => __('Coupons', 'woonoow'), 'mode' => 'spa', 'path' => '/dashboard/coupons'],
['label' => __('Taxes', 'woonoow'), 'mode' => 'spa', 'path' => '/dashboard/taxes'],
],
],
[
'key' => 'orders',
'label' => __('Orders', 'woonoow'),
'path' => '/orders',
'icon' => 'receipt-text',
'children' => [], // Orders has no submenu by design
],
[
'key' => 'products',
'label' => __('Products', 'woonoow'),
'path' => '/products',
'icon' => 'package',
'children' => [
['label' => __('All products', 'woonoow'), 'mode' => 'spa', 'path' => '/products'],
['label' => __('New', 'woonoow'), 'mode' => 'spa', 'path' => '/products/new'],
['label' => __('Categories', 'woonoow'), 'mode' => 'spa', 'path' => '/products/categories'],
['label' => __('Tags', 'woonoow'), 'mode' => 'spa', 'path' => '/products/tags'],
['label' => __('Attributes', 'woonoow'), 'mode' => 'spa', 'path' => '/products/attributes'],
],
],
[
'key' => 'coupons',
'label' => __('Coupons', 'woonoow'),
'path' => '/coupons',
'icon' => 'tag',
'children' => [
['label' => __('All coupons', 'woonoow'), 'mode' => 'spa', 'path' => '/coupons'],
['label' => __('New', 'woonoow'), 'mode' => 'spa', 'path' => '/coupons/new'],
],
],
[
'key' => 'customers',
'label' => __('Customers', 'woonoow'),
'path' => '/customers',
'icon' => 'users',
'children' => [
['label' => __('All customers', 'woonoow'), 'mode' => 'spa', 'path' => '/customers'],
],
],
[
'key' => 'settings',
'label' => __('Settings', 'woonoow'),
'path' => '/settings',
'icon' => 'settings',
'children' => [], // Settings children will be added by SettingsProvider
],
];
}
/**
* Get the complete navigation tree
*
* @return array Navigation tree
*/
public static function get_nav_tree(): array {
$data = get_option(self::NAV_OPTION, []);
return $data['tree'] ?? self::get_base_tree();
}
/**
* Get a specific section by key
*
* @param string $key Section key
* @return array|null Section data or null if not found
*/
public static function get_section(string $key): ?array {
$tree = self::get_nav_tree();
foreach ($tree as $section) {
if (($section['key'] ?? '') === $key) {
return $section;
}
}
return null;
}
/**
* Flush the navigation cache
*/
public static function flush() {
delete_option(self::NAV_OPTION);
}
/**
* Get navigation tree for frontend
*
* @return array Array suitable for JSON encoding
*/
public static function get_frontend_nav_tree(): array {
return self::get_nav_tree();
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace WooNooW\Compat;
if ( ! defined('ABSPATH') ) exit;
/**
* Payment Channels Handler
*
* Automatically detects and exposes payment gateway channels (e.g., bank accounts)
* to the WooNooW admin SPA.
*
* @since 1.0.0
*/
class PaymentChannels {
/**
* Initialize hooks
*/
public static function init() {
add_filter('woonoow/payment_gateway_channels', [__CLASS__, 'detect_bacs_accounts'], 10, 3);
add_filter('woonoow/payment_gateway_channels', [__CLASS__, 'detect_custom_channels'], 20, 3);
}
/**
* Detect BACS (Bank Transfer) accounts
*
* WooCommerce BACS gateway supports multiple bank accounts.
* This automatically exposes them as channels.
*/
public static function detect_bacs_accounts( array $channels, string $gateway_id, $gateway ): array {
if ( $gateway_id !== 'bacs' ) {
return $channels;
}
// Get account details from gateway settings
// Try multiple methods to get account details
$accounts = [];
// Method 1: get_option (standard WooCommerce method)
if ( method_exists( $gateway, 'get_option' ) ) {
$accounts = $gateway->get_option( 'account_details', [] );
}
// Method 2: Direct property access (fallback)
if ( empty( $accounts ) && isset( $gateway->account_details ) ) {
$accounts = $gateway->account_details;
}
// Method 3: Get from WordPress options directly (last resort)
if ( empty( $accounts ) ) {
$gateway_settings = get_option( 'woocommerce_bacs_settings', [] );
if ( isset( $gateway_settings['account_details'] ) ) {
$accounts = $gateway_settings['account_details'];
}
}
if ( empty( $accounts ) || ! is_array( $accounts ) ) {
return $channels;
}
// Convert accounts to channels
foreach ( $accounts as $index => $account ) {
if ( ! is_array( $account ) || empty( $account['account_name'] ) ) {
continue;
}
$bank_name = $account['bank_name'] ?? '';
$account_name = $account['account_name'] ?? '';
$account_num = $account['account_number'] ?? '';
// Build channel title
$title = $bank_name ? "{$bank_name} - {$account_name}" : $account_name;
if ( $account_num ) {
$title .= " ({$account_num})";
}
$channels[] = [
'id' => 'bacs_' . sanitize_title( $account_name . '_' . $index ),
'title' => $title,
'meta' => $account,
];
}
return $channels;
}
/**
* Detect custom channels from other gateways
*
* This is a placeholder for future gateway integrations.
* Other plugins can hook into woonoow/payment_gateway_channels
* to provide their own channel logic.
*/
public static function detect_custom_channels( array $channels, string $gateway_id, $gateway ): array {
/**
* Hook for third-party payment gateway channel detection
*
* Example for Stripe with multiple accounts:
*
* add_filter('woonoow/payment_gateway_channels', function($channels, $gateway_id, $gateway) {
* if ($gateway_id === 'stripe') {
* $accounts = get_option('stripe_connected_accounts', []);
* foreach ($accounts as $account) {
* $channels[] = [
* 'id' => 'stripe_' . $account['id'],
* 'title' => $account['name'],
* 'meta' => $account,
* ];
* }
* }
* return $channels;
* }, 30, 3);
*/
return $channels;
}
}

View File

@@ -0,0 +1,176 @@
<?php
namespace WooNooW\Compat;
if ( ! defined('ABSPATH') ) exit;
/**
* Route Registry
*
* Manages SPA route registration for addons. Allows addons to register
* custom routes that will be dynamically loaded in the React SPA.
*
* @since 1.0.0
*/
class RouteRegistry {
const ROUTES_OPTION = 'wnw_spa_routes';
const ROUTES_VERSION = '1.0.0';
/**
* Initialize hooks
*/
public static function init() {
add_action('plugins_loaded', [__CLASS__, 'collect_routes'], 25);
add_action('activated_plugin', [__CLASS__, 'flush']);
add_action('deactivated_plugin', [__CLASS__, 'flush']);
}
/**
* Collect and validate routes from addons
*/
public static function collect_routes() {
// Start with empty routes
$routes = [];
/**
* Filter: woonoow/spa_routes
*
* Allows addons to register SPA routes.
*
* @param array $routes Array of route configurations
*
* Example:
* add_filter('woonoow/spa_routes', function($routes) {
* $routes[] = [
* 'path' => '/subscriptions',
* 'component_url' => plugin_dir_url(__FILE__) . 'dist/SubscriptionsList.js',
* 'capability' => 'manage_woocommerce',
* 'title' => 'Subscriptions',
* 'exact' => false,
* ];
* return $routes;
* });
*/
$routes = apply_filters('woonoow/spa_routes', $routes);
// Validate and normalize each route
$validated = [];
foreach ($routes as $route) {
$validated_route = self::validate_route($route);
if ($validated_route) {
$validated[] = $validated_route;
}
}
// Store in option
update_option(self::ROUTES_OPTION, [
'version' => self::ROUTES_VERSION,
'routes' => $validated,
'updated' => time(),
], false);
}
/**
* Validate and normalize route configuration
*
* @param array $route Route configuration
* @return array|null Validated route or null if invalid
*/
private static function validate_route(array $route): ?array {
// Path is required
if (empty($route['path'])) {
return null;
}
// Component URL is required
if (empty($route['component_url'])) {
return null;
}
// Normalize path (must start with /)
$path = $route['path'];
if (substr($path, 0, 1) !== '/') {
$path = '/' . $path;
}
$defaults = [
'path' => $path,
'component_url' => '',
'capability' => 'manage_woocommerce',
'title' => '',
'exact' => false,
'props' => [],
];
$validated = wp_parse_args($route, $defaults);
// Sanitize
$validated['path'] = sanitize_text_field($validated['path']);
$validated['component_url'] = esc_url_raw($validated['component_url']);
$validated['capability'] = sanitize_text_field($validated['capability']);
$validated['title'] = sanitize_text_field($validated['title']);
$validated['exact'] = (bool) $validated['exact'];
return $validated;
}
/**
* Get all registered routes
*
* @param bool $check_capability Filter by current user capability
* @return array Array of route configurations
*/
public static function get_routes(bool $check_capability = false): array {
$data = get_option(self::ROUTES_OPTION, []);
$routes = $data['routes'] ?? [];
if ($check_capability) {
$routes = array_filter($routes, function($route) {
return current_user_can($route['capability'] ?? 'manage_woocommerce');
});
}
return array_values($routes);
}
/**
* Get a route by path
*
* @param string $path Route path
* @return array|null Route configuration or null if not found
*/
public static function get_route(string $path): ?array {
$routes = self::get_routes();
foreach ($routes as $route) {
if ($route['path'] === $path) {
return $route;
}
}
return null;
}
/**
* Check if a route exists
*
* @param string $path Route path
* @return bool True if route exists
*/
public static function has_route(string $path): bool {
return self::get_route($path) !== null;
}
/**
* Flush the routes cache
*/
public static function flush() {
delete_option(self::ROUTES_OPTION);
}
/**
* Get routes for frontend (filtered by capability)
*
* @return array Array suitable for JSON encoding
*/
public static function get_frontend_routes(): array {
return self::get_routes(true);
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace WooNooW\Compat;
if ( ! defined('ABSPATH') ) exit;
class SettingsProvider {
public static function init() {
// later: cache if needed
}
public static function get_tabs(): array {
$pages = apply_filters('woocommerce_get_settings_pages', []);
$out = [];
foreach ($pages as $page) {
if (!is_object($page) || !isset($page->id)) continue;
$label = method_exists($page, 'get_label') ? $page->get_label() : ($page->label ?? ucfirst($page->id));
$sections = method_exists($page, 'get_sections') ? $page->get_sections() : ['' => $label];
$out[] = ['id' => $page->id, 'label' => $label, 'sections' => $sections];
}
return $out;
}
public static function get_tab_schema(string $tab, string $section = ''): array {
$pages = apply_filters('woocommerce_get_settings_pages', []);
foreach ($pages as $page) {
if (!is_object($page) || !isset($page->id)) continue;
if ($page->id !== $tab) continue;
$settings = $page->get_settings($section);
return [
'tab' => $tab,
'section' => $section,
'fields' => self::normalize_fields($settings),
];
}
return ['tab' => $tab, 'section' => $section, 'fields' => []];
}
private static function normalize_fields(array $settings): array {
$out = [];
foreach ($settings as $field) {
$type = $field['type'] ?? 'text';
$id = $field['id'] ?? '';
$title= $field['title']?? '';
if (in_array($type, ['title','sectionend'], true)) {
$out[] = ['type' => $type, 'id' => $id, 'label' => $title];
continue;
}
$value = \WC_Admin_Settings::get_option($id, $field['default'] ?? '');
$out[] = [
'type' => $type,
'id' => $id,
'label' => $title,
'desc' => $field['desc'] ?? ($field['description'] ?? ''),
'options' => $field['options'] ?? null,
'default' => $field['default'] ?? null,
'value' => $value,
'attrs' => $field['custom_attributes'] ?? null,
];
}
return $out;
}
public static function save_tab(string $tab, string $section = '', array $payload = []): array {
$pages = apply_filters('woocommerce_get_settings_pages', []);
foreach ($pages as $page) {
if (!is_object($page) || !isset($page->id)) continue;
if ($page->id !== $tab) continue;
$settings = $page->get_settings($section);
// Rebuild $_POST for Woo's saver
foreach ($settings as $field) {
if (empty($field['id']) || empty($field['type'])) continue;
$id = $field['id'];
if ($field['type'] === 'checkbox') {
$_POST[$id] = !empty($payload[$id]) ? 'yes' : 'no';
} else {
$_POST[$id] = isset($payload[$id]) ? $payload[$id] : null;
}
}
\WC_Admin_Settings::save_fields($settings);
return ['ok' => true];
}
return ['ok' => false, 'error' => 'Tab not found'];
}
}