\s*(\[(?:aiwest_directory_map|aiwest_company_map)[^\]]*\])\s*<\/div>\s*$/is',
'$1',
(string) $content
);
return trim((string) $content);
}
public static function migrate_map_page_presentation() {
if (!current_user_can('manage_options')) return;
if (get_option(self::PRESENTATION_MIGRATION_OPTION, '') === 'yes') return;
$page = self::resolve_map_page();
if ($page instanceof WP_Post) {
$labels = self::labels();
$title = trim((string) ($labels['directory_name'] ?? '')) ?: __('Locations & Partners Map', 'ai-webmaster-studio');
$content = self::normalize_map_page_content((string) $page->post_content);
$update = ['ID' => (int) $page->ID];
if ($content !== (string) $page->post_content) {
$update['post_content'] = $content;
}
if (self::is_legacy_generated_map_title((string) $page->post_title)) {
$update['post_title'] = $title;
}
if (count($update) > 1) {
wp_update_post($update);
}
}
update_option(self::PRESENTATION_MIGRATION_OPTION, 'yes', false);
}
public static function save_settings() {
if (!current_user_can('manage_options')) wp_die(esc_html(__('Insufficient permissions.', 'ai-webmaster-studio')));
check_admin_referer('aiwest_directory_save_settings');
update_option('aiwest_directory_name', AIWEST_Security::post_text('aiwest_directory_name', 'Locations & Partners Map'));
update_option('aiwest_directory_item_singular', AIWEST_Security::post_text('aiwest_directory_item_singular', 'Location'));
update_option('aiwest_directory_item_plural', AIWEST_Security::post_text('aiwest_directory_item_plural', 'Locations & Partners'));
update_option('aiwest_directory_button_text', AIWEST_Security::post_text('aiwest_directory_button_text', 'Find with AI'));
update_option('aiwest_directory_lead_button_text', AIWEST_Security::post_text('aiwest_directory_lead_button_text', 'Generate an inquiry for selected partners'));
update_option('aiwest_directory_industry_context', AIWEST_Security::post_textarea('aiwest_directory_industry_context'));
update_option('aiwest_directory_map_description', AIWEST_Security::post_textarea('aiwest_directory_map_description'));
update_option('aiwest_directory_google_api_key', AIWEST_Security::post_text('aiwest_directory_google_api_key'));
update_option('aiwest_directory_map_page_url', '/ai-directory-map/');
update_option('aiwest_directory_mode', self::sanitize_mode(AIWEST_Security::post_key('aiwest_directory_mode', 'business_location')));
update_option('aiwest_directory_layout', self::sanitize_layout(AIWEST_Security::post_key('aiwest_directory_layout', 'map_above')));
update_option('aiwest_directory_width', self::sanitize_width(AIWEST_Security::post_key('aiwest_directory_width', 'wide')));
update_option('aiwest_directory_height', self::sanitize_height(AIWEST_Security::post_text('aiwest_directory_height', '650')));
update_option('aiwest_directory_split_ratio', self::sanitize_split_ratio(AIWEST_Security::post_key('aiwest_directory_split_ratio', '70_30')));
update_option('aiwest_directory_directions_provider', self::sanitize_directions_provider(AIWEST_Security::post_key('aiwest_directory_directions_provider', 'google')));
$provider = AIWEST_Security::post_key('aiwest_directory_map_provider', 'osm');
update_option('aiwest_directory_map_provider', in_array($provider, ['osm','google'], true) ? $provider : 'osm');
$source = AIWEST_Security::post_key('aiwest_directory_recommendation_source', 'catalog_only');
update_option('aiwest_directory_recommendation_source', in_array($source, ['catalog_only','catalog_first_public_allowed'], true) ? $source : 'catalog_only');
$lead_flow = AIWEST_Security::post_key('aiwest_directory_lead_flow_mode', 'send_immediately');
update_option('aiwest_directory_lead_flow_mode', in_array($lead_flow, ['send_immediately','admin_review'], true) ? $lead_flow : 'send_immediately');
$from_name = AIWEST_Security::post_text('aiwest_directory_email_from_name', self::default_email_from_name());
$from_email = sanitize_email(AIWEST_Security::post_text('aiwest_directory_email_from_email', self::default_email_from_email()));
update_option('aiwest_directory_email_from_name', $from_name !== '' ? $from_name : self::default_email_from_name());
update_option('aiwest_directory_email_from_email', is_email($from_email) ? $from_email : self::default_email_from_email());
$show_header = AIWEST_Security::post_key('aiwest_directory_show_header', 'yes');
update_option('aiwest_directory_show_header', $show_header === 'no' ? 'no' : 'yes');
wp_safe_redirect(add_query_arg('aiwest_directory_saved', '1', self::admin_page_url()));
exit;
}
public static function seed_demo_items() {
if (!current_user_can('manage_options')) wp_die(esc_html(__('Insufficient permissions.', 'ai-webmaster-studio')));
check_admin_referer('aiwest_directory_seed_demo');
$demo = [
'new-york' => ['title'=>__('Demo Partner — New York', 'ai-webmaster-studio'), 'city'=>__('New York', 'ai-webmaster-studio'), 'region'=>__('New York', 'ai-webmaster-studio'), 'country'=>__('United States', 'ai-webmaster-studio'), 'lat'=>'40.712776', 'lng'=>'-74.005974', 'features'=>__('Editable partner example for map testing.', 'ai-webmaster-studio'), 'service_area'=>__('New York metropolitan area', 'ai-webmaster-studio'), 'priority'=>70],
'berlin' => ['title'=>__('Demo Partner — Berlin', 'ai-webmaster-studio'), 'city'=>__('Berlin', 'ai-webmaster-studio'), 'region'=>__('Berlin', 'ai-webmaster-studio'), 'country'=>__('Germany', 'ai-webmaster-studio'), 'lat'=>'52.520008', 'lng'=>'13.404954', 'features'=>__('Editable partner example for map testing.', 'ai-webmaster-studio'), 'service_area'=>__('Berlin and surrounding area', 'ai-webmaster-studio'), 'priority'=>75],
'shanghai' => ['title'=>__('Demo Partner — Shanghai', 'ai-webmaster-studio'), 'city'=>__('Shanghai', 'ai-webmaster-studio'), 'region'=>__('Shanghai', 'ai-webmaster-studio'), 'country'=>__('China', 'ai-webmaster-studio'), 'lat'=>'31.230391', 'lng'=>'121.473701', 'features'=>__('Editable partner example for map testing.', 'ai-webmaster-studio'), 'service_area'=>__('Shanghai metropolitan area', 'ai-webmaster-studio'), 'priority'=>72],
];
foreach ($demo as $key => $item) {
// Demo records use a plugin-owned metadata key to prevent duplicates.
$existing = get_posts([
'post_type' => self::CPT,
'post_status' => ['publish','draft','pending','private'],
'numberposts' => 1,
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Plugin-owned key prevents duplicate demo records.
'meta_key' => '_aiwest_directory_demo_key',
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Bounded lookup for one known demo identifier.
'meta_value' => $key,
'fields' => 'ids',
]);
$postarr = [
'post_type' => self::CPT,
'post_status' => 'draft',
'post_title' => $item['title'],
'post_content' => __('Demo record for testing the map. Replace every example value and review the record before publishing.', 'ai-webmaster-studio'),
];
if ($existing) {
$postarr['ID'] = (int)$existing[0];
$post_id = wp_update_post($postarr, true);
} else {
$post_id = wp_insert_post($postarr, true);
}
if ($post_id && !is_wp_error($post_id)) {
foreach ($item as $k=>$v) if ($k !== 'title') update_post_meta($post_id, '_aiwest_directory_' . $k, $v);
update_post_meta($post_id, '_aiwest_directory_visible', 'yes');
update_post_meta($post_id, '_aiwest_directory_status', 'partner');
update_post_meta($post_id, '_aiwest_directory_record_type', 'partner');
update_post_meta($post_id, '_aiwest_directory_demo', '1');
update_post_meta($post_id, '_aiwest_directory_demo_key', $key);
}
}
wp_safe_redirect(add_query_arg('aiwest_directory_seeded', '1', self::admin_page_url()));
exit;
}
public static function create_demo_business() {
if (!current_user_can('manage_options')) wp_die(esc_html(__('Insufficient permissions.', 'ai-webmaster-studio')));
check_admin_referer('aiwest_directory_create_demo_business');
$existing = get_posts([
'post_type' => self::CPT,
'post_status' => ['publish', 'draft', 'pending', 'private'],
'posts_per_page' => 1,
'fields' => 'ids',
'no_found_rows' => true,
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Bounded lookup for the plugin-owned demo identifier.
'meta_key' => '_aiwest_directory_demo_key',
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Bounded lookup for one known demo identifier.
'meta_value' => 'business-headquarters',
]);
$postarr = [
'post_type' => self::CPT,
'post_status' => 'draft',
'post_title' => __('My Business Headquarters — replace demo data', 'ai-webmaster-studio'),
'post_content' => __('Replace this demo description, address and coordinates with the real business information before publishing.', 'ai-webmaster-studio'),
];
if ($existing) $postarr['ID'] = (int) $existing[0];
$post_id = $existing ? wp_update_post($postarr, true) : wp_insert_post($postarr, true);
if (!is_wp_error($post_id) && $post_id) {
$values = [
'address' => '350 Fifth Avenue',
'city' => 'New York',
'region' => 'New York',
'country' => 'United States',
'lat' => '40.748817',
'lng' => '-73.985428',
'features' => __('Demo location — replace with your business address and description.', 'ai-webmaster-studio'),
'opening_hours' => __('Monday–Friday 09:00–17:00', 'ai-webmaster-studio'),
'priority' => '100',
'status' => 'draft',
'visible' => 'yes',
'record_type' => 'primary_business',
];
foreach ($values as $key => $value) update_post_meta((int) $post_id, '_aiwest_directory_' . $key, $value);
update_post_meta((int) $post_id, '_aiwest_directory_demo', '1');
update_post_meta((int) $post_id, '_aiwest_directory_demo_key', 'business-headquarters');
}
wp_safe_redirect(add_query_arg('aiwest_directory_business_draft', '1', self::admin_page_url()));
exit;
}
public static function delete_demo_items() {
if (!current_user_can('manage_options')) wp_die(esc_html(__('Insufficient permissions.', 'ai-webmaster-studio')));
check_admin_referer('aiwest_directory_delete_demo');
// Administrator cleanup is bounded and targets plugin-owned demo metadata.
$posts = get_posts([
'post_type' => self::CPT,
'post_status' => ['publish','draft','pending','private','trash'],
'numberposts' => 200,
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Administrator cleanup targets plugin-owned demo records.
'meta_key' => '_aiwest_directory_demo',
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Paired marker value for bounded cleanup.
'meta_value' => '1',
'fields' => 'ids',
]);
foreach ($posts as $post_id) wp_delete_post((int)$post_id, true);
wp_safe_redirect(add_query_arg('aiwest_directory_demo_deleted', '1', self::admin_page_url()));
exit;
}
public static function cleanup_map_page() {
if (!current_user_can('manage_options')) wp_die(esc_html(__('Insufficient permissions.', 'ai-webmaster-studio')));
check_admin_referer('aiwest_directory_cleanup_map_page');
$labels = self::labels();
$page_id = self::map_page_id_from_setting($labels['map_page_url']);
if ($page_id) {
$post = get_post($page_id);
if ($post && $post->post_type === 'page') {
$content = $post->post_content;
$patterns = [
'/\[\s*Mapa Google z naniesionymi[^\]]*integracja do wdrożenia przez administratora\s*\]/u',
'/
\s*\[\s*Mapa Google z naniesionymi[^<]*integracja do wdrożenia przez administratora\s*\]\s*<\/p>/u',
];
$new = preg_replace($patterns, '', $content);
if (is_string($new) && $new !== $content) {
wp_update_post(['ID'=>$page_id, 'post_content'=>$new]);
}
}
}
wp_safe_redirect(add_query_arg('aiwest_directory_cleaned', '1', self::admin_page_url()));
exit;
}
protected static function map_page_id_from_setting($setting) {
$setting = trim((string)$setting);
if ($setting === '') return absint(get_option(self::PAGE_ID_OPTION, 0));
if (ctype_digit($setting)) return (int)$setting;
$id = url_to_postid($setting);
if (!$id && strpos($setting, '/') === 0) $id = url_to_postid(home_url($setting));
if (!$id) {
$path = trim((string) wp_parse_url($setting, PHP_URL_PATH), '/');
$page = $path ? get_page_by_path($path, OBJECT, 'page') : null;
if ($page instanceof WP_Post && $page->post_status !== 'trash') $id = (int) $page->ID;
}
return (int)$id;
}
protected static function virtual_demo_items($mode) {
$headquarters = [
'id' => 'demo-business-headquarters',
'title' => __('Demo Company Headquarters', 'ai-webmaster-studio'),
'excerpt' => __('Demo location — replace with your business address.', 'ai-webmaster-studio'),
'city' => __('New York', 'ai-webmaster-studio'),
'region' => __('New York', 'ai-webmaster-studio'),
'country' => __('United States', 'ai-webmaster-studio'),
'address' => '350 Fifth Avenue',
'lat' => 40.748817,
'lng' => -73.985428,
'phone' => '',
'email' => '',
'website' => '',
'opening_hours' => __('Monday–Friday 09:00–17:00', 'ai-webmaster-studio'),
'marker_label' => __('Demo HQ', 'ai-webmaster-studio'),
'features' => __('Demo location — replace with your business address and description.', 'ai-webmaster-studio'),
'service_area' => '',
'status' => 'draft',
'priority' => 100,
'record_type' => 'primary_business',
'permalink' => '',
'is_demo' => true,
];
if (!self::mode_uses_partners($mode)) return [$headquarters];
$partners = [
['key' => 'new-york', 'title' => __('Demo Partner — New York', 'ai-webmaster-studio'), 'city' => __('New York', 'ai-webmaster-studio'), 'region' => __('New York', 'ai-webmaster-studio'), 'country' => __('United States', 'ai-webmaster-studio'), 'lat' => 40.712776, 'lng' => -74.005974],
['key' => 'berlin', 'title' => __('Demo Partner — Berlin', 'ai-webmaster-studio'), 'city' => __('Berlin', 'ai-webmaster-studio'), 'region' => __('Berlin', 'ai-webmaster-studio'), 'country' => __('Germany', 'ai-webmaster-studio'), 'lat' => 52.520008, 'lng' => 13.404954],
['key' => 'shanghai', 'title' => __('Demo Partner — Shanghai', 'ai-webmaster-studio'), 'city' => __('Shanghai', 'ai-webmaster-studio'), 'region' => __('Shanghai', 'ai-webmaster-studio'), 'country' => __('China', 'ai-webmaster-studio'), 'lat' => 31.230391, 'lng' => 121.473701],
];
$items = $mode === 'company_partners' ? [$headquarters] : [];
foreach ($partners as $partner) {
$items[] = [
'id' => 'demo-partner-' . $partner['key'],
'title' => $partner['title'],
'excerpt' => __('Virtual demo record — add and publish a real partner to replace it.', 'ai-webmaster-studio'),
'city' => $partner['city'],
'region' => $partner['region'],
'country' => $partner['country'],
'address' => '',
'lat' => $partner['lat'],
'lng' => $partner['lng'],
'phone' => '',
'email' => '',
'website' => '',
'opening_hours' => '',
'marker_label' => __('Demo', 'ai-webmaster-studio'),
'features' => __('Virtual demo partner — not a real directory listing.', 'ai-webmaster-studio'),
'service_area' => '',
'status' => 'draft',
'priority' => 50,
'record_type' => 'partner',
'permalink' => '',
'is_demo' => true,
];
}
return $items;
}
protected static function get_display_items($limit, $mode) {
$items = self::get_items($limit, $mode);
return $items ?: self::virtual_demo_items($mode);
}
protected static function directions_url($item, $provider = '') {
if (!isset($item['lat'], $item['lng']) || !is_numeric($item['lat']) || !is_numeric($item['lng'])) return '';
$lat = number_format((float) $item['lat'], 6, '.', '');
$lng = number_format((float) $item['lng'], 6, '.', '');
$provider = self::sanitize_directions_provider($provider ?: get_option('aiwest_directory_directions_provider', 'google'));
if ($provider === 'osm') return 'https://www.openstreetmap.org/directions?to=' . rawurlencode($lat . ',' . $lng);
if ($provider === 'apple') return 'https://maps.apple.com/?daddr=' . rawurlencode($lat . ',' . $lng);
return 'https://www.google.com/maps/dir/?api=1&destination=' . rawurlencode($lat . ',' . $lng);
}
protected static function positioned_items($items) {
$positioned = [];
foreach ((array) $items as $item) {
if (!isset($item['lat'], $item['lng']) || !is_numeric($item['lat']) || !is_numeric($item['lng'])) continue;
$positioned[] = $item;
}
return $positioned;
}
protected static function static_map_fallback($items) {
$positioned = self::positioned_items($items);
if (empty($positioned)) {
return '
' . esc_html(__('The map will be displayed after records with coordinates are added. The results list remains available.', 'ai-webmaster-studio')) . '
';
}
return '
' . esc_html(__('Loading interactive map...', 'ai-webmaster-studio')) . '
';
}
protected static function static_results_fallback($items) {
$items = array_slice((array) $items, 0, 30);
if (empty($items)) {
return '
' . esc_html(__('No directory records to display.', 'ai-webmaster-studio')) . '
';
}
$html = '';
foreach ($items as $item) {
$id = isset($item['id']) ? (string) $item['id'] : '';
$title = isset($item['title']) ? (string) $item['title'] : '';
$location = trim(implode(', ', array_filter([(string)($item['city'] ?? ''), (string)($item['region'] ?? ''), (string)($item['country'] ?? '')])));
$desc = (string)($item['features'] ?? '');
if ($desc === '') $desc = (string)($item['excerpt'] ?? '');
$is_demo = !empty($item['is_demo']);
$html .= '
';
if ($is_demo) {
$html .= '' . esc_html(__('Demo location — replace in map settings', 'ai-webmaster-studio')) . ' ';
} elseif (($item['record_type'] ?? 'partner') === 'partner') {
$html .= ' ' . esc_html(__('Select for inquiry', 'ai-webmaster-studio')) . ' ';
}
$html .= '' . esc_html($title) . ' ';
if ($location !== '') $html .= '' . esc_html($location) . '
';
if ($desc !== '') $html .= '' . esc_html(wp_trim_words($desc, 24)) . '
';
if (!empty($item['opening_hours'])) $html .= '' . esc_html($item['opening_hours']) . '
';
if (!empty($item['website'])) $html .= '' . esc_html(__('Website', 'ai-webmaster-studio')) . '
';
$directions = self::directions_url($item);
if ($directions !== '') $html .= '' . esc_html(__('Get Directions', 'ai-webmaster-studio')) . '
';
$html .= ' ';
}
return $html;
}
public static function shortcode_legacy($atts = []) { return self::shortcode($atts); }
public static function allowed_shortcode_html() {
$allowed = wp_kses_allowed_html('post');
$allowed['section'] = [
'class' => true,
'id' => true,
'data-map-id' => true,
'data-provider' => true,
'data-map-mode' => true,
];
$allowed['div'] = array_merge($allowed['div'] ?? [], [
'class' => true,
'style' => true,
'hidden' => true,
'role' => true,
'aria-live' => true,
'aria-modal' => true,
'aria-label' => true,
]);
$allowed['input'] = [
'type' => true,
'class' => true,
'style' => true,
'placeholder' => true,
'value' => true,
'checked' => true,
'data-aiwest-dir-filter' => true,
'data-aiwest-lead' => true,
];
$allowed['textarea'] = [
'class' => true,
'rows' => true,
'data-aiwest-lead' => true,
];
$allowed['button'] = [
'type' => true,
'class' => true,
'aria-label' => true,
'data-id' => true,
'data-lat' => true,
'data-lng' => true,
];
$allowed['article'] = array_merge($allowed['article'] ?? [], [
'class' => true,
'data-id' => true,
'style' => true,
]);
$allowed['label'] = array_merge($allowed['label'] ?? [], [
'class' => true,
'style' => true,
]);
return $allowed;
}
public static function get_items($limit = 200, $mode = '') {
$mode = self::sanitize_mode($mode ?: get_option('aiwest_directory_mode', 'business_location'));
// Visibility is stored as plugin-owned post metadata for this dedicated CPT.
$q = new WP_Query([
'post_type' => self::CPT,
'post_status' => 'publish',
'posts_per_page' => absint($limit),
'orderby' => 'title',
'order' => 'ASC',
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Visibility is plugin-owned metadata on a dedicated, bounded CPT query.
'meta_query' => [
'relation' => 'OR',
['key'=>'_aiwest_directory_visible','compare'=>'NOT EXISTS'],
['key'=>'_aiwest_directory_visible','value'=>'yes','compare'=>'='],
],
]);
$items = [];
foreach ($q->posts as $p) {
$lat = get_post_meta($p->ID, '_aiwest_directory_lat', true);
$lng = get_post_meta($p->ID, '_aiwest_directory_lng', true);
$record_type = get_post_meta($p->ID, '_aiwest_directory_record_type', true) ?: 'partner';
if ($mode === 'business_location' && $record_type !== 'primary_business') continue;
if ($mode === 'business_locations' && !in_array($record_type, ['primary_business', 'branch'], true)) continue;
if ($mode === 'partner_directory' && $record_type !== 'partner') continue;
$items[] = [
'id' => $p->ID,
'title' => get_the_title($p),
'excerpt' => wp_strip_all_tags(get_the_excerpt($p) ?: wp_trim_words($p->post_content, 24)),
'city' => get_post_meta($p->ID, '_aiwest_directory_city', true),
'region' => get_post_meta($p->ID, '_aiwest_directory_region', true),
'country' => get_post_meta($p->ID, '_aiwest_directory_country', true),
'address' => get_post_meta($p->ID, '_aiwest_directory_address', true),
'lat' => is_numeric($lat) ? round((float)$lat, 6) : null,
'lng' => is_numeric($lng) ? round((float)$lng, 6) : null,
'phone' => get_post_meta($p->ID, '_aiwest_directory_phone', true),
'email' => get_post_meta($p->ID, '_aiwest_directory_email', true),
'website' => get_post_meta($p->ID, '_aiwest_directory_website', true),
'opening_hours' => get_post_meta($p->ID, '_aiwest_directory_opening_hours', true),
'marker_label' => get_post_meta($p->ID, '_aiwest_directory_marker_label', true),
'features' => get_post_meta($p->ID, '_aiwest_directory_features', true),
'service_area' => get_post_meta($p->ID, '_aiwest_directory_service_area', true),
'status' => self::normalize_directory_status(get_post_meta($p->ID, '_aiwest_directory_status', true) ?: 'active'),
'priority' => (int)get_post_meta($p->ID, '_aiwest_directory_priority', true),
'record_type' => $record_type,
'permalink' => get_permalink($p),
'is_demo' => false,
];
}
wp_reset_postdata();
return $items;
}
public static function shortcode($atts = []) {
$atts = shortcode_atts(['limit'=>200, 'height'=>'', 'show_header'=>''], $atts, 'aiwest_directory_map');
$labels = self::labels();
$mode = self::sanitize_mode($labels['mode']);
$items = self::get_display_items((int)$atts['limit'], $mode);
foreach ($items as &$item) {
$item['directions_url'] = self::directions_url($item, $labels['directions_provider']);
}
unset($item);
$height = $atts['height'] !== '' ? self::sanitize_height($atts['height']) : $labels['height'];
$uid = 'aiwest-dir-' . wp_rand(10000,99999);
$provider = self::active_provider();
self::enqueue_frontend_assets();
wp_localize_script('aiwest-directory-map', 'AIWESTDirectoryMap', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce(self::NONCE_ACTION),
'provider' => $provider,
'googleApiKey' => $provider === 'google' ? $labels['google_api_key'] : '',
'i18n' => [
'ajaxUnavailable' => __('The WordPress AJAX endpoint is unavailable.', 'ai-webmaster-studio'),
'noResults'=>__('No results for the selected filters.', 'ai-webmaster-studio'),
'matching'=>__('AI is analyzing the match...', 'ai-webmaster-studio'),
'weak'=>__('No full match found. Showing the closest directory results.', 'ai-webmaster-studio'),
'leadSelect'=>__('Select at least one partner.', 'ai-webmaster-studio'),
'leadGenerating'=>__('Preparing inquiry...', 'ai-webmaster-studio'),
'leadSending'=>__('Sending inquiry...', 'ai-webmaster-studio'),
'selectForInquiry'=>__('Select for inquiry', 'ai-webmaster-studio'),
'website'=>__('Website', 'ai-webmaster-studio'),
'directions'=>__('Get Directions', 'ai-webmaster-studio'),
'demoBadge'=>__('Demo location — replace in map settings', 'ai-webmaster-studio'),
'selectedPrefix'=>__('Selected: ', 'ai-webmaster-studio'),
'canPrepareInquiry'=>__('You can prepare an inquiry.', 'ai-webmaster-studio'),
'selectPartnersHint'=>__('Select partners from the results list, then prepare an inquiry.', 'ai-webmaster-studio'),
'leadFormMissing'=>__('Inquiry form was not found on the page.', 'ai-webmaster-studio'),
'leadEditable'=>__('You can edit the inquiry before sending.', 'ai-webmaster-studio'),
'leadGenerateFailed'=>__('Could not prepare the message.', 'ai-webmaster-studio'),
'emailInvalid'=>__('Enter a valid email address.', 'ai-webmaster-studio'),
'messageRequired'=>__('Generate or enter the inquiry message before sending.', 'ai-webmaster-studio'),
'consentRequired'=>__('Confirm consent to send the inquiry to selected partners.', 'ai-webmaster-studio'),
'leadSaved'=>__('Inquiry saved.', 'ai-webmaster-studio'),
'leadSendFailed'=>__('Could not send the inquiry.', 'ai-webmaster-studio'),
'mapLoading'=>__('Loading map...', 'ai-webmaster-studio'),
'mapLoadFailed'=>__('Map could not be loaded. The results list remains available.', 'ai-webmaster-studio'),
'googleLoading'=>__('Loading Google Maps...', 'ai-webmaster-studio'),
'googleLoadFailed'=>__('Google Maps could not be loaded. The results list remains available.', 'ai-webmaster-studio'),
'matchFailed'=>__('Could not perform matching.', 'ai-webmaster-studio'),
'matcherConnectionFailed'=>__('Could not connect to AI Matcher.', 'ai-webmaster-studio'),
'staticMapLabel'=>__('Locations and partners map', 'ai-webmaster-studio'),
'directoryMap'=>__('Locations & Partners', 'ai-webmaster-studio'),
'openMap'=>__('open map', 'ai-webmaster-studio'),
'matchingStopwords'=>__('the,a,an,and,or,to,for,from,with,in,on,at,company,companies,find,search,looking,service,services,offer,partner,partners', 'ai-webmaster-studio'),
],
]);
$instance_data = wp_json_encode(
['items' => $items, 'labels' => $labels, 'provider' => $provider],
JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT
);
$instance_id = wp_json_encode($uid, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
if (is_string($instance_data) && is_string($instance_id)) {
wp_add_inline_script(
'aiwest-directory-map',
'window.AIWESTDirectoryInstances=window.AIWESTDirectoryInstances||{};window.AIWESTDirectoryInstances[' . $instance_id . ']=' . $instance_data . ';',
'before'
);
}
$show_header = $atts['show_header'] !== '' ? ($atts['show_header'] !== 'no') : ($labels['show_header'] !== 'no');
$layout_class = 'aiwest-directory-layout-' . str_replace('_', '-', $labels['layout']);
$width_class = 'aiwest-directory-width-' . $labels['width'];
$alignment_class = $labels['width'] === 'full'
? 'alignfull'
: ($labels['width'] === 'wide' ? 'alignwide' : '');
$ratio_class = 'aiwest-directory-ratio-' . str_replace('_', '-', $labels['split_ratio']);
$partners_enabled = self::mode_uses_partners($mode);
ob_start();
?>
post_content, 'aiwest_directory_map')) return;
$mode = self::sanitize_mode(get_option('aiwest_directory_mode', 'business_location'));
if (!self::mode_uses_business($mode)) return;
$items = self::get_items(100, $mode);
$business = null;
foreach ($items as $item) {
if (($item['record_type'] ?? '') === 'primary_business') {
$business = $item;
break;
}
}
if (!$business) return;
$schema = [
'@context' => 'https://schema.org',
'@type' => 'LocalBusiness',
'name' => (string) $business['title'],
'url' => !empty($business['website']) ? (string) $business['website'] : home_url('/'),
];
$address = array_filter([
'streetAddress' => (string) ($business['address'] ?? ''),
'addressLocality' => (string) ($business['city'] ?? ''),
'addressRegion' => (string) ($business['region'] ?? ''),
'addressCountry' => (string) ($business['country'] ?? ''),
]);
if ($address) $schema['address'] = array_merge(['@type' => 'PostalAddress'], $address);
if (is_numeric($business['lat'] ?? null) && is_numeric($business['lng'] ?? null)) {
$schema['geo'] = [
'@type' => 'GeoCoordinates',
'latitude' => round((float) $business['lat'], 6),
'longitude' => round((float) $business['lng'], 6),
];
}
if (!empty($business['phone'])) $schema['telephone'] = (string) $business['phone'];
if (!empty($business['opening_hours'])) $schema['openingHours'] = (string) $business['opening_hours'];
$json = wp_json_encode($schema, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
if (is_string($json)) wp_print_inline_script_tag($json, ['type' => 'application/ld+json']);
}
public static function ajax_match() {
check_ajax_referer(self::NONCE_ACTION, 'nonce');
$location = AIWEST_Security::post_text('location');
$need = AIWEST_Security::post_text('need');
$items = self::get_items(300);
$inferred_location = self::infer_location_from_need($need, $items);
if ($location === '' && $inferred_location !== '') $location = $inferred_location;
$matches = self::score_items($items, $location, $need);
$message = self::build_match_message($matches, $location, $need);
wp_send_json_success(['matches'=>array_slice($matches,0,8), 'message'=>$message, 'location'=>$location]);
}
public static function ajax_generate_lead_message() {
check_ajax_referer(self::NONCE_ACTION, 'nonce');
$location = AIWEST_Security::post_text('location');
$need = AIWEST_Security::post_text('need');
$ids = self::sanitize_selected_ids(AIWEST_Security::post_array('selected_ids'));
$items = self::get_items_by_ids($ids);
if (empty($items)) {
wp_send_json_error(['message' => __('Select at least one directory partner.', 'ai-webmaster-studio')]);
}
$fallback = self::build_default_lead_message($location, $need, $items);
$message = $fallback;
if (!get_transient('aiwest_ai_draft_'.hash('sha256',AIWEST_Security::remote_address())) && class_exists('AIWEST_OpenAI') && AIWEST_OpenAI::get_api_key() && AIWEST_Site_Workspace::consume_public_draft()) {
set_transient('aiwest_ai_draft_'.hash('sha256',AIWEST_Security::remote_address()),1,2*MINUTE_IN_SECONDS);
$need=substr($need,0,2000);$location=substr($location,0,250);
$labels = self::labels();
$partner_summary = [];
foreach ($items as $item) {
$partner_summary[] = $item['title'] . ' — ' . trim(($item['city'] ?? '') . ' ' . ($item['region'] ?? '') . '; ' . ($item['features'] ?? ''));
}
$prompt = "Prepare an editable inquiry from the user to selected directory partners. Use ".AIWEST_Languages::language_name(AIWEST_Languages::content_locale()).", a professional tone and specific questions. Do not use square brackets or empty placeholders. Do not promise cooperation; ask for an offer and terms." . "\n\n" .
__('Administrator context:', 'ai-webmaster-studio') . "\n" . ($labels['industry_context'] ?: __('Universal services/partners directory.', 'ai-webmaster-studio')) . "\n\n" .
sprintf(
/* translators: %s: Location entered by the directory visitor. */
__('User location: %s', 'ai-webmaster-studio'),
$location
) . "\n" .
sprintf(
/* translators: %s: Service or project need entered by the directory visitor. */
__('User need: %s', 'ai-webmaster-studio'),
$need
) . "\n" .
__('Selected partners:', 'ai-webmaster-studio') . "\n- " . implode("\n- ", $partner_summary);
$ai = AIWEST_OpenAI::chat([
['role' => 'system', 'content' => __('You are an assistant preparing safe, specific inquiry messages. Return only the message body, without comments. Do not add empty square-bracket placeholders.', 'ai-webmaster-studio')],
['role' => 'user', 'content' => $prompt],
], 0.35);
if (!is_wp_error($ai) && trim((string)$ai) !== '') {
$message = self::cleanup_lead_message(trim(wp_strip_all_tags((string)$ai)), $location, $need, '', '', '');
}
}
wp_send_json_success(['message' => $message]);
}
public static function ajax_submit_lead() {
check_ajax_referer(self::NONCE_ACTION, 'nonce');
$name = AIWEST_Security::post_text('name');
$phone = AIWEST_Security::post_text('phone');
$email = sanitize_email(AIWEST_Security::post_text('email'));
$location = AIWEST_Security::post_text('location');
$need = AIWEST_Security::post_text('need');
$message = AIWEST_Security::post_textarea('message');
$consent = AIWEST_Security::post_key('consent');
$ids = self::sanitize_selected_ids(AIWEST_Security::post_array('selected_ids'));
$items = self::get_items_by_ids($ids);
$labels = self::labels();
$flow_mode = $labels['lead_flow_mode'] === 'admin_review' ? 'admin_review' : 'send_immediately';
if (!$email || !is_email($email)) wp_send_json_error(['message' => __('Enter a valid email address.', 'ai-webmaster-studio')]);
if ($consent !== 'yes') wp_send_json_error(['message' => __('Confirm consent before sending the inquiry.', 'ai-webmaster-studio')]);
if (empty($items)) wp_send_json_error(['message' => __('Select at least one partner.', 'ai-webmaster-studio')]);
$message = self::cleanup_lead_message($message, $location, $need, $email, $name, $phone);
if (mb_strlen(trim($message)) < 20) wp_send_json_error(['message' => __('Inquiry message is too short.', 'ai-webmaster-studio')]);
$ip = AIWEST_Security::remote_address();
$rate_key = 'aiwest_dir_lead_' . md5($ip . '|' . $email);
if (get_transient($rate_key)) wp_send_json_error(['message' => __('An inquiry was sent recently. Please wait before sending another one.', 'ai-webmaster-studio')]);
set_transient($rate_key, '1', MINUTE_IN_SECONDS);
$partners_meta = [];
$partner_emails = [];
$seen_partners = [];
foreach ($items as $item) {
$title = sanitize_text_field($item['title'] ?? '');
$mail = sanitize_email($item['email'] ?? '');
$city = sanitize_text_field($item['city'] ?? '');
$key = self::normalize($title . '|' . $mail . '|' . $city);
if (isset($seen_partners[$key])) continue;
$seen_partners[$key] = true;
$partners_meta[] = ['id'=>(int)$item['id'], 'title'=>$title, 'email'=>$mail, 'city'=>$city];
if ($mail && is_email($mail)) $partner_emails[] = $mail;
}
$subject = __('New Site Directory Inquiry', 'ai-webmaster-studio');
$partner_lines = [];
foreach ($partners_meta as $i) {
$line = $i['title'];
if (!empty($i['city'])) $line .= ' — ' . $i['city'];
if (!empty($i['email'])) $line .= ' <' . $i['email'] . '>';
$partner_lines[] = $line;
}
$partner_list = $partner_lines ? implode("
- ", $partner_lines) : __('No partners selected.', 'ai-webmaster-studio');
$body_parts = [__('A new inquiry was received from the directory form.', 'ai-webmaster-studio')];
if ($name) {
$body_parts[] = sprintf(
/* translators: %s: Name of the person submitting a directory inquiry. */
__('Name / contact: %s', 'ai-webmaster-studio'),
$name
);
}
$body_parts[] = sprintf(
/* translators: %s: Email address of the person submitting a directory inquiry. */
__('User email: %s', 'ai-webmaster-studio'),
$email
);
if ($phone) {
$body_parts[] = sprintf(
/* translators: %s: Phone number supplied with a directory inquiry. */
__('Phone: %s', 'ai-webmaster-studio'),
$phone
);
}
if ($location) {
$body_parts[] = sprintf(
/* translators: %s: Location supplied with a directory inquiry. */
__('Location: %s', 'ai-webmaster-studio'),
$location
);
}
if ($need) {
$body_parts[] = sprintf(
/* translators: %s: Service or project need supplied with a directory inquiry. */
__('Need: %s', 'ai-webmaster-studio'),
$need
);
}
$body_parts[] = sprintf(
/* translators: %s: Inquiry message body prepared by the visitor. */
__('Message:
%s', 'ai-webmaster-studio'),
$message
);
$body_parts[] = __('Selected partners:
- ', 'ai-webmaster-studio') . $partner_list;
$body = implode("
", $body_parts);
$headers = self::email_headers($email);
$sent_to_partners = 0;
$admin_email = get_option('admin_email');
if ($flow_mode === 'send_immediately') {
foreach (array_unique($partner_emails) as $to) {
if (wp_mail($to, $subject, $body, $headers)) $sent_to_partners++;
}
}
wp_mail($email, __('Copy of Directory Inquiry', 'ai-webmaster-studio'), $body, self::email_headers($admin_email));
if ($admin_email && is_email($admin_email)) {
$admin_subject = $flow_mode === 'admin_review'
? __('New directory inquiry awaiting approval', 'ai-webmaster-studio')
: __('New directory inquiry', 'ai-webmaster-studio');
wp_mail($admin_email, $admin_subject, $body, self::email_headers($email));
}
$lead_status = 'new';
if ($flow_mode === 'admin_review') $lead_status = 'admin_review';
elseif ($sent_to_partners > 0) $lead_status = 'sent';
else $lead_status = 'no_partner_email';
$lead_id = wp_insert_post([
'post_type' => self::LEAD_CPT,
'post_status' => 'private',
'post_title' => __('Directory inquiry', 'ai-webmaster-studio') . ' — ' . current_time('Y-m-d H:i'),
'post_content' => $message,
], true);
if (!is_wp_error($lead_id) && $lead_id) {
update_post_meta($lead_id, '_aiwest_lead_user_name', $name);
update_post_meta($lead_id, '_aiwest_lead_user_phone', $phone);
update_post_meta($lead_id, '_aiwest_lead_user_email', $email);
update_post_meta($lead_id, '_aiwest_lead_location', $location);
update_post_meta($lead_id, '_aiwest_lead_need', $need);
update_post_meta($lead_id, '_aiwest_lead_partners', $partners_meta);
update_post_meta($lead_id, '_aiwest_lead_sent_at', current_time('mysql'));
update_post_meta($lead_id, '_aiwest_lead_sent_to_partners', $sent_to_partners);
update_post_meta($lead_id, '_aiwest_lead_status', $lead_status);
update_post_meta($lead_id, '_aiwest_lead_flow_mode', $flow_mode);
}
if ($flow_mode === 'admin_review') {
$msg = __('The inquiry was saved and sent to the administrator. A copy was sent to the provided email address.', 'ai-webmaster-studio');
} elseif ($sent_to_partners === 0) {
$msg = __('The inquiry was saved and sent to the administrator and the provided email address. Selected partners do not have email addresses yet.', 'ai-webmaster-studio');
} else {
$msg = __('The inquiry was saved and sent to the selected partners. A copy was sent to the provided email address.', 'ai-webmaster-studio');
}
wp_send_json_success(['message' => $msg, 'lead_id' => is_wp_error($lead_id) ? 0 : (int)$lead_id, 'status' => $lead_status]);
}
protected static function sanitize_selected_ids($raw) {
if (is_string($raw)) {
$decoded = json_decode(wp_unslash($raw), true);
$raw = is_array($decoded) ? $decoded : explode(',', $raw);
}
if (!is_array($raw)) $raw = [];
$ids = [];
foreach ($raw as $id) {
$id = absint($id);
if ($id) $ids[] = $id;
}
return array_values(array_unique(array_slice($ids, 0, 20)));
}
protected static function get_items_by_ids($ids) {
if (empty($ids)) return [];
$all = self::get_items(500);
$out = [];
foreach ($all as $item) {
if (in_array((int)$item['id'], $ids, true)) $out[] = $item;
}
return $out;
}
protected static function cleanup_lead_message($message, $location = '', $need = '', $email = '', $name = '', $phone = '') {
$message = wp_strip_all_tags((string)$message);
$replacements = [
'[proszę wpisać adres e-mail]' => $email,
'[proszę wpisać numer telefonu]' => $phone,
'[Imię i nazwisko]' => $name,
'[Opcjonalnie: nazwa firmy]' => '',
'[Opcjonalnie: dodatkowe informacje kontaktowe]' => '',
'[proszę wpisać]' => '',
];
foreach ($replacements as $from => $to) $message = str_replace($from, (string)$to, $message);
$message = preg_replace('/\[[^\]]*(proszę|prosze|opcjonalnie|uzupełnij|uzupelnij)[^\]]*\]/iu', '', $message);
$lines = preg_split('/\R/u', $message);
$clean = [];
foreach ($lines as $line) {
$trim = trim($line);
if ($trim === '') { $clean[] = ''; continue; }
if (preg_match('/^[-*]?\s*(typ inwestycji|technologia budowy|etap budowy|preferowany termin|dodatkowe informacje ode mnie)\s*:\s*$/iu', $trim)) continue;
if (preg_match('/^[-*]?\s*(potrzeba|opis potrzeby)\s*:\s*$/iu', $trim)) continue;
$clean[] = rtrim($line);
}
$message = implode("\n", $clean);
$message = preg_replace("/\n{3,}/", "\n\n", $message);
return trim($message);
}
protected static function build_default_lead_message($location, $need, $items) {
$names = implode(', ', array_unique(array_map(function($i){ return $i['title']; }, $items)));
$lines = [
__('Hello,', 'ai-webmaster-studio'),
__('please prepare initial information or an offer regarding my request.', 'ai-webmaster-studio'),
];
if ($location) {
$lines[] = sprintf(
/* translators: %s: Location supplied with a directory inquiry. */
__('Location: %s', 'ai-webmaster-studio'),
$location
);
}
if ($need) {
$lines[] = sprintf(
/* translators: %s: Description supplied with a directory inquiry. */
__('Need description: %s', 'ai-webmaster-studio'),
$need
);
}
if ($names) {
$lines[] = sprintf(
/* translators: %s: Comma-separated list of directory partners receiving the inquiry. */
__('Selected inquiry recipients: %s', 'ai-webmaster-studio'),
$names
);
}
$lines[] = __('Please provide information about:', 'ai-webmaster-studio');
$lines[] = __('1. Do you provide this scope in the specified location?', 'ai-webmaster-studio');
$lines[] = __('2. What exactly is included in the offer and what is charged separately?', 'ai-webmaster-studio');
$lines[] = __('3. What is the earliest possible start date?', 'ai-webmaster-studio');
$lines[] = __('4. What information is required to prepare a more accurate quote?', 'ai-webmaster-studio');
$lines[] = __('5. What are the schedule, payment method and cooperation terms?', 'ai-webmaster-studio');
$lines[] = __('6. Can you provide similar projects or relevant experience?', 'ai-webmaster-studio');
$lines[] = __('Regards', 'ai-webmaster-studio');
return implode("
", $lines);
}
protected static function normalize($text) {
return strtolower(remove_accents(wp_strip_all_tags((string) $text)));
}
protected static function query_tokens($text) {
$text = self::normalize($text);
$words = preg_split('/[^a-z0-9]+/u', $text);
$stop_words = explode(',', __('the,a,an,and,or,to,for,from,with,in,on,at,company,companies,find,search,looking,service,services,offer,partner,partners', 'ai-webmaster-studio'));
$stop_words = array_filter(array_map([__CLASS__, 'normalize'], $stop_words));
$stop = array_flip($stop_words);
$out = [];
foreach ($words as $w) {
$w = trim($w);
if (mb_strlen($w) < 3 || isset($stop[$w])) continue;
$out[] = $w;
}
return array_values(array_unique($out));
}
protected static function infer_location_from_need($need, $items) {
$need_n = self::normalize($need);
if ($need_n === '') return '';
foreach ($items as $item) {
foreach ([$item['city'] ?? '', $item['region'] ?? ''] as $candidate) {
$candidate_n = self::normalize($candidate);
if ($candidate_n && mb_strpos($need_n, $candidate_n) !== false) return (string)$candidate;
}
}
return '';
}
public static function score_items($items, $location, $need) {
$loc = self::normalize($location);
$tokens = self::query_tokens($need);
$has_query = ($loc !== '' || !empty($tokens));
$scored = [];
foreach ($items as $item) {
$hay = self::normalize(implode(' ', [$item['title'], $item['city'], $item['region'], $item['address'], $item['features'], $item['service_area'], $item['excerpt']]));
$locHay = self::normalize($item['city'].' '.$item['region'].' '.$item['service_area'].' '.$item['address']);
$score = min(20, max(0, (int)$item['priority'] / 5));
$signals = 0;
$need_hits = 0;
$loc_match = false;
if ($loc && (mb_strpos($locHay, $loc) !== false || mb_strpos($hay, $loc) !== false)) { $score += 42; $signals++; $loc_match = true; }
foreach ($tokens as $word) {
if (mb_strpos($hay, $word) !== false) { $score += 14; $signals++; $need_hits++; }
}
if (in_array($item['status'], ['partner','featured'], true)) $score += $item['status'] === 'featured' ? 18 : 10;
if (!empty($item['lat']) && !empty($item['lng'])) $score += 5;
$item['score'] = min(100, (int)round($score));
$item['match_reason'] = self::match_reason($item, $location, $need, $loc_match, $need_hits, count($tokens));
$item['match_strength'] = ($loc_match && ($need_hits > 0 || empty($tokens))) ? 'strong' : (($signals > 0) ? 'partial' : 'weak');
if (!$has_query || $signals > 0) $scored[] = $item;
}
usort($scored, function($a,$b){ return ($b['score'] <=> $a['score']) ?: strcmp($a['title'], $b['title']); });
return $scored;
}
protected static function match_reason($item, $location, $need, $loc_match = false, $need_hits = 0, $need_total = 0) {
$reasons = [];
if ($location && $loc_match) $reasons[] = __('location match', 'ai-webmaster-studio');
if ($need && $need_hits > 0) $reasons[] = __('partial need match with description/services', 'ai-webmaster-studio');
if ($need && $need_total > 0 && $need_hits === 0) $reasons[] = __('location match without full need coverage', 'ai-webmaster-studio');
if (in_array($item['status'], ['partner','featured'], true)) $reasons[] = __('partner or featured status', 'ai-webmaster-studio');
if (!$reasons) $reasons[] = __('closest available directory match', 'ai-webmaster-studio');
return implode(', ', $reasons);
}
protected static function build_match_message($matches, $location, $need) {
$source = get_option('aiwest_directory_recommendation_source', 'catalog_only');
if (!$matches) {
$msg = __('No matching records were found in the administrator directory. Expand the location, refine the need or add more records to the directory.', 'ai-webmaster-studio');
if ($source === 'catalog_first_public_allowed') $msg .= "\n" . __('Public sources are allowed in settings as a future direction, but this version does not perform external web search yet.', 'ai-webmaster-studio');
return $msg;
}
$top = array_slice($matches, 0, 3);
$has_strong = false;
foreach ($top as $m) if (($m['match_strength'] ?? '') === 'strong') $has_strong = true;
$lines = [];
if (!$has_strong && ($location || $need)) $lines[] = __('No full location and need match found. Showing the closest results from the administrator directory:', 'ai-webmaster-studio');
else $lines[] = __('Best matches from the administrator directory:', 'ai-webmaster-studio');
foreach ($top as $i => $m) $lines[] = ($i+1) . '. ' . $m['title'] . ' — ' . $m['match_reason'] . ' (score: ' . $m['score'] . ').';
$lines[] = __('Recommendation considers location, description, features/services, status and administrator-defined priority.', 'ai-webmaster-studio');
if ($source === 'catalog_only') $lines[] = __('Source: only records added and approved in this site directory.', 'ai-webmaster-studio');
else $lines[] = __('Source: currently the site directory; public source handling may be added as a separate integration.', 'ai-webmaster-studio');
return implode("\n", $lines);
}
}
AIWEST_Directory_Map::init();
function aiwest_directory_map_admin_page() { AIWEST_Directory_Map::admin_page(); }