load_services(); } private function load_services() { $base = AIEO_DIR . 'includes/dmm/google-reviews/'; foreach (array( 'class-aieo-google-reviews-settings.php', 'class-aieo-google-reviews-locations.php', 'class-aieo-google-reviews-cache.php', 'class-aieo-google-reviews-place-lookup.php', 'class-aieo-google-reviews-fetcher.php', 'class-aieo-google-reviews-cron.php', 'class-aieo-google-reviews-helpers.php', 'class-aieo-google-reviews-block.php', 'class-aieo-store-info-block.php', 'class-aieo-store-locator-block.php', 'class-aieo-opening-hours-block.php', 'class-aieo-business-info-block.php', 'class-aieo-store-hero-block.php', 'class-aieo-store-map-block.php', 'class-aieo-store-templates.php', 'class-aieo-store-cpt.php', 'class-aieo-store-migration.php', ) as $f) { $path = $base . $f; if (file_exists($path)) { require_once $path; } } if (class_exists('AIEO_Google_Reviews_Cron')) { AIEO_Google_Reviews_Cron::init(); } if (class_exists('AIEO_Store_CPT')) { AIEO_Store_CPT::register(); } if ( ! defined( 'AIEO_RUNTIME_OWNS_AJAX' ) ) { if (class_exists('AIEO_Google_Reviews_Block')) { AIEO_Google_Reviews_Block::register(); } if (class_exists('AIEO_Store_Info_Block')) { AIEO_Store_Info_Block::register(); } if (class_exists('AIEO_Store_Locator_Block')) { AIEO_Store_Locator_Block::register(); } if (class_exists('AIEO_Opening_Hours_Block')) { AIEO_Opening_Hours_Block::register(); } if (class_exists('AIEO_Business_Info_Block')) { AIEO_Business_Info_Block::register(); } if (class_exists('AIEO_Store_Hero_Block')) { AIEO_Store_Hero_Block::register(); } if (class_exists('AIEO_Store_Map_Block')) { AIEO_Store_Map_Block::register(); } } } public function enqueue_admin_assets() { wp_enqueue_style('aieo-google-reviews-admin', AIEO_URI . 'assets/css/aieo-google-reviews-admin.min.css', array(), AIEO_VERSION); } public function register_hooks() { add_action('wp_ajax_aieo_grv_save_settings', array($this, 'ajax_save_settings')); add_action('wp_ajax_aieo_grv_save_location', array($this, 'ajax_save_location')); add_action('wp_ajax_aieo_grv_delete_location', array($this, 'ajax_delete_location')); add_action('wp_ajax_aieo_grv_lookup_place', array($this, 'ajax_lookup_place')); add_action('wp_ajax_aieo_grv_fetch_now', array($this, 'ajax_fetch_now')); add_action('wp_ajax_aieo_grv_fetch_all', array($this, 'ajax_fetch_all')); add_action('rest_api_init', array($this, 'register_order_route')); add_filter('aieo_report_bulk', array($this, 'report_bulk'), 10, 3); } public function report_bulk($result, $ctx, $def) { unset($def); if (false !== $result || 'google_reviews_history' !== (string) ($ctx['report'] ?? '') || !in_array((string) ($ctx['action'] ?? ''), array('hide', 'show'), true)) { return $result; } if (!$this->can_manage()) { return __('You do not have permission to change the reviews.', 'ai-eshop-optimizer'); } $ids = array_values(array_unique(array_filter(array_map('absint', (array) ($ctx['ids'] ?? array()))))); if (empty($ids)) { return array('updated' => 0, 'skipped' => 0); } global $wpdb; $t = $wpdb->prefix . 'aieo_google_reviews'; $visible = 'show' === (string) $ctx['action'] ? 1 : 0; $in = implode(',', array_fill(0, count($ids), '%d')); $locations = $wpdb->get_results($wpdb->prepare("SELECT DISTINCT r.location_id, l.slug FROM `{$t}` r LEFT JOIN `{$wpdb->prefix}aieo_google_locations` l ON l.id = r.location_id WHERE r.id IN ({$in})", $ids), ARRAY_A); $changed = (int) $wpdb->query($wpdb->prepare("UPDATE `{$t}` SET is_visible = %d WHERE is_visible <> %d AND id IN ({$in})", array_merge(array($visible, $visible), $ids))); if (class_exists('AIEO_Google_Reviews_Cache')) { foreach ((array) $locations as $loc) { AIEO_Google_Reviews_Cache::bump_version((int) $loc['location_id']); if (!empty($loc['slug'])) { AIEO_Google_Reviews_Cache::bump_version((string) $loc['slug']); } } } return array('updated' => $changed, 'skipped' => count($ids) - $changed); } public function register_order_route() { register_rest_route('aieo/v1', '/grv-locations-order', array( 'methods' => 'POST', 'permission_callback' => function () { return $this->can_manage(); }, 'callback' => array($this, 'rest_save_order'), )); } public function rest_save_order($request) { $params = (array) $request->get_json_params(); $order = isset($params['order']) && is_array($params['order']) ? array_map('intval', $params['order']) : array(); $known = array_map(static function ($l) { return (int) $l['id']; }, AIEO_Google_Reviews_Locations::list_all()); if (count($order) !== count($known) || array_diff($order, $known) || count(array_unique($order)) !== count($order)) { return new WP_Error('aieo_grv_order', __('The list of locations changed. Reload the page and try again.', 'ai-eshop-optimizer'), array('status' => 409)); } global $wpdb; $table = AIEO_Google_Reviews_Locations::table(); foreach ($order as $i => $rid) { $wpdb->update($table, array('sort_order' => $i + 1), array('id' => (int) $rid)); } return new WP_REST_Response(array('order' => $order), 200); } public function get_tab_arg() { return 'grv_section'; } public function get_tabs() { return array( 'locations' => array('label' => __('Locations', 'ai-eshop-optimizer'), 'icon' => 'map-pin'), 'history' => array('label' => __('Review History', 'ai-eshop-optimizer'), 'icon' => 'clock'), 'settings' => array('label' => __('Settings', 'ai-eshop-optimizer'), 'icon' => 'settings'), ); } public function render_subtab() { $tabs = $this->get_tabs(); $section = isset($_GET['grv_section']) ? sanitize_key((string) $_GET['grv_section']) : 'locations'; if (!isset($tabs[$section])) { $section = 'locations'; } $base_url = admin_url('admin.php?page=' . AIEO_DMM::MENU_SLUG . '&dmm_tab=google_reviews'); AIEO_DMM_UI::tab_strip($this->get_id(), $tabs, $section, array( 'arg' => $this->get_tab_arg(), 'wrapper' => 'div', 'margin' => '14px', )); if ($section === 'locations') { $this->render_locations(); } if ($section === 'history') { $this->render_history(); } if ($section === 'settings') { $this->render_settings(); } } private function render_locations() { AIEO_Google_Reviews_Locations::ensure_contact_columns(); $locations = AIEO_Google_Reviews_Locations::list_all(); $settings = AIEO_Google_Reviews_Settings::get_all(); $nonce = wp_create_nonce('aieo_grv_locations'); $ajax = admin_url('admin-ajax.php'); $has_global_key = trim((string) $settings['global_api_key']) !== ''; $enabled_count = 0; foreach ($locations as $l) { if (!empty($l['enabled'])) { $enabled_count++; } } list($grv_open, $grv_close) = AIEO_DMM_UI::card(array( 'anchor' => 'google-reviews-overview', 'icon' => 'map-pin', 'title' => __( 'Locations', 'ai-eshop-optimizer' ), 'description' => __('Pull Google reviews per location into a local table, surface them via a block pattern, and keep a permanent history (Google\'s Places API only ever returns the latest 5 — we diff each fetch and append unseen reviews so nothing is lost over time).', 'ai-eshop-optimizer'), 'flush' => true, )); echo $grv_open; echo '
'; $grv_fetch_all = \Rooster\Admin\Ui::button( sprintf( __('Fetch all enabled (%d)', 'ai-eshop-optimizer'), $enabled_count ), array('variant' => 'primary', 'attrs' => array('id' => 'aieo-grv-fetch-all') + \Rooster\Admin\States::confirm_attrs('aieo-grv-fetch-all-dialog')) + ($enabled_count > 0 ? array() : array('reason' => __('No enabled locations yet.', 'ai-eshop-optimizer'))) ); echo $grv_fetch_all; $grv_fetch_all_ask = \Rooster\Admin\States::confirm( 'aieo-grv-fetch-all-dialog', 'apply', __('Fetch from Google for every enabled location? This consumes one paid API call per location.', 'ai-eshop-optimizer'), '

' . esc_html(sprintf( __('Fetch all enabled (%d)', 'ai-eshop-optimizer'), $enabled_count )) . '

', array('confirm' => __('Fetch now', 'ai-eshop-optimizer')) ); echo $grv_fetch_all_ask; echo '' . esc_html__('Hits Google once per enabled location and writes any new reviews to the local table.', 'ai-eshop-optimizer') . ''; echo '
'; $grv_rows = array(); foreach ($locations as $l) { $pid = (int) ($l['branch_id'] ?? 0); $grv_rows[] = array( 'id' => (int) $l['id'], 'sort_order' => (int) ($l['sort_order'] ?? 0), 'label' => (string) $l['label'], 'slug' => (string) $l['slug'], 'place_id' => (string) $l['place_id'], 'place_note' => (0 === strpos((string) $l['place_id'], 'ChIJ')) ? '' : __('Not a ChIJ id — resolve before fetching', 'ai-eshop-optimizer'), 'language' => (string) $l['language'], 'rating' => (null !== $l['baseline_rating']) ? (float) $l['baseline_rating'] : null, 'reviews' => (null !== $l['baseline_review_count']) ? (int) $l['baseline_review_count'] : null, 'last_fetch' => (string) ($l['last_fetched_at'] ?? ''), 'fetch_note' => ('' !== (string) ($l['last_fetch_status'] ?? '') && 'OK' !== (string) $l['last_fetch_status']) ? (string) $l['last_fetch_status'] : '', 'stock' => $this->location_stock_cell($l), 'enabled' => ! empty($l['enabled']), ); } AIEO_Table::render(array( 'id' => 'aieo-grv-locations', 'page_id' => $this->get_id(), 'row_key' => 'id', 'rows' => $grv_rows, 'empty' => __('No locations yet. Add one below.', 'ai-eshop-optimizer'), 'per_page' => 0, 'chooser' => true, 'move' => array( 'label' => __('Locations', 'ai-eshop-optimizer'), 'save' => array('url' => rest_url('aieo/v1/grv-locations-order'), 'nonce' => wp_create_nonce('wp_rest')), ), 'columns' => array( array('key' => 'label', 'label' => __('Location', 'ai-eshop-optimizer'), 'type' => 'text', 'subline' => 'slug'), array( 'key' => 'place_id', 'label' => __('place_id', 'ai-eshop-optimizer'), 'type' => 'code', 'truncate' => 28, 'tooltip' => true, 'subline' => 'place_note', 'width' => '210px', ), array('key' => 'language', 'label' => __('Lang', 'ai-eshop-optimizer'), 'type' => 'code', 'width' => '60px'), array( 'key' => 'rating', 'label' => __('Rating', 'ai-eshop-optimizer'), 'type' => 'decimal', 'width' => '80px', 'format' => array('precision' => 1), 'empty' => '—', ), array('key' => 'reviews', 'label' => __('Reviews', 'ai-eshop-optimizer'), 'type' => 'integer', 'width' => '80px'), array( 'key' => 'last_fetch', 'label' => __('Last fetch', 'ai-eshop-optimizer'), 'type' => 'datetime', 'width' => '150px', 'nowrap' => true, 'tone' => 'secondary', 'subline' => 'fetch_note', 'format' => array('tz' => 'utc'), 'empty' => '—', ), array( 'key' => 'stock', 'label' => __('Stock', 'ai-eshop-optimizer'), 'type' => 'html', 'width' => '150px', ), array( 'key' => 'enabled', 'label' => __('Enabled', 'ai-eshop-optimizer'), 'type' => 'boolean', 'width' => '80px', 'align' => 'center', ), ), 'actions' => array( array('id' => 'edit', 'label' => __('Edit', 'ai-eshop-optimizer')), array('id' => 'fetch', 'label' => __('Fetch now', 'ai-eshop-optimizer'), 'variant' => 'primary'), array( 'id' => 'delete', 'label' => __('Delete', 'ai-eshop-optimizer'), 'variant' => 'ghost', 'destructive' => true, 'confirm_noun' => __('location', 'ai-eshop-optimizer'), 'confirm' => __('Its review history stays in the table.', 'ai-eshop-optimizer'), ), ), )); echo \Rooster\Admin\States::outcome('', '', array('id' => 'aieo-grv-action-status')); echo $grv_close; wp_enqueue_media(); $form_id = 'aieo-card-google-reviews-add-a-location-form'; $id_of = static function ($name) { return 'aieo-grv-loc-' . str_replace('_', '-', $name); }; $row = static function ($name, $label, $control, $help = '', $extra = array()) use ($id_of) { $field = array('id' => $id_of($name), 'label' => $label, 'control' => $control) + $extra; if ('' !== $help) { $field['help'] = $help; } return \Rooster\Admin\Ui::field_row($field); }; $text = static function ($name, $opts = array()) use ($id_of) { return \Rooster\Admin\Ui::input($name, '', $opts + array('id' => $id_of($name))); }; $switch = static function ($name, $on, $label) use ($id_of) { return \Rooster\Admin\Ui::toggle($name, $on, array('id' => $id_of($name), 'hidden' => false, 'label' => $label)); }; $fold = static function ($summary, $guide, $rows, $after = '', $question = '') { return \Rooster\Admin\Ui::fold( $summary, \Rooster\Admin\Guidance::text($guide, 'guidance', array('question' => $question)) . '
' . $rows . '
' . $after ); }; $rows = $row('slug', __('Slug', 'ai-eshop-optimizer'), $text('slug', array('medium' => true, 'code' => true, 'required' => true, 'pattern' => '[a-z0-9_-]+', 'placeholder' => __('e.g. main-store, downtown, branch-2', 'ai-eshop-optimizer'))), __('URL-safe identifier, used internally + in block-pattern attributes. Letters, numbers, dash and underscore only.', 'ai-eshop-optimizer'), array('required' => true)); $rows .= $row('label', __('Display name', 'ai-eshop-optimizer'), $text('label', array('medium' => true, 'required' => true, 'placeholder' => __('Customer-facing label for this location', 'ai-eshop-optimizer'))), '', array('required' => true)); $rows .= $row('place_id', __('place_id', 'ai-eshop-optimizer'), $text('place_id', array('medium' => true, 'code' => true, 'required' => true, 'placeholder' => 'ChIJ…')), __('Paste the ChIJ identifier — or use the lookup helper below.', 'ai-eshop-optimizer'), array('required' => true)); $lookup = array( 'id' => 'aieo-grv-lookup-query', 'label' => __('Lookup place_id', 'ai-eshop-optimizer'), 'control' => \Rooster\Admin\Ui::input('', '', array('id' => 'aieo-grv-lookup-query', 'medium' => true, 'placeholder' => __('Business name, address, or Google Maps / g.page URL', 'ai-eshop-optimizer'))) . ' ' . \Rooster\Admin\Ui::button(__('Resolve', 'ai-eshop-optimizer'), array('attrs' => array('id' => 'aieo-grv-lookup-btn')) + ($has_global_key ? array() : array('reason' => __('Disabled — set the global API key under Settings first.', 'ai-eshop-optimizer')))) . ' ' . \Rooster\Admin\States::outcome('', '', array('id' => 'aieo-grv-lookup-status')), ); if ($has_global_key) { $lookup['help'] = __('Hits Google\'s findplacefromtext (~$0.017 per call). On success, the place_id field above is auto-filled.', 'ai-eshop-optimizer'); } $rows .= \Rooster\Admin\Ui::field_row($lookup); $key_ph = __('Leave empty to use the global key', 'ai-eshop-optimizer'); $key_box = \Rooster\Admin\Fields::secret('api_key', false, array('id' => $id_of('api_key'), 'placeholder' => $key_ph, 'attrs' => array('data-placeholder' => $key_ph))) . ' '; $rows .= $row('api_key', __('Per-location API key', 'ai-eshop-optimizer'), $key_box, __('Optional. Useful when you want this location\'s fetches billed to a different Google Cloud project.', 'ai-eshop-optimizer')); $rows .= $row('review_url', __('"Rate us" URL', 'ai-eshop-optimizer'), $text('review_url', array('code' => true, 'placeholder' => 'https://g.page/r/…/review')), __('Optional. Paste the g.page/r/.../review shortlink Google issued for in-store QR codes. If empty, we auto-construct https://search.google.com/local/writereview?placeid=… from the place_id.', 'ai-eshop-optimizer')); $rows .= $row('language', __('Language', 'ai-eshop-optimizer'), \Rooster\Admin\Ui::input('language', (string) $settings['language'], array('id' => $id_of('language'), 'short' => true, 'maxlength' => 10))); $rows .= $row('enabled', __('Enabled', 'ai-eshop-optimizer'), $switch('enabled', true, __('Include in scheduled fetches', 'ai-eshop-optimizer'))); $rows .= $row('sort_order', __('Sort order', 'ai-eshop-optimizer'), \Rooster\Admin\Ui::input('sort_order', '0', array('id' => $id_of('sort_order'), 'type' => 'number', 'xs' => true, 'min' => 0, 'step' => 1)), __('Lower numbers appear first on the store locator (and in the reviews block / schema). Default 0.', 'ai-eshop-optimizer')); $pickup_names = $this->pickup_location_names(); $pickup_opts = array('' => __('— none —', 'ai-eshop-optimizer')); foreach ($pickup_names as $pn) { $pickup_opts[$pn] = $pn; } $rows .= $row('pickup_name', __('Associated pickup location', 'ai-eshop-optimizer'), \Rooster\Admin\Ui::select('pickup_name', $pickup_opts, '', array('id' => $id_of('pickup_name'))), '', array( 'help_html' => (empty($pickup_names) ? esc_html__('No pickup locations defined yet — add them in the Local Pickup tab first.', 'ai-eshop-optimizer') . ' ' : '') . esc_html__('Link this Google location to a store pickup. The pickup then shows a "View on Google Maps" link at checkout and in the order email. Leave empty if this location has no pickup.', 'ai-eshop-optimizer'), )); $rows .= $row('stock_scanner', __('Stock-taking', 'ai-eshop-optimizer'), $switch('stock_scanner', false, __('This store keeps its own stock — create its stock board and barcode-scanner page', 'ai-eshop-optimizer')), __('Saving with this on provisions everything the Branch Stock Scanner needs for this store: the stock-board table, its audit and pre-order triggers, a seed of your catalogue so the first scan already finds its product, and a published scanner page locked to this branch (staff-only — the page shows nothing to anyone else). Unticking disables the branch but keeps its table, history and page.', 'ai-eshop-optimizer')); $rows .= $row('is_hq', __('Headquarters', 'ai-eshop-optimizer'), $switch('is_hq', false, __('This is the HQ / primary location — its address backs the Organization schema (only one location can be HQ).', 'ai-eshop-optimizer'))); $address = $row('street', __('Street address', 'ai-eshop-optimizer'), $text('street', array('medium' => true))); $address .= $row('locality', __('City', 'ai-eshop-optimizer'), $text('locality', array('medium' => true))); $address .= $row('region', __('Region', 'ai-eshop-optimizer'), $text('region', array('medium' => true))); $address .= $row('postcode', __('Postcode', 'ai-eshop-optimizer'), $text('postcode', array('short' => true))); $address .= $row('country', __('Country', 'ai-eshop-optimizer'), $text('country', array('xs' => true, 'maxlength' => 2, 'placeholder' => 'GR'))); $address .= $row('phone', __('Phone', 'ai-eshop-optimizer'), $text('phone', array('short' => true, 'type' => 'tel', 'placeholder' => __('Phone (+30…)', 'ai-eshop-optimizer')))); $address .= $row('lat', __('Latitude', 'ai-eshop-optimizer'), $text('lat', array('short' => true, 'placeholder' => 'lat'))); $address .= $row('lng', __('Longitude', 'ai-eshop-optimizer'), $text('lng', array('short' => true, 'placeholder' => 'lng'))); $folds = $fold(__('Address and contact', 'ai-eshop-optimizer'), __('Auto-filled from Google when you fetch this location — edit to override. Click "Fetch now" on a saved location to populate these.', 'ai-eshop-optimizer'), $address, '

', __('Where do these come from?', 'ai-eshop-optimizer')); $channels = $row('email', __('Email', 'ai-eshop-optimizer'), $text('email', array('medium' => true, 'type' => 'email', 'placeholder' => __('store@example.com', 'ai-eshop-optimizer'))), __('The entries of a form placed for this location (its form button set to it) are also mailed here. Visitors do not see it.', 'ai-eshop-optimizer')); $channels .= $row('whatsapp', __('WhatsApp', 'ai-eshop-optimizer'), $text('whatsapp', array('medium' => true, 'placeholder' => __('+30 69… or https://wa.me/…', 'ai-eshop-optimizer'))), __('The mobile number WhatsApp runs on (with its country code, or the location\'s country is assumed), or its wa.me link.', 'ai-eshop-optimizer')); $channels .= $row('viber', __('Viber', 'ai-eshop-optimizer'), $text('viber', array('medium' => true, 'placeholder' => __('+30 69… or viber://…', 'ai-eshop-optimizer'))), __('The mobile number Viber runs on, or its viber:// link.', 'ai-eshop-optimizer')); $channels .= $row('messenger', __('Messenger', 'ai-eshop-optimizer'), $text('messenger', array('medium' => true, 'placeholder' => __('m.me/yourpage', 'ai-eshop-optimizer'))), __('Your Facebook page\'s m.me link, or the page\'s name.', 'ai-eshop-optimizer')); $folds .= $fold(__('Contact channels', 'ai-eshop-optimizer'), __('Yours to set: Google does not return these. A form button can show a button to call (the phone above), and one for each chat app set here.', 'ai-eshop-optimizer'), $channels, '', __('What do the channels do?', 'ai-eshop-optimizer')); $pr_sym = function_exists( 'get_woocommerce_currency_symbol' ) ? html_entity_decode( get_woocommerce_currency_symbol(), ENT_QUOTES, 'UTF-8' ) : '€'; $pr_opts = array('' => __('— not set —', 'ai-eshop-optimizer')); foreach ( array( 1 => __( '%s — Budget', 'ai-eshop-optimizer' ), 2 => __( '%s — Moderate', 'ai-eshop-optimizer' ), 3 => __( '%s — Pricey', 'ai-eshop-optimizer' ), 4 => __( '%s — High-end', 'ai-eshop-optimizer' ), ) as $pr_n => $pr_tpl ) { $pr_val = str_repeat( $pr_sym, $pr_n ); $pr_opts[ $pr_val ] = sprintf( $pr_tpl, $pr_val ); } $extras = $row('price_range', __('Price range', 'ai-eshop-optimizer'), \Rooster\Admin\Ui::select('price_range', $pr_opts, '', array('id' => $id_of('price_range'))), __('Google-recognised price tier (schema.org priceRange).', 'ai-eshop-optimizer')); $extras .= $row('gbp_url', __('Google Business Profile / Maps URL (sameAs)', 'ai-eshop-optimizer'), $text('gbp_url', array('type' => 'url', 'code' => true, 'placeholder' => __('Leave blank to auto-derive from place_id', 'ai-eshop-optimizer')))); $extras .= \Rooster\Admin\Ui::field_row(array( 'id' => 'aieo-grv-image-url', 'label' => __('Storefront image', 'ai-eshop-optimizer'), 'control' => \Rooster\Admin\Ui::input('image_url', '', array('id' => 'aieo-grv-image-url', 'type' => 'url', 'medium' => true, 'code' => true, 'placeholder' => 'https://…')) . ' ' . \Rooster\Admin\Ui::button(__('Select / upload image', 'ai-eshop-optimizer'), array('attrs' => array('id' => 'aieo-grv-image-pick'))) . '
', )); $folds .= $fold(__('Schema extras', 'ai-eshop-optimizer'), __('Operator-set — Google\'s API does not return these.', 'ai-eshop-optimizer'), $extras, '', __('Who sets these?', 'ai-eshop-optimizer')); $hero = $row('listing_title', __('Listing title (top)', 'ai-eshop-optimizer'), $text('listing_title', array('medium' => true, 'maxlength' => 190, 'placeholder' => __('e.g. Our store in Glyfada', 'ai-eshop-optimizer')))); $hero .= $row('hero_heading', __('Heading (left)', 'ai-eshop-optimizer'), $text('hero_heading', array('medium' => true, 'maxlength' => 190, 'placeholder' => __('Leave blank to use the location label', 'ai-eshop-optimizer')))); $hero .= $row('hero_description', __('Description (right)', 'ai-eshop-optimizer'), \Rooster\Admin\Ui::textarea('hero_description', '', array('id' => $id_of('hero_description'), 'rows' => 3, 'placeholder' => __('A short welcome / what makes this store special.', 'ai-eshop-optimizer')))); $folds .= $fold(__('Shop-page hero', 'ai-eshop-optimizer'), __('Header for this location\'s store page (the aieo/store-hero block / "Shop page" pattern). Listing title plays top, heading left, description right — over the storefront image above.', 'ai-eshop-optimizer'), $hero, '', __('Where does it show?', 'ai-eshop-optimizer')); list($grv_open, $grv_close) = AIEO_DMM_UI::card(array( 'anchor' => 'google-reviews-add-a-location', 'icon' => 'plus', 'title' => __('Add a location', 'ai-eshop-optimizer'), 'description' => __('Connect one of your shops to its Google listing, so its reviews are fetched and can be shown.', 'ai-eshop-optimizer'), 'footer' => \Rooster\Admin\States::save_button($form_id, array('label' => __('Save location', 'ai-eshop-optimizer'))) . ' ' . \Rooster\Admin\Ui::button(__('Reset form', 'ai-eshop-optimizer'), array('attrs' => array('id' => 'aieo-grv-cancel-loc'))), )); echo $grv_open; $grv_form = AIEO_DMM_UI::card_form('aieo_grv_save_location', 'google_reviews_location', $form_id, '
' . $rows . '
' . $folds, array('nonce' => $nonce, 'nonce_name' => 'nonce')); echo $grv_form; echo $grv_close; $locations_json = wp_json_encode(array_map(function($l) { return array( 'id' => (int) $l['id'], 'slug' => (string) $l['slug'], 'label' => (string) $l['label'], 'place_id' => (string) $l['place_id'], 'has_api_key' => '' !== (string) ($l['api_key'] ?? ''), 'language' => (string) $l['language'], 'enabled' => (int) $l['enabled'], 'review_url' => isset($l['review_url']) ? (string) $l['review_url'] : '', 'pickup_name'=> $this->pickup_for_review_slug((string) $l['slug']), 'stock_scanner' => $this->location_branch_active($l) ? 1 : 0, 'is_hq' => (int) ($l['is_hq'] ?? 0), 'price_range'=> (string) ($l['price_range'] ?? ''), 'gbp_url' => (string) ($l['gbp_url'] ?? ''), 'image_url' => (string) ($l['image_url'] ?? ''), 'street' => (string) ($l['street'] ?? ''), 'locality' => (string) ($l['locality'] ?? ''), 'region' => (string) ($l['region'] ?? ''), 'postcode' => (string) ($l['postcode'] ?? ''), 'country' => (string) ($l['country'] ?? ''), 'phone' => (string) ($l['phone'] ?? ''), 'email' => (string) ($l['email'] ?? ''), 'whatsapp' => (string) ($l['whatsapp'] ?? ''), 'viber' => (string) ($l['viber'] ?? ''), 'messenger' => (string) ($l['messenger'] ?? ''), 'lat' => (isset($l['lat']) && $l['lat'] !== null) ? (string) $l['lat'] : '', 'lng' => (isset($l['lng']) && $l['lng'] !== null) ? (string) $l['lng'] : '', 'hours_summary' => $this->hours_summary($l['hours_json'] ?? ''), 'sort_order' => (int) ($l['sort_order'] ?? 0), 'listing_title' => (string) ($l['listing_title'] ?? ''), 'hero_heading' => (string) ($l['hero_heading'] ?? ''), 'hero_description' => (string) ($l['hero_description'] ?? ''), ); }, $locations)); echo ''; } private function render_history() { if (!class_exists('AIEO_Report_Render')) { return; } $aieo_grv_def = class_exists( 'AIEO_Report_Registry' ) ? AIEO_Report_Registry::get( 'google_reviews_history' ) : null; if ( $aieo_grv_def && ! $aieo_grv_def->user_can( 'view' ) ) { return; } AIEO_Report_Render::card('google_reviews_history', array( 'anchor' => 'google-reviews-history', 'icon' => 'clock', 'heading' => 'h2', 'title' => __('Review history', 'ai-eshop-optimizer'), 'description' => __('Every review this site has ever seen, per location. Google\'s Places API only returns the latest five, so this table is the only place the older ones exist.', 'ai-eshop-optimizer'), 'overrides' => array('page_id' => $this->get_id()), )); } private function render_settings() { $s = AIEO_Google_Reviews_Settings::get_all(); $nonce = wp_create_nonce('aieo_grv_settings'); $band = function_exists('rooster_admin_band_open'); if ($band) { rooster_admin_band_open(array('cols' => 2)); } $form_id = 'aieo-card-google-reviews-settings-form'; $id_of = static function ($name) { return 'aieo-grv-' . str_replace('_', '-', $name); }; $row = static function ($name, $label, $control, $help = '') use ($id_of) { $field = array('id' => $id_of($name), 'label' => $label, 'control' => $control); if ('' !== $help) { $field['help'] = $help; } return \Rooster\Admin\Ui::field_row($field); }; $score = (float) $s['match_customer_min_score']; $tz = wp_timezone_string(); $fields = $row('global_api_key', __('Global API key', 'ai-eshop-optimizer'), \Rooster\Admin\Fields::secret('global_api_key', '' !== trim((string) $s['global_api_key']), array('id' => $id_of('global_api_key'), 'label' => __('Global API key', 'ai-eshop-optimizer'), 'placeholder' => 'AIza...')), __('Google Places API key used as fallback when a location row has no key of its own. Per-location keys (set in phase 2) override this so you can shard quota across multiple Google Cloud projects.', 'ai-eshop-optimizer')); $fields .= $row('language', __('Default language', 'ai-eshop-optimizer'), \Rooster\Admin\Ui::input('language', (string) $s['language'], array('id' => $id_of('language'), 'short' => true, 'maxlength' => 10)), __('ISO code (e.g. el, en). Per-location override available in phase 2.', 'ai-eshop-optimizer')); $fields .= $row('schedule', __('Refresh schedule', 'ai-eshop-optimizer'), \Rooster\Admin\Ui::select('schedule', array( 'twice_daily' => __('Twice daily', 'ai-eshop-optimizer'), 'daily' => __('Once a day', 'ai-eshop-optimizer'), 'weekly' => __('Once a week', 'ai-eshop-optimizer'), 'monthly' => __('Once a month', 'ai-eshop-optimizer'), ), (string) $s['schedule'], array('id' => $id_of('schedule'))), __('How often the cron pulls fresh reviews from Google. Each fetch costs one Places Details API call per location.', 'ai-eshop-optimizer')); $fields .= $row('run_time', __('Run time', 'ai-eshop-optimizer'), \Rooster\Admin\Ui::input('run_time', (string) $s['run_time'], array('id' => $id_of('run_time'), 'type' => 'time')), sprintf( __('Time of day (in site timezone: %s) when the cron runs. For "twice daily" the second run happens 12 hours later.', 'ai-eshop-optimizer'), $tz ?: 'UTC')); $fields .= $row('rate_limit_per_hour', __('Rate limit / hour', 'ai-eshop-optimizer'), \Rooster\Admin\Ui::input('rate_limit_per_hour', (string) $s['rate_limit_per_hour'], array('id' => $id_of('rate_limit_per_hour'), 'type' => 'number', 'short' => true, 'min' => 1, 'max' => max(10000, (int) $s['rate_limit_per_hour']))), __('Soft cap on Google API calls per hour, shared across locations. Prevents runaway billing if cron schedules misfire.', 'ai-eshop-optimizer')); $fields .= $row('rate_limit_per_day', __('Rate limit / day', 'ai-eshop-optimizer'), \Rooster\Admin\Ui::input('rate_limit_per_day', (string) $s['rate_limit_per_day'], array('id' => $id_of('rate_limit_per_day'), 'type' => 'number', 'short' => true, 'min' => 1, 'max' => max(100000, (int) $s['rate_limit_per_day'])))); $fields .= $row('block_cache_enabled', __('Object-cache block render', 'ai-eshop-optimizer'), \Rooster\Admin\Ui::toggle('block_cache_enabled', !empty($s['block_cache_enabled']), array('id' => $id_of('block_cache_enabled'), 'label' => __('Cache rendered review blocks via wp_cache (Redis/APCu when present).', 'ai-eshop-optimizer'))), __('Cache key is bound to a per-location version counter that the cron bumps after each successful fetch — so cached HTML is invalidated the moment new data lands, without iterating cache entries.', 'ai-eshop-optimizer')); $fields .= $row('match_customer_min_score', __('Customer-match threshold', 'ai-eshop-optimizer'), \Rooster\Admin\Ui::input('match_customer_min_score', (string) $score, array('id' => $id_of('match_customer_min_score'), 'type' => 'number', 'xs' => true, 'min' => 0, 'max' => 1, 'step' => abs($score / 0.05 - round($score / 0.05)) < 1e-6 ? 0.05 : 'any')), __('Fuzzy similarity threshold (0.00–1.00) for auto-linking a Google reviewer name to a WC customer. Manual links from the Review History tab override auto-matches.', 'ai-eshop-optimizer')); list($grv_open, $grv_close) = AIEO_DMM_UI::card(array( 'anchor' => 'google-reviews-settings', 'icon' => 'settings', 'title' => __( 'Settings', 'ai-eshop-optimizer' ), 'description' => __('The language, refresh schedule, API limits and caching that every location\'s review fetches share.', 'ai-eshop-optimizer'), 'col' => 1, 'footer' => \Rooster\Admin\States::save_button($form_id), )); echo $grv_open; echo AIEO_DMM_UI::card_form('aieo_grv_save_settings', 'google_reviews', $form_id, '
' . $fields . '
', array('nonce' => $nonce, 'nonce_name' => 'nonce')); echo $grv_close; list($grv_open, $grv_close) = AIEO_DMM_UI::card(array( 'anchor' => 'google-reviews-how-to-set-up-a-location', 'icon' => 'circle-question-mark', 'title' => __('How to set up a location', 'ai-eshop-optimizer'), 'description' => __('What a location needs from Google before its reviews can be fetched, and where to find each.', 'ai-eshop-optimizer'), 'col' => 2, )); echo $grv_open; $grv_g = static function ($text, $question, $html = '') { return \Rooster\Admin\Guidance::text($text, 'guidance', array('question' => $question, 'html' => $html)); }; echo $grv_g(__('Each location needs two distinct things from Google. Do not confuse them — they look nothing alike:', 'ai-eshop-optimizer'), __('What each location needs', 'ai-eshop-optimizer')); echo '
    '; echo '
  1. ' . esc_html__('A Google Places API key', 'ai-eshop-optimizer') . ' '; echo esc_html__('(format:', 'ai-eshop-optimizer') . ' AIzaSy…' . esc_html__(') — set once above. Optionally override per-location to shard quota across multiple Google Cloud projects.', 'ai-eshop-optimizer') . '
    '; echo '' . esc_html__('Get one:', 'ai-eshop-optimizer') . ' '; echo 'console.cloud.google.com → APIs & Services → Credentials. '; echo esc_html__('Then enable the "Places API" library on the same project. Restrict the key by HTTP referrer or by your server\'s IP.', 'ai-eshop-optimizer'); echo '
  2. '; echo '
  3. ' . esc_html__('A place_id', 'ai-eshop-optimizer') . ' '; echo esc_html__('(format:', 'ai-eshop-optimizer') . ' ChIJ…' . esc_html__(') — one per location. This is Google\'s identifier for the physical place.', 'ai-eshop-optimizer') . '
    '; echo '' . esc_html__('Quickest way to get one: open the', 'ai-eshop-optimizer') . ' '; echo '' . esc_html__('Place ID Finder', 'ai-eshop-optimizer') . ', '; echo esc_html__('search your business name, click the pin, copy the', 'ai-eshop-optimizer') . ' ChIJ… ' . esc_html__('value. Or, on the Locations tab, paste a business name or Google Maps URL into the lookup helper and we\'ll resolve the place_id via the API.', 'ai-eshop-optimizer'); echo '
  4. '; echo '
'; $grv_cap = esc_html__('Google\'s Places Details API returns at most 5 reviews per call — there is no pagination, no way to get the full archive in one shot. We diff each fetch against the local table and INSERT only unseen reviews, so over weeks of polling, our archive grows past Google\'s 5-row cap. The first fetch of any location will always be 5; that is a Google constraint, not ours.', 'ai-eshop-optimizer'); if (class_exists('\Rooster\Admin\Ui')) { $grv_cap = \Rooster\Admin\Ui::notice($grv_cap, 'info', array('inline' => true, 'title' => __('Important: about the "5 reviews max" cap', 'ai-eshop-optimizer'))); } else { $grv_cap = '

' . esc_html__('Important: about the "5 reviews max" cap', 'ai-eshop-optimizer') . '
' . $grv_cap . '

'; } echo $grv_cap; $grv_sl = __('Those are public review-request URLs. They contain an internal Google "FID" that is NOT a place_id and cannot be pasted into the place_id field directly. Use the Place ID Finder or the Lookup helper on the Locations tab.', 'ai-eshop-optimizer'); echo $grv_g($grv_sl, '', '' . esc_html__('Note about review shortlinks (g.page/r/…):', 'ai-eshop-optimizer') . ' ' . esc_html($grv_sl)); echo $grv_close; if ($band) { rooster_admin_band_close(); rooster_admin_band_open(array('cols' => 1)); } list($grv_open, $grv_close) = AIEO_DMM_UI::card(array( 'anchor' => 'google-reviews-how-to-display-the-reviews-on-the-storefront', 'icon' => 'layout-template', 'title' => __('Showing Google reviews', 'ai-eshop-optimizer'), 'description' => __('Three surfaces, one renderer and cache: the Rooster One Google Reviews block, the reviews and locations-grid shortcodes with their attributes, and do_shortcode() in PHP.', 'ai-eshop-optimizer'), 'span' => 'all', )); echo $grv_open; $grv_section = static function ($title, $id) { return function_exists('rooster_admin_card_section_html') ? rooster_admin_card_section_html($title, array('id' => $id)) : '

' . esc_html($title) . '

'; }; $grv_gh = static function ($html, $question) use ($grv_g) { return $grv_g(wp_strip_all_tags($html), $question, $html); }; echo $grv_g(__('Three rendering surfaces are available — all share the same render code, the same cache, and the same CSS, so output is identical across themes:', 'ai-eshop-optimizer'), __('How do they differ?', 'ai-eshop-optimizer')); echo $grv_section(__('1. FSE / block themes (RoosterX, Twenty Twenty-Five, etc.)', 'ai-eshop-optimizer'), 'aieo-section-google-reviews-block-themes'); echo $grv_gh(esc_html__('Open the page or template in the Site Editor → Block Inserter → search', 'ai-eshop-optimizer') . ' "Rooster One Google Reviews". ' . esc_html__('Inserts the', 'ai-eshop-optimizer') . ' aieo/google-reviews ' . esc_html__('block. Pick Single / Mixed / Cards Grid mode + per-location slug in the InspectorControls sidebar.', 'ai-eshop-optimizer'), __('How to insert the block', 'ai-eshop-optimizer')); echo $grv_g(__('Bonus: the block is also exposed by the runtime plugin from version 5.7, so it renders correctly on storefront pages even when the admin plugin isn\'t in the request\'s plugin whitelist (runtime-only contexts).', 'ai-eshop-optimizer'), __('Where else it renders', 'ai-eshop-optimizer')); echo $grv_section(__('2. Classic themes / page builders / sidebars / widget areas', 'ai-eshop-optimizer'), 'aieo-section-google-reviews-shortcodes'); echo $grv_gh(esc_html__('Use one of these shortcodes anywhere shortcodes are accepted (post content, widgets, Elementor / Bricks / Divi text widgets, theme template overrides via', 'ai-eshop-optimizer') . ' do_shortcode()' . esc_html__('):', 'ai-eshop-optimizer'), __('Where shortcodes work', 'ai-eshop-optimizer')); echo ''; echo $grv_gh('' . esc_html__('Shortcode attributes', 'ai-eshop-optimizer') . ' ' . esc_html__('(all optional, all mirror the block\'s InspectorControls):', 'ai-eshop-optimizer'), ''); echo ''; echo $grv_gh('' . esc_html__('Example — full carousel of 4+ star reviews from one location', 'ai-eshop-optimizer') . ':', __('Example', 'ai-eshop-optimizer')); echo '
[aieo_google_reviews location="downtown" min_rating="4" max_count="12" layout="carousel" presentation="slider" autoplay_sec="8" heading="What our customers say"]
'; echo $grv_section(__('3. PHP templates (theme child / mu-plugin)', 'ai-eshop-optimizer'), 'aieo-section-google-reviews-php-templates'); echo $grv_gh(esc_html__('Echo the shortcode via', 'ai-eshop-optimizer') . ' do_shortcode() ' . esc_html__('from any PHP template:', 'ai-eshop-optimizer'), __('In a PHP template', 'ai-eshop-optimizer')); echo '
echo do_shortcode(\'[aieo_google_reviews location="downtown"]\');
'; echo $grv_close; if ($band) { rooster_admin_band_close(); } } public function ajax_save_settings() { check_ajax_referer('aieo_grv_settings', 'nonce'); if (!$this->can_manage()) { wp_send_json_error(array('message' => __('Permission denied.', 'ai-eshop-optimizer'))); } $patch = array(); foreach (array_keys(AIEO_Google_Reviews_Settings::defaults()) as $k) { if (array_key_exists($k, $_POST)) { $patch[$k] = wp_unslash($_POST[$k]); } } if (array_key_exists('global_api_key', $patch) && '' === trim(str_replace("\u{2022}", '', (string) $patch['global_api_key']))) { unset($patch['global_api_key']); } AIEO_Google_Reviews_Settings::update($patch); if (class_exists('AIEO_Google_Reviews_Cron')) { AIEO_Google_Reviews_Cron::reschedule(); } wp_send_json_success(); } private function hours_summary($json) { $json = (string) $json; if ('' === $json) { return ''; } $periods = json_decode($json, true); if (!is_array($periods) || empty($periods)) { return ''; } return sprintf( _n('Opening hours: %d entry auto-filled from Google.', 'Opening hours: %d entries auto-filled from Google.', count($periods), 'ai-eshop-optimizer'), count($periods) ); } public function ajax_save_location() { check_ajax_referer('aieo_grv_locations', 'nonce'); if (!$this->can_manage()) { wp_send_json_error(array('message' => __('Permission denied.', 'ai-eshop-optimizer'))); } $data = array( 'id' => isset($_POST['id']) ? (int) $_POST['id'] : 0, 'slug' => isset($_POST['slug']) ? sanitize_key(wp_unslash((string) $_POST['slug'])) : '', 'label' => isset($_POST['label']) ? sanitize_text_field(wp_unslash((string) $_POST['label'])) : '', 'place_id' => isset($_POST['place_id']) ? sanitize_text_field(wp_unslash((string) $_POST['place_id'])) : '', 'api_key' => isset($_POST['api_key']) ? sanitize_text_field(wp_unslash((string) $_POST['api_key'])) : '', 'remove_api_key' => !empty($_POST['remove_api_key']), 'language' => isset($_POST['language']) ? sanitize_text_field(wp_unslash((string) $_POST['language'])) : 'el', 'review_url' => isset($_POST['review_url']) ? esc_url_raw(wp_unslash((string) $_POST['review_url'])) : '', 'enabled' => !empty($_POST['enabled']), 'is_hq' => !empty($_POST['is_hq']) ? 1 : 0, 'price_range' => isset($_POST['price_range']) ? sanitize_text_field(wp_unslash((string) $_POST['price_range'])) : '', 'gbp_url' => isset($_POST['gbp_url']) ? esc_url_raw(wp_unslash((string) $_POST['gbp_url'])) : '', 'image_url' => isset($_POST['image_url']) ? esc_url_raw(wp_unslash((string) $_POST['image_url'])) : '', 'street' => isset($_POST['street']) ? sanitize_text_field(wp_unslash((string) $_POST['street'])) : '', 'locality' => isset($_POST['locality']) ? sanitize_text_field(wp_unslash((string) $_POST['locality'])) : '', 'region' => isset($_POST['region']) ? sanitize_text_field(wp_unslash((string) $_POST['region'])) : '', 'postcode' => isset($_POST['postcode']) ? sanitize_text_field(wp_unslash((string) $_POST['postcode'])) : '', 'country' => isset($_POST['country']) ? sanitize_text_field(wp_unslash((string) $_POST['country'])) : '', 'phone' => isset($_POST['phone']) ? sanitize_text_field(wp_unslash((string) $_POST['phone'])) : '', 'email' => isset($_POST['email']) ? sanitize_text_field(wp_unslash((string) $_POST['email'])) : '', 'whatsapp' => isset($_POST['whatsapp']) ? sanitize_text_field(wp_unslash((string) $_POST['whatsapp'])) : '', 'viber' => isset($_POST['viber']) ? sanitize_text_field(wp_unslash((string) $_POST['viber'])) : '', 'messenger' => isset($_POST['messenger']) ? sanitize_text_field(wp_unslash((string) $_POST['messenger'])) : '', 'lat' => isset($_POST['lat']) ? wp_unslash((string) $_POST['lat']) : '', 'lng' => isset($_POST['lng']) ? wp_unslash((string) $_POST['lng']) : '', 'sort_order' => isset($_POST['sort_order']) ? (int) wp_unslash((string) $_POST['sort_order']) : 0, 'listing_title' => isset($_POST['listing_title']) ? sanitize_text_field(wp_unslash((string) $_POST['listing_title'])) : '', 'hero_heading' => isset($_POST['hero_heading']) ? sanitize_text_field(wp_unslash((string) $_POST['hero_heading'])) : '', 'hero_description' => isset($_POST['hero_description']) ? wp_unslash((string) $_POST['hero_description']) : '', ); AIEO_Google_Reviews_Locations::ensure_contact_columns(); $result = AIEO_Google_Reviews_Locations::save($data); if (is_wp_error($result)) { wp_send_json_error(array('message' => $result->get_error_message())); } $pickup_name = isset($_POST['pickup_name']) ? sanitize_text_field(wp_unslash((string) $_POST['pickup_name'])) : ''; $review_row = AIEO_Google_Reviews_Locations::get((int) $result); $this->set_pickup_association((string) $data['slug'], is_array($review_row) ? $review_row : array(), $pickup_name); $branch = $this->apply_stock_association((int) $result, (string) $data['slug'], (string) $data['label'], !empty($_POST['stock_scanner'])); if (is_wp_error($branch)) { wp_send_json_error(array('message' => sprintf( __('The location was saved, but its stock board could not be provisioned: %s', 'ai-eshop-optimizer'), $branch->get_error_message() ))); } wp_send_json_success(array('id' => (int) $result, 'branch' => is_array($branch) ? $branch : null)); } private function pickup_location_names() { $raw = get_option('pickup_location_pickup_locations', array()); $names = array(); if (is_array($raw)) { foreach ($raw as $loc) { if (is_array($loc) && !empty($loc['name'])) { $names[] = (string) $loc['name']; } } } return $names; } private function pickup_for_review_slug($review_slug) { if ($review_slug === '') { return ''; } $raw = get_option('pickup_location_pickup_locations', array()); if (is_array($raw)) { foreach ($raw as $loc) { if (is_array($loc) && isset($loc['review_slug']) && (string) $loc['review_slug'] === (string) $review_slug && !empty($loc['name'])) { return (string) $loc['name']; } } } return ''; } private function set_pickup_association($review_slug, $review_row, $pickup_name) { $raw = get_option('pickup_location_pickup_locations', array()); if (!is_array($raw)) { return; } $maps_url = ''; if ($review_slug !== '' && !empty($review_row) && class_exists('AIEO_Google_Reviews_Helpers')) { $maps_url = (string) AIEO_Google_Reviews_Helpers::maps_url($review_row); } $changed = false; foreach ($raw as $i => $loc) { if (!is_array($loc)) { continue; } $name = isset($loc['name']) ? (string) $loc['name'] : ''; $cur = isset($loc['review_slug']) ? (string) $loc['review_slug'] : ''; if ($pickup_name !== '' && $name === $pickup_name) { $raw[$i]['review_slug'] = $review_slug; $raw[$i]['maps_url'] = $maps_url; $changed = true; } elseif ($review_slug !== '' && $cur === $review_slug) { $raw[$i]['review_slug'] = ''; $raw[$i]['maps_url'] = ''; $changed = true; } } if ($changed) { update_option('pickup_location_pickup_locations', $raw); } } private function location_branch_row($location) { static $branches = null; $branch_id = (int) ($location['branch_id'] ?? 0); if ($branch_id <= 0) { return null; } if ($branches === null) { global $wpdb; $branches = array(); $reg = $wpdb->prefix . 'aieo_dmm_branches'; if (function_exists('aieo_dmm_table_exists') && aieo_dmm_table_exists($reg)) { foreach ((array) $wpdb->get_results("SELECT * FROM `{$reg}`", ARRAY_A) as $b) { $branches[(int) $b['id']] = $b; } } } return $branches[$branch_id] ?? null; } private function location_branch_active($location) { $b = $this->location_branch_row($location); return $b && !empty($b['enabled']); } private function location_stock_cell($location) { $b = $this->location_branch_row($location); if (!$b) { return '—'; } $out = '' . esc_html((string) $b['stock_table']) . ''; if (empty($b['enabled'])) { $out .= ' ' . \Rooster\Admin\Ui::badge(__('disabled', 'ai-eshop-optimizer'), 'warning', array('sm' => true)); return $out; } $page_id = (int) ($b['scanner_page_id'] ?? 0); if ($page_id > 0 && get_post_status($page_id) === 'publish') { $out .= '
' . esc_html__('Scanner page', 'ai-eshop-optimizer') . ''; } return $out; } private function apply_stock_association($location_id, $slug, $label, $want) { global $wpdb; if (!class_exists('AIEO_DMM_Branch_Provisioner')) { return null; } $loc_table = AIEO_Google_Reviews_Locations::table(); if ($want) { $res = AIEO_DMM_Branch_Provisioner::provision($slug, $label); if (is_wp_error($res)) { return $res; } $wpdb->update($loc_table, array('branch_id' => (int) $res['branch_id']), array('id' => (int) $location_id)); return $res; } $branch_id = (int) $wpdb->get_var($wpdb->prepare( "SELECT branch_id FROM `{$loc_table}` WHERE id = %d", (int) $location_id )); if ($branch_id > 0) { $reg = $wpdb->prefix . 'aieo_dmm_branches'; $branch_slug = (string) $wpdb->get_var($wpdb->prepare( "SELECT slug FROM `{$reg}` WHERE id = %d", $branch_id )); if ($branch_slug !== '') { AIEO_DMM_Branch_Provisioner::deactivate($branch_slug); } } return null; } public function ajax_delete_location() { check_ajax_referer('aieo_grv_locations', 'nonce'); if (!$this->can_manage()) { wp_send_json_error(array('message' => __('Permission denied.', 'ai-eshop-optimizer'))); } $id = isset($_POST['id']) ? (int) $_POST['id'] : 0; if ($id > 0) { AIEO_Google_Reviews_Locations::delete($id); } wp_send_json_success(); } public function ajax_lookup_place() { check_ajax_referer('aieo_grv_locations', 'nonce'); if (!$this->can_manage()) { wp_send_json_error(array('message' => __('Permission denied.', 'ai-eshop-optimizer'))); } $query = isset($_POST['query']) ? sanitize_text_field(wp_unslash((string) $_POST['query'])) : ''; $language = isset($_POST['language']) ? sanitize_text_field(wp_unslash((string) $_POST['language'])) : 'el'; $api_key = (string) AIEO_Google_Reviews_Settings::get('global_api_key'); if ($api_key === '') { wp_send_json_error(array('message' => __('Set the global API key under Settings first.', 'ai-eshop-optimizer'))); } $r = AIEO_Google_Reviews_Place_Lookup::resolve($query, $api_key, $language); if (!empty($r['error'])) { wp_send_json_error(array('message' => (string) $r['error'])); } wp_send_json_success($r); } public function ajax_fetch_now() { check_ajax_referer('aieo_grv_locations', 'nonce'); if (!$this->can_manage()) { wp_send_json_error(array('message' => __('Permission denied.', 'ai-eshop-optimizer'))); } $id = isset($_POST['id']) ? (int) $_POST['id'] : 0; $location = AIEO_Google_Reviews_Locations::get($id); if (!$location) { wp_send_json_error(array('message' => __('Location not found.', 'ai-eshop-optimizer'))); } @set_time_limit(60); $result = AIEO_Google_Reviews_Fetcher::fetch_one($location); if (empty($result['ok'])) { wp_send_json_error(array('message' => sprintf( __('%1$s — %2$s', 'ai-eshop-optimizer'), (string) $result['status'], (string) ($result['error'] ?? '') ))); } $result['label'] = (string) $location['label']; wp_send_json_success($result); } public function ajax_fetch_all() { check_ajax_referer('aieo_grv_locations', 'nonce'); if (!$this->can_manage()) { wp_send_json_error(array('message' => __('Permission denied.', 'ai-eshop-optimizer'))); } @set_time_limit(300); $totals = AIEO_Google_Reviews_Fetcher::fetch_all_enabled(false); wp_send_json_success($totals); } private function can_manage() { return current_user_can(defined('AIEO_CAPABILITY') ? AIEO_CAPABILITY : 'manage_options'); } }