'patient_guid', 'first_name' => 'First Name', 'last_name' => 'Last Name', 'email' => 'Email', ]; protected array $headers = []; public function __construct() { global $wpdb; $this->wpdb = $wpdb; $this->jane_clients_table = $wpdb->prefix . BASE . 'jane_clients'; } /** * Import client list from CSV file * * @param string $file_path Path to the CSV file * @param array $options Import options (e.g., update_existing, send_welcome_email) * @return array|WP_Error Import results with stats and errors */ public function importFromCSV(string $file_path, array $options = []): array|WP_Error { $this->skipped_details = []; $this->lineNumber = 0; // Reset line number // Initialize stats $this->import_stats = [ 'total_rows' => 0, 'processed' => 0, 'created' => 0, 'updated' => 0, 'skipped' => 0, 'errors' => [], 'unmatched_emails' => [], 'skipped_details' => [] // Add this ]; // Validate file exists if (!file_exists($file_path)) { return new WP_Error('file_not_found', 'CSV file not found'); } // Parse options $update_existing = $options['update_existing'] ?? true; $send_welcome_email = $options['send_welcome_email'] ?? false; $create_users = $options['create_users'] ?? true; // Open and parse CSV $handle = fopen($file_path, 'r'); if (!$handle) { return new WP_Error('file_open_error', 'Could not open CSV file'); } // Get header row $headers = fgetcsv($handle); if (!$headers) { fclose($handle); return new WP_Error('invalid_csv', 'CSV file is empty or invalid'); } $this->headers = array_map('trim', $headers); // Map column indices $column_indices = $this->mapColumnIndices($this->headers); if (is_wp_error($column_indices)) { fclose($handle); return $column_indices; } // Start transaction for data integrity $this->wpdb->query('START TRANSACTION'); try { // Process each row while (($row = fgetcsv($handle)) !== false) { $this->import_stats['total_rows']++; $this->lineNumber++; $result = $this->processClientRow($row, $column_indices, [ 'update_existing' => $update_existing, 'send_welcome_email' => $send_welcome_email, 'create_users' => $create_users, 'default_role' => $options['default_role'] ?? null ]); if (is_wp_error($result)) { $this->import_stats['errors'][] = [ 'row' => $this->import_stats['total_rows'], 'error' => $result->get_error_message() ]; $this->import_stats['skipped']++; } else { $this->import_stats['processed']++; if ($result['action'] === 'created') { $this->import_stats['created']++; } elseif ($result['action'] === 'updated') { $this->import_stats['updated']++; } } } // Commit transaction $this->wpdb->query('COMMIT'); } catch (\Exception $e) { // Rollback on error $this->wpdb->query('ROLLBACK'); fclose($handle); return new WP_Error('import_error', 'Import failed: ' . $e->getMessage()); } fclose($handle); // Add skipped details to stats $this->import_stats['skipped_details'] = $this->skipped_details; return $this->import_stats; } /** * Map CSV column headers to internal field names * * @param array $headers CSV header row * @return array|WP_Error Column index mapping or error */ protected function mapColumnIndices(array $headers): array|WP_Error { $indices = []; foreach ($this->column_map as $field => $csv_column) { $index = array_search($csv_column, $headers); if ($index === false) { return new WP_Error( 'missing_column', sprintf('Required column "%s" not found in CSV', $csv_column) ); } $indices[$field] = $index; } return $indices; } /** * Process a single client row from CSV * * @param array $row CSV row data * @param array $column_indices Column mapping * @param array $options Processing options * @return array|WP_Error Result of processing */ protected function processClientRow(array $row, array $column_indices, array $options): array|WP_Error { // Extract data from row $patient_guid = trim($row[$column_indices['patient_guid']] ?? ''); $first_name = trim($row[$column_indices['first_name']] ?? ''); $last_name = trim($row[$column_indices['last_name']] ?? ''); $email = trim($row[$column_indices['email']] ?? ''); // Validate required fields if (empty($patient_guid) || empty($email)) { $this->skipped_details[] = [ 'name' => $first_name . ' ' . $last_name, 'guid' => $patient_guid, 'email' => $email, 'reason' => 'Missing guid or email', 'line' => $this->lineNumber ]; return new WP_Error('invalid_data', 'Missing patient_guid or email'); } // Sanitize email $email = sanitize_email($email); if (!is_email($email)) { $this->skipped_details[] = [ 'name' => $first_name . ' ' . $last_name, 'guid' => $patient_guid, 'email' => $email, 'reason' => 'Invalid Email', 'line' => $this->lineNumber ]; return new WP_Error('invalid_email', 'Invalid email address: ' . $email); } // Check if client already exists in mapping table $existing_mapping = $this->getClientByGuid($patient_guid); // Build full data array with all CSV columns for meta storage $data = []; foreach ($row as $index => $value) { $header = $this->headers[$index] ?? 'unknown_' . $index; $data[$header] = trim($value); } // Ensure these keys exist for backward compatibility $data['patient_guid'] = $patient_guid; $data['First Name'] = $first_name; $data['Last Name'] = $last_name; $data['Email'] = $email; // Find or create WordPress user $user = get_user_by('email', $email); $action = 'existing'; if ($user) { if ($options['update_existing']) { $this->updateExistingClient($user->ID, $data, $options); $action = 'updated'; } else { $this->skipped_details[] = [ 'name' => $first_name . ' ' . $last_name, 'email' => $email, 'reason' => 'User already exists (update not enabled)', 'line' => $this->lineNumber ]; return new WP_Error('user_exists', 'User already exists'); } } elseif ($options['create_users']) { // Create new user $user_id = $this->createClientUser($data, $options); if (is_wp_error($user_id)) { $this->skipped_details[] = [ 'name' => $first_name . ' ' . $last_name, 'guid' => $patient_guid, 'email' => $email, 'reason' => $user_id->get_error_message(), 'line' => $this->lineNumber ]; return $user_id; } $user = get_user_by('ID', $user_id); $action = 'created'; } else { // User doesn't exist and we're not creating users $this->skipped_details[] = [ 'name' => $first_name . ' ' . $last_name, 'guid' => $patient_guid, 'email' => $email, 'reason' => 'User not found and create_users is false', 'line' => $this->lineNumber ]; $this->import_stats['unmatched_emails'][] = $email; return new WP_Error('user_not_found', 'User not found and create_users is false'); } // Update or insert client mapping if ($existing_mapping) { if ($options['update_existing']) { $this->updateClientMapping($existing_mapping->id, [ 'user_id' => $user->ID, 'first_name' => $first_name, 'last_name' => $last_name, 'email' => $email ]); } } else { $this->insertClientMapping([ 'patient_guid' => $patient_guid, 'user_id' => $user->ID, 'first_name' => $first_name, 'last_name' => $last_name, 'email' => $email ]); } return [ 'action' => $action, 'user_id' => $user->ID, 'patient_guid' => $patient_guid ]; } /** * Create a new client user from Jane App data * * @param array $data Client data from CSV * @param array $options Import options * @return int|WP_Error User ID or error */ protected function createClientUser(array $data, array $options) { $email = sanitize_email($data['Email']); $first_name = sanitize_text_field($data['First Name'] ?? ''); $last_name = sanitize_text_field($data['Last Name'] ?? ''); // Generate username from email $username = sanitize_user($email); // Ensure unique username $base_username = $username; $counter = 1; while (username_exists($username)) { $username = $base_username . $counter; $counter++; } // Get the role from options with proper fallback $role = $options['default_role'] ?? get_option(BASE . 'client_import_role', JVB_USER); // Ensure role exists, fallback to JVB_USER if not if (!get_role($role)) { $role = JVB_USER; } // Create user $user_data = [ 'user_login' => $username, 'user_email' => $email, 'first_name' => $first_name, 'last_name' => $last_name, 'display_name' => trim($first_name . ' ' . $last_name), 'role' => $role, 'user_pass' => wp_generate_password(16, true, true) ]; $user_id = wp_insert_user($user_data); if (is_wp_error($user_id)) { return $user_id; } // Store Jane App data as user meta $this->storeClientMeta($user_id, $data); // Send welcome email if enabled if ($options['send_welcome_email'] ?? false) { wp_new_user_notification($user_id, null, 'user'); } return $user_id; } /** * Store Jane App client data as user meta * * @param int $user_id * @param array $data */ protected function storeClientMeta(int $user_id, array $data): void { // Store Jane App specific fields if (!empty($data['patient_guid'])) { update_user_meta($user_id, BASE . 'jane_patient_guid', sanitize_text_field($data['patient_guid'])); } if (!empty($data['Patient Number'])) { update_user_meta($user_id, BASE . 'jane_patient_number', sanitize_text_field($data['Patient Number'])); } if (!empty($data['Member Since'])) { update_user_meta($user_id, BASE . 'member_since', sanitize_text_field($data['Member Since'])); } if (!empty($data['Mobile Phone'])) { update_user_meta($user_id, BASE . 'phone', sanitize_text_field($data['Mobile Phone'])); } if (!empty($data['Birth Date'])) { update_user_meta($user_id, BASE . 'birth_date', sanitize_text_field($data['Birth Date'])); } if (!empty($data['Referral Source'])) { update_user_meta($user_id, BASE . 'referral_source', sanitize_text_field($data['Referral Source'])); } // Store full Jane App data as JSON for reference update_user_meta($user_id, BASE . 'jane_import_data', $data); update_user_meta($user_id, BASE . 'jane_import_date', current_time('mysql')); } /** * Update existing client with Jane App data * * @param int $user_id * @param array $data * @param array $options */ protected function updateExistingClient(int $user_id, array $data, array $options): void { // Update user fields if they're empty $user_data = ['ID' => $user_id]; $current_user = get_user_by('ID', $user_id); if (empty($current_user->first_name) && !empty($data['First Name'])) { $user_data['first_name'] = sanitize_text_field($data['First Name']); } if (empty($current_user->last_name) && !empty($data['Last Name'])) { $user_data['last_name'] = sanitize_text_field($data['Last Name']); } if (count($user_data) > 1) { wp_update_user($user_data); } // Always update meta data $this->storeClientMeta($user_id, $data); } /** * Generate unique username from email * * @param string $email Email address * @return string Unique username */ protected function generateUsername(string $email): string { $base_username = sanitize_user(substr($email, 0, strpos($email, '@'))); $username = $base_username; $counter = 1; while (username_exists($username)) { $username = $base_username . $counter; $counter++; } return $username; } /** * Get client by patient GUID * * @param string $patient_guid Patient GUID * @return object|null Client data or null */ protected function getClientByGuid(string $patient_guid): ?object { return $this->wpdb->get_row($this->wpdb->prepare( "SELECT * FROM {$this->jane_clients_table} WHERE patient_guid = %s", $patient_guid )); } /** * Get client by user ID * * @param int $user_id WordPress user ID * @return object|null Client data or null */ public function getClientByUserId(int $user_id): ?object { return $this->wpdb->get_row($this->wpdb->prepare( "SELECT * FROM {$this->jane_clients_table} WHERE user_id = %d", $user_id )); } /** * Insert new client mapping * * @param array $data Client data * @return int|false Insert ID or false on failure */ protected function insertClientMapping(array $data): int|false { $result = $this->wpdb->insert( $this->jane_clients_table, $data, ['%s', '%d', '%s', '%s', '%s'] ); return $result ? $this->wpdb->insert_id : false; } /** * Update existing client mapping * * @param int $id Mapping ID * @param array $data Updated data * @return bool Success */ protected function updateClientMapping(int $id, array $data): bool { return (bool) $this->wpdb->update( $this->jane_clients_table, $data, ['id' => $id], ['%d', '%s', '%s', '%s'], ['%d'] ); } /** * Get user ID by patient GUID * * @param string $patient_guid Patient GUID * @return int|null User ID or null if not found */ public function getUserIdByGuid(string $patient_guid): ?int { $client = $this->getClientByGuid($patient_guid); return $client ? (int) $client->user_id : null; } /** * Get import statistics * * @return array Import statistics */ public function getImportStats(): array { return $this->import_stats; } }