createClientFromUser($user); } /** * Get all clients * * @param array $args Optional arguments to filter the results * @return array Array of Client models */ public function all($args = []) { $clients = []; // Get all users to include administrators and other users $user_query = new WP_User_Query([ 'number' => -1, 'orderby' => 'ID', 'order' => 'DESC', 'role__not_in' => ['Administrator'], // exclude admins ]); foreach ($user_query->get_results() as $user) { $client = $this->createClientFromUser($user); if ($client) { $clients[] = $client; } } return $clients; } /** * Create a new client * * @param array $data The client data * @return Client The created client model */ public function create($data) { // Create regular client as WordPress user return $this->createRegularClient($data); } /** * Create a regular client (WordPress user) * * @param array $data The client data * @return Client|null The created client model */ protected function createRegularClient($data) { // Create a new user if email is provided if (empty($data[ClientFields::EMAIL])) { return null; } $user_data = [ 'user_email' => $data[ClientFields::EMAIL], 'user_login' => $data[ClientFields::USERNAME], 'user_pass' => !empty($data[ClientFields::PASSWORD]) ? $data[ClientFields::PASSWORD] : wp_generate_password(), 'display_name' => $data[ClientFields::FIRST_NAME] . ' ' . $data[ClientFields::LAST_NAME], 'first_name' => $data[ClientFields::FIRST_NAME], 'last_name' => $data[ClientFields::LAST_NAME], 'role' => 'customer', ]; $user_id = wp_insert_user($user_data); if (is_wp_error($user_id)) { return null; } // Deny backend access on new clients via a per-user capability // override. The user keeps the `customer` role (so Pro Client // Portal's `in_array('customer', $user->roles)` check still // recognises them) but `read` is explicitly set to false at the // user level, which beats the role's `read => true` when // WP_User::has_cap() resolves the merged capability map. Result: // `current_user_can('read')` is false, /wp-admin/ is blocked, // login still works (auth itself is cap-free). // // Per-user override only — existing customer-role users elsewhere // on the site (including pre-existing EI clients and WooCommerce // customers) are NOT affected; only the user we just created. $wp_user = new \WP_User($user_id); $wp_user->add_cap('read', false); // Create client model $client = new Client($user_id); // Set the client data $this->setClientData($client, $data); return $client; } /** * Update an existing client * * @param int $id The client ID * @param array $data The client data * @return Client|null The updated client model or null if not found */ public function update($id, $data) { $client = $this->find($id); if (!$client) { return null; } // Regular client - update user data if email is provided $user_data = ['ID' => $id]; if (!empty($data[ClientFields::EMAIL])) { $user_data['user_email'] = $data[ClientFields::EMAIL]; } if (!empty($data[ClientFields::USERNAME])) { $user_data['user_login'] = $data[ClientFields::USERNAME]; } if (!empty($data[ClientFields::PASSWORD])) { $user_data['user_pass'] = $data[ClientFields::PASSWORD]; } if (!empty($data[ClientFields::FIRST_NAME]) && !empty($data[ClientFields::LAST_NAME])) { $user_data['display_name'] = $data[ClientFields::FIRST_NAME] . ' ' . $data[ClientFields::LAST_NAME]; } if (!empty($data[ClientFields::FIRST_NAME])) { $user_data['first_name'] = $data[ClientFields::FIRST_NAME]; } if (!empty($data[ClientFields::LAST_NAME])) { $user_data['last_name'] = $data[ClientFields::LAST_NAME]; } // Only update user data if we have more than just the ID if(count($user_data) > 1) { $result = wp_update_user($user_data); if (is_wp_error($result)) { // Log the error but continue with client data update } } // Set the client data $this->setClientData($client, $data); return $client; } /** * Delete a client * * @param int $id The client ID * @return bool True if successful, false otherwise */ public function delete($id) { // Check if client exists $client = $this->find($id); if (!$client) { return false; } // Check if user exists and is not an administrator $user = get_user_by('ID', $id); if (!$user || in_array('administrator', $user->roles)) { return false; } // Delete all invoices and quotes associated with this client global $wpdb; // Get all invoices and quotes for this client $posts = $wpdb->get_results($wpdb->prepare( "SELECT ID, post_type FROM {$wpdb->posts} WHERE post_type IN ('easy_invoice', 'easy_invoice_quote') AND ID IN ( SELECT post_id FROM {$wpdb->postmeta} WHERE (meta_key = '_easy_invoice_client_id' OR meta_key = '_easy_invoice_quote_client_id') AND meta_value = %d )", $id )); // Delete each post and its meta foreach ($posts as $post) { wp_delete_post($post->ID, true); } // Delete all payments associated with this client $payments = $wpdb->get_col($wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'easy_payment' AND ID IN ( SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_easy_payment_client_id' AND meta_value = %d )", $id )); foreach ($payments as $payment_id) { wp_delete_post($payment_id, true); } // Delete the WordPress user (this will also delete all user meta) $result = wp_delete_user($id); // Return the result return $result; return $result; } /** * Find clients by email * * @param string $email The client email * @return array Array of Client models */ public function findByEmail($email) { $clients = []; // Find regular clients by email $user = get_user_by('email', $email); if ($user) { $clients[] = $this->createClientFromUser($user); } return $clients; } /** * Find clients by business/client name * * @param string $business_client_name The business/client name * @return array Array of Client models */ public function findByBusinessClientName($business_client_name) { $args = [ 'meta_query' => [ [ 'key' => ClientFields::BUSINESS_CLIENT_NAME, 'value' => $business_client_name, 'compare' => 'LIKE', ], ], ]; return $this->all($args); } /** * Search clients by name, email, company, or phone. * * Matches against: * - User table columns: display_name, user_login, user_email, * user_nicename, user_url (these are what WP_User_Query's * `search` + `search_columns` can actually index). * - WP standard meta: first_name, last_name, nickname. * - WooCommerce billing meta: billing_first_name, billing_last_name, * billing_email, billing_company, billing_phone — so customers * imported via WooCommerce can be found by their billing details. * - Easy Invoice's own meta: first/last/email/business name. * * The previous implementation gated every query on a meta_query * requiring `_easy_invoice_client_business_client_name` OR * `_easy_invoice_client_email` to EXIST, which silently excluded * every WooCommerce customer (they don't have those EI-specific * keys until they've been edited inside Easy Invoice). On stores * with hundreds of imported WC customers, search returned nothing * even though the unfiltered list showed every client. * * @param string $query The search query * @return array Array of Client models */ public function search($query) { $query = trim((string) $query); if ($query === '') { return $this->all(); } $clients = []; $seen_ids = []; $like = '*' . $query . '*'; // ── 1) User table columns ────────────────────────────────────── // These are the only columns WP_User_Query's `search_columns` // accepts; passing `first_name` etc. is silently ignored, so we // pick those up via meta in step 2 below. $user_query = new WP_User_Query([ 'number' => 50, 'orderby' => 'display_name', 'order' => 'ASC', 'search' => $like, 'search_columns' => ['user_login', 'user_email', 'user_nicename', 'display_name', 'user_url'], 'role__not_in' => ['Administrator'], ]); foreach ($user_query->get_results() as $user) { if (isset($seen_ids[$user->ID])) { continue; } $seen_ids[$user->ID] = true; $client = $this->createClientFromUser($user); if ($client) { $clients[] = $client; } } // ── 2) User-meta columns ─────────────────────────────────────── // Search across every meta key we know a client's name / email / // company / phone could be stored under. WP-standard, WooCommerce // billing_*, and Easy Invoice's own keys are all OR'd together so // the user only has to match ONE for a row to qualify. $meta_keys = [ // WP standard 'first_name', 'last_name', 'nickname', // WooCommerce billing — covers imported store customers 'billing_first_name', 'billing_last_name', 'billing_email', 'billing_company', 'billing_phone', // Easy Invoice's own — note PHONE here too so EI-native clients // (whose phone lives in _easy_invoice_client_phone, not billing_*) // are searchable by phone number on equal footing with WC customers. ClientFields::FIRST_NAME, ClientFields::LAST_NAME, ClientFields::EMAIL, ClientFields::BUSINESS_CLIENT_NAME, ClientFields::PHONE, ]; $meta_query = ['relation' => 'OR']; foreach ($meta_keys as $key) { $meta_query[] = [ 'key' => $key, 'value' => $query, 'compare' => 'LIKE', ]; } $meta_user_query = new WP_User_Query([ 'number' => 50, 'orderby' => 'display_name', 'order' => 'ASC', 'role__not_in' => ['Administrator'], 'meta_query' => $meta_query, ]); foreach ($meta_user_query->get_results() as $user) { if (isset($seen_ids[$user->ID])) { continue; } $seen_ids[$user->ID] = true; $client = $this->createClientFromUser($user); if ($client) { $clients[] = $client; } } // Cap the dropdown at 50 results so the autocomplete stays // responsive on stores with thousands of customers. The user // can refine the query to narrow further. return array_slice($clients, 0, 50); } /** * Create a client model from a WordPress user * * @param WP_User $user The WordPress user * @return Client The client model */ protected function createClientFromUser(WP_User $user) { try { $client = new Client($user->ID); return $client; } catch (\Exception $e) { return null; } } /** * Set the client data * * @param Client $client The client model * @param array $data The client data */ protected function setClientData(Client $client, array $data) { if (isset($data[ClientFields::BUSINESS_CLIENT_NAME])) { $client->business_client_name = $data[ClientFields::BUSINESS_CLIENT_NAME]; update_user_meta($client->getId(), ClientFields::BUSINESS_CLIENT_NAME, $data[ClientFields::BUSINESS_CLIENT_NAME]); } if (isset($data[ClientFields::EMAIL])) { $client->email = $data[ClientFields::EMAIL]; update_user_meta($client->getId(), ClientFields::EMAIL, $data[ClientFields::EMAIL]); } if (isset($data[ClientFields::USERNAME])) { $client->username = $data[ClientFields::USERNAME]; update_user_meta($client->getId(), ClientFields::USERNAME, $data[ClientFields::USERNAME]); } if (isset($data[ClientFields::ADDRESS])) { $client->address = $data[ClientFields::ADDRESS]; update_user_meta($client->getId(), ClientFields::ADDRESS, $data[ClientFields::ADDRESS]); } if (isset($data[ClientFields::EXTRA_INFO])) { $client->extra_info = $data[ClientFields::EXTRA_INFO]; update_user_meta($client->getId(), ClientFields::EXTRA_INFO, $data[ClientFields::EXTRA_INFO]); } if (isset($data[ClientFields::FIRST_NAME])) { $client->first_name = $data[ClientFields::FIRST_NAME]; update_user_meta($client->getId(), ClientFields::FIRST_NAME, $data[ClientFields::FIRST_NAME]); } if (isset($data[ClientFields::LAST_NAME])) { $client->last_name = $data[ClientFields::LAST_NAME]; update_user_meta($client->getId(), ClientFields::LAST_NAME, $data[ClientFields::LAST_NAME]); } if (isset($data[ClientFields::WEBSITE])) { $client->website = $data[ClientFields::WEBSITE]; update_user_meta($client->getId(), ClientFields::WEBSITE, $data[ClientFields::WEBSITE]); } if (isset($data[ClientFields::PHONE])) { $client->phone = $data[ClientFields::PHONE]; update_user_meta($client->getId(), ClientFields::PHONE, $data[ClientFields::PHONE]); } // Reset the dirty flag after saving $client->resetDirty(); } }