Files
WooDoo/includes/class-woodoo-admin.php
Malin 38d1352e9a fix: customer search & linking fully implemented
- Add ajax_search_wc_customers() – searches user_login, user_email,
  display_name AND first_name/last_name meta so any query returns hits
- Add ajax_unlink_customer() to remove an existing Odoo link
- Replace placeholder Customers tab with two-panel UI:
  Step 1 search WC customers → Step 2 search Odoo partners independently
  (WC email and Odoo email do not need to match)
- Results table shows current link status; inline Link/Unlink actions
  update the row in-place without page reload
- Admin JS fully wired: both search inputs respond to Enter key and button
- Add two-panel layout CSS and results table styles

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 17:18:01 +02:00

512 lines
25 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Admin: settings page, customer linking, connection test.
*/
defined( 'ABSPATH' ) || exit;
class WooDoo_Admin {
public static function init(): void {
add_action( 'admin_menu', [ __CLASS__, 'add_menu' ] );
add_action( 'admin_init', [ __CLASS__, 'register_settings' ] );
add_action( 'admin_enqueue_scripts', [ __CLASS__, 'enqueue_admin_assets' ] );
// User profile: add Odoo Partner ID field
add_action( 'show_user_profile', [ __CLASS__, 'user_profile_fields' ] );
add_action( 'edit_user_profile', [ __CLASS__, 'user_profile_fields' ] );
add_action( 'personal_options_update',[ __CLASS__, 'save_user_profile_fields' ] );
add_action( 'edit_user_profile_update',[ __CLASS__, 'save_user_profile_fields' ] );
// AJAX handlers (admin only)
add_action( 'wp_ajax_woodoo_test_connection', [ __CLASS__, 'ajax_test_connection' ] );
add_action( 'wp_ajax_woodoo_search_partners', [ __CLASS__, 'ajax_search_partners' ] );
add_action( 'wp_ajax_woodoo_search_wc_customers',[ __CLASS__, 'ajax_search_wc_customers' ] );
add_action( 'wp_ajax_woodoo_link_customer', [ __CLASS__, 'ajax_link_customer' ] );
add_action( 'wp_ajax_woodoo_unlink_customer', [ __CLASS__, 'ajax_unlink_customer' ] );
// WC customer list column
add_filter( 'manage_users_columns', [ __CLASS__, 'add_users_column' ] );
add_filter( 'manage_users_custom_column', [ __CLASS__, 'render_users_column' ], 10, 3 );
}
// ── Menu & Settings ───────────────────────────────────────────────────
public static function add_menu(): void {
add_submenu_page(
'woocommerce',
__( 'WooDoo Odoo Integration', 'woodoo' ),
__( 'Odoo Integration', 'woodoo' ),
'manage_woocommerce',
'woodoo-settings',
[ __CLASS__, 'render_settings_page' ]
);
}
public static function register_settings(): void {
$fields = [
'woodoo_odoo_url' => __( 'Odoo URL (e.g. https://odoo.example.com)', 'woodoo' ),
'woodoo_odoo_db' => __( 'Database Name', 'woodoo' ),
'woodoo_odoo_username' => __( 'Username / Login', 'woodoo' ),
'woodoo_odoo_api_key' => __( 'API Key', 'woodoo' ),
];
foreach ( $fields as $key => $label ) {
register_setting( 'woodoo_settings', $key, [ 'sanitize_callback' => 'sanitize_text_field' ] );
}
// Meeting settings
$meeting_fields = [
'woodoo_meeting_duration' => 30, // minutes
'woodoo_available_days' => [ 1, 2, 3, 4, 5 ], // Mon-Fri
'woodoo_available_from' => '09:00',
'woodoo_available_to' => '17:00',
'woodoo_meeting_title_prefix'=> 'Meeting via WooDoo',
];
foreach ( $meeting_fields as $key => $default ) {
register_setting( 'woodoo_settings', $key );
}
// Order sync toggle
register_setting( 'woodoo_settings', 'woodoo_sync_orders', [ 'sanitize_callback' => 'rest_sanitize_boolean' ] );
}
public static function enqueue_admin_assets( string $hook ): void {
if ( strpos( $hook, 'woodoo-settings' ) === false
&& $hook !== 'user-edit.php'
&& $hook !== 'profile.php' ) {
return;
}
wp_enqueue_style(
'woodoo-admin',
WOODOO_URL . 'assets/css/woodoo-admin.css',
[],
WOODOO_VERSION
);
wp_enqueue_script(
'woodoo-admin',
WOODOO_URL . 'assets/js/woodoo-admin.js',
[ 'jquery' ],
WOODOO_VERSION,
true
);
wp_localize_script( 'woodoo-admin', 'WooDooAdmin', [
'nonce' => wp_create_nonce( 'woodoo_admin' ),
'ajax_url' => admin_url( 'admin-ajax.php' ),
'i18n' => [
'testing' => __( 'Testing…', 'woodoo' ),
'searching' => __( 'Searching…', 'woodoo' ),
'no_wc_customers' => __( 'No customers found.', 'woodoo' ),
'no_odoo_partners' => __( 'No Odoo partners found.', 'woodoo' ),
'link_done' => __( 'Linked!', 'woodoo' ),
'unlink_confirm' => __( 'Remove the Odoo link for this customer?', 'woodoo' ),
'unlinked' => __( 'Unlinked', 'woodoo' ),
'error' => __( 'Error. Check console.', 'woodoo' ),
'search_odoo' => __( 'Search Odoo partners…', 'woodoo' ),
'linked_to' => __( 'Linked to', 'woodoo' ),
'not_linked' => __( 'Not linked', 'woodoo' ),
],
] );
}
// ── Settings Page HTML ────────────────────────────────────────────────
public static function render_settings_page(): void {
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_die( esc_html__( 'Access denied.', 'woodoo' ) );
}
$days_map = [
1 => __( 'Monday', 'woodoo' ),
2 => __( 'Tuesday', 'woodoo' ),
3 => __( 'Wednesday', 'woodoo' ),
4 => __( 'Thursday', 'woodoo' ),
5 => __( 'Friday', 'woodoo' ),
6 => __( 'Saturday', 'woodoo' ),
7 => __( 'Sunday', 'woodoo' ),
];
$saved_days = (array) get_option( 'woodoo_available_days', [ 1, 2, 3, 4, 5 ] );
?>
<div class="wrap woodoo-settings">
<h1><?php esc_html_e( 'WooDoo Odoo Integration', 'woodoo' ); ?></h1>
<nav class="nav-tab-wrapper">
<a href="#tab-connection" class="nav-tab nav-tab-active"><?php esc_html_e( 'Connection', 'woodoo' ); ?></a>
<a href="#tab-meetings" class="nav-tab"><?php esc_html_e( 'Meetings', 'woodoo' ); ?></a>
<a href="#tab-orders" class="nav-tab"><?php esc_html_e( 'Order Sync', 'woodoo' ); ?></a>
<a href="#tab-customers" class="nav-tab"><?php esc_html_e( 'Customers', 'woodoo' ); ?></a>
</nav>
<form method="post" action="options.php">
<?php settings_fields( 'woodoo_settings' ); ?>
<!-- Connection Tab -->
<div id="tab-connection" class="woodoo-tab active">
<h2><?php esc_html_e( 'Odoo Connection', 'woodoo' ); ?></h2>
<table class="form-table">
<tr>
<th><?php esc_html_e( 'Odoo URL', 'woodoo' ); ?></th>
<td>
<input type="url" name="woodoo_odoo_url" value="<?php echo esc_attr( get_option( 'woodoo_odoo_url' ) ); ?>" class="regular-text" placeholder="https://odoo.yourcompany.com">
<p class="description"><?php esc_html_e( 'Include https://, no trailing slash.', 'woodoo' ); ?></p>
</td>
</tr>
<tr>
<th><?php esc_html_e( 'Database Name', 'woodoo' ); ?></th>
<td><input type="text" name="woodoo_odoo_db" value="<?php echo esc_attr( get_option( 'woodoo_odoo_db' ) ); ?>" class="regular-text"></td>
</tr>
<tr>
<th><?php esc_html_e( 'Username', 'woodoo' ); ?></th>
<td><input type="text" name="woodoo_odoo_username" value="<?php echo esc_attr( get_option( 'woodoo_odoo_username' ) ); ?>" class="regular-text" autocomplete="off"></td>
</tr>
<tr>
<th><?php esc_html_e( 'API Key', 'woodoo' ); ?></th>
<td>
<input type="password" name="woodoo_odoo_api_key" value="<?php echo esc_attr( get_option( 'woodoo_odoo_api_key' ) ); ?>" class="regular-text" autocomplete="off">
<p class="description"><?php esc_html_e( 'Generate in Odoo: Settings → Users → Your User → Account Security → New API Key.', 'woodoo' ); ?></p>
</td>
</tr>
</table>
<p>
<button type="button" id="woodoo-test-connection" class="button button-secondary">
<?php esc_html_e( 'Test Connection', 'woodoo' ); ?>
</button>
<span id="woodoo-test-result" style="margin-left:12px;"></span>
</p>
</div>
<!-- Meetings Tab -->
<div id="tab-meetings" class="woodoo-tab" style="display:none;">
<h2><?php esc_html_e( 'Meeting / Booking Settings', 'woodoo' ); ?></h2>
<table class="form-table">
<tr>
<th><?php esc_html_e( 'Slot Duration (minutes)', 'woodoo' ); ?></th>
<td>
<input type="number" name="woodoo_meeting_duration" value="<?php echo esc_attr( get_option( 'woodoo_meeting_duration', 30 ) ); ?>" min="15" max="240" step="15" class="small-text">
</td>
</tr>
<tr>
<th><?php esc_html_e( 'Available Days', 'woodoo' ); ?></th>
<td>
<?php foreach ( $days_map as $num => $label ) : ?>
<label style="margin-right:12px;">
<input type="checkbox" name="woodoo_available_days[]" value="<?php echo esc_attr( $num ); ?>"
<?php checked( in_array( $num, $saved_days, true ) ); ?>>
<?php echo esc_html( $label ); ?>
</label>
<?php endforeach; ?>
</td>
</tr>
<tr>
<th><?php esc_html_e( 'Available From', 'woodoo' ); ?></th>
<td><input type="time" name="woodoo_available_from" value="<?php echo esc_attr( get_option( 'woodoo_available_from', '09:00' ) ); ?>"></td>
</tr>
<tr>
<th><?php esc_html_e( 'Available To', 'woodoo' ); ?></th>
<td><input type="time" name="woodoo_available_to" value="<?php echo esc_attr( get_option( 'woodoo_available_to', '17:00' ) ); ?>"></td>
</tr>
<tr>
<th><?php esc_html_e( 'Meeting Title Prefix', 'woodoo' ); ?></th>
<td><input type="text" name="woodoo_meeting_title_prefix" value="<?php echo esc_attr( get_option( 'woodoo_meeting_title_prefix', 'Meeting via WooDoo' ) ); ?>" class="regular-text"></td>
</tr>
</table>
</div>
<!-- Order Sync Tab -->
<div id="tab-orders" class="woodoo-tab" style="display:none;">
<h2><?php esc_html_e( 'Order → Odoo Sales Order Sync', 'woodoo' ); ?></h2>
<table class="form-table">
<tr>
<th><?php esc_html_e( 'Enable Order Sync', 'woodoo' ); ?></th>
<td>
<label>
<input type="checkbox" name="woodoo_sync_orders" value="1" <?php checked( get_option( 'woodoo_sync_orders', 1 ) ); ?>>
<?php esc_html_e( 'Automatically create a Sales Order in Odoo when a WooCommerce order is placed (status: processing).', 'woodoo' ); ?>
</label>
</td>
</tr>
</table>
<div class="woodoo-info-box">
<strong><?php esc_html_e( 'How it works:', 'woodoo' ); ?></strong>
<ul>
<li><?php esc_html_e( 'When a WooCommerce order reaches "Processing" status, WooDoo finds or creates the customer in Odoo.', 'woodoo' ); ?></li>
<li><?php esc_html_e( 'Products are matched by SKU (default_code). Unmatched items are added as generic lines.', 'woodoo' ); ?></li>
<li><?php esc_html_e( 'The Odoo Sales Order is left in "Quotation" (draft) state, ready for invoicing.', 'woodoo' ); ?></li>
<li><?php esc_html_e( 'The Odoo SO reference is saved as order meta and visible in the WC order screen.', 'woodoo' ); ?></li>
</ul>
</div>
</div>
<!-- Customer Linking Tab -->
<div id="tab-customers" class="woodoo-tab" style="display:none;">
<h2><?php esc_html_e( 'Customer Linking', 'woodoo' ); ?></h2>
<p><?php esc_html_e( 'Search a WooCommerce customer, then search and pick their matching Odoo partner. Emails do not need to match.', 'woodoo' ); ?></p>
<div class="woodoo-linker-panel">
<div class="woodoo-linker-col">
<h3><?php esc_html_e( '1. Find WooCommerce Customer', 'woodoo' ); ?></h3>
<div class="woodoo-search-row">
<input type="text" id="woo-customer-search"
class="regular-text"
placeholder="<?php esc_attr_e( 'Name or email…', 'woodoo' ); ?>">
<button type="button" id="woo-customer-search-btn" class="button button-secondary">
<?php esc_html_e( 'Search', 'woodoo' ); ?>
</button>
</div>
<div id="woo-customer-results" class="woodoo-results-list"></div>
</div>
<div class="woodoo-linker-col" id="woodoo-odoo-col" style="display:none;">
<h3><?php esc_html_e( '2. Find Odoo Partner', 'woodoo' ); ?></h3>
<p id="woodoo-linking-for" class="woodoo-linking-for"></p>
<div class="woodoo-search-row">
<input type="text" id="woodoo-odoo-partner-search"
class="regular-text"
placeholder="<?php esc_attr_e( 'Name or email…', 'woodoo' ); ?>">
<button type="button" id="woodoo-odoo-partner-search-btn" class="button button-secondary">
<?php esc_html_e( 'Search', 'woodoo' ); ?>
</button>
</div>
<div id="woodoo-odoo-partner-results" class="woodoo-results-list"></div>
</div>
</div>
</div>
<?php submit_button(); ?>
</form>
</div>
<?php
}
// ── User Profile Fields ───────────────────────────────────────────────
public static function user_profile_fields( WP_User $user ): void {
if ( ! current_user_can( 'edit_users' ) ) return;
$partner_id = get_user_meta( $user->ID, 'woodoo_odoo_partner_id', true );
$partner_name = get_user_meta( $user->ID, 'woodoo_odoo_partner_name', true );
?>
<h2><?php esc_html_e( 'Odoo Integration (WooDoo)', 'woodoo' ); ?></h2>
<table class="form-table">
<tr>
<th><?php esc_html_e( 'Odoo Partner ID', 'woodoo' ); ?></th>
<td>
<input type="number" id="woodoo_odoo_partner_id" name="woodoo_odoo_partner_id"
value="<?php echo esc_attr( $partner_id ); ?>"
class="small-text" min="1">
<?php if ( $partner_name ) : ?>
<span id="woodoo-partner-name-display" style="margin-left:8px;color:#666;">
<?php echo esc_html( $partner_name ); ?>
</span>
<?php endif; ?>
<p class="description">
<?php esc_html_e( 'Leave blank to auto-match by email on next sync. Or type an ID and the name will resolve automatically.', 'woodoo' ); ?>
</p>
<input type="hidden" name="woodoo_nonce" value="<?php echo esc_attr( wp_create_nonce( 'woodoo_user_profile_' . $user->ID ) ); ?>">
<p>
<strong><?php esc_html_e( 'Search Odoo partners:', 'woodoo' ); ?></strong><br>
<input type="text" id="woodoo-partner-search-input" class="regular-text"
placeholder="<?php esc_attr_e( 'Name or email…', 'woodoo' ); ?>"
data-user-id="<?php echo esc_attr( $user->ID ); ?>">
<button type="button" id="woodoo-partner-search-btn" class="button button-secondary" style="margin-left:4px;">
<?php esc_html_e( 'Search', 'woodoo' ); ?>
</button>
<div id="woodoo-partner-search-results" style="margin-top:8px;"></div>
</p>
</td>
</tr>
<?php
$odoo_so_count = get_user_meta( $user->ID, 'woodoo_so_count', true );
if ( $odoo_so_count ) : ?>
<tr>
<th><?php esc_html_e( 'Odoo Sales Orders', 'woodoo' ); ?></th>
<td>
<span><?php echo esc_html( $odoo_so_count ); ?> <?php esc_html_e( 'orders synced to Odoo', 'woodoo' ); ?></span>
</td>
</tr>
<?php endif; ?>
</table>
<?php
}
public static function save_user_profile_fields( int $user_id ): void {
if ( ! isset( $_POST['woodoo_nonce'] )
|| ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['woodoo_nonce'] ) ), 'woodoo_user_profile_' . $user_id )
) {
return;
}
if ( ! current_user_can( 'edit_user', $user_id ) ) return;
$partner_id = isset( $_POST['woodoo_odoo_partner_id'] )
? absint( $_POST['woodoo_odoo_partner_id'] )
: 0;
if ( $partner_id > 0 ) {
update_user_meta( $user_id, 'woodoo_odoo_partner_id', $partner_id );
// Resolve name from Odoo
$api = woodoo_api();
if ( $api ) {
$partners = $api->read( 'res.partner', [ $partner_id ], [ 'name', 'email' ] );
if ( ! empty( $partners[0]['name'] ) ) {
update_user_meta( $user_id, 'woodoo_odoo_partner_name', sanitize_text_field( $partners[0]['name'] ) );
}
}
} else {
delete_user_meta( $user_id, 'woodoo_odoo_partner_id' );
delete_user_meta( $user_id, 'woodoo_odoo_partner_name' );
}
}
// ── Users List Column ─────────────────────────────────────────────────
public static function add_users_column( array $columns ): array {
$columns['woodoo_odoo'] = __( 'Odoo Partner', 'woodoo' );
return $columns;
}
public static function render_users_column( string $value, string $column, int $user_id ): string {
if ( $column !== 'woodoo_odoo' ) return $value;
$partner_id = get_user_meta( $user_id, 'woodoo_odoo_partner_id', true );
$partner_name = get_user_meta( $user_id, 'woodoo_odoo_partner_name', true );
if ( $partner_id ) {
return '<span style="color:#2271b1;">' . esc_html( $partner_name ?: "#$partner_id" ) . '</span>';
}
return '<span style="color:#999;">—</span>';
}
// ── AJAX Handlers ─────────────────────────────────────────────────────
public static function ajax_test_connection(): void {
check_ajax_referer( 'woodoo_admin', 'nonce' );
if ( ! current_user_can( 'manage_woocommerce' ) ) wp_send_json_error( 'Forbidden', 403 );
$api = woodoo_api();
if ( ! $api ) {
wp_send_json_error( 'Credentials not saved yet. Save settings first.' );
}
wp_send_json_success( $api->test_connection() );
}
public static function ajax_search_partners(): void {
check_ajax_referer( 'woodoo_admin', 'nonce' );
if ( ! current_user_can( 'edit_users' ) ) wp_send_json_error( 'Forbidden', 403 );
$q = sanitize_text_field( wp_unslash( $_POST['query'] ?? '' ) );
$api = woodoo_api();
if ( ! $api || empty( $q ) ) wp_send_json_success( [] );
$results = $api->search_read(
'res.partner',
[ '|', [ 'name', 'ilike', $q ], [ 'email', 'ilike', $q ] ],
[ 'id', 'name', 'email', 'is_company' ],
10
);
wp_send_json_success( $results );
}
public static function ajax_link_customer(): void {
check_ajax_referer( 'woodoo_admin', 'nonce' );
if ( ! current_user_can( 'edit_users' ) ) wp_send_json_error( 'Forbidden', 403 );
$user_id = absint( $_POST['user_id'] ?? 0 );
$partner_id = absint( $_POST['partner_id'] ?? 0 );
if ( ! $user_id || ! $partner_id ) wp_send_json_error( 'Invalid IDs' );
$api = woodoo_api();
if ( ! $api ) wp_send_json_error( 'API not configured' );
$partners = $api->read( 'res.partner', [ $partner_id ], [ 'name', 'email' ] );
if ( empty( $partners ) ) wp_send_json_error( 'Partner not found in Odoo' );
update_user_meta( $user_id, 'woodoo_odoo_partner_id', $partner_id );
update_user_meta( $user_id, 'woodoo_odoo_partner_name', sanitize_text_field( $partners[0]['name'] ) );
wp_send_json_success( [
'partner_id' => $partner_id,
'partner_name' => $partners[0]['name'],
] );
}
public static function ajax_unlink_customer(): void {
check_ajax_referer( 'woodoo_admin', 'nonce' );
if ( ! current_user_can( 'edit_users' ) ) wp_send_json_error( 'Forbidden', 403 );
$user_id = absint( $_POST['user_id'] ?? 0 );
if ( ! $user_id ) wp_send_json_error( 'Invalid user ID' );
delete_user_meta( $user_id, 'woodoo_odoo_partner_id' );
delete_user_meta( $user_id, 'woodoo_odoo_partner_name' );
wp_send_json_success( [ 'user_id' => $user_id ] );
}
/**
* Search WooCommerce customers by name or email.
* Searches user_login, user_email, display_name in the users table,
* AND first_name / last_name in user meta — so the email in WC doesn't
* need to match Odoo at all.
*/
public static function ajax_search_wc_customers(): void {
check_ajax_referer( 'woodoo_admin', 'nonce' );
if ( ! current_user_can( 'edit_users' ) ) wp_send_json_error( 'Forbidden', 403 );
$q = sanitize_text_field( wp_unslash( $_POST['query'] ?? '' ) );
if ( strlen( $q ) < 2 ) wp_send_json_error( 'Query too short' );
// Search main user table columns
$query1 = new WP_User_Query( [
'search' => '*' . $q . '*',
'search_columns' => [ 'user_login', 'user_email', 'display_name' ],
'number' => 20,
'fields' => 'all',
] );
// Search first_name / last_name meta fields
$query2 = new WP_User_Query( [
'number' => 20,
'fields' => 'all',
'meta_query' => [
'relation' => 'OR',
[ 'key' => 'first_name', 'value' => $q, 'compare' => 'LIKE' ],
[ 'key' => 'last_name', 'value' => $q, 'compare' => 'LIKE' ],
],
] );
// Merge and deduplicate
$all = array_merge( $query1->get_results(), $query2->get_results() );
$seen = [];
$users = [];
foreach ( $all as $user ) {
if ( isset( $seen[ $user->ID ] ) ) continue;
$seen[ $user->ID ] = true;
$users[] = $user;
}
if ( empty( $users ) ) {
wp_send_json_success( [] );
}
$results = [];
foreach ( $users as $user ) {
$partner_id = (int) get_user_meta( $user->ID, 'woodoo_odoo_partner_id', true );
$partner_name = get_user_meta( $user->ID, 'woodoo_odoo_partner_name', true );
$results[] = [
'id' => $user->ID,
'display_name' => $user->display_name,
'email' => $user->user_email,
'partner_id' => $partner_id ?: null,
'partner_name' => $partner_name ?: null,
'edit_url' => esc_url( get_edit_user_link( $user->ID ) ),
];
}
wp_send_json_success( $results );
}
}