Fixed all 6 issues in Customer index: 1. ✅ Search Input - Match Coupon Module: - Mobile: Native input with proper styling - Desktop: Native input with proper styling - Consistent with Coupon module pattern - Better focus states and padding 2. ✅ Filter - Not Needed: - Customer data is simple (name, email, stats) - Search is sufficient for finding customers - No complex filtering like products/coupons 3. ✅ Stats Display - FIXED: - Backend: Changed format_customer() to include stats (detailed=true) - Now shows actual order count and total spent - No more zero orders or dashed values 4. ✅ Member/Guest Column - Added: - New 'Type' column in table - Shows badge: Member (blue) or Guest (gray) - Based on customer.role field 5. ✅ Actions Column - Added: - New 'Actions' column with Edit button - Edit icon + text link - Navigates to /customers/:id/edit 6. ✅ Navigation - Fixed: - Name click → Detail page (/customers/:id) - Edit button → Edit page (/customers/:id/edit) - Mobile cards also link to detail page - Separation of concerns: view vs edit Changes Made: Backend (CustomersController.php): - Line 96: format_customer(, true) to include stats Frontend (Customers/index.tsx): - Search inputs: Match Coupon module styling - Table: Added Type and Actions columns - Type badge: Member (blue) / Guest (gray) - Actions: Edit button with icon - Navigation: Name → detail, Edit → edit - Mobile cards: Link to detail page Table Structure: - Checkbox | Customer | Email | Type | Orders | Total Spent | Registered | Actions - 8 columns total (was 6) Next: Create customer detail page with related orders and stats
437 lines
16 KiB
PHP
437 lines
16 KiB
PHP
<?php
|
|
namespace WooNooW\Api;
|
|
|
|
use WP_REST_Request;
|
|
use WP_REST_Response;
|
|
use WP_Error;
|
|
use WP_User;
|
|
use WC_Customer;
|
|
|
|
/**
|
|
* CustomersController
|
|
*
|
|
* Handles customer CRUD operations via REST API
|
|
*
|
|
* @package WooNooW\Api
|
|
*/
|
|
class CustomersController {
|
|
|
|
/**
|
|
* Register REST API routes
|
|
*/
|
|
public static function register_routes() {
|
|
// List customers
|
|
register_rest_route('woonoow/v1', '/customers', [
|
|
'methods' => 'GET',
|
|
'callback' => [__CLASS__, 'list_customers'],
|
|
'permission_callback' => function () { return current_user_can('list_users'); },
|
|
]);
|
|
|
|
// Get single customer
|
|
register_rest_route('woonoow/v1', '/customers/(?P<id>\d+)', [
|
|
'methods' => 'GET',
|
|
'callback' => [__CLASS__, 'get_customer'],
|
|
'permission_callback' => function () { return current_user_can('list_users'); },
|
|
]);
|
|
|
|
// Create customer
|
|
register_rest_route('woonoow/v1', '/customers', [
|
|
'methods' => 'POST',
|
|
'callback' => [__CLASS__, 'create_customer'],
|
|
'permission_callback' => function () { return current_user_can('create_users'); },
|
|
]);
|
|
|
|
// Update customer
|
|
register_rest_route('woonoow/v1', '/customers/(?P<id>\d+)', [
|
|
'methods' => 'PUT',
|
|
'callback' => [__CLASS__, 'update_customer'],
|
|
'permission_callback' => function () { return current_user_can('edit_users'); },
|
|
]);
|
|
|
|
// Delete customer
|
|
register_rest_route('woonoow/v1', '/customers/(?P<id>\d+)', [
|
|
'methods' => 'DELETE',
|
|
'callback' => [__CLASS__, 'delete_customer'],
|
|
'permission_callback' => function () { return current_user_can('delete_users'); },
|
|
]);
|
|
|
|
// Search customers (for autocomplete)
|
|
register_rest_route('woonoow/v1', '/customers/search', [
|
|
'methods' => 'GET',
|
|
'callback' => [__CLASS__, 'search_customers'],
|
|
'permission_callback' => function () { return current_user_can('list_users'); },
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* GET /woonoow/v1/customers
|
|
* List all customers with pagination and filtering
|
|
*/
|
|
public static function list_customers(WP_REST_Request $request): WP_REST_Response {
|
|
$page = max(1, (int) $request->get_param('page') ?: 1);
|
|
$per_page = max(1, min(100, (int) $request->get_param('per_page') ?: 20));
|
|
$search = sanitize_text_field($request->get_param('search') ?: '');
|
|
$role = sanitize_text_field($request->get_param('role') ?: 'customer');
|
|
|
|
$args = [
|
|
'role' => $role,
|
|
'number' => $per_page,
|
|
'paged' => $page,
|
|
'orderby' => 'registered',
|
|
'order' => 'DESC',
|
|
];
|
|
|
|
// Search by name or email
|
|
if ($search) {
|
|
$args['search'] = '*' . $search . '*';
|
|
$args['search_columns'] = ['user_login', 'user_email', 'display_name'];
|
|
}
|
|
|
|
$user_query = new \WP_User_Query($args);
|
|
$users = $user_query->get_results();
|
|
$total = $user_query->get_total();
|
|
|
|
$customers = [];
|
|
foreach ($users as $user) {
|
|
$customers[] = self::format_customer($user, true); // Include stats for list view
|
|
}
|
|
|
|
return new WP_REST_Response([
|
|
'data' => $customers,
|
|
'pagination' => [
|
|
'total' => $total,
|
|
'total_pages' => ceil($total / $per_page),
|
|
'current' => $page,
|
|
'per_page' => $per_page,
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* GET /woonoow/v1/customers/{id}
|
|
* Get single customer by ID
|
|
*/
|
|
public static function get_customer(WP_REST_Request $request): WP_REST_Response|WP_Error {
|
|
$id = (int) $request->get_param('id');
|
|
$user = get_user_by('ID', $id);
|
|
|
|
if (!$user) {
|
|
return new WP_Error('customer_not_found', __('Customer not found', 'woonoow'), ['status' => 404]);
|
|
}
|
|
|
|
return new WP_REST_Response(self::format_customer($user, true));
|
|
}
|
|
|
|
/**
|
|
* POST /woonoow/v1/customers
|
|
* Create new customer
|
|
*/
|
|
public static function create_customer(WP_REST_Request $request): WP_REST_Response|WP_Error {
|
|
$data = $request->get_json_params();
|
|
|
|
// Validate required fields
|
|
if (empty($data['email'])) {
|
|
return new WP_Error('missing_email', __('Email is required', 'woonoow'), ['status' => 400]);
|
|
}
|
|
|
|
if (empty($data['first_name'])) {
|
|
return new WP_Error('missing_first_name', __('First name is required', 'woonoow'), ['status' => 400]);
|
|
}
|
|
|
|
if (empty($data['last_name'])) {
|
|
return new WP_Error('missing_last_name', __('Last name is required', 'woonoow'), ['status' => 400]);
|
|
}
|
|
|
|
// Check if email already exists
|
|
if (email_exists($data['email'])) {
|
|
return new WP_Error('email_exists', __('Email already exists', 'woonoow'), ['status' => 400]);
|
|
}
|
|
|
|
// Generate username from email if not provided
|
|
$username = !empty($data['username']) ? sanitize_user($data['username']) : sanitize_user($data['email']);
|
|
|
|
// Check if username exists
|
|
if (username_exists($username)) {
|
|
// Append number to make it unique
|
|
$base_username = $username;
|
|
$counter = 1;
|
|
while (username_exists($username)) {
|
|
$username = $base_username . $counter;
|
|
$counter++;
|
|
}
|
|
}
|
|
|
|
// Generate password if not provided
|
|
$password = !empty($data['password']) ? $data['password'] : wp_generate_password(12, true, true);
|
|
|
|
// Create user
|
|
$user_id = wp_create_user($username, $password, sanitize_email($data['email']));
|
|
|
|
if (is_wp_error($user_id)) {
|
|
return new WP_Error('create_failed', $user_id->get_error_message(), ['status' => 500]);
|
|
}
|
|
|
|
// Set role to customer
|
|
$user = new WP_User($user_id);
|
|
$user->set_role('customer');
|
|
|
|
// Update user meta
|
|
wp_update_user([
|
|
'ID' => $user_id,
|
|
'first_name' => sanitize_text_field($data['first_name']),
|
|
'last_name' => sanitize_text_field($data['last_name']),
|
|
'display_name' => sanitize_text_field($data['first_name'] . ' ' . $data['last_name']),
|
|
]);
|
|
|
|
// Update WooCommerce customer data
|
|
$customer = new WC_Customer($user_id);
|
|
|
|
// Billing address
|
|
if (!empty($data['billing'])) {
|
|
$billing = $data['billing'];
|
|
$customer->set_billing_first_name(sanitize_text_field($billing['first_name'] ?? $data['first_name']));
|
|
$customer->set_billing_last_name(sanitize_text_field($billing['last_name'] ?? $data['last_name']));
|
|
$customer->set_billing_company(sanitize_text_field($billing['company'] ?? ''));
|
|
$customer->set_billing_address_1(sanitize_text_field($billing['address_1'] ?? ''));
|
|
$customer->set_billing_address_2(sanitize_text_field($billing['address_2'] ?? ''));
|
|
$customer->set_billing_city(sanitize_text_field($billing['city'] ?? ''));
|
|
$customer->set_billing_state(sanitize_text_field($billing['state'] ?? ''));
|
|
$customer->set_billing_postcode(sanitize_text_field($billing['postcode'] ?? ''));
|
|
$customer->set_billing_country(sanitize_text_field($billing['country'] ?? ''));
|
|
$customer->set_billing_phone(sanitize_text_field($billing['phone'] ?? ''));
|
|
}
|
|
|
|
// Shipping address
|
|
if (!empty($data['shipping'])) {
|
|
$shipping = $data['shipping'];
|
|
$customer->set_shipping_first_name(sanitize_text_field($shipping['first_name'] ?? $data['first_name']));
|
|
$customer->set_shipping_last_name(sanitize_text_field($shipping['last_name'] ?? $data['last_name']));
|
|
$customer->set_shipping_company(sanitize_text_field($shipping['company'] ?? ''));
|
|
$customer->set_shipping_address_1(sanitize_text_field($shipping['address_1'] ?? ''));
|
|
$customer->set_shipping_address_2(sanitize_text_field($shipping['address_2'] ?? ''));
|
|
$customer->set_shipping_city(sanitize_text_field($shipping['city'] ?? ''));
|
|
$customer->set_shipping_state(sanitize_text_field($shipping['state'] ?? ''));
|
|
$customer->set_shipping_postcode(sanitize_text_field($shipping['postcode'] ?? ''));
|
|
$customer->set_shipping_country(sanitize_text_field($shipping['country'] ?? ''));
|
|
}
|
|
|
|
$customer->save();
|
|
|
|
// Send new account email if requested
|
|
if (!empty($data['send_email'])) {
|
|
wp_new_user_notification($user_id, null, 'both');
|
|
}
|
|
|
|
return new WP_REST_Response(self::format_customer(get_user_by('ID', $user_id), true), 201);
|
|
}
|
|
|
|
/**
|
|
* PUT /woonoow/v1/customers/{id}
|
|
* Update existing customer
|
|
*/
|
|
public static function update_customer(WP_REST_Request $request): WP_REST_Response|WP_Error {
|
|
$id = (int) $request->get_param('id');
|
|
$data = $request->get_json_params();
|
|
|
|
$user = get_user_by('ID', $id);
|
|
if (!$user) {
|
|
return new WP_Error('customer_not_found', __('Customer not found', 'woonoow'), ['status' => 404]);
|
|
}
|
|
|
|
// Update user data
|
|
$user_data = ['ID' => $id];
|
|
|
|
if (!empty($data['email'])) {
|
|
// Check if email is changing and if new email exists
|
|
if ($data['email'] !== $user->user_email && email_exists($data['email'])) {
|
|
return new WP_Error('email_exists', __('Email already exists', 'woonoow'), ['status' => 400]);
|
|
}
|
|
$user_data['user_email'] = sanitize_email($data['email']);
|
|
}
|
|
|
|
if (!empty($data['first_name'])) {
|
|
$user_data['first_name'] = sanitize_text_field($data['first_name']);
|
|
}
|
|
|
|
if (!empty($data['last_name'])) {
|
|
$user_data['last_name'] = sanitize_text_field($data['last_name']);
|
|
}
|
|
|
|
if (isset($data['first_name']) || isset($data['last_name'])) {
|
|
$first = $data['first_name'] ?? get_user_meta($id, 'first_name', true);
|
|
$last = $data['last_name'] ?? get_user_meta($id, 'last_name', true);
|
|
$user_data['display_name'] = trim($first . ' ' . $last);
|
|
}
|
|
|
|
if (!empty($data['password'])) {
|
|
$user_data['user_pass'] = $data['password'];
|
|
}
|
|
|
|
$result = wp_update_user($user_data);
|
|
if (is_wp_error($result)) {
|
|
return new WP_Error('update_failed', $result->get_error_message(), ['status' => 500]);
|
|
}
|
|
|
|
// Update WooCommerce customer data
|
|
$customer = new WC_Customer($id);
|
|
|
|
// Billing address
|
|
if (!empty($data['billing'])) {
|
|
$billing = $data['billing'];
|
|
if (isset($billing['first_name'])) $customer->set_billing_first_name(sanitize_text_field($billing['first_name']));
|
|
if (isset($billing['last_name'])) $customer->set_billing_last_name(sanitize_text_field($billing['last_name']));
|
|
if (isset($billing['company'])) $customer->set_billing_company(sanitize_text_field($billing['company']));
|
|
if (isset($billing['address_1'])) $customer->set_billing_address_1(sanitize_text_field($billing['address_1']));
|
|
if (isset($billing['address_2'])) $customer->set_billing_address_2(sanitize_text_field($billing['address_2']));
|
|
if (isset($billing['city'])) $customer->set_billing_city(sanitize_text_field($billing['city']));
|
|
if (isset($billing['state'])) $customer->set_billing_state(sanitize_text_field($billing['state']));
|
|
if (isset($billing['postcode'])) $customer->set_billing_postcode(sanitize_text_field($billing['postcode']));
|
|
if (isset($billing['country'])) $customer->set_billing_country(sanitize_text_field($billing['country']));
|
|
if (isset($billing['phone'])) $customer->set_billing_phone(sanitize_text_field($billing['phone']));
|
|
}
|
|
|
|
// Shipping address
|
|
if (!empty($data['shipping'])) {
|
|
$shipping = $data['shipping'];
|
|
if (isset($shipping['first_name'])) $customer->set_shipping_first_name(sanitize_text_field($shipping['first_name']));
|
|
if (isset($shipping['last_name'])) $customer->set_shipping_last_name(sanitize_text_field($shipping['last_name']));
|
|
if (isset($shipping['company'])) $customer->set_shipping_company(sanitize_text_field($shipping['company']));
|
|
if (isset($shipping['address_1'])) $customer->set_shipping_address_1(sanitize_text_field($shipping['address_1']));
|
|
if (isset($shipping['address_2'])) $customer->set_shipping_address_2(sanitize_text_field($shipping['address_2']));
|
|
if (isset($shipping['city'])) $customer->set_shipping_city(sanitize_text_field($shipping['city']));
|
|
if (isset($shipping['state'])) $customer->set_shipping_state(sanitize_text_field($shipping['state']));
|
|
if (isset($shipping['postcode'])) $customer->set_shipping_postcode(sanitize_text_field($shipping['postcode']));
|
|
if (isset($shipping['country'])) $customer->set_shipping_country(sanitize_text_field($shipping['country']));
|
|
}
|
|
|
|
$customer->save();
|
|
|
|
return new WP_REST_Response(self::format_customer(get_user_by('ID', $id), true));
|
|
}
|
|
|
|
/**
|
|
* DELETE /woonoow/v1/customers/{id}
|
|
* Delete customer
|
|
*/
|
|
public static function delete_customer(WP_REST_Request $request): WP_REST_Response|WP_Error {
|
|
$id = (int) $request->get_param('id');
|
|
|
|
$user = get_user_by('ID', $id);
|
|
if (!$user) {
|
|
return new WP_Error('customer_not_found', __('Customer not found', 'woonoow'), ['status' => 404]);
|
|
}
|
|
|
|
// Don't allow deleting current user
|
|
if ($id === get_current_user_id()) {
|
|
return new WP_Error('cannot_delete_self', __('You cannot delete your own account', 'woonoow'), ['status' => 403]);
|
|
}
|
|
|
|
require_once(ABSPATH . 'wp-admin/includes/user.php');
|
|
$result = wp_delete_user($id);
|
|
|
|
if (!$result) {
|
|
return new WP_Error('delete_failed', __('Failed to delete customer', 'woonoow'), ['status' => 500]);
|
|
}
|
|
|
|
return new WP_REST_Response(['success' => true]);
|
|
}
|
|
|
|
/**
|
|
* GET /woonoow/v1/customers/search
|
|
* Search customers for autocomplete
|
|
*/
|
|
public static function search_customers(WP_REST_Request $request): WP_REST_Response {
|
|
$search = sanitize_text_field($request->get_param('q') ?: '');
|
|
$limit = max(1, min(50, (int) $request->get_param('limit') ?: 10));
|
|
|
|
if (empty($search)) {
|
|
return new WP_REST_Response([]);
|
|
}
|
|
|
|
$args = [
|
|
'role' => 'customer',
|
|
'number' => $limit,
|
|
'search' => '*' . $search . '*',
|
|
'search_columns' => ['user_login', 'user_email', 'display_name'],
|
|
];
|
|
|
|
$user_query = new \WP_User_Query($args);
|
|
$users = $user_query->get_results();
|
|
|
|
$results = [];
|
|
foreach ($users as $user) {
|
|
$results[] = [
|
|
'id' => $user->ID,
|
|
'name' => $user->display_name,
|
|
'email' => $user->user_email,
|
|
];
|
|
}
|
|
|
|
return new WP_REST_Response($results);
|
|
}
|
|
|
|
/**
|
|
* Format customer data for API response
|
|
*/
|
|
private static function format_customer(WP_User $user, bool $detailed = false): array {
|
|
$customer = new WC_Customer($user->ID);
|
|
|
|
$data = [
|
|
'id' => $user->ID,
|
|
'username' => $user->user_login,
|
|
'email' => $user->user_email,
|
|
'first_name' => $user->first_name,
|
|
'last_name' => $user->last_name,
|
|
'display_name' => $user->display_name,
|
|
'registered' => $user->user_registered,
|
|
'role' => !empty($user->roles) ? $user->roles[0] : '',
|
|
];
|
|
|
|
if ($detailed) {
|
|
$data['billing'] = [
|
|
'first_name' => $customer->get_billing_first_name(),
|
|
'last_name' => $customer->get_billing_last_name(),
|
|
'company' => $customer->get_billing_company(),
|
|
'address_1' => $customer->get_billing_address_1(),
|
|
'address_2' => $customer->get_billing_address_2(),
|
|
'city' => $customer->get_billing_city(),
|
|
'state' => $customer->get_billing_state(),
|
|
'postcode' => $customer->get_billing_postcode(),
|
|
'country' => $customer->get_billing_country(),
|
|
'phone' => $customer->get_billing_phone(),
|
|
];
|
|
|
|
$data['shipping'] = [
|
|
'first_name' => $customer->get_shipping_first_name(),
|
|
'last_name' => $customer->get_shipping_last_name(),
|
|
'company' => $customer->get_shipping_company(),
|
|
'address_1' => $customer->get_shipping_address_1(),
|
|
'address_2' => $customer->get_shipping_address_2(),
|
|
'city' => $customer->get_shipping_city(),
|
|
'state' => $customer->get_shipping_state(),
|
|
'postcode' => $customer->get_shipping_postcode(),
|
|
'country' => $customer->get_shipping_country(),
|
|
];
|
|
|
|
// Get order stats
|
|
$orders = wc_get_orders([
|
|
'customer_id' => $user->ID,
|
|
'limit' => -1,
|
|
'status' => ['wc-completed', 'wc-processing'],
|
|
]);
|
|
|
|
$total_spent = 0;
|
|
foreach ($orders as $order) {
|
|
$total_spent += $order->get_total();
|
|
}
|
|
|
|
$data['stats'] = [
|
|
'total_orders' => count($orders),
|
|
'total_spent' => $total_spent,
|
|
];
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
}
|