diff --git a/admin-spa/src/App.tsx b/admin-spa/src/App.tsx index a9c7c18..9421b90 100644 --- a/admin-spa/src/App.tsx +++ b/admin-spa/src/App.tsx @@ -199,6 +199,7 @@ import SettingsShipping from '@/routes/Settings/Shipping'; import SettingsTax from '@/routes/Settings/Tax'; import SettingsLocalPickup from '@/routes/Settings/LocalPickup'; import SettingsNotifications from '@/routes/Settings/Notifications'; +import SettingsDeveloper from '@/routes/Settings/Developer'; import MorePage from '@/routes/More'; // Addon Route Component - Dynamically loads addon components @@ -436,6 +437,7 @@ function AppRoutes() { } /> } /> } /> + } /> {/* Dynamic Addon Routes */} {addonRoutes.map((route: any) => ( diff --git a/admin-spa/src/routes/Settings/Developer.tsx b/admin-spa/src/routes/Settings/Developer.tsx new file mode 100644 index 0000000..73da8c7 --- /dev/null +++ b/admin-spa/src/routes/Settings/Developer.tsx @@ -0,0 +1,283 @@ +import React, { useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { api } from '@/lib/api'; +import { SettingsLayout } from './components/SettingsLayout'; +import { SettingsCard } from './components/SettingsCard'; +import { SettingsSection } from './components/SettingsSection'; +import { Switch } from '@/components/ui/switch'; +import { Button } from '@/components/ui/button'; +import { toast } from 'sonner'; +import { RefreshCw, Trash2 } from 'lucide-react'; + +interface DeveloperSettings { + debugMode: boolean; + showApiLogs: boolean; + enableReactDevTools: boolean; +} + +interface SystemInfo { + woonoowVersion: string; + woocommerceVersion: string; + wordpressVersion: string; + phpVersion: string; + hposEnabled: boolean; +} + +export default function DeveloperPage() { + const queryClient = useQueryClient(); + const [settings, setSettings] = useState({ + debugMode: false, + showApiLogs: false, + enableReactDevTools: false, + }); + const [clearingCache, setClearingCache] = useState(null); + + // Fetch developer settings + const { data: devData, isLoading } = useQuery({ + queryKey: ['developer-settings'], + queryFn: () => api.get('/settings/developer'), + }); + + // Update settings when data changes + React.useEffect(() => { + if (devData) { + setSettings({ + debugMode: devData.debug_mode || false, + showApiLogs: devData.show_api_logs || false, + enableReactDevTools: devData.enable_react_devtools || false, + }); + } + }, [devData]); + + // Fetch system info + const { data: systemInfo } = useQuery({ + queryKey: ['system-info'], + queryFn: () => api.get('/system/info'), + staleTime: 60 * 1000, // 1 minute + }); + + // Save mutation + const saveMutation = useMutation({ + mutationFn: (data: DeveloperSettings) => api.post('/settings/developer', { + debug_mode: data.debugMode, + show_api_logs: data.showApiLogs, + enable_react_devtools: data.enableReactDevTools, + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['developer-settings'] }); + toast.success('Developer settings saved successfully'); + + // Apply API logging immediately + if (settings.showApiLogs) { + localStorage.setItem('woonoow_api_logs', 'true'); + } else { + localStorage.removeItem('woonoow_api_logs'); + } + + // Apply React DevTools + if (settings.enableReactDevTools) { + localStorage.setItem('woonoow_react_devtools', 'true'); + } else { + localStorage.removeItem('woonoow_react_devtools'); + } + }, + onError: () => { + toast.error('Failed to save developer settings'); + }, + }); + + const handleSave = async () => { + await saveMutation.mutateAsync(settings); + }; + + const updateSetting = ( + key: K, + value: DeveloperSettings[K] + ) => { + setSettings((prev) => ({ ...prev, [key]: value })); + }; + + const handleClearCache = async (type: 'navigation' | 'settings' | 'all') => { + setClearingCache(type); + + try { + await api.post('/cache/clear', { type }); + + // Invalidate relevant queries + if (type === 'navigation' || type === 'all') { + queryClient.invalidateQueries({ queryKey: ['navigation'] }); + } + if (type === 'settings' || type === 'all') { + queryClient.invalidateQueries({ queryKey: ['store-settings'] }); + queryClient.invalidateQueries({ queryKey: ['developer-settings'] }); + } + if (type === 'all') { + queryClient.clear(); + } + + toast.success(`${type === 'all' ? 'All caches' : type + ' cache'} cleared successfully`); + } catch (error) { + toast.error('Failed to clear cache'); + } finally { + setClearingCache(null); + } + }; + + return ( + + {/* Debug Mode */} + + + updateSetting('debugMode', checked)} + /> + + + {settings.debugMode && ( + <> + + updateSetting('showApiLogs', checked)} + /> + + + + updateSetting('enableReactDevTools', checked)} + /> + + + )} + + + {/* System Information */} + +
+
+ WooNooW Version: + {systemInfo?.woonoowVersion || 'Loading...'} +
+
+ WooCommerce Version: + {systemInfo?.woocommerceVersion || 'Loading...'} +
+
+ WordPress Version: + {systemInfo?.wordpressVersion || 'Loading...'} +
+
+ PHP Version: + {systemInfo?.phpVersion || 'Loading...'} +
+
+ HPOS Enabled: + + {systemInfo?.hposEnabled ? ( + ✓ Yes + ) : ( + ✗ No + )} + +
+
+
+ + {/* Cache Management */} + +
+
+
+

Navigation Cache

+

Clear menu and navigation data

+
+ +
+ +
+
+

Settings Cache

+

Clear store and developer settings

+
+ +
+ +
+
+

Clear All Caches

+

Clear all cached data (use with caution)

+
+ +
+
+
+
+ ); +} diff --git a/includes/Api/DeveloperController.php b/includes/Api/DeveloperController.php new file mode 100644 index 0000000..d2dff8d --- /dev/null +++ b/includes/Api/DeveloperController.php @@ -0,0 +1,109 @@ +namespace, '/' . $this->rest_base, [ + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [$this, 'get_settings'], + 'permission_callback' => [$this, 'check_permission'], + ], + ]); + + // POST /woonoow/v1/settings/developer + register_rest_route($this->namespace, '/' . $this->rest_base, [ + [ + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => [$this, 'save_settings'], + 'permission_callback' => [$this, 'check_permission'], + ], + ]); + } + + /** + * Get developer settings + * + * @param WP_REST_Request $request Request object + * @return WP_REST_Response|WP_Error Response object or error + */ + public function get_settings(WP_REST_Request $request) { + return rest_ensure_response([ + 'debug_mode' => (bool) get_option('woonoow_debug_mode', false), + 'show_api_logs' => (bool) get_option('woonoow_show_api_logs', false), + 'enable_react_devtools' => (bool) get_option('woonoow_enable_react_devtools', false), + ]); + } + + /** + * Save developer settings + * + * @param WP_REST_Request $request Request object + * @return WP_REST_Response|WP_Error Response object or error + */ + public function save_settings(WP_REST_Request $request) { + $settings = $request->get_json_params(); + + if (empty($settings)) { + return new WP_Error( + 'missing_settings', + 'No settings provided', + ['status' => 400] + ); + } + + // Save settings + if (isset($settings['debug_mode'])) { + update_option('woonoow_debug_mode', (bool) $settings['debug_mode']); + } + if (isset($settings['show_api_logs'])) { + update_option('woonoow_show_api_logs', (bool) $settings['show_api_logs']); + } + if (isset($settings['enable_react_devtools'])) { + update_option('woonoow_enable_react_devtools', (bool) $settings['enable_react_devtools']); + } + + return rest_ensure_response([ + 'success' => true, + 'message' => 'Developer settings saved successfully', + ]); + } + + /** + * Check permission + * + * @return bool True if user has permission + */ + public function check_permission() { + return current_user_can('manage_woocommerce'); + } +} diff --git a/includes/Api/Routes.php b/includes/Api/Routes.php index b3c2f55..5fa5dea 100644 --- a/includes/Api/Routes.php +++ b/includes/Api/Routes.php @@ -13,6 +13,8 @@ use WooNooW\Api\ShippingController; use WooNooW\Api\TaxController; use WooNooW\Api\PickupLocationsController; use WooNooW\Api\EmailController; +use WooNooW\API\DeveloperController; +use WooNooW\API\SystemController; class Routes { public static function init() { @@ -69,6 +71,14 @@ class Routes { // Email controller $email_controller = new EmailController(); $email_controller->register_routes(); + + // Developer controller + $developer_controller = new DeveloperController(); + $developer_controller->register_routes(); + + // System controller + $system_controller = new SystemController(); + $system_controller->register_routes(); }); } } diff --git a/includes/Api/SystemController.php b/includes/Api/SystemController.php new file mode 100644 index 0000000..da5df38 --- /dev/null +++ b/includes/Api/SystemController.php @@ -0,0 +1,140 @@ +namespace, '/' . $this->rest_base . '/info', [ + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [$this, 'get_info'], + 'permission_callback' => [$this, 'check_permission'], + ], + ]); + + // POST /woonoow/v1/cache/clear + register_rest_route($this->namespace, '/cache/clear', [ + [ + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => [$this, 'clear_cache'], + 'permission_callback' => [$this, 'check_permission'], + ], + ]); + } + + /** + * Get system information + * + * @param WP_REST_Request $request Request object + * @return WP_REST_Response|WP_Error Response object or error + */ + public function get_info(WP_REST_Request $request) { + global $wp_version; + + // Get WooNooW version + $plugin_data = get_file_data( + dirname(dirname(dirname(__FILE__))) . '/woonoow.php', + ['Version' => 'Version'] + ); + + // Get WooCommerce version + $wc_version = defined('WC_VERSION') ? WC_VERSION : 'N/A'; + + // Check HPOS + $hpos_enabled = get_option('woocommerce_custom_orders_table_enabled') === 'yes'; + + $response = rest_ensure_response([ + 'woonoowVersion' => $plugin_data['Version'] ?? '0.1.0', + 'woocommerceVersion' => $wc_version, + 'wordpressVersion' => $wp_version, + 'phpVersion' => PHP_VERSION, + 'hposEnabled' => $hpos_enabled, + ]); + + $response->header('Cache-Control', 'max-age=60'); + + return $response; + } + + /** + * Clear cache + * + * @param WP_REST_Request $request Request object + * @return WP_REST_Response|WP_Error Response object or error + */ + public function clear_cache(WP_REST_Request $request) { + $type = $request->get_param('type') ?? 'all'; + + switch ($type) { + case 'navigation': + // Clear navigation cache + delete_transient('woonoow_navigation_tree'); + break; + + case 'settings': + // Clear settings cache + delete_transient('woonoow_store_settings'); + delete_transient('woonoow_developer_settings'); + break; + + case 'all': + // Clear all WooNooW transients + global $wpdb; + $wpdb->query( + "DELETE FROM {$wpdb->options} + WHERE option_name LIKE '_transient_woonoow_%' + OR option_name LIKE '_transient_timeout_woonoow_%'" + ); + break; + + default: + return new WP_Error( + 'invalid_cache_type', + 'Invalid cache type', + ['status' => 400] + ); + } + + return rest_ensure_response([ + 'success' => true, + 'message' => 'Cache cleared successfully', + ]); + } + + /** + * Check permission + * + * @return bool True if user has permission + */ + public function check_permission() { + return current_user_can('manage_woocommerce'); + } +} diff --git a/includes/Branding.php b/includes/Branding.php new file mode 100644 index 0000000..d280f93 --- /dev/null +++ b/includes/Branding.php @@ -0,0 +1,192 @@ + + + + + + + + 'image'|'text', 'value' => string] + */ + public static function get_logo() { + $logo = get_option('woonoow_store_logo', ''); + + if (!empty($logo)) { + return [ + 'type' => 'image', + 'value' => $logo, + ]; + } + + return [ + 'type' => 'text', + 'value' => get_option('blogname', 'WooNooW'), + ]; + } +} diff --git a/includes/Compat/StoreSettingsProvider.php b/includes/Compat/StoreSettingsProvider.php index 8a884d6..65be259 100644 --- a/includes/Compat/StoreSettingsProvider.php +++ b/includes/Compat/StoreSettingsProvider.php @@ -133,6 +133,13 @@ class StoreSettingsProvider { 'timezone' => get_option('timezone_string', 'UTC') ?: 'UTC', 'weight_unit' => get_option('woocommerce_weight_unit', 'kg'), 'dimension_unit' => get_option('woocommerce_dimension_unit', 'cm'), + // Branding + 'store_logo' => get_option('woonoow_store_logo', ''), + 'store_icon' => get_option('woonoow_store_icon', ''), + 'store_tagline' => get_option('blogdescription', ''), + 'primary_color' => get_option('woonoow_primary_color', '#3b82f6'), + 'accent_color' => get_option('woonoow_accent_color', '#10b981'), + 'error_color' => get_option('woonoow_error_color', '#ef4444'), ]; } @@ -161,6 +168,13 @@ class StoreSettingsProvider { 'timezone' => 'timezone_string', 'weight_unit' => 'woocommerce_weight_unit', 'dimension_unit' => 'woocommerce_dimension_unit', + // Branding + 'store_logo' => 'woonoow_store_logo', + 'store_icon' => 'woonoow_store_icon', + 'store_tagline' => 'blogdescription', + 'primary_color' => 'woonoow_primary_color', + 'accent_color' => 'woonoow_accent_color', + 'error_color' => 'woonoow_error_color', ]; foreach ($settings as $key => $value) { diff --git a/includes/Core/Bootstrap.php b/includes/Core/Bootstrap.php index bb8d747..93d4928 100644 --- a/includes/Core/Bootstrap.php +++ b/includes/Core/Bootstrap.php @@ -18,6 +18,7 @@ use WooNooW\Api\Routes; use WooNooW\Core\Mail\MailQueue; use WooNooW\Core\Mail\WooEmailOverride; use WooNooW\Core\DataStores\OrderStore; +use WooNooW\Branding; class Bootstrap { public static function init() { @@ -26,6 +27,7 @@ class Bootstrap { Menu::init(); Assets::init(); StandaloneAdmin::init(); + Branding::init(); // Addon system (order matters: Registry → Routes → Navigation) AddonRegistry::init();