db = $db ?: new MLS_DB(); // Load geocoder if available if (class_exists('MLS_Geocoder')) { $this->geocoder = new MLS_Geocoder(); } $this->init_hooks(); } /** * Initialize hooks */ private function init_hooks() { // Register CPT add_action('init', array($this, 'register_post_type')); // Register ACF fields add_action('acf/init', array($this, 'register_acf_fields')); // Sync to database on save add_action('acf/save_post', array($this, 'sync_to_database'), 20); // Delete from database on trash/delete add_action('wp_trash_post', array($this, 'on_trash_post')); add_action('before_delete_post', array($this, 'on_delete_post')); // Restore from trash add_action('untrashed_post', array($this, 'on_untrash_post')); // Admin columns add_filter('manage_manual_property_posts_columns', array($this, 'add_admin_columns')); add_action('manage_manual_property_posts_custom_column', array($this, 'render_admin_columns'), 10, 2); add_filter('manage_edit-manual_property_sortable_columns', array($this, 'sortable_columns')); // Admin row actions add_filter('post_row_actions', array($this, 'add_row_actions'), 10, 2); // Clone from MLS AJAX handlers add_action('wp_ajax_mls_search_for_clone', array($this, 'ajax_search_for_clone')); add_action('wp_ajax_mls_clone_listing', array($this, 'ajax_clone_listing')); // Admin scripts add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts')); // Add clone button to admin add_action('edit_form_top', array($this, 'render_clone_button')); } /** * Register the Custom Post Type */ public function register_post_type() { $labels = array( 'name' => 'Manual Properties', 'singular_name' => 'Manual Property', 'menu_name' => 'Manual Properties', 'add_new' => 'Add Property', 'add_new_item' => 'Add New Property', 'edit_item' => 'Edit Property', 'new_item' => 'New Property', 'view_item' => 'View Property', 'search_items' => 'Search Properties', 'not_found' => 'No properties found', 'not_found_in_trash' => 'No properties found in trash', 'all_items' => 'All Properties', ); $args = array( 'labels' => $labels, 'public' => false, 'show_ui' => true, 'show_in_menu' => true, 'menu_position' => 26, 'menu_icon' => 'dashicons-building', 'capability_type' => 'post', 'hierarchical' => false, 'supports' => array('title'), 'has_archive' => false, 'rewrite' => false, 'query_var' => false, 'show_in_rest' => false, ); register_post_type('manual_property', $args); } /** * Register ACF fields programmatically */ public function register_acf_fields() { if (!function_exists('acf_add_local_field_group')) { return; } acf_add_local_field_group(array( 'key' => 'group_manual_property', 'title' => 'Property Details', 'fields' => $this->get_field_definitions(), 'location' => array( array( array( 'param' => 'post_type', 'operator' => '==', 'value' => 'manual_property', ), ), ), 'menu_order' => 0, 'position' => 'normal', 'style' => 'default', 'label_placement' => 'top', 'instruction_placement' => 'label', )); } /** * Get ACF field definitions */ private function get_field_definitions() { return array( // Tab: Basic Info array( 'key' => 'field_mp_tab_basic', 'label' => 'Basic Info', 'type' => 'tab', ), array( 'key' => 'field_mp_listing_id', 'label' => 'MLS #', 'name' => 'listing_id', 'type' => 'text', 'instructions' => 'Optional. If set, this listing will override any MLS-synced listing with the same MLS ID.', ), array( 'key' => 'field_mp_standard_status', 'label' => 'Status', 'name' => 'standard_status', 'type' => 'select', 'choices' => array( 'Active' => 'Active', 'Pending' => 'Pending', 'Sold' => 'Sold', 'Cancelled' => 'Cancelled', 'Expired' => 'Expired', 'Withdrawn' => 'Withdrawn', ), 'default_value' => 'Active', 'required' => 1, ), array( 'key' => 'field_mp_list_price', 'label' => 'List Price', 'name' => 'list_price', 'type' => 'number', 'prepend' => '$', 'min' => 0, ), array( 'key' => 'field_mp_close_price', 'label' => 'Close Price', 'name' => 'close_price', 'type' => 'number', 'prepend' => '$', 'min' => 0, 'instructions' => 'Final sale price (for sold properties).', ), array( 'key' => 'field_mp_property_type', 'label' => 'Property Type', 'name' => 'property_type', 'type' => 'select', 'choices' => array( 'Residential' => 'Residential', 'Land' => 'Land', 'Commercial' => 'Commercial', 'Multi-Family' => 'Multi-Family', 'Farm' => 'Farm', ), 'default_value' => 'Residential', ), array( 'key' => 'field_mp_property_sub_type', 'label' => 'Property Subtype', 'name' => 'property_sub_type', 'type' => 'text', 'instructions' => 'E.g., Single Family, Townhouse, Condo, etc.', ), array( 'key' => 'field_mp_is_homeproz', 'label' => 'HomeProz Listing', 'name' => 'is_homeproz', 'type' => 'true_false', 'message' => 'This is a HomeProz listing', 'default_value' => 1, 'ui' => 1, ), array( 'key' => 'field_mp_is_featured', 'label' => 'Featured Listing', 'name' => 'is_featured', 'type' => 'true_false', 'message' => 'Feature this listing on the homepage', 'default_value' => 0, 'ui' => 1, ), // Tab: Location array( 'key' => 'field_mp_tab_location', 'label' => 'Location', 'type' => 'tab', ), array( 'key' => 'field_mp_full_address', 'label' => 'Full Address', 'name' => 'full_address', 'type' => 'text', 'instructions' => 'Enter the full address. City, state, zip, and coordinates will be auto-filled via geocoding.', 'placeholder' => '123 Main St, Albert Lea, MN 56007', ), array( 'key' => 'field_mp_unit_number', 'label' => 'Unit Number', 'name' => 'unit_number', 'type' => 'text', ), array( 'key' => 'field_mp_city', 'label' => 'City', 'name' => 'city', 'type' => 'text', 'instructions' => 'Auto-filled from geocoding, but can be edited.', ), array( 'key' => 'field_mp_state', 'label' => 'State', 'name' => 'state_or_province', 'type' => 'text', 'default_value' => 'MN', ), array( 'key' => 'field_mp_postal_code', 'label' => 'ZIP Code', 'name' => 'postal_code', 'type' => 'text', ), array( 'key' => 'field_mp_county', 'label' => 'County', 'name' => 'county', 'type' => 'text', ), array( 'key' => 'field_mp_latitude', 'label' => 'Latitude', 'name' => 'latitude', 'type' => 'number', 'step' => 'any', 'instructions' => 'Auto-filled from geocoding.', ), array( 'key' => 'field_mp_longitude', 'label' => 'Longitude', 'name' => 'longitude', 'type' => 'number', 'step' => 'any', 'instructions' => 'Auto-filled from geocoding.', ), // Tab: Property Details array( 'key' => 'field_mp_tab_details', 'label' => 'Details', 'type' => 'tab', ), array( 'key' => 'field_mp_beds', 'label' => 'Bedrooms', 'name' => 'bedrooms_total', 'type' => 'number', 'min' => 0, ), array( 'key' => 'field_mp_baths', 'label' => 'Bathrooms', 'name' => 'bathrooms_total', 'type' => 'number', 'min' => 0, 'step' => 0.5, ), array( 'key' => 'field_mp_baths_full', 'label' => 'Full Bathrooms', 'name' => 'bathrooms_full', 'type' => 'number', 'min' => 0, ), array( 'key' => 'field_mp_baths_half', 'label' => 'Half Bathrooms', 'name' => 'bathrooms_half', 'type' => 'number', 'min' => 0, ), array( 'key' => 'field_mp_sqft', 'label' => 'Square Feet', 'name' => 'living_area', 'type' => 'number', 'min' => 0, 'append' => 'sq ft', ), array( 'key' => 'field_mp_lot_acres', 'label' => 'Lot Size (Acres)', 'name' => 'lot_size_area', 'type' => 'number', 'min' => 0, 'step' => 0.01, ), array( 'key' => 'field_mp_year_built', 'label' => 'Year Built', 'name' => 'year_built', 'type' => 'number', 'min' => 1800, 'max' => 2100, ), array( 'key' => 'field_mp_stories', 'label' => 'Stories', 'name' => 'stories', 'type' => 'number', 'min' => 0, ), array( 'key' => 'field_mp_garage', 'label' => 'Garage Spaces', 'name' => 'garage_spaces', 'type' => 'number', 'min' => 0, ), array( 'key' => 'field_mp_style', 'label' => 'Architectural Style', 'name' => 'architectural_style', 'type' => 'text', ), // Tab: Description array( 'key' => 'field_mp_tab_description', 'label' => 'Description', 'type' => 'tab', ), array( 'key' => 'field_mp_remarks', 'label' => 'Public Description', 'name' => 'public_remarks', 'type' => 'textarea', 'rows' => 6, ), array( 'key' => 'field_mp_private_remarks', 'label' => 'Private Notes', 'name' => 'private_remarks', 'type' => 'textarea', 'rows' => 3, 'instructions' => 'Internal notes (not shown on frontend).', ), // Tab: Media array( 'key' => 'field_mp_tab_media', 'label' => 'Media', 'type' => 'tab', ), array( 'key' => 'field_mp_gallery', 'label' => 'Property Photos', 'name' => 'gallery', 'type' => 'gallery', 'return_format' => 'array', 'preview_size' => 'medium', 'library' => 'all', 'min' => 0, 'max' => 50, 'instructions' => 'Upload property photos. First image is the primary photo.', ), // Tab: Agent array( 'key' => 'field_mp_tab_agent', 'label' => 'Agent', 'type' => 'tab', ), array( 'key' => 'field_mp_agent', 'label' => 'Listing Agent', 'name' => 'agent', 'type' => 'post_object', 'post_type' => array('agent'), 'return_format' => 'id', 'allow_null' => 1, ), array( 'key' => 'field_mp_co_agent', 'label' => 'Co-Listing Agent', 'name' => 'co_list_agent', 'type' => 'post_object', 'post_type' => array('agent'), 'return_format' => 'id', 'allow_null' => 1, ), // Tab: Dates array( 'key' => 'field_mp_tab_dates', 'label' => 'Dates', 'type' => 'tab', ), array( 'key' => 'field_mp_list_date', 'label' => 'List Date', 'name' => 'list_date', 'type' => 'date_picker', 'display_format' => 'F j, Y', 'return_format' => 'Y-m-d', ), array( 'key' => 'field_mp_contract_date', 'label' => 'Contract Date', 'name' => 'contract_date', 'type' => 'date_picker', 'display_format' => 'F j, Y', 'return_format' => 'Y-m-d', ), array( 'key' => 'field_mp_close_date', 'label' => 'Close Date', 'name' => 'close_date', 'type' => 'date_picker', 'display_format' => 'F j, Y', 'return_format' => 'Y-m-d', ), array( 'key' => 'field_mp_expiration_date', 'label' => 'Expiration Date', 'name' => 'expiration_date', 'type' => 'date_picker', 'display_format' => 'F j, Y', 'return_format' => 'Y-m-d', ), // Tab: Additional array( 'key' => 'field_mp_tab_additional', 'label' => 'Additional', 'type' => 'tab', ), array( 'key' => 'field_mp_virtual_tour', 'label' => 'Virtual Tour URL', 'name' => 'virtual_tour_url', 'type' => 'url', ), array( 'key' => 'field_mp_directions', 'label' => 'Directions', 'name' => 'directions', 'type' => 'textarea', 'rows' => 3, ), array( 'key' => 'field_mp_hoa_fee', 'label' => 'HOA/Association Fee', 'name' => 'association_fee', 'type' => 'number', 'prepend' => '$', 'append' => '/month', 'min' => 0, ), ); } /** * Sync property data to database on save */ public function sync_to_database($post_id) { // Only for manual_property if (get_post_type($post_id) !== 'manual_property') { return; } // Skip autosave if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { return; } // Skip revisions if (wp_is_post_revision($post_id)) { return; } // Skip if trashed if (get_post_status($post_id) === 'trash') { return; } global $wpdb; // Generate listing key $listing_key = 'MANUAL-' . $post_id; // Geocode address if changed $full_address = get_field('full_address', $post_id); $this->maybe_geocode_address($post_id, $full_address); // Parse address components $address_parts = $this->parse_address($full_address); // Get gallery photos count $gallery = get_field('gallery', $post_id); $photos_count = is_array($gallery) ? count($gallery) : 0; // Prepare data $data = array( 'wp_post_id' => $post_id, 'listing_key' => $listing_key, 'listing_id' => get_field('listing_id', $post_id) ?: null, 'standard_status' => get_field('standard_status', $post_id) ?: 'Active', 'list_price' => get_field('list_price', $post_id) ?: null, 'close_price' => get_field('close_price', $post_id) ?: null, 'original_list_price' => get_field('list_price', $post_id) ?: null, 'street_number' => $address_parts['street_number'] ?? null, 'street_name' => $address_parts['street_name'] ?? null, 'street_suffix' => $address_parts['street_suffix'] ?? null, 'unit_number' => get_field('unit_number', $post_id) ?: null, 'full_address' => $full_address ?: null, 'city' => get_field('city', $post_id) ?: null, 'state_or_province' => get_field('state_or_province', $post_id) ?: 'MN', 'postal_code' => get_field('postal_code', $post_id) ?: null, 'county' => get_field('county', $post_id) ?: null, 'latitude' => get_field('latitude', $post_id) ?: null, 'longitude' => get_field('longitude', $post_id) ?: null, 'property_type' => get_field('property_type', $post_id) ?: null, 'property_sub_type' => get_field('property_sub_type', $post_id) ?: null, 'bedrooms_total' => get_field('bedrooms_total', $post_id) ?: null, 'bathrooms_total' => get_field('bathrooms_total', $post_id) ?: null, 'bathrooms_full' => get_field('bathrooms_full', $post_id) ?: null, 'bathrooms_half' => get_field('bathrooms_half', $post_id) ?: null, 'living_area' => get_field('living_area', $post_id) ?: null, 'lot_size_area' => get_field('lot_size_area', $post_id) ?: null, 'lot_size_units' => 'Acres', 'year_built' => get_field('year_built', $post_id) ?: null, 'stories' => get_field('stories', $post_id) ?: null, 'garage_spaces' => get_field('garage_spaces', $post_id) ?: null, 'architectural_style' => get_field('architectural_style', $post_id) ?: null, 'public_remarks' => get_field('public_remarks', $post_id) ?: null, 'private_remarks' => get_field('private_remarks', $post_id) ?: null, 'directions' => get_field('directions', $post_id) ?: null, 'list_agent_post_id' => get_field('agent', $post_id) ?: null, 'co_list_agent_post_id' => get_field('co_list_agent', $post_id) ?: null, 'is_homeproz' => get_field('is_homeproz', $post_id) ? 1 : 0, 'is_featured' => get_field('is_featured', $post_id) ? 1 : 0, 'virtual_tour_url' => get_field('virtual_tour_url', $post_id) ?: null, 'association_fee' => get_field('association_fee', $post_id) ?: null, 'list_date' => get_field('list_date', $post_id) ?: null, 'contract_date' => get_field('contract_date', $post_id) ?: null, 'close_date' => get_field('close_date', $post_id) ?: null, 'expiration_date' => get_field('expiration_date', $post_id) ?: null, 'photos_count' => $photos_count, ); // Check if record exists $table = $this->db->manual_properties_table(); $exists = $wpdb->get_var($wpdb->prepare( "SELECT id FROM {$table} WHERE wp_post_id = %d", $post_id )); if ($exists) { $wpdb->update($table, $data, array('wp_post_id' => $post_id)); } else { $wpdb->insert($table, $data); } } /** * Geocode address if changed */ private function maybe_geocode_address($post_id, $full_address) { if (!$this->geocoder || empty($full_address)) { return; } // Check if address changed $stored_address = get_post_meta($post_id, '_geocoded_address', true); if ($stored_address === $full_address) { return; // Already geocoded } // Geocode the address $result = $this->geocoder->geocode($full_address); if (!$result) { return; } // Update fields if (!empty($result['city'])) { update_field('city', $result['city'], $post_id); } if (!empty($result['state'])) { update_field('state_or_province', $result['state'], $post_id); } if (!empty($result['postal_code'])) { update_field('postal_code', $result['postal_code'], $post_id); } if (!empty($result['county'])) { update_field('county', $result['county'], $post_id); } if (!empty($result['latitude'])) { update_field('latitude', $result['latitude'], $post_id); } if (!empty($result['longitude'])) { update_field('longitude', $result['longitude'], $post_id); } // Mark as geocoded update_post_meta($post_id, '_geocoded_address', $full_address); } /** * Parse address string into components */ private function parse_address($address) { if (empty($address)) { return array(); } $parts = array(); // Try to extract street number if (preg_match('/^(\d+)\s+/', $address, $matches)) { $parts['street_number'] = $matches[1]; $address = trim(substr($address, strlen($matches[0]))); } // Try to extract street suffix from common patterns $suffixes = array('St', 'Street', 'Ave', 'Avenue', 'Blvd', 'Boulevard', 'Dr', 'Drive', 'Ln', 'Lane', 'Rd', 'Road', 'Way', 'Ct', 'Court', 'Cir', 'Circle', 'Pl', 'Place'); foreach ($suffixes as $suffix) { if (preg_match('/^(.+?)\s+(' . preg_quote($suffix, '/') . ')\b/i', $address, $matches)) { $parts['street_name'] = $matches[1]; $parts['street_suffix'] = $matches[2]; break; } } return $parts; } /** * Handle post trash */ public function on_trash_post($post_id) { if (get_post_type($post_id) !== 'manual_property') { return; } global $wpdb; $table = $this->db->manual_properties_table(); // Update status to indicate trashed (optional: could also delete) $wpdb->update( $table, array('standard_status' => 'Withdrawn'), array('wp_post_id' => $post_id) ); } /** * Handle post delete */ public function on_delete_post($post_id) { if (get_post_type($post_id) !== 'manual_property') { return; } global $wpdb; $table = $this->db->manual_properties_table(); $wpdb->delete($table, array('wp_post_id' => $post_id)); } /** * Handle post restore from trash */ public function on_untrash_post($post_id) { if (get_post_type($post_id) !== 'manual_property') { return; } // Re-sync to database $this->sync_to_database($post_id); } /** * Add admin columns */ public function add_admin_columns($columns) { $new_columns = array(); foreach ($columns as $key => $value) { $new_columns[$key] = $value; if ($key === 'title') { $new_columns['mp_status'] = 'Status'; $new_columns['mp_price'] = 'Price'; $new_columns['mp_city'] = 'City'; $new_columns['mp_homeproz'] = 'HomeProz'; $new_columns['mp_featured'] = 'Featured'; $new_columns['mp_view'] = 'View'; } } // Remove date column, move to end if (isset($new_columns['date'])) { unset($new_columns['date']); $new_columns['date'] = 'Date'; } return $new_columns; } /** * Render admin column values */ public function render_admin_columns($column, $post_id) { switch ($column) { case 'mp_status': $status = get_field('standard_status', $post_id); $status_colors = array( 'Active' => '#28a745', 'Pending' => '#ffc107', 'Sold' => '#6c757d', 'Cancelled' => '#dc3545', 'Expired' => '#6c757d', 'Withdrawn' => '#6c757d', ); $color = $status_colors[$status] ?? '#6c757d'; echo '' . esc_html($status) . ''; break; case 'mp_price': $price = get_field('list_price', $post_id); echo $price ? '$' . number_format($price) : '-'; break; case 'mp_city': echo esc_html(get_field('city', $post_id) ?: '-'); break; case 'mp_homeproz': echo get_field('is_homeproz', $post_id) ? 'Yes' : 'No'; break; case 'mp_featured': echo get_field('is_featured', $post_id) ? 'Yes' : 'No'; break; case 'mp_view': $listing_key = 'MANUAL-' . $post_id; $url = add_query_arg('listing', $listing_key, home_url('/properties/')); echo 'View'; break; } } /** * Make columns sortable */ public function sortable_columns($columns) { $columns['mp_status'] = 'mp_status'; $columns['mp_price'] = 'mp_price'; $columns['mp_city'] = 'mp_city'; return $columns; } /** * Add row actions */ public function add_row_actions($actions, $post) { if ($post->post_type !== 'manual_property') { return $actions; } // Add view link $listing_key = 'MANUAL-' . $post->ID; $url = add_query_arg('listing', $listing_key, home_url('/properties/')); $actions['view_property'] = 'View Property'; return $actions; } /** * Enqueue admin scripts */ public function enqueue_admin_scripts($hook) { global $post_type; if ($post_type !== 'manual_property') { return; } wp_enqueue_script( 'mls-manual-property-admin', MLS_PLUGIN_URL . 'admin/js/manual-property.js', array('jquery'), MLS_PLUGIN_VERSION, true ); wp_localize_script('mls-manual-property-admin', 'mlsManualProperty', array( 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('mls_clone_listing'), )); wp_enqueue_style( 'mls-manual-property-admin', MLS_PLUGIN_URL . 'admin/css/manual-property.css', array(), MLS_PLUGIN_VERSION ); } /** * Render clone button on add new page */ public function render_clone_button($post) { if ($post->post_type !== 'manual_property') { return; } // Only show on new posts if ($post->post_status !== 'auto-draft') { return; } ?>

Clone from MLS Listing

Enter an MLS ID to copy data from an existing MLS listing:

$property->listing_key, 'listing_id' => $property->listing_id, 'address' => trim($property->street_number . ' ' . $property->street_name . ' ' . $property->street_suffix), 'city' => $property->city, 'price' => $property->list_price, )); } /** * AJAX: Clone MLS listing */ public function ajax_clone_listing() { check_ajax_referer('mls_clone_listing', 'nonce'); if (!current_user_can('edit_posts')) { wp_send_json_error('Unauthorized'); } $listing_key = sanitize_text_field($_POST['listing_key'] ?? ''); $post_id = intval($_POST['post_id'] ?? 0); if (empty($listing_key) || !$post_id) { wp_send_json_error('Missing parameters'); } $property = mls_get_property($listing_key); if (!$property) { wp_send_json_error('Listing not found'); } // Clone field values $this->clone_property_fields($post_id, $property); // Clone images $images_imported = $this->clone_property_images($post_id, $listing_key); wp_send_json_success(array( 'message' => 'Listing cloned successfully', 'images_imported' => $images_imported, )); } /** * Clone property fields from MLS listing */ private function clone_property_fields($post_id, $property) { // Update post title and change status from auto-draft to publish $address = trim($property->street_number . ' ' . $property->street_name . ' ' . $property->street_suffix); $title = $address . ', ' . $property->city . ', ' . $property->state_or_province . ' ' . $property->postal_code; wp_update_post(array( 'ID' => $post_id, 'post_title' => $title, 'post_status' => 'publish', )); // Map MLS fields to ACF fields $field_map = array( 'listing_id' => $property->listing_id, 'standard_status' => $property->standard_status, 'list_price' => $property->list_price, 'close_price' => $property->close_price, 'property_type' => $property->property_type, 'property_sub_type' => $property->property_sub_type, 'is_homeproz' => $property->is_homeproz ? 1 : 0, 'full_address' => $address . ', ' . $property->city . ', ' . $property->state_or_province . ' ' . $property->postal_code, 'unit_number' => $property->unit_number, 'city' => $property->city, 'state_or_province' => $property->state_or_province, 'postal_code' => $property->postal_code, 'county' => $property->county, 'latitude' => $property->latitude, 'longitude' => $property->longitude, 'bedrooms_total' => $property->bedrooms_total, 'bathrooms_total' => $property->bathrooms_total, 'bathrooms_full' => $property->bathrooms_full, 'bathrooms_half' => $property->bathrooms_half, 'living_area' => $property->living_area, 'lot_size_area' => $property->lot_size_area, 'year_built' => $property->year_built, 'garage_spaces' => $property->garage_spaces, 'public_remarks' => $property->public_remarks, 'directions' => $property->directions, 'list_date' => $property->listing_contract_date, 'close_date' => $property->close_date, ); foreach ($field_map as $field_name => $value) { if ($value !== null && $value !== '') { update_field($field_name, $value, $post_id); } } // Match and set listing agent by MLS ID if (!empty($property->list_agent_mls_id)) { $agent_post_id = $this->find_agent_by_mls_id($property->list_agent_mls_id); if ($agent_post_id) { update_field('agent', $agent_post_id, $post_id); } } // Match and set co-listing agent by MLS ID if (!empty($property->co_list_agent_mls_id)) { $co_agent_post_id = $this->find_agent_by_mls_id($property->co_list_agent_mls_id); if ($co_agent_post_id) { update_field('co_list_agent', $co_agent_post_id, $post_id); } } // Mark the geocoded address update_post_meta($post_id, '_geocoded_address', $field_map['full_address']); } /** * Clone property images from MLS listing */ private function clone_property_images($post_id, $listing_key) { // Get all images for the listing (fetch up to 20 on-demand) $media = mls_get_property_images($listing_key, 20); if (empty($media)) { return 0; } $attachment_ids = array(); $imported = 0; foreach ($media as $item) { // Try local_url first, then use the image endpoint URL which fetches on-demand $image_url = null; if (!empty($item->local_url)) { $image_url = $item->local_url; } elseif (function_exists('mls_get_image_url')) { // Use the image endpoint which will fetch on-demand and return the image $image_url = mls_get_image_url($listing_key, $item->media_order, 'full'); } if (empty($image_url)) { continue; } // Download image to media library $attachment_id = $this->sideload_image($image_url, $post_id); if ($attachment_id) { $attachment_ids[] = $attachment_id; $imported++; } // Limit to 20 images to avoid timeout if ($imported >= 20) { break; } } // Update gallery field if (!empty($attachment_ids)) { update_field('gallery', $attachment_ids, $post_id); } return $imported; } /** * Find an Agent CPT post by MLS ID * * @param string $mls_id The agent's MLS ID * @return int|null Agent post ID or null if not found */ private function find_agent_by_mls_id($mls_id) { if (empty($mls_id)) { return null; } $agent_query = new WP_Query(array( 'post_type' => 'agent', 'posts_per_page' => 1, 'post_status' => 'publish', 'meta_query' => array( array( 'key' => 'agent_mls_id', 'value' => $mls_id, 'compare' => '=', ), ), )); $agent_id = null; if ($agent_query->have_posts()) { $agent_id = $agent_query->posts[0]->ID; } wp_reset_postdata(); return $agent_id; } /** * Sideload image to media library */ private function sideload_image($url, $post_id) { require_once ABSPATH . 'wp-admin/includes/media.php'; require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; // Download file $tmp = download_url($url); if (is_wp_error($tmp)) { return 0; } // Get filename from URL $filename = basename(parse_url($url, PHP_URL_PATH)); $file_array = array( 'name' => $filename, 'tmp_name' => $tmp, ); // Sideload to media library $attachment_id = media_handle_sideload($file_array, $post_id); if (is_wp_error($attachment_id)) { @unlink($tmp); return 0; } return $attachment_id; } /** * Get all manual property listing IDs that should override MLS * * @return array Array of MLS IDs (listing_id) that have manual overrides */ public static function get_override_listing_ids() { global $wpdb; $db = new MLS_DB(); $table = $db->manual_properties_table(); // Check if table exists $table_exists = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s", DB_NAME, $table )); if (!$table_exists) { return array(); } $ids = $wpdb->get_col( "SELECT listing_id FROM {$table} WHERE listing_id IS NOT NULL AND listing_id != '' AND standard_status NOT IN ('Withdrawn')" ); return array_filter($ids); } /** * Get images for a manual property * * @param string $listing_key Manual property listing key (MANUAL-xxx) * @return array Array of image objects compatible with MLS media format */ public static function get_manual_property_images($listing_key) { // Extract post ID from listing key if (strpos($listing_key, 'MANUAL-') !== 0) { return array(); } $post_id = (int) str_replace('MANUAL-', '', $listing_key); if (!$post_id) { return array(); } $gallery = get_field('gallery', $post_id); if (empty($gallery) || !is_array($gallery)) { return array(); } $images = array(); $order = 0; foreach ($gallery as $image) { $order++; $images[] = (object) array( 'id' => $image['ID'], 'listing_key' => $listing_key, 'media_key' => 'manual-' . $image['ID'], 'media_type' => 'Photo', 'media_order' => $order, 'media_url' => $image['url'], 'local_path' => get_attached_file($image['ID']), 'local_url' => $image['url'], 'file_size' => filesize(get_attached_file($image['ID'])) ?: null, 'mime_type' => $image['mime_type'], 'image_width' => $image['width'], 'image_height' => $image['height'], 'downloaded_at' => get_the_date('Y-m-d H:i:s', $image['ID']), 'download_status' => 'complete', ); } return $images; } /** * Get a manual property by listing key or listing ID * * @param string $identifier Listing key (MANUAL-xxx) or listing_id * @return object|null */ public static function get_manual_property($identifier) { global $wpdb; $db = new MLS_DB(); $table = $db->manual_properties_table(); // Check if table exists $table_exists = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s", DB_NAME, $table )); if (!$table_exists) { return null; } // Try by listing_key first $property = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$table} WHERE listing_key = %s", $identifier )); if (!$property) { // Try by listing_id $property = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$table} WHERE listing_id = %s", $identifier )); } return $property; } }