Files
WooNooW/includes/Api/StoreController.php
dwindown 9c31b4ce6c feat: Mobile chart optimization + VIP customer settings
## Task 4: Mobile Chart Optimization 

**Problem:** Too many data points = tight/crowded lines on mobile

**Solution:** Horizontal scroll container

**Implementation:**
- ChartCard component enhanced with mobile scroll
- Calculates minimum width based on data points (40px per point)
- Desktop: Full width responsive
- Mobile: Fixed width chart in scrollable container

```tsx
// ChartCard.tsx
const mobileMinWidth = Math.max(600, dataPoints * 40);

<div className="overflow-x-auto -mx-6 px-6 md:mx-0 md:px-0">
  <div style={{ minWidth: `${mobileMinWidth}px` }}>
    {children}
  </div>
</div>
```

**Benefits:**
-  All data visible (no loss)
-  Natural swipe gesture
-  Readable spacing
-  Works for all chart types
-  No data aggregation needed

---

## Task 5: VIP Customer Settings 

**New Feature:** Configure VIP customer qualification criteria

### Backend (PHP)

**Files Created:**
- `includes/Compat/CustomerSettingsProvider.php`
  - VIP settings management
  - VIP detection logic
  - Customer stats calculation

**API Endpoints:**
- `GET /store/customer-settings` - Fetch settings
- `POST /store/customer-settings` - Save settings

**Settings:**
```php
woonoow_vip_min_spent = 1000
woonoow_vip_min_orders = 10
woonoow_vip_timeframe = 'all' | '30' | '90' | '365'
woonoow_vip_require_both = true
woonoow_vip_exclude_refunded = true
```

**VIP Detection:**
```php
CustomerSettingsProvider::is_vip_customer($customer_id)
CustomerSettingsProvider::get_vip_stats($customer_id)
```

### Frontend (React)

**Files Created:**
- `admin-spa/src/routes/Settings/Customers.tsx`

**Features:**
- 💰 Minimum total spent (currency input)
- �� Minimum order count (number input)
- 📅 Timeframe selector (all-time, 30/90/365 days)
- ⚙️ Require both criteria toggle
- 🚫 Exclude refunded orders toggle
- 👑 Live preview of VIP qualification

**Navigation:**
- Added to Settings menu
- Route: `/settings/customers`
- Position: After Tax, before Notifications

---

## Summary

 **Mobile Charts:** Horizontal scroll for readable spacing
 **VIP Settings:** Complete backend + frontend implementation

**Mobile Chart Strategy:**
- Minimum 600px width
- 40px per data point
- Smooth horizontal scroll
- Desktop remains responsive

**VIP Customer System:**
- Flexible qualification criteria
- Multiple timeframes
- AND/OR logic support
- Refunded order exclusion
- Ready for customer list integration

**All tasks complete!** 🎉
2025-11-11 00:49:07 +07:00

341 lines
11 KiB
PHP

<?php
/**
* Store REST API Controller
*
* Provides REST endpoints for store settings management.
*
* @package WooNooW
*/
namespace WooNooW\API;
use WooNooW\Compat\StoreSettingsProvider;
use WooNooW\Compat\CustomerSettingsProvider;
use WP_REST_Controller;
use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
class StoreController extends WP_REST_Controller {
/**
* Namespace
*/
protected $namespace = 'woonoow/v1';
/**
* Rest base
*/
protected $rest_base = 'store';
/**
* Register routes
*/
public function register_routes() {
// GET /woonoow/v1/store/branding (PUBLIC - for login page)
register_rest_route($this->namespace, '/' . $this->rest_base . '/branding', [
[
'methods' => WP_REST_Server::READABLE,
'callback' => [$this, 'get_branding'],
'permission_callback' => '__return_true', // Public endpoint
],
]);
// GET /woonoow/v1/store/settings
register_rest_route($this->namespace, '/' . $this->rest_base . '/settings', [
[
'methods' => WP_REST_Server::READABLE,
'callback' => [$this, 'get_settings'],
'permission_callback' => [$this, 'check_permission'],
],
]);
// POST /woonoow/v1/store/settings
register_rest_route($this->namespace, '/' . $this->rest_base . '/settings', [
[
'methods' => WP_REST_Server::EDITABLE,
'callback' => [$this, 'save_settings'],
'permission_callback' => [$this, 'check_permission'],
],
]);
// GET /woonoow/v1/store/countries
register_rest_route($this->namespace, '/' . $this->rest_base . '/countries', [
[
'methods' => WP_REST_Server::READABLE,
'callback' => [$this, 'get_countries'],
'permission_callback' => [$this, 'check_permission'],
],
]);
// GET /woonoow/v1/store/timezones
register_rest_route($this->namespace, '/' . $this->rest_base . '/timezones', [
[
'methods' => WP_REST_Server::READABLE,
'callback' => [$this, 'get_timezones'],
'permission_callback' => [$this, 'check_permission'],
],
]);
// GET /woonoow/v1/store/currencies
register_rest_route($this->namespace, '/' . $this->rest_base . '/currencies', [
[
'methods' => WP_REST_Server::READABLE,
'callback' => [$this, 'get_currencies'],
'permission_callback' => [$this, 'check_permission'],
],
]);
// GET /woonoow/v1/store/customer-settings
register_rest_route($this->namespace, '/' . $this->rest_base . '/customer-settings', [
[
'methods' => WP_REST_Server::READABLE,
'callback' => [$this, 'get_customer_settings'],
'permission_callback' => [$this, 'check_permission'],
],
]);
// POST /woonoow/v1/store/customer-settings
register_rest_route($this->namespace, '/' . $this->rest_base . '/customer-settings', [
[
'methods' => WP_REST_Server::EDITABLE,
'callback' => [$this, 'save_customer_settings'],
'permission_callback' => [$this, 'check_permission'],
],
]);
}
/**
* Get store branding (PUBLIC - for login page)
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response Response object
*/
public function get_branding(WP_REST_Request $request) {
$branding = [
'store_name' => get_option('blogname', 'WooNooW'),
'store_logo' => get_option('woonoow_store_logo', ''),
'store_icon' => get_option('woonoow_store_icon', ''),
'store_tagline' => get_option('woonoow_store_tagline', ''),
];
$response = rest_ensure_response($branding);
$response->header('Cache-Control', 'max-age=300'); // Cache for 5 minutes
return $response;
}
/**
* Get store 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) {
try {
$settings = StoreSettingsProvider::get_settings();
$response = rest_ensure_response($settings);
$response->header('Cache-Control', 'max-age=60');
return $response;
} catch (\Exception $e) {
return new WP_Error(
'get_settings_failed',
$e->getMessage(),
['status' => 500]
);
}
}
/**
* Save store 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]
);
}
try {
$result = StoreSettingsProvider::save_settings($settings);
if (!$result) {
return new WP_Error(
'save_failed',
'Failed to save settings',
['status' => 500]
);
}
return rest_ensure_response([
'success' => true,
'message' => 'Settings saved successfully',
'settings' => StoreSettingsProvider::get_settings(),
]);
} catch (\Exception $e) {
return new WP_Error(
'save_settings_failed',
$e->getMessage(),
['status' => 500]
);
}
}
/**
* Get countries
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response|WP_Error Response object or error
*/
public function get_countries(WP_REST_Request $request) {
try {
$countries = StoreSettingsProvider::get_countries();
$response = rest_ensure_response($countries);
$response->header('Cache-Control', 'max-age=3600'); // Cache for 1 hour
return $response;
} catch (\Exception $e) {
return new WP_Error(
'get_countries_failed',
$e->getMessage(),
['status' => 500]
);
}
}
/**
* Get timezones
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response|WP_Error Response object or error
*/
public function get_timezones(WP_REST_Request $request) {
try {
$timezones = StoreSettingsProvider::get_timezones();
$response = rest_ensure_response($timezones);
$response->header('Cache-Control', 'max-age=3600'); // Cache for 1 hour
return $response;
} catch (\Exception $e) {
return new WP_Error(
'get_timezones_failed',
$e->getMessage(),
['status' => 500]
);
}
}
/**
* Get currencies
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response|WP_Error Response object or error
*/
public function get_currencies(WP_REST_Request $request) {
try {
$currencies = StoreSettingsProvider::get_currencies();
$response = rest_ensure_response($currencies);
$response->header('Cache-Control', 'max-age=3600'); // Cache for 1 hour
return $response;
} catch (\Exception $e) {
return new WP_Error(
'get_currencies_failed',
$e->getMessage(),
['status' => 500]
);
}
}
/**
* Get customer settings
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response|WP_Error Response object or error
*/
public function get_customer_settings(WP_REST_Request $request) {
try {
$settings = CustomerSettingsProvider::get_settings();
$response = rest_ensure_response($settings);
$response->header('Cache-Control', 'max-age=60');
return $response;
} catch (\Exception $e) {
return new WP_Error(
'get_customer_settings_failed',
$e->getMessage(),
['status' => 500]
);
}
}
/**
* Save customer settings
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response|WP_Error Response object or error
*/
public function save_customer_settings(WP_REST_Request $request) {
try {
$settings = $request->get_json_params();
if (empty($settings)) {
return new WP_Error(
'invalid_settings',
__('Invalid settings data', 'woonoow'),
['status' => 400]
);
}
$updated = CustomerSettingsProvider::update_settings($settings);
if (!$updated) {
return new WP_Error(
'update_failed',
__('Failed to update customer settings', 'woonoow'),
['status' => 500]
);
}
// Return updated settings
$new_settings = CustomerSettingsProvider::get_settings();
return new WP_REST_Response([
'success' => true,
'message' => __('Customer settings updated successfully', 'woonoow'),
'settings' => $new_settings,
], 200);
} catch (\Exception $e) {
return new WP_Error(
'save_customer_settings_failed',
$e->getMessage(),
['status' => 500]
);
}
}
/**
* Check permission
*
* @return bool True if user has permission
*/
public function check_permission() {
return current_user_can('manage_woocommerce');
}
}