Integrate MLS listings with property map and add smart sync

Property Map:
- Replace ACF-based property display with MLS database queries
- Use real lat/lng coordinates from MLS (100% coverage)
- Create property-card-mls.php template for MLS property cards
- Update AJAX handler to filter MLS properties

MLS Plugin Enhancements:
- Add 'wp mls run' smart sync command (auto-detects full/incremental/resume)
- Add database index migrations for lat/lng and composite search indexes
- Add comprehensive README.md documentation

Documentation:
- Update site README.md with sysadmin quick reference
- Add FEATURES_PENDING_12_15.md tracking client feature requests

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Hanson.xyz Dev
2025-12-15 22:32:41 -06:00
parent b9cddd2f64
commit fc018ca604
13 changed files with 2346 additions and 308 deletions
+10 -10
View File
@@ -1,18 +1,18 @@
{"id":"html-2fp","title":"Separate Residential and Commercial listings on homepage","description":"","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-30T02:24:09.984594683-06:00","updated_at":"2025-11-30T02:33:12.32537052-06:00","closed_at":"2025-11-30T02:33:12.32537052-06:00"} {"id":"html-2fp","title":"Separate Residential and Commercial listings on homepage","description":"","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-30T02:24:09.984594683-06:00","updated_at":"2025-11-30T02:33:12.32537052-06:00","closed_at":"2025-11-30T02:33:12.32537052-06:00","close_reason":"Separated Featured Homes and Commercial/Land into distinct homepage sections"}
{"id":"html-3fb","title":"MLS by HansonXyz Plugin - Phase 5: Public API","description":"Query class with filter support, global helper functions (mls_get_properties, etc), integration hooks for themes","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-14T21:02:45.05131653-06:00","updated_at":"2025-12-14T21:21:46.940975819-06:00","closed_at":"2025-12-14T21:21:46.940975819-06:00","dependencies":[{"issue_id":"html-3fb","depends_on_id":"html-5j7","type":"blocks","created_at":"2025-12-14T21:04:05.308661828-06:00","created_by":"unknown"}]} {"id":"html-3fb","title":"MLS by HansonXyz Plugin - Phase 5: Public API","description":"Query class with filter support, global helper functions (mls_get_properties, etc), integration hooks for themes","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-14T21:02:45.05131653-06:00","updated_at":"2025-12-14T21:21:46.940975819-06:00","closed_at":"2025-12-14T21:21:46.940975819-06:00","dependencies":[{"issue_id":"html-3fb","depends_on_id":"html-5j7","type":"blocks","created_at":"2025-12-14T21:04:05.308661828-06:00","created_by":"unknown"}]}
{"id":"html-3nq","title":"Enhance footer with office hours, professional logos, license numbers","description":"","status":"closed","priority":3,"issue_type":"task","created_at":"2025-11-30T02:24:30.889106857-06:00","updated_at":"2025-11-30T02:46:52.35661921-06:00","closed_at":"2025-11-30T02:46:52.35661921-06:00"} {"id":"html-3nq","title":"Enhance footer with office hours, professional logos, license numbers","description":"","status":"closed","priority":3,"issue_type":"task","created_at":"2025-11-30T02:24:30.889106857-06:00","updated_at":"2025-11-30T02:46:52.35661921-06:00","closed_at":"2025-11-30T02:46:52.35661921-06:00","close_reason":"Enhanced footer with office hours, professional logos (REALTOR, Equal Housing), and license number"}
{"id":"html-4q8","title":"MLS by HansonXyz Plugin - Phase 6: Admin Interface","description":"Settings page under Settings menu, API token configuration, sync status display, manual sync triggers","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-14T21:02:50.276941526-06:00","updated_at":"2025-12-14T21:21:56.543615901-06:00","closed_at":"2025-12-14T21:21:56.543615901-06:00","dependencies":[{"issue_id":"html-4q8","depends_on_id":"html-3fb","type":"blocks","created_at":"2025-12-14T21:04:10.388690618-06:00","created_by":"unknown"}]} {"id":"html-4q8","title":"MLS by HansonXyz Plugin - Phase 6: Admin Interface","description":"Settings page under Settings menu, API token configuration, sync status display, manual sync triggers","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-14T21:02:50.276941526-06:00","updated_at":"2025-12-14T21:21:56.543615901-06:00","closed_at":"2025-12-14T21:21:56.543615901-06:00","dependencies":[{"issue_id":"html-4q8","depends_on_id":"html-3fb","type":"blocks","created_at":"2025-12-14T21:04:10.388690618-06:00","created_by":"unknown"}]}
{"id":"html-4za","title":"MLS by HansonXyz Plugin - Phase 2: API Client","description":"API Client class with auth, gzip, error handling. Rate Limiter class. CLI test commands (wp mls test connection/auth)","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-14T21:02:29.352944416-06:00","updated_at":"2025-12-14T21:21:36.854872754-06:00","closed_at":"2025-12-14T21:21:36.854872754-06:00","dependencies":[{"issue_id":"html-4za","depends_on_id":"html-ha4","type":"blocks","created_at":"2025-12-14T21:03:50.090841814-06:00","created_by":"unknown"}]} {"id":"html-4za","title":"MLS by HansonXyz Plugin - Phase 2: API Client","description":"API Client class with auth, gzip, error handling. Rate Limiter class. CLI test commands (wp mls test connection/auth)","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-14T21:02:29.352944416-06:00","updated_at":"2025-12-14T21:21:36.854872754-06:00","closed_at":"2025-12-14T21:21:36.854872754-06:00","dependencies":[{"issue_id":"html-4za","depends_on_id":"html-ha4","type":"blocks","created_at":"2025-12-14T21:03:50.090841814-06:00","created_by":"unknown"}]}
{"id":"html-5bw","title":"Add service cards section (Buy/Rent/Sell) to homepage","description":"","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-30T02:24:04.779591681-06:00","updated_at":"2025-11-30T02:32:12.064316318-06:00","closed_at":"2025-11-30T02:32:12.064316318-06:00"} {"id":"html-5bw","title":"Add service cards section (Buy/Rent/Sell) to homepage","description":"","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-30T02:24:04.779591681-06:00","updated_at":"2025-11-30T02:32:12.064316318-06:00","closed_at":"2025-11-30T02:32:12.064316318-06:00","close_reason":"Added service cards section with Buy/Rent/Sell options"}
{"id":"html-5j7","title":"MLS by HansonXyz Plugin - Phase 3: Sync Engine","description":"Sync Engine class, full sync with pagination, incremental sync with ModificationTimestamp, sync state tracking for resume, CLI sync commands","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-14T21:02:34.581442915-06:00","updated_at":"2025-12-14T21:21:46.933578124-06:00","closed_at":"2025-12-14T21:21:46.933578124-06:00","dependencies":[{"issue_id":"html-5j7","depends_on_id":"html-4za","type":"blocks","created_at":"2025-12-14T21:03:55.163778736-06:00","created_by":"unknown"}]} {"id":"html-5j7","title":"MLS by HansonXyz Plugin - Phase 3: Sync Engine","description":"Sync Engine class, full sync with pagination, incremental sync with ModificationTimestamp, sync state tracking for resume, CLI sync commands","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-14T21:02:34.581442915-06:00","updated_at":"2025-12-14T21:21:46.933578124-06:00","closed_at":"2025-12-14T21:21:46.933578124-06:00","dependencies":[{"issue_id":"html-5j7","depends_on_id":"html-4za","type":"blocks","created_at":"2025-12-14T21:03:55.163778736-06:00","created_by":"unknown"}]}
{"id":"html-7jz","title":"Add Communities section to navigation and create community pages structure","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-30T02:24:15.204568226-06:00","updated_at":"2025-11-30T02:43:22.075934867-06:00","closed_at":"2025-11-30T02:43:22.075934867-06:00"} {"id":"html-7jz","title":"Add Communities section to navigation and create community pages structure","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-30T02:24:15.204568226-06:00","updated_at":"2025-11-30T02:43:22.075934867-06:00","closed_at":"2025-11-30T02:43:22.075934867-06:00","close_reason":"Created Communities landing page, community page template, 3 community pages, and added to navigation"}
{"id":"html-98b","title":"Add location search dropdown to homepage hero","description":"","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-30T02:23:59.555310037-06:00","updated_at":"2025-11-30T02:30:59.92891882-06:00","closed_at":"2025-11-30T02:30:59.92891882-06:00"} {"id":"html-98b","title":"Add location search dropdown to homepage hero","description":"","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-30T02:23:59.555310037-06:00","updated_at":"2025-11-30T02:30:59.92891882-06:00","closed_at":"2025-11-30T02:30:59.92891882-06:00","close_reason":"Added location search dropdown to hero section with community taxonomy"}
{"id":"html-bfd","title":"Update DESIGN-DOCUMENT.md and IMPLEMENTATION-PLAN.md with RHR structural changes","description":"","status":"closed","priority":0,"issue_type":"task","created_at":"2025-11-30T02:24:40.504170573-06:00","updated_at":"2025-11-30T02:28:50.551587345-06:00","closed_at":"2025-11-30T02:28:50.551587345-06:00"} {"id":"html-bfd","title":"Update DESIGN-DOCUMENT.md and IMPLEMENTATION-PLAN.md with RHR structural changes","description":"","status":"closed","priority":0,"issue_type":"task","created_at":"2025-11-30T02:24:40.504170573-06:00","updated_at":"2025-11-30T02:28:50.551587345-06:00","closed_at":"2025-11-30T02:28:50.551587345-06:00","close_reason":"Updated DESIGN-DOCUMENT.md and IMPLEMENTATION-PLAN.md with RHR structural changes"}
{"id":"html-clv","title":"Analyze Robert Hoffman Realty site structure for HomeProz redesign","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-30T02:11:43.511290155-06:00","updated_at":"2025-11-30T02:21:47.665340956-06:00","closed_at":"2025-11-30T02:21:47.665340956-06:00"} {"id":"html-clv","title":"Analyze Robert Hoffman Realty site structure for HomeProz redesign","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-30T02:11:43.511290155-06:00","updated_at":"2025-11-30T02:21:47.665340956-06:00","closed_at":"2025-11-30T02:21:47.665340956-06:00","close_reason":"Completed site analysis comparing RHR to HomeProz design"}
{"id":"html-cpd","title":"Add map view to property listings archive page","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-30T02:24:20.442584472-06:00","updated_at":"2025-11-30T02:48:58.865691376-06:00","closed_at":"2025-11-30T02:48:58.865691376-06:00"} {"id":"html-cpd","title":"Add map view to property listings archive page","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-30T02:24:20.442584472-06:00","updated_at":"2025-11-30T02:48:58.865691376-06:00","closed_at":"2025-11-30T02:48:58.865691376-06:00","close_reason":"Added map view to property archive with Grid/Map toggle using Leaflet, city-based property markers, and split layout"}
{"id":"html-ha4","title":"MLS by HansonXyz Plugin - Phase 1: Foundation","description":"Create plugin structure, main file, activator/deactivator, database schema with dbDelta, options handling, logger class","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-14T21:02:24.104655824-06:00","updated_at":"2025-12-14T21:21:36.848348568-06:00","closed_at":"2025-12-14T21:21:36.848348568-06:00"} {"id":"html-ha4","title":"MLS by HansonXyz Plugin - Phase 1: Foundation","description":"Create plugin structure, main file, activator/deactivator, database schema with dbDelta, options handling, logger class","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-14T21:02:24.104655824-06:00","updated_at":"2025-12-14T21:21:36.848348568-06:00","closed_at":"2025-12-14T21:21:36.848348568-06:00"}
{"id":"html-k37","title":"MLS by HansonXyz Plugin - Phase 8: Documentation \u0026 Testing","description":"CLAUDE.md, API.md, USAGE.md documentation. Full CLI test sequence. Final verification.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-14T21:03:00.728802408-06:00","updated_at":"2025-12-14T21:22:16.755253009-06:00","closed_at":"2025-12-14T21:22:16.755253009-06:00","dependencies":[{"issue_id":"html-k37","depends_on_id":"html-x03","type":"blocks","created_at":"2025-12-14T21:04:20.543019628-06:00","created_by":"unknown"}]} {"id":"html-k37","title":"MLS by HansonXyz Plugin - Phase 8: Documentation \u0026 Testing","description":"CLAUDE.md, API.md, USAGE.md documentation. Full CLI test sequence. Final verification.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-14T21:03:00.728802408-06:00","updated_at":"2025-12-14T21:22:16.755253009-06:00","closed_at":"2025-12-14T21:22:16.755253009-06:00","dependencies":[{"issue_id":"html-k37","depends_on_id":"html-x03","type":"blocks","created_at":"2025-12-14T21:04:20.543019628-06:00","created_by":"unknown"}]}
{"id":"html-lci","title":"Scrape homeprozrealestate.com property listings and import to WordPress","description":"","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-30T17:37:41.948645374-06:00","updated_at":"2025-11-30T18:06:33.347607321-06:00","closed_at":"2025-11-30T18:06:33.347607321-06:00"} {"id":"html-lci","title":"Scrape homeprozrealestate.com property listings and import to WordPress","description":"","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-30T17:37:41.948645374-06:00","updated_at":"2025-11-30T18:06:33.347607321-06:00","closed_at":"2025-11-30T18:06:33.347607321-06:00","close_reason":"Imported 5 properties with images, ACF fields, and external listing URLs"}
{"id":"html-sbh","title":"MLS by HansonXyz Plugin - Phase 4: Media Handler","description":"Media Handler class, download and organize media files, PhotosChangeTimestamp detection, CLI media commands","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-14T21:02:39.793324508-06:00","updated_at":"2025-12-14T21:21:46.94012466-06:00","closed_at":"2025-12-14T21:21:46.94012466-06:00","dependencies":[{"issue_id":"html-sbh","depends_on_id":"html-5j7","type":"blocks","created_at":"2025-12-14T21:04:00.24472923-06:00","created_by":"unknown"}]} {"id":"html-sbh","title":"MLS by HansonXyz Plugin - Phase 4: Media Handler","description":"Media Handler class, download and organize media files, PhotosChangeTimestamp detection, CLI media commands","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-14T21:02:39.793324508-06:00","updated_at":"2025-12-14T21:21:46.94012466-06:00","closed_at":"2025-12-14T21:21:46.94012466-06:00","dependencies":[{"issue_id":"html-sbh","depends_on_id":"html-5j7","type":"blocks","created_at":"2025-12-14T21:04:00.24472923-06:00","created_by":"unknown"}]}
{"id":"html-t8u","title":"Add Resources section to navigation and create resource pages","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-30T02:24:25.662824938-06:00","updated_at":"2025-11-30T02:45:31.13972652-06:00","closed_at":"2025-11-30T02:45:31.13972652-06:00"} {"id":"html-t8u","title":"Add Resources section to navigation and create resource pages","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-30T02:24:25.662824938-06:00","updated_at":"2025-11-30T02:45:31.13972652-06:00","closed_at":"2025-11-30T02:45:31.13972652-06:00","close_reason":"Created Resources landing page, resource page template, Buyer's Guide, Seller's Guide, and added to navigation"}
{"id":"html-x03","title":"MLS by HansonXyz Plugin - Phase 7: Cron \u0026 Automation","description":"WP Cron scheduling (configurable interval), standalone cron script for Unix cron","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-14T21:02:55.483012095-06:00","updated_at":"2025-12-14T21:22:06.519310981-06:00","closed_at":"2025-12-14T21:22:06.519310981-06:00","dependencies":[{"issue_id":"html-x03","depends_on_id":"html-4q8","type":"blocks","created_at":"2025-12-14T21:04:15.476790917-06:00","created_by":"unknown"}]} {"id":"html-x03","title":"MLS by HansonXyz Plugin - Phase 7: Cron \u0026 Automation","description":"WP Cron scheduling (configurable interval), standalone cron script for Unix cron","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-14T21:02:55.483012095-06:00","updated_at":"2025-12-14T21:22:06.519310981-06:00","closed_at":"2025-12-14T21:22:06.519310981-06:00","dependencies":[{"issue_id":"html-x03","depends_on_id":"html-4q8","type":"blocks","created_at":"2025-12-14T21:04:15.476790917-06:00","created_by":"unknown"}]}
+823
View File
@@ -0,0 +1,823 @@
# HomeProz Website - Pending Features Analysis
**Date:** December 15, 2025
**Compiled by:** Hanson Development
**Status:** Internal Review Document (Not for Client Distribution)
---
## Executive Summary
This document consolidates feedback from multiple stakeholders (Dayna, Sonny, Anna, Davy, Brian Haugen) collected in early December 2025. Features are grouped by category, scored, and include implementation recommendations.
**Scoring Scale:**
- **Feasibility:** How technically achievable (Trivial / Easy / Moderate / Complex / Impractical)
- **Priority:** Business importance per stakeholder emphasis (Low / Medium / High / Critical)
- **Practicality:** ROI vs effort ratio (Poor / Fair / Good / Excellent)
- **Recommendation:** Overall verdict (Skip / Defer / Consider / Implement / Must-Do)
---
## Category 1: MLS Integration & Property Display
### 1.1 Co-Listing Agent Support
**Description:** Allow multiple agents to be displayed on a single property listing when co-listed.
**Requested by:** Davy
**Details:** Davy noticed only one agent shows on co-listed properties. Dayna confirmed "each property will include space for additional realtors/agents."
| Metric | Score |
|--------|-------|
| Feasibility | Moderate |
| Priority | High |
| Practicality | Good |
| Recommendation | **Implement** |
**Analysis:** MLS data includes co-listing agent fields (CoListAgentKey, CoListAgentMlsId, etc.). Requires schema update to store and display multiple agents per listing. The MLS API provides this data - we just need to capture and render it.
**Implementation Notes:** Update `class-mls-db.php` to store co-agent data, update property templates to display both agents with proper attribution.
---
### 1.2 Featured Photo Control
**Description:** Ensure the main cover photo from MLS is used as the featured photo, with ability to override.
**Requested by:** Davy, Dayna
**Details:** Davy noted featured photos didn't match MLS during presentation. Dayna confirmed "each property's featured photo will carry over from MLS or you can edit or choose whichever photos you would like."
| Metric | Score |
|--------|-------|
| Feasibility | Easy |
| Priority | High |
| Practicality | Excellent |
| Recommendation | **Must-Do** |
**Analysis:** MLS media has `Order` field - image with Order=1 should be primary. This should already work based on our media ordering system. May need verification and possible admin override capability.
**Implementation Notes:** Verify `media_order` is respected. Consider adding ACF field for manual override if needed.
---
### 1.3 Sold/Pending Properties Display
**Description:** Show pending and sold properties, possibly on a dedicated "Results" page.
**Requested by:** Davy, Anna, Dayna, Brian Haugen
**Details:**
- Davy wants "running list or Sold in the last year column" to show sales success
- Anna believes "Brian mentioned that is in the works"
- Dayna says "we will need to discuss how and where we would like these to sort and appear - perhaps a results page!"
- Meeting transcript confirms sold/pending display is expected
| Metric | Score |
|--------|-------|
| Feasibility | Complex |
| Priority | High |
| Practicality | Fair |
| Recommendation | **Consider** |
**Analysis:** Current sync strategy DELETES sold/pending-changed-to-closed properties per MLS Grid best practices. Showing sold properties requires:
1. Changing sync strategy to retain closed listings (storage implications - 1.3M vs 30K records)
2. OR maintaining a separate "sold by us" table
3. Time-boxing (last 12 months) to limit scope
**Contradictions/Concerns:**
- MLS Grid rules may restrict display of sold properties
- Storage/performance impact of keeping closed listings
- Need clarification: show ALL sold in area, or only OUR sold listings?
**Recommendation:** Create manual "Results/Success Stories" section where agents can highlight their closed deals rather than automated MLS sync of all closed properties. This is more practical and better marketing.
---
### 1.4 Geolocation Search ("Properties Near Me")
**Description:** Add location-based property search using browser geolocation.
**Requested by:** Sonny, Dayna
**Details:**
- Sonny: "Could do an all properties near you under featured properties. and have the website do a 'allow website your location' when you open it. Similar to Keller williams realty on mobile."
- Dayna: "add geolocation to search (properties near me)"
| Metric | Score |
|--------|-------|
| Feasibility | Moderate |
| Priority | Medium |
| Practicality | Good |
| Recommendation | **Implement** |
**Analysis:** Database already has lat/lng columns with indexes (added this session). Implementation requires:
1. Browser Geolocation API prompt
2. Distance calculation query (or bounding box filter)
3. "Near Me" button/section on homepage and search
**Implementation Notes:** Use bounding box query first, then haversine formula for precise sorting. Already have index infrastructure from this session's work.
---
### 1.5 Expanded Search Filters
**Description:** Add property type, zip code, and additional search criteria.
**Requested by:** Sonny, Dayna
**Details:**
- Dayna: "Expand out search (Select a community.... Do we like that? Add additional? Search by zip?)"
- Dayna: "Add property type to search and at bottom like Edina Realty - commercial, new construction, acreages, town homes, etc."
- Sonny: "Could add 'specialty real estate' boxes -commercial, new construction, acreages, town homes, etc.- on the bottom like Edina Realty"
| Metric | Score |
|--------|-------|
| Feasibility | Easy |
| Priority | High |
| Practicality | Excellent |
| Recommendation | **Implement** |
**Analysis:** Database already has `property_type`, `property_sub_type`, `postal_code` columns. Query class supports these filters. Just need UI additions.
**Implementation Notes:** Add filter dropdowns to search form. Add property type quick-filter boxes on homepage.
---
### 1.6 Property Type Showcase Boxes
**Description:** Add category boxes on homepage for Commercial, Acreages, New Construction, Townhomes, etc.
**Requested by:** Sonny, Dayna, Anna
**Details:**
- Sonny: "specialty real estate boxes... like Edina Realty, with boxes and when we integrate IDX have it populate accordingly"
- Anna: "We've branded HomeProz specialty as 'Residential, Acreages, and Commercial'... but this website currently only reflects Homes"
| Metric | Score |
|--------|-------|
| Feasibility | Easy |
| Priority | High |
| Practicality | Excellent |
| Recommendation | **Implement** |
**Analysis:** Simple UI component with links to filtered search results. Supports brand positioning.
---
## Category 2: Visual Design & UX
### 2.1 Texture/Luxury Aesthetic
**Description:** Add texture overlays to solid colors (grey/black) for more luxury feel.
**Requested by:** Sonny, Dayna
**Details:**
- Sonny: "add some sort of texture to solid colors (the grey and black) to make it feel more luxury and produced"
- Dayna: "Add some texture to the Entire site - adds visual interest while keeping it modern and polished"
| Metric | Score |
|--------|-------|
| Feasibility | Easy |
| Priority | Medium |
| Practicality | Good |
| Recommendation | **Implement** |
**Analysis:** CSS-only change. Add subtle noise/grain texture overlay to dark backgrounds.
**Bikeshedding Risk:** "Luxury" is subjective. Get sign-off on specific texture samples before site-wide implementation.
---
### 2.2 Hover Animations
**Description:** Add hover effects to elements like "Our Story" and "Our Mission" titles.
**Requested by:** Sonny, Dayna
**Details:**
- Sonny: "'our story' & 'our mission' have them toggle so the words appear if clicked on or hovered over. (cleans up home page for mobile, less clutter)"
- Dayna: "Some visual animations - ex when hovering over Our Story or Our Mission - have the titles expand and bold"
| Metric | Score |
|--------|-------|
| Feasibility | Easy |
| Priority | Low |
| Practicality | Fair |
| Recommendation | **Consider** |
**Analysis:** Simple CSS transitions. However, CLAUDE.md explicitly states "No custom animations - keep it static and fast."
**Contradiction:** Project rules say no animations, but client wants them.
**Recommendation:** Clarify with client - subtle hover states (bold, underline) are fine. Avoid complex animations that conflict with project principles. Propose simple hover states rather than toggle/expand behavior.
---
### 2.3 4K/High-Quality Stock Images
**Description:** Replace placeholder images with high-quality 4K stock photos.
**Requested by:** Sonny
**Details:** "make sure photos that are not agent related like (personal agent page) are all 4k and top notch photos" and "add new header picture off stock images 4k with logo"
| Metric | Score |
|--------|-------|
| Feasibility | Easy |
| Priority | Medium |
| Practicality | Good |
| Recommendation | **Implement** |
**Analysis:** Content task, not development. Need to source and optimize images.
**Implementation Notes:** Client needs to provide or approve stock image purchases. Optimize for web (not literally 4K - that's overkill for web).
---
### 2.4 Logo Consistency
**Description:** Update logos throughout site, ensure mobile consistency, use condensed HP logo.
**Requested by:** Dayna, Anna, Sonny
**Details:**
- Dayna: "Updated HP logos and be consistent on mobile"
- Anna: "We don't have our condensed HP logo anywhere on here... and it's so awesome! Need it on here"
- Sonny: "top left 'homeproz' change that to a logo or do all caps to make it look uniform"
| Metric | Score |
|--------|-------|
| Feasibility | Trivial |
| Priority | High |
| Practicality | Excellent |
| Recommendation | **Must-Do** |
**Analysis:** Branding consistency is important. Need assets from client.
**Implementation Notes:** Request condensed logo SVG from client. Update header, mobile nav, favicon.
---
### 2.5 Mobile Homepage Optimization
**Description:** Make homepage sections more compact on mobile to reach listings faster.
**Requested by:** Sonny
**Details:** "make 'go with the proz, sell your home, find a home, resources and guides' smaller and side by side on mobile so it goes to listed properties faster"
| Metric | Score |
|--------|-------|
| Feasibility | Easy |
| Priority | Medium |
| Practicality | Good |
| Recommendation | **Implement** |
**Analysis:** CSS/responsive layout adjustment. Good UX improvement.
---
## Category 3: Team/Agent Pages
### 3.1 Agent Page Redesign (LandProz Style)
**Description:** Redesign agent cards/pages to match LandProz model with boxes instead of PNG backgrounds.
**Requested by:** Sonny, Dayna
**Details:**
- Sonny: "Make the meet our team page (about page) like landproz model"
- Sonny: "do the per agent in boxes not a png background like how landproz is *4k stock image of a house (looks cleaner and less cookie cutter)"
- Dayna: "Build out meet our team page - updated high quality headshots, improved background photos, bios, reviews"
| Metric | Score |
|--------|-------|
| Feasibility | Moderate |
| Priority | High |
| Practicality | Good |
| Recommendation | **Implement** |
**Analysis:** Requires design reference from LandProz site. Template refactoring needed.
**Implementation Notes:** Need access to LandProz site for reference. May need new headshot photos from agents.
---
### 3.2 Agent Tiers/Categories
**Description:** Categorize agents by role/level (Leadership, Elite, etc.)
**Requested by:** Sonny
**Details:** "Add categories of agents, (leadership, elite, show more agents)"
| Metric | Score |
|--------|-------|
| Feasibility | Easy |
| Priority | Medium |
| Practicality | Fair |
| Recommendation | **Consider** |
**Analysis:** Add taxonomy or ACF field for agent tier. Display in groups on team page.
**Potential Issue:** May create internal politics if agents disagree with their tier. Need clear criteria.
---
### 3.3 Agent Testimonials
**Description:** Allow testimonials/reviews on individual agent pages.
**Requested by:** Sonny, Dayna
**Details:**
- Sonny: "per agent page 'make it so we can add testimonials'"
- Dayna: "Build out meet our team page... reviews... and webforms to collect further feedback"
| Metric | Score |
|--------|-------|
| Feasibility | Moderate |
| Priority | Medium |
| Practicality | Good |
| Recommendation | **Implement** |
**Analysis:** Add ACF repeater field for testimonials on agent post type. Or integrate with Google Reviews.
---
### 3.4 Agent Contact Info Placement
**Description:** Move contact info above listings on agent profile pages.
**Requested by:** Sonny
**Details:** "on per agent page have contact info on top of listings not below (right below biography)"
| Metric | Score |
|--------|-------|
| Feasibility | Trivial |
| Priority | Medium |
| Practicality | Excellent |
| Recommendation | **Implement** |
**Analysis:** Template reordering. Simple change.
---
### 3.5 Agent Biographies & Personal Photos
**Description:** Add detailed bios and personal photo galleries to agent pages.
**Requested by:** Sonny, Dayna
**Details:**
- Sonny: "per each agent have them write out a biography and add personal photos to their agent page"
- Dayna: "bios... their properties"
| Metric | Score |
|--------|-------|
| Feasibility | Easy |
| Priority | Medium |
| Practicality | Good |
| Recommendation | **Implement** |
**Analysis:** ACF fields already exist for bio. May need gallery field addition. Content from agents required.
---
### 3.6 New Headshots
**Description:** Get professional updated headshots for all agents.
**Requested by:** Sonny, Dayna
**Details:** "get new headshots" / "updated high quality headshots"
| Metric | Score |
|--------|-------|
| Feasibility | N/A (content task) |
| Priority | High |
| Practicality | Good |
| Recommendation | **Client Action Required** |
**Analysis:** Not a development task. Client needs to arrange photography session.
---
### 3.7 Expanded Team Members
**Description:** Add non-agent team members (broker, marketing, officers, production staff).
**Requested by:** Anna, Dayna
**Details:**
- Anna: "I definitely agree that we need to include the entire team including broker, marketing team, officers, etc in meet the team, so it's a true reflection of the experience we have backing HomeProz"
- Dayna: "Add additional team members - owners, office, production ect with outlines of each and what they do (experience and results focused)"
| Metric | Score |
|--------|-------|
| Feasibility | Easy |
| Priority | Medium |
| Practicality | Good |
| Recommendation | **Implement** |
**Analysis:** Use existing Agent post type with role/category field to differentiate. Or create separate Staff post type.
---
### 3.8 Agent State Licenses
**Description:** Ensure each agent profile shows correct state licensure.
**Requested by:** Sonny, Dayna
**Details:**
- Sonny: "make sure each agent has correct states"
- Dayna: "I will need any additional license numbers you all hold and I'll add that to the footer"
| Metric | Score |
|--------|-------|
| Feasibility | Trivial |
| Priority | High |
| Practicality | Excellent |
| Recommendation | **Must-Do** |
**Analysis:** Legal requirement. Add/verify ACF field for licensed states per agent.
---
## Category 4: Contact & Forms
### 4.1 Property Inquiry Form Auto-Population
**Description:** Contact form should auto-populate with property address/link that cannot be edited by user.
**Requested by:** Brian Haugen (meeting)
**Details:** From transcript: "Brian Hanson will make the property request form automatically populate with the address or a link to the MLS listing, which must not be editable by the user, and should be sent to an office email, while still allowing a personalized message."
| Metric | Score |
|--------|-------|
| Feasibility | Easy |
| Priority | Critical |
| Practicality | Excellent |
| Recommendation | **Must-Do** |
**Analysis:** Use hidden field or read-only display field populated from URL parameter. Already have `?property=` parameter support documented.
**Implementation Notes:** Add hidden/readonly field to contact form. Ensure it's tamper-resistant (server-side validation).
---
### 4.2 Multi-Recipient Email Routing
**Description:** Form submissions go to both the listing agent AND office email.
**Requested by:** Dayna, Anna
**Details:**
- Dayna: "When someone fills out a web form it will send that entry to the agent plus office@homeprozrealestate.com"
- Anna: "Wondering if more than one person can receive emails sent to info@homeprozrealestate.com"
| Metric | Score |
|--------|-------|
| Feasibility | Easy |
| Priority | High |
| Practicality | Excellent |
| Recommendation | **Implement** |
**Analysis:** Contact Form 7 or similar supports multiple recipients. Configure CC/BCC to office address.
---
### 4.3 Office Hours Update
**Description:** Update footer office hours.
**Requested by:** Davy
**Details:**
```
Office Hours
Mon-Fri 9:00am - 4:00pm
Saturday By Appointment
Sunday By Appointment
```
| Metric | Score |
|--------|-------|
| Feasibility | Trivial |
| Priority | Medium |
| Practicality | Excellent |
| Recommendation | **Implement** |
**Analysis:** Simple theme options update.
---
## Category 5: Footer & Legal
### 5.1 Broker Footer Update
**Description:** Update broker attribution in footer with correct legal text.
**Requested by:** Dayna
**Details:** "broker footer will be updated to: HomeProz Real Estate LLC DBA LandProz Real Estate, LLC | 111 East Clark Street, Albert Lea, MN 56007 - 507-516-4870"
Also: "Broker Brian Haugen - MN | Broker/Auctioneer Greg Jensen - MN, IA - 24-21"
| Metric | Score |
|--------|-------|
| Feasibility | Trivial |
| Priority | Critical |
| Practicality | Excellent |
| Recommendation | **Must-Do** |
**Analysis:** Legal requirement. Simple text update.
**Note:** Davy spotted "Bridge Realty" text somewhere - needs to be found and removed.
---
### 5.2 Phone Number Update
**Description:** Update main contact number throughout site.
**Requested by:** Dayna, Davy
**Details:**
- Dayna: "507-516-4870 is the Minnesota state advertising phone number... We will need to choose a number you want as HomeProz main contact number... (Will provided updated new phone number- dayna to purchase and add to phone system)"
- Davy: "IDK whose number that is" (referring to current placeholder)
| Metric | Score |
|--------|-------|
| Feasibility | Trivial |
| Priority | Critical |
| Practicality | Excellent |
| Recommendation | **Must-Do** |
**Analysis:** Update theme options. Awaiting final number from Dayna.
**Blocker:** Need confirmed phone number from client.
---
### 5.3 Service Area Update (MN + IA)
**Description:** Update service area from "Southern Minnesota" to include Iowa.
**Requested by:** Dayna, Davy
**Details:**
- Dayna: "Right now the site reads 'Southern Minnesota'. How would we like to present this? Southern MN and Northern IA? or Minnesota and Iowa?" Answer: "Minnesota and Iowa (for service areas)"
- Davy: "Should we have this say and Northern Iowa? We don't have any in IA right now but when we do..."
- Davy also asked "This is on the bottom but I am wondering if we need it to say IA too since I have an IA license?"
| Metric | Score |
|--------|-------|
| Feasibility | Trivial |
| Priority | High |
| Practicality | Excellent |
| Recommendation | **Implement** |
**Analysis:** Text/content update. Confirmed decision: "Minnesota and Iowa"
---
## Category 6: Reviews & Social Proof
### 6.1 Google Reviews Integration
**Description:** Automatically pull and display Google Reviews on website.
**Requested by:** Anna, Dayna
**Details:**
- Anna: "Google Review process" / "A place to view past Testimonials and new ones"
- Dayna: "Google reviews can be automated to your website"
| Metric | Score |
|--------|-------|
| Feasibility | Moderate |
| Priority | Medium |
| Practicality | Good |
| Recommendation | **Implement** |
**Analysis:** Requires Google Places API integration or third-party widget. Several WordPress plugins available.
**Implementation Notes:** Recommend plugin like "Widget for Google Reviews" for simplicity. Or custom integration if more control needed.
---
### 6.2 Community Involvement Section
**Description:** Showcase photos/videos of community events and involvement.
**Requested by:** Davy, Anna, Dayna
**Details:**
- Davy: "I noticed he didn't include our 'In the Community' pics that we have on the bottom of our current page. That has our reviews in there and I would like to include all of those too"
- Anna: "a link for showcasing photos and videos with our community involvement to add the LOCAL aspect and relatability such as the Arcadian Bank event, Freeborn Co Fair video produced each year, and more"
- Dayna: "community involvement and reviews (need to include these and decide if it belongs on the about page or I can create a new page)"
| Metric | Score |
|--------|-------|
| Feasibility | Easy |
| Priority | Medium |
| Practicality | Good |
| Recommendation | **Implement** |
**Analysis:** Gallery/carousel section. Could be on About page or dedicated Community page.
**Decision Needed:** Standalone page vs section on About page?
---
### 6.3 Results/Success Page
**Description:** Dedicated page showing sold properties, reviews, and agent successes.
**Requested by:** Dayna
**Details:** "A results page would also be a good spot to collect and display reviews (agent profiles as well)"
| Metric | Score |
|--------|-------|
| Feasibility | Moderate |
| Priority | Medium |
| Practicality | Good |
| Recommendation | **Consider** |
**Analysis:** Combines sold properties display with testimonials. Better than trying to sync all closed MLS listings.
---
## Category 7: New Pages
### 7.1 Join Our Team / Careers Page
**Description:** Recruitment page for prospective agents.
**Requested by:** Sonny, Anna, Dayna
**Details:**
- Sonny: "add a 'Become a Homeproz agent' page and why we are better what we offer. Tech, residential auctions, in house marketing, etc."
- Anna: "Yes, we definitely need a 'Join our team' tab and share why"
- Dayna: "Add tired experience to meet our team page and button/form to employment application"
| Metric | Score |
|--------|-------|
| Feasibility | Easy |
| Priority | Medium |
| Practicality | Good |
| Recommendation | **Implement** |
**Analysis:** Static content page with application form. Content needed from client about benefits/culture.
---
### 7.2 LandProz History on About Page
**Description:** Include backstory about LandProz 82-year history on About page.
**Requested by:** Anna
**Details:** "on the 'About HomeProz' page... I think we need to include a little back story about LandProz 82 year success story and how we're a division of LandProz, so it doesn't look like we're just a brand new stand alone pop up Broker without history, experience, knowledge."
| Metric | Score |
|--------|-------|
| Feasibility | Trivial |
| Priority | Medium |
| Practicality | Excellent |
| Recommendation | **Implement** |
**Analysis:** Content addition. Need copy from client about LandProz history.
---
### 7.3 Additional Logos on About Page
**Description:** Add more partner/association logos to About page.
**Requested by:** Sonny
**Details:** "need more logos on about page"
| Metric | Score |
|--------|-------|
| Feasibility | Trivial |
| Priority | Low |
| Practicality | Good |
| Recommendation | **Implement** |
**Analysis:** Content addition. Need logo assets and approval for which associations to display.
---
### 7.4 Buyers/Sellers Guide Enhancement
**Description:** Improve existing guides with more visual appeal.
**Requested by:** Sonny
**Details:** "I like it but it needs more of a flare, pictures maybe?"
| Metric | Score |
|--------|-------|
| Feasibility | Easy |
| Priority | Low |
| Practicality | Fair |
| Recommendation | **Defer** |
**Analysis:** Content/design polish. Lower priority than functional features.
---
## Category 8: Social & External Integrations
### 8.1 Additional Social Platform Links
**Description:** Add links to Instagram, LinkedIn, Google My Business, etc.
**Requested by:** Dayna, Anna
**Details:**
- Dayna: "Additional social platforms can be linked as you build them (Instagram, linkedIn, Google my Business ect)"
- Anna: "Are we on Instagram?"
| Metric | Score |
|--------|-------|
| Feasibility | Trivial |
| Priority | Low |
| Practicality | Excellent |
| Recommendation | **Implement** |
**Analysis:** Add fields to theme options. Display in footer/header as icons.
**Blocker:** Client needs to create accounts first.
---
### 8.2 Email Migration to Google
**Description:** Migrate HomeProz email addresses to Google Workspace.
**Requested by:** Dayna
**Details:** "I need to move your existing HomeProz email addresses over to google and add a few like office@homeprozrealestate and info@homeprozrealestate."
| Metric | Score |
|--------|-------|
| Feasibility | N/A (not dev task) |
| Priority | High |
| Practicality | Good |
| Recommendation | **Client/IT Action Required** |
**Analysis:** Infrastructure task, not website development. Coordinate with launch timing.
---
## Category 9: Technical/Infrastructure
### 9.1 Bridge Realty Text Removal
**Description:** Find and remove errant "Bridge Realty" text from site.
**Requested by:** Davy
**Details:** "This says Bridge Realty ??" (with screenshot reference)
| Metric | Score |
|--------|-------|
| Feasibility | Trivial |
| Priority | Critical |
| Practicality | Excellent |
| Recommendation | **Must-Do** |
**Analysis:** Bug/error from template or previous site. Need to search codebase for "Bridge Realty" and remove.
---
## Priority Summary
### Must-Do (Critical)
1. Broker footer legal text update (5.1)
2. Phone number update (5.2)
3. Property inquiry form auto-population (4.1)
4. Featured photo from MLS (1.2)
5. Logo consistency (2.4)
6. Agent state licenses (3.8)
7. Bridge Realty text removal (9.1)
### High Priority (Implement)
1. Co-listing agent support (1.1)
2. Service area MN + IA (5.3)
3. Expanded search filters (1.5)
4. Property type showcase boxes (1.6)
5. Multi-recipient email routing (4.2)
6. Agent page redesign (3.1)
7. Geolocation search (1.4)
### Medium Priority (Implement When Time Allows)
1. Texture/luxury aesthetic (2.1)
2. Mobile homepage optimization (2.5)
3. Agent testimonials (3.3)
4. Agent contact info placement (3.4)
5. Expanded team members (3.7)
6. Google Reviews integration (6.1)
7. Community involvement section (6.2)
8. Join our team page (7.1)
9. LandProz history content (7.2)
### Consider/Defer
1. Sold/pending properties display (1.3) - Complex, needs strategy discussion
2. Hover animations (2.2) - Conflicts with project rules
3. Agent tiers/categories (3.2) - Potential internal politics
4. Results/success page (6.3) - Good idea, needs scoping
5. Buyers/sellers guide enhancement (7.4) - Low impact
### Client Action Required
1. New headshots (3.6)
2. Email migration (8.2)
3. Social account creation (8.1)
4. High-quality stock images (2.3)
5. Content for various sections
---
## Contradictions & Notes
1. **Animation Policy:** CLAUDE.md says "No custom animations" but Sonny/Dayna want hover effects. Resolution: Implement subtle CSS transitions only, not complex JS animations.
2. **Sold Properties:** Multiple stakeholders want to show sold properties, but current MLS sync strategy deletes closed listings. Resolution: Manual "Results" section rather than MLS sync change.
3. **Service Area:** Initial uncertainty about "Southern MN" vs "MN and IA" - resolved as "Minnesota and Iowa"
4. **Phone Number:** Multiple references to needing correct number - awaiting final number from Dayna
5. **Team Tiers:** Sonny wants "Leadership, Elite" categories - could create internal friction. Needs careful handling.
6. **Design Reference:** Multiple references to "like LandProz" - need access to that site for design reference.
---
## Implementation Phases Recommendation
### Phase 1: Launch Blockers (Must complete before launch)
- Legal/footer updates
- Phone number
- Bridge Realty removal
- Form auto-population
- Logo consistency
### Phase 2: Core Features (First week post-launch)
- Co-listing support
- Search enhancements
- Property type boxes
- Agent page improvements
- Service area update
### Phase 3: Enhancements (Following weeks)
- Geolocation search
- Google Reviews
- Community section
- Careers page
- Visual polish (textures, etc.)
### Phase 4: Deferred/TBD
- Sold properties strategy
- Full agent tier system
- Buyers/sellers guide revamp
+209 -14
View File
@@ -1,33 +1,228 @@
# HomeProz Real Estate WordPress Site # HomeProz Real Estate WordPress Site
Custom WordPress theme for HomeProz Real Estate (Albert Lea, MN). Custom WordPress site for HomeProz Real Estate (Albert Lea, MN) with MLS Grid integration.
**Production URL:** https://homeprozrealestate.com
## Quick Reference for Sysadmins
### Required Cron Job
Add to system crontab (`crontab -e`):
```bash
# MLS property sync - runs every 15 minutes
*/15 * * * * cd /var/www/html && wp mls run --silent --allow-root >> /var/log/mls-sync.log 2>&1
```
This single cron job handles everything:
- Initial full sync on first run (~30K properties, takes 30-45 min)
- Incremental updates on subsequent runs
- Automatic recovery from failures
- Safe concurrent execution (aborts if already running)
### Manual Sync Commands
```bash
# Check sync status
wp --allow-root mls status
# Run sync manually
wp --allow-root mls run
# View database statistics
wp --allow-root mls stats
# Test API connection
wp --allow-root mls test connection
```
### Log Files
| Log | Location | Purpose |
|-----|----------|---------|
| MLS Sync | `/var/log/mls-sync.log` | Cron sync output |
| WordPress | `/var/www/html/wp-content/debug.log` | PHP errors (if WP_DEBUG) |
| Missing Media | `/var/www/html/wp-content/uploads/mls-missing-media.log` | Failed image downloads |
### API Credentials
MLS Grid credentials are in `wp-config.php`:
```php
define('MLSGRID_ACCESS_TOKEN', '...');
```
Contact MLS Grid support (support@mlsgrid.com) for token issues.
## Project Structure ## Project Structure
- `/wp-content/themes/homeproz/` - Custom theme ```
- `/db-snapshots/` - Database snapshots for each development phase /var/www/html/
- `/contract/` - Project documentation and planning ├── wp-config.php # WordPress + MLS Grid config
- `/CLAUDE.md` - Development mandates and specifications ├── wp-content/
│ ├── themes/homeproz/ # Custom theme
│ ├── plugins/
│ │ └── mls-by-hansonxyz/ # MLS sync plugin
│ └── uploads/
│ └── mls-listings/ # Cached property images
├── db-snapshots/ # Database snapshots
├── contract/ # Project documentation
├── CLAUDE.md # AI development context
└── DEPENDENCIES.md # System dependencies
```
## Development ## Key Components
### Custom Theme: HomeProz
Location: `/wp-content/themes/homeproz/`
Dark/rust brand aesthetic with ACF-powered property listings.
```bash ```bash
# Build theme assets
cd wp-content/themes/homeproz cd wp-content/themes/homeproz
npm install npm run build
npm run dev # Development with hot reload
npm run build # Production build
``` ```
### MLS Plugin: mls-by-hansonxyz
Location: `/wp-content/plugins/mls-by-hansonxyz/`
Syncs property data from NorthStar MLS via MLS Grid API.
See `/wp-content/plugins/mls-by-hansonxyz/README.md` for full documentation.
### Custom Post Types
| Post Type | URL | Description |
|-----------|-----|-------------|
| Property | `/properties/` | MLS listings (ACF fields) |
| Agent | `/agents/` | Team member profiles |
## Technology Stack ## Technology Stack
- WordPress 6.x - WordPress 6.x
- PHP 8.1+ - PHP 8.1+
- Tailwind CSS - MySQL 8.0
- SCSS (via Vite) - Tailwind CSS + SCSS (via Vite)
- jQuery - jQuery
- ACF Pro - ACF Pro
- WP-CLI
## Client ## Development Commands
HomeProz Real Estate, LLC ```bash
111 E Clark St, Albert Lea, MN 56007 # Theme development
cd wp-content/themes/homeproz
npm install
npm run dev # Dev server with hot reload
npm run build # Production build
# Database snapshot (includes timestamp)
./dev_commit.sh "commit message"
# WP-CLI
wp --allow-root <command>
```
## MLS Data Flow
```
MLS Grid API (NorthStar MLS)
|
v
wp mls run (cron every 15 min)
|
v
wp_mls_properties table (~30K Active/Pending listings)
|
v
Theme displays via mls_get_properties() API
|
v
Images fetched on-demand from MLS Grid
|
v
Cached in wp-content/uploads/mls-listings/
```
## Maintenance Tasks
### Daily
- Cron job runs automatically every 15 minutes
- Monitor `/var/log/mls-sync.log` for errors
### Weekly
- Check `wp --allow-root mls stats` for data health
- Review disk space for cached images
### Monthly
- Review MLS Grid rate limit usage: `wp --allow-root mls status rate-limits`
- Clear orphaned media: `wp --allow-root mls cache cleanup`
### After Server Migration
1. Verify `wp-config.php` has MLS Grid credentials
2. Set up cron job (see above)
3. Run initial sync: `wp --allow-root mls run`
4. Verify: `wp --allow-root mls stats`
## Troubleshooting
### No Properties Showing
```bash
# Check if data exists
wp --allow-root mls stats
# Check sync status
wp --allow-root mls status
# Run manual sync
wp --allow-root mls run --verbose
```
### Sync Failing
```bash
# Test API connection
wp --allow-root mls test connection
wp --allow-root mls test auth
# Check rate limits
wp --allow-root mls status rate-limits
# View resumable syncs
wp --allow-root mls recovery list
```
### Images Not Loading
```bash
# Check cache stats
wp --allow-root mls media status
# Check directory permissions
ls -la wp-content/uploads/mls-listings/
# Pre-cache a listing manually
wp --allow-root mls media fetch --listing=<key> --limit=10
```
### Clear Everything and Start Fresh
```bash
wp --allow-root mls cache clear --confirm
wp --allow-root mls run
```
## Contacts
- **Client:** HomeProz Real Estate, LLC - 111 E Clark St, Albert Lea, MN 56007
- **MLS Grid Support:** support@mlsgrid.com
- **Developer:** HansonXyz - https://hanson.xyz
@@ -0,0 +1,637 @@
# MLS by HansonXyz
WordPress plugin for syncing MLS Grid API data (NorthStar MLS) into a local database with WP-CLI tools and a public API for themes and plugins.
## Table of Contents
- [Features](#features)
- [Requirements](#requirements)
- [Installation](#installation)
- [Configuration](#configuration)
- [Running Sync](#running-sync)
- [WP-CLI Commands](#wp-cli-commands)
- [Cron Setup](#cron-setup)
- [Public API](#public-api)
- [Database Schema](#database-schema)
- [Media Handling](#media-handling)
- [Sync Strategy](#sync-strategy)
- [Error Recovery](#error-recovery)
- [Troubleshooting](#troubleshooting)
## Features
- Syncs Active and Pending property listings from MLS Grid API
- Automatic incremental updates via replication
- On-demand image fetching and local caching
- Self-healing sync with automatic error recovery
- Rate limit compliance (MLS Grid limits enforced)
- Resume capability for interrupted syncs
- WP-CLI commands for all operations
- Public PHP API for theme/plugin integration
- Optimized database indexes for search queries
## Requirements
- WordPress 5.0+
- PHP 7.4+
- MySQL 5.7+ or MariaDB 10.2+
- WP-CLI (for command-line operations)
- MLS Grid API access token
## Installation
1. Upload the `mls-by-hansonxyz` folder to `/wp-content/plugins/`
2. Activate the plugin through WordPress admin
3. Configure API credentials (see Configuration)
4. Run initial sync: `wp mls run`
## Configuration
### API Credentials
Add to your `wp-config.php`:
```php
define('MLSGRID_API_URL', 'https://api.mlsgrid.com/v2');
define('MLSGRID_ACCESS_TOKEN', 'your-access-token-here');
```
### WordPress Admin Settings
Navigate to **Settings > MLS Settings** to configure:
| Setting | Description | Default |
|---------|-------------|---------|
| Originating System | MLS identifier | `northstar` |
| Auto Sync | Enable WP-Cron sync | Disabled |
| Sync Interval | WP-Cron frequency | Hourly |
## Running Sync
### Smart Sync (Recommended)
The `wp mls run` command handles all scenarios automatically:
```bash
wp mls run # Smart sync with progress
wp mls run --quiet # Status messages only
wp mls run --verbose # Full API details
wp mls run --silent # For cron (exit code only)
```
**Automatic behavior:**
- If no data exists: runs full sync
- If data exists: runs incremental sync
- If previous sync failed: resumes from checkpoint
- If sync already running: safely aborts
### Manual Sync Commands
For more control over sync operations:
```bash
# Full sync (Active/Pending properties only)
wp mls sync full
# Incremental sync (changes since last sync)
wp mls sync incremental
# Resume a specific failed sync
wp mls sync resume --id=<sync_id>
# Dry run (no changes)
wp mls sync full --dry-run --limit=100
```
### Progress Indicators
During sync, progress characters indicate activity:
| Symbol | Meaning |
|--------|---------|
| `.` | Property created |
| `#` | Property updated |
| `x` | Property deleted |
| `!` | Error occurred |
| `\|` | Page complete |
Use `--verbose` for detailed timestamped output.
## WP-CLI Commands
### Testing
```bash
wp mls test connection # Test API connectivity
wp mls test auth # Verify authentication
```
### Status and Statistics
```bash
wp mls status # Full status overview
wp mls status rate-limits # Rate limit usage only
wp mls stats # Database statistics
```
### Sync Operations
```bash
# Smart sync (recommended)
wp mls run [--quiet] [--verbose] [--silent]
# Manual sync
wp mls sync full [--dry-run] [--limit=N] [--verbose]
wp mls sync incremental [--dry-run] [--verbose]
wp mls sync resume --id=<sync_id>
```
### Media Management
Images are fetched on-demand when properties are viewed. These commands manage the cache:
```bash
wp mls media status # Cache statistics
wp mls media fetch --listing=<key> # Pre-cache a listing's images
wp mls media fetch --listing=<key> --limit=10
wp mls media clear --listing=<key> # Clear cached images
```
### Cache Management
```bash
wp mls cache clear --confirm # Delete ALL synced data
wp mls cache cleanup # Remove orphaned media files
wp mls cache missing # View failed media downloads
wp mls cache missing --clear # Clear the missing media log
```
### Recovery
```bash
wp mls recovery list # Show resumable syncs
wp mls recovery auto # Auto-resume most recent failed sync
wp mls recovery cleanup # Mark stale syncs as failed
```
## Cron Setup
### Recommended Setup
Add to system crontab (`crontab -e`):
```bash
# Smart sync every 15 minutes (handles everything automatically)
*/15 * * * * cd /var/www/html && wp mls run --silent --allow-root >> /var/log/mls-sync.log 2>&1
```
This single entry handles:
- Initial full sync on first run
- Incremental updates on subsequent runs
- Automatic recovery from failures
- Safe concurrent execution (aborts if already running)
### Alternative: Manual Control
```bash
# Incremental sync every 15 minutes
*/15 * * * * cd /var/www/html && wp mls sync incremental --allow-root >> /var/log/mls-sync.log 2>&1
# Full rebuild weekly (Sunday 3am)
0 3 * * 0 cd /var/www/html && wp mls cache clear --confirm --allow-root && wp mls sync full --allow-root >> /var/log/mls-sync.log 2>&1
```
### Important Notes
- Use `--allow-root` when running as root
- MLS Grid requires refresh at least every 12 hours per IDX rules
- Rate limits are handled automatically (plugin waits when approaching limits)
- No separate media cron needed - images are fetched on-demand
## Public API
### Available Functions
```php
// Get properties with filters
$properties = mls_get_properties([
'status' => 'Active',
'city' => 'Albert Lea',
'min_price' => 100000,
'max_price' => 500000,
'min_beds' => 3,
'property_type' => 'Residential',
'limit' => 20,
'offset' => 0,
'orderby' => 'list_price',
'order' => 'DESC',
]);
// Get single property by listing key or MLS ID
$property = mls_get_property('NST123456');
// Get primary image (fetches on-demand if not cached)
$image_url = mls_get_property_image('NST123456');
$image_url = mls_get_property_image('NST123456', false); // Don't fetch, return null if uncached
// Get all images for a listing
$images = mls_get_property_images('NST123456'); // Fetch first 1 if uncached
$images = mls_get_property_images('NST123456', 10); // Fetch first 10 if uncached
$images = mls_get_property_images('NST123456', 0); // Don't fetch any
// Get media metadata (no fetching)
$media = mls_get_property_media('NST123456');
// Get distinct cities with listings
$cities = mls_get_cities(); // All cities
$cities = mls_get_cities('Active'); // Cities with active listings only
// Get property count
$count = mls_get_property_count(['status' => 'Active']);
// Check if data is available
if (mls_is_available()) {
// Show property search
}
// Get cache statistics
$stats = mls_get_cache_stats();
// Returns: ['total_media' => 50000, 'cached' => 1200, 'uncached' => 48800]
```
### Query Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `status` | string | Active, Pending, Closed |
| `property_type` | string | Residential, Land, Commercial, etc. |
| `city` | string | City name |
| `county` | string | County name |
| `postal_code` | string | ZIP code |
| `min_price` | int | Minimum list price |
| `max_price` | int | Maximum list price |
| `min_beds` | int | Minimum bedrooms |
| `max_beds` | int | Maximum bedrooms |
| `min_baths` | int | Minimum bathrooms |
| `min_sqft` | int | Minimum living area |
| `max_sqft` | int | Maximum living area |
| `year_built_min` | int | Minimum year built |
| `year_built_max` | int | Maximum year built |
| `listing_key` | string | Specific listing key |
| `listing_id` | string | Specific MLS ID |
| `search` | string | Search address/remarks |
| `limit` | int | Results per page (default: 20) |
| `offset` | int | Pagination offset |
| `orderby` | string | Sort field |
| `order` | string | ASC or DESC |
| `include_media` | bool | Include media array |
| `fields` | array | Specific fields to return |
### Property Object Fields
```php
$property->listing_key // Unique identifier
$property->listing_id // MLS number
$property->standard_status // Active, Pending, Closed
$property->list_price // Current price
$property->original_list_price
$property->close_price
// Address
$property->street_number
$property->street_name
$property->street_suffix
$property->unit_number
$property->city
$property->state_or_province
$property->postal_code
$property->county
$property->latitude
$property->longitude
// Property details
$property->property_type
$property->property_sub_type
$property->bedrooms_total
$property->bathrooms_total
$property->bathrooms_full
$property->bathrooms_half
$property->living_area // Square feet
$property->lot_size_area
$property->lot_size_units
$property->year_built
$property->garage_spaces
// Description
$property->public_remarks
$property->directions
// Listing info
$property->list_agent_key
$property->list_agent_mls_id
$property->list_agent_name
$property->list_office_key
$property->list_office_mls_id
$property->list_office_name
// Dates and timestamps
$property->photos_count
$property->modification_timestamp
$property->photos_change_timestamp
$property->listing_contract_date
$property->close_date
$property->days_on_market
$property->created_at
$property->updated_at
```
## Database Schema
### Tables
All tables use the WordPress prefix (e.g., `wp_mls_properties`).
#### mls_properties
Main property listing data. Only Active and Pending properties are stored.
| Column | Type | Description |
|--------|------|-------------|
| id | BIGINT | Auto-increment primary key |
| listing_key | VARCHAR(50) | Unique MLS Grid key |
| listing_id | VARCHAR(50) | MLS number |
| standard_status | VARCHAR(30) | Active, Pending |
| list_price | DECIMAL(15,2) | Current price |
| city | VARCHAR(100) | City name |
| latitude | DECIMAL(10,8) | GPS latitude |
| longitude | DECIMAL(11,8) | GPS longitude |
| ... | ... | See property fields above |
| raw_data | LONGTEXT | Full API response (JSON) |
| modification_timestamp | DATETIME | Last modified in MLS |
| created_at | DATETIME | Record creation |
| updated_at | DATETIME | Record update |
**Indexes:**
- `listing_key` (UNIQUE)
- `listing_id`
- `standard_status`
- `city`
- `property_type`
- `list_price`
- `modification_timestamp`
- `bedrooms_total`
- `county`
- `idx_latitude` - for geo queries
- `idx_longitude` - for geo queries
- `idx_status_city_price` - composite for search
- `idx_status_type` - composite for filtering
#### mls_media
Media metadata and cache status. Images are downloaded on-demand.
| Column | Type | Description |
|--------|------|-------------|
| id | BIGINT | Auto-increment primary key |
| listing_key | VARCHAR(50) | Property reference |
| media_key | VARCHAR(100) | Unique media identifier |
| media_type | VARCHAR(30) | Photo, Document, etc. |
| media_order | INT | Display order |
| media_url | VARCHAR(1000) | Original MLS Grid URL |
| local_path | VARCHAR(500) | Cached file path |
| local_url | VARCHAR(500) | Cached file URL |
| downloaded_at | DATETIME | When cached |
#### mls_sync_state
Sync progress tracking for resume capability.
| Column | Type | Description |
|--------|------|-------------|
| id | BIGINT | Sync operation ID |
| sync_type | VARCHAR(30) | full, incremental |
| status | VARCHAR(20) | pending, running, completed, failed |
| last_next_link | VARCHAR(2000) | Resume checkpoint |
| records_processed | INT | Total processed |
| records_created | INT | New records |
| records_updated | INT | Updated records |
| records_deleted | INT | Deleted records |
#### mls_rate_limits
API rate limit tracking.
#### mls_sync_log
Debug logging for sync operations.
#### mls_media_log
Media download audit trail.
## Media Handling
### On-Demand Fetching
Per MLS Grid rules, media URLs cannot be used directly on websites. Images must be downloaded and served from your own server.
**How it works:**
1. Property sync stores media metadata (URLs, keys, order) but does NOT download images
2. When `mls_get_property_image()` is called, the image is fetched and cached locally
3. Subsequent requests serve from local cache
4. Cache location: `wp-content/uploads/mls-listings/{prefix}/{listing_key}/`
**Benefits:**
- No rate limit issues from bulk downloading
- Images cached only when needed
- Automatic re-fetch if cache cleared
- Works with MLS Grid's URL expiration
### Pre-caching Images
To pre-cache images for specific listings:
```bash
wp mls media fetch --listing=NST123456 --limit=10
```
### Cache Statistics
```bash
wp mls media status
```
Shows total media records, cached count, and uncached count.
## Sync Strategy
### Initial Import (Full Sync)
- Fetches ONLY Active and Pending properties
- Filter: `MlgCanView eq true AND (StandardStatus eq 'Active' OR StandardStatus eq 'Pending')`
- Uses `@odata.nextLink` for pagination (NOT `$skip`)
- Approximately 30,000 records for NorthStar MLS
- Takes 30-45 minutes on first run
### Replication (Incremental Sync)
- Fetches ALL properties modified since last sync
- No filter on status (need to detect changes)
- For each record:
- If `MlgCanView = false`: DELETE from local DB
- If `StandardStatus` not Active/Pending: DELETE from local DB
- Otherwise: INSERT or UPDATE
### Why This Approach?
1. MLS Grid API limits `$skip` to ~80,000 - bulk scanning fails
2. Only Active/Pending properties needed for display
3. Replication is efficient - only fetches changes
4. Proper deletion handling when properties sell
## Error Recovery
### Automatic Recovery
The plugin saves progress after each API page. If a sync fails:
1. Progress is preserved in `mls_sync_state` table
2. Next `wp mls run` automatically resumes from checkpoint
3. Failed syncs older than 1 hour are marked for resume
### Manual Recovery
```bash
# View resumable syncs
wp mls recovery list
# Auto-resume most recent
wp mls recovery auto
# Resume specific sync
wp mls sync resume --id=<sync_id>
# Mark stale syncs as failed
wp mls recovery cleanup
```
## Troubleshooting
### Connection Failed
```bash
wp mls test connection
wp mls test auth
```
Check:
- API token in wp-config.php
- Network connectivity
- MLS Grid API status
### No Data After Sync
```bash
wp mls status
wp mls stats
```
Check:
- Rate limits (may need to wait)
- WordPress debug log for API errors
- Sync state for failures
### Media Not Loading
```bash
wp mls media status
```
Check:
- Upload directory permissions
- Disk space
- MLS Grid media URL validity
### Sync Taking Too Long
Initial sync of ~30K properties takes 30-45 minutes. Use `--verbose` to monitor progress.
### Rate Limit Exceeded
The plugin automatically waits when approaching limits. If persistent:
- Reduce sync frequency
- Check for other API consumers
- Contact MLS Grid support
### Clearing Data
To start fresh:
```bash
wp mls cache clear --confirm
wp mls run
```
### Database Issues
If indexes are missing, trigger recreation:
```bash
wp eval "MLS_DB::create_tables();"
```
## Rate Limits
MLS Grid enforces these limits:
| Limit | Value |
|-------|-------|
| Per second | 2 requests |
| Per hour | 7,200 requests |
| Per day | 40,000 requests |
| Data per hour | 4 GB |
The plugin automatically:
- Waits 500ms between requests
- Tracks hourly/daily usage
- Pauses when approaching limits
- Retries with exponential backoff on 429 errors
## File Structure
```
mls-by-hansonxyz/
├── mls-by-hansonxyz.php # Main plugin file, public API
├── uninstall.php # Cleanup on uninstall
├── README.md # This file
├── admin/
│ └── class-mls-admin.php # WordPress admin interface
├── cli/
│ └── class-mls-cli.php # WP-CLI commands
├── includes/
│ ├── class-mls-activator.php # Plugin activation
│ ├── class-mls-api-client.php # MLS Grid API communication
│ ├── class-mls-db.php # Database operations
│ ├── class-mls-deactivator.php # Plugin deactivation
│ ├── class-mls-logger.php # Event logging
│ ├── class-mls-media-handler.php # On-demand image caching
│ ├── class-mls-options.php # Configuration management
│ ├── class-mls-query.php # Public query API
│ ├── class-mls-rate-limiter.php # Rate limit compliance
│ └── class-mls-sync-engine.php # Sync orchestration
└── docs/
├── API.md # MLS Grid API reference
├── CLAUDE.md # AI assistant context
└── USAGE.md # User documentation
```
## Support
- Plugin logs: Settings > MLS Settings in WordPress admin
- Debug log: `wp-content/debug.log` (if WP_DEBUG enabled)
- MLS Grid API: support@mlsgrid.com
## License
GPL-2.0+
@@ -34,6 +34,7 @@ class MLS_CLI {
WP_CLI::add_command('mls test', array($instance, 'test')); WP_CLI::add_command('mls test', array($instance, 'test'));
WP_CLI::add_command('mls status', array($instance, 'status')); WP_CLI::add_command('mls status', array($instance, 'status'));
WP_CLI::add_command('mls sync', array($instance, 'sync')); WP_CLI::add_command('mls sync', array($instance, 'sync'));
WP_CLI::add_command('mls run', array($instance, 'run'));
WP_CLI::add_command('mls stats', array($instance, 'stats')); WP_CLI::add_command('mls stats', array($instance, 'stats'));
WP_CLI::add_command('mls cache', array($instance, 'cache')); WP_CLI::add_command('mls cache', array($instance, 'cache'));
WP_CLI::add_command('mls recovery', array($instance, 'recovery')); WP_CLI::add_command('mls recovery', array($instance, 'recovery'));
@@ -369,6 +370,149 @@ class MLS_CLI {
} }
} }
/**
* Run smart sync - autonomous self-healing sync.
*
* This is the recommended command for automated/cron usage. It automatically
* determines the best action based on current state:
*
* - If a sync is running: abort (prevents duplicate syncs)
* - If a previous sync failed/interrupted: resume it
* - If no data exists: run full sync
* - Otherwise: run incremental sync
*
* Failed syncs are automatically recoverable on the next run.
*
* ## OPTIONS
*
* [--quiet]
* : Suppress progress output (still shows status messages)
*
* [--verbose]
* : Show detailed output including API requests
*
* [--silent]
* : Suppress all output except errors (for cron)
*
* ## EXAMPLES
*
* wp mls run # Smart sync with progress
* wp mls run --quiet # Smart sync, status only
* wp mls run --verbose # Smart sync with full details
* wp mls run --silent # For cron jobs
*
* @subcommand run
*/
public function run($args, $assoc_args) {
$quiet = isset($assoc_args['quiet']);
$verbose = isset($assoc_args['verbose']);
$silent = isset($assoc_args['silent']);
$sync_engine = $this->plugin->get_sync_engine();
// Status callback for high-level messages
$status_callback = null;
if (!$silent) {
$status_callback = function($message, $level = 'info') {
$timestamp = date('H:i:s');
switch ($level) {
case 'warning':
WP_CLI::warning("[{$timestamp}] {$message}");
break;
case 'error':
WP_CLI::warning("[{$timestamp}] {$message}");
break;
default:
WP_CLI::line("[{$timestamp}] {$message}");
}
};
}
// Progress callback for record-level progress
$progress_callback = null;
if (!$quiet && !$silent) {
$progress_callback = function($event, $data = array()) use ($verbose) {
if ($verbose) {
$this->output_verbose_event($event, $data);
} else {
switch ($event) {
case 'property_created':
echo '.';
break;
case 'property_updated':
echo '#';
break;
case 'property_deleted':
echo 'x';
break;
case 'property_error':
echo '!';
break;
case 'page_complete':
echo '|';
break;
}
}
};
}
if (!$silent) {
WP_CLI::line('');
WP_CLI::line('=== MLS Smart Sync ===');
WP_CLI::line('');
}
// Run smart sync
$result = $sync_engine->smart_sync($progress_callback, $status_callback);
// Handle aborted case (sync already running)
if (isset($result['action']) && $result['action'] === 'aborted') {
if (!$silent) {
WP_CLI::warning('Sync aborted: ' . ($result['reason'] ?? 'Unknown reason'));
}
return;
}
// Output newline after progress dots
if (!$quiet && !$silent && !$verbose) {
echo "\n";
}
// Output results
if (!$silent) {
$action_labels = array(
'full' => 'Full sync',
'incremental' => 'Incremental sync',
'resumed' => 'Resumed sync',
);
$action_label = $action_labels[$result['action']] ?? 'Sync';
if ($result['success']) {
WP_CLI::success("{$action_label} completed successfully!");
} else {
WP_CLI::warning("{$action_label} failed: " . ($result['error'] ?? 'Unknown error'));
WP_CLI::line('The sync can be resumed on the next run.');
}
if (isset($result['stats'])) {
$stats = $result['stats'];
WP_CLI::line(sprintf(
'Processed: %d | Created: %d | Updated: %d | Deleted: %d | Errors: %d',
$stats['processed'],
$stats['created'],
$stats['updated'],
$stats['deleted'],
$stats['errors']
));
}
}
// Exit with error code if failed (for cron monitoring)
if (!$result['success']) {
WP_CLI::halt(1);
}
}
/** /**
* Print progress legend * Print progress legend
* *
@@ -63,7 +63,13 @@ wp mls test auth
wp mls status wp mls status
wp mls status rate-limits wp mls status rate-limits
# Run property sync # SMART SYNC (recommended for automation)
wp mls run # Auto-detect: full, incremental, or resume
wp mls run --quiet # Status messages only
wp mls run --silent # For cron (exit code only)
wp mls run --verbose # Full details
# Manual sync commands (for more control)
wp mls sync full [--dry-run] [--limit=N] [--verbose] # Initial: Active/Pending only wp mls sync full [--dry-run] [--limit=N] [--verbose] # Initial: Active/Pending only
wp mls sync incremental [--dry-run] [--verbose] # Replication: all changes wp mls sync incremental [--dry-run] [--verbose] # Replication: all changes
wp mls sync resume --id=<sync_id> wp mls sync resume --id=<sync_id>
@@ -171,7 +177,20 @@ The sync engine saves progress after each page:
### Recommended Cron Setup ### Recommended Cron Setup
```bash ```bash
# Replication sync every 15 minutes (MLS Grid recommended) # Smart sync every 15 minutes (recommended - handles everything automatically)
*/15 * * * * cd /var/www/html && wp mls run --silent --allow-root >> /var/log/mls-sync.log 2>&1
```
The `wp mls run` command automatically:
- Runs full sync if no data exists
- Runs incremental sync if data exists
- Resumes failed/interrupted syncs
- Aborts safely if another sync is running
For manual control, use individual commands:
```bash
# Replication sync every 15 minutes
*/15 * * * * cd /var/www/html && wp mls sync incremental --allow-root >> /var/log/mls-sync.log 2>&1 */15 * * * * cd /var/www/html && wp mls sync incremental --allow-root >> /var/log/mls-sync.log 2>&1
# Full re-sync weekly (Sunday 3am) - rebuilds from scratch # Full re-sync weekly (Sunday 3am) - rebuilds from scratch
@@ -43,6 +43,29 @@ Navigate to **Settings > MLS Settings** to configure:
### Via WP-CLI ### Via WP-CLI
#### Smart Sync (Recommended)
The `wp mls run` command is the recommended way to sync. It automatically handles all scenarios:
```bash
wp mls run # Smart sync with progress
wp mls run --quiet # Status messages only
wp mls run --verbose # Full API details
wp mls run --silent # For cron (no output)
```
**What it does automatically:**
- If a sync is already running: aborts (prevents duplicates)
- If a previous sync failed/interrupted: resumes it
- If no data exists: runs full sync
- Otherwise: runs incremental sync
Failed syncs are automatically resumed on the next run - no manual intervention needed.
#### Manual Sync Commands
For more control, use the individual sync commands:
```bash ```bash
# Test connection first # Test connection first
wp mls test connection wp mls test connection
@@ -54,9 +77,6 @@ wp mls sync full
# Run incremental updates # Run incremental updates
wp mls sync incremental wp mls sync incremental
# Download pending media
wp mls sync media
# Use --verbose for detailed output # Use --verbose for detailed output
wp mls sync full --verbose wp mls sync full --verbose
wp mls sync incremental --verbose wp mls sync incremental --verbose
@@ -80,17 +100,25 @@ Use `--verbose` for detailed timestamped output showing API requests and individ
Add to your system crontab (`crontab -e`) for scheduled sync: Add to your system crontab (`crontab -e`) for scheduled sync:
```bash ```bash
# Incremental sync every hour (recommended for production) # Smart sync every 15 minutes (recommended)
# Automatically handles: initial sync, incremental updates, and error recovery
*/15 * * * * cd /var/www/html && wp mls run --silent --allow-root >> /var/log/mls-sync.log 2>&1
```
That's it! The `wp mls run` command handles everything automatically:
- First run: performs full initial sync
- Subsequent runs: performs incremental sync
- After failures: resumes from where it left off
- Concurrent runs: safely aborts if another sync is running
**For more control**, use the individual commands:
```bash
# Incremental sync every hour
0 * * * * cd /var/www/html && wp mls sync incremental --allow-root >> /var/log/mls-sync.log 2>&1 0 * * * * cd /var/www/html && wp mls sync incremental --allow-root >> /var/log/mls-sync.log 2>&1
# Or every 30 minutes for more frequent updates # Full sync weekly (Sunday at 3am) to rebuild from scratch
*/30 * * * * cd /var/www/html && wp mls sync incremental --allow-root >> /var/log/mls-sync.log 2>&1
# Full sync weekly (Sunday at 3am) to catch any missed records
0 3 * * 0 cd /var/www/html && wp mls sync full --allow-root >> /var/log/mls-sync.log 2>&1 0 3 * * 0 cd /var/www/html && wp mls sync full --allow-root >> /var/log/mls-sync.log 2>&1
# Download any pending media every 15 minutes
*/15 * * * * cd /var/www/html && wp mls sync media --limit=50 --allow-root >> /var/log/mls-sync.log 2>&1
``` ```
**Important Notes:** **Important Notes:**
@@ -98,6 +126,7 @@ Add to your system crontab (`crontab -e`) for scheduled sync:
- Redirect output to a log file for debugging - Redirect output to a log file for debugging
- MLS Grid requires refresh at least every 12 hours per IDX rules - MLS Grid requires refresh at least every 12 hours per IDX rules
- The plugin handles rate limits automatically (waits if approaching limits) - The plugin handles rate limits automatically (waits if approaching limits)
- Media images are fetched on-demand when properties are viewed (no separate cron needed)
### Via WP-Cron (Alternative) ### Via WP-Cron (Alternative)
@@ -9,6 +9,12 @@ if (!defined('ABSPATH')) {
class MLS_DB { class MLS_DB {
/**
* Schema version for index migrations
* Increment this when adding new indexes
*/
const SCHEMA_VERSION = 2;
/** /**
* Get table name with prefix * Get table name with prefix
* *
@@ -275,6 +281,78 @@ class MLS_DB {
) {$charset_collate};"; ) {$charset_collate};";
dbDelta($sql_media_log); dbDelta($sql_media_log);
// Run index migrations
self::run_index_migrations();
}
/**
* Run index migrations that dbDelta cannot handle
*
* dbDelta can create tables and add columns, but cannot add indexes
* to existing tables. This method handles incremental index additions.
*/
public static function run_index_migrations() {
global $wpdb;
$current_schema = (int) get_option('mls_schema_version', 1);
// Migration to schema version 2: Add search and geo indexes
if ($current_schema < 2) {
$table_properties = $wpdb->prefix . MLS_TABLE_PROPERTIES;
$table_media = $wpdb->prefix . MLS_TABLE_MEDIA;
// Check and add indexes only if they don't exist
$existing_indexes = self::get_existing_indexes($table_properties);
// Geospatial indexes for bounding box queries
if (!isset($existing_indexes['idx_latitude'])) {
$wpdb->query("ALTER TABLE {$table_properties} ADD INDEX idx_latitude (latitude)");
}
if (!isset($existing_indexes['idx_longitude'])) {
$wpdb->query("ALTER TABLE {$table_properties} ADD INDEX idx_longitude (longitude)");
}
// Composite index for common search pattern (status + city + price)
if (!isset($existing_indexes['idx_status_city_price'])) {
$wpdb->query("ALTER TABLE {$table_properties} ADD INDEX idx_status_city_price (standard_status, city, list_price)");
}
// Composite index for status + property_type searches
if (!isset($existing_indexes['idx_status_type'])) {
$wpdb->query("ALTER TABLE {$table_properties} ADD INDEX idx_status_type (standard_status, property_type)");
}
// Media table: composite index for listing + order (eliminates filesort)
$media_indexes = self::get_existing_indexes($table_media);
if (!isset($media_indexes['idx_listing_order'])) {
$wpdb->query("ALTER TABLE {$table_media} ADD INDEX idx_listing_order (listing_key, media_order)");
}
update_option('mls_schema_version', 2);
}
// Future migrations go here:
// if ($current_schema < 3) { ... }
}
/**
* Get existing indexes for a table
*
* @param string $table Full table name
* @return array Index names as keys
*/
private static function get_existing_indexes($table) {
global $wpdb;
$indexes = array();
$results = $wpdb->get_results("SHOW INDEX FROM {$table}");
foreach ($results as $row) {
$indexes[$row->Key_name] = true;
}
return $indexes;
} }
/** /**
@@ -937,4 +937,113 @@ class MLS_Sync_Engine {
return $this->resume_sync($resumable->id, $progress_callback); return $this->resume_sync($resumable->id, $progress_callback);
} }
/**
* Smart sync - autonomous self-healing sync that handles all scenarios
*
* Decision logic:
* 1. If a sync is currently running (and not stale), abort
* 2. If there's a resumable failed/interrupted sync, resume it
* 3. If no data exists, run full sync
* 4. Otherwise, run incremental sync
*
* On failure, the sync state is preserved for future resume.
*
* @param callable|null $progress_callback Progress callback
* @param callable|null $status_callback Callback for status messages: function(string $message, string $level)
* @return array Sync results with 'action' key indicating what was done
*/
public function smart_sync($progress_callback = null, $status_callback = null) {
// Helper to emit status messages
$status = function($message, $level = 'info') use ($status_callback) {
if ($status_callback) {
call_user_func($status_callback, $message, $level);
}
$this->logger->log($level, $message);
};
// Step 1: Clean up stale syncs (running > 1 hour = probably dead)
$stale_cleaned = $this->cleanup_stale_syncs();
if ($stale_cleaned > 0) {
$status("Cleaned up {$stale_cleaned} stale sync(s)", 'info');
}
// Step 2: Check if a sync is actively running
$running = $this->get_running_sync();
if ($running) {
$status("Sync #{$running->id} is already running (started {$running->started_at})", 'warning');
return array(
'success' => false,
'action' => 'aborted',
'reason' => 'Sync already running',
'running_sync' => $running,
);
}
// Step 3: Check for resumable syncs
$resumable = $this->get_latest_resumable();
if ($resumable) {
$status("Found resumable sync #{$resumable->id} ({$resumable->sync_type}), processed {$resumable->records_processed} records", 'info');
$status("Resuming...", 'info');
$result = $this->resume_sync($resumable->id, $progress_callback);
$result['action'] = 'resumed';
$result['resumed_sync_id'] = $resumable->id;
return $result;
}
// Step 4: Check if we have any data
$has_data = $this->has_synced_data();
if (!$has_data) {
// No data - need full sync
$status("No existing data found, starting full sync", 'info');
$result = $this->run_full_sync(false, null, $progress_callback);
$result['action'] = 'full';
return $result;
}
// Step 5: We have data - run incremental sync
$last_timestamp = $this->get_last_modification_timestamp();
$status("Running incremental sync (changes since {$last_timestamp})", 'info');
$result = $this->run_incremental_sync(false, $progress_callback);
$result['action'] = 'incremental';
return $result;
}
/**
* Check if there's a currently running sync (not stale)
*
* @return object|null Running sync state or null
*/
public function get_running_sync() {
global $wpdb;
$one_hour_ago = date('Y-m-d H:i:s', strtotime('-1 hour'));
return $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$this->db->sync_state_table()}
WHERE status = 'running' AND updated_at >= %s
ORDER BY started_at DESC
LIMIT 1",
$one_hour_ago
));
}
/**
* Check if we have any synced property data
*
* @return bool
*/
public function has_synced_data() {
global $wpdb;
$count = $wpdb->get_var(
"SELECT COUNT(*) FROM {$this->db->properties_table()} WHERE mlg_can_view = 1"
);
return (int) $count > 0;
}
} }
+41 -42
View File
@@ -106,54 +106,53 @@ $view_class = $show_map ? 'is-map-view' : 'is-grid-view';
</main> </main>
<?php <?php
// Always load map data for responsive switching // Load MLS properties for map markers
$map_properties = new WP_Query(array( $markers = array();
'post_type' => 'property',
'posts_per_page' => -1, if (function_exists('mls_get_properties')) {
$mls_properties = mls_get_properties(array(
'status' => 'Active',
'limit' => 1000, // Reasonable limit for map performance
'orderby' => 'modification_timestamp',
'order' => 'DESC',
)); ));
$markers = array(); foreach ($mls_properties as $property) {
$city_coords = array( // Skip properties without coordinates
'Albert Lea' => array(43.6480, -93.3685), if (empty($property->latitude) || empty($property->longitude)) {
'Austin' => array(43.6666, -92.9746), continue;
'Glenville' => array(43.5733, -93.2779), }
'Emmons' => array(43.5013, -93.4896),
'Clarks Grove' => array(43.7627, -93.3196),
'Alden' => array(43.6719, -93.5768),
'Hartland' => array(43.8030, -93.4846),
'Geneva' => array(43.8255, -93.2682),
'Owatonna' => array(44.0838, -93.2260),
'Faribault' => array(44.2949, -93.2688),
'Rochester' => array(44.0234, -92.4699),
'Mankato' => array(44.1636, -93.9994),
);
if ($map_properties->have_posts()) : // Format address
while ($map_properties->have_posts()) : $address_parts = array();
$map_properties->the_post(); if ($property->street_number) {
$city = get_field('city'); $address_parts[] = $property->street_number;
$price = get_field('property_price'); }
$address = get_field('street_address'); if ($property->street_name) {
$address_parts[] = $property->street_name;
// Get coords for city, default to Albert Lea }
$coords = isset($city_coords[$city]) ? $city_coords[$city] : $city_coords['Albert Lea']; if ($property->street_suffix) {
// Add small random offset (seeded by post ID for consistency) $address_parts[] = $property->street_suffix;
srand(get_the_ID()); }
$lat = $coords[0] + (rand(-50, 50) / 10000); $street = implode(' ', $address_parts);
$lng = $coords[1] + (rand(-50, 50) / 10000); $full_address = $street ? $street . ', ' . $property->city : $property->city;
$markers[] = array( $markers[] = array(
'id' => get_the_ID(), 'id' => $property->listing_key,
'lat' => $lat, 'lat' => (float) $property->latitude,
'lng' => $lng, 'lng' => (float) $property->longitude,
'title' => get_the_title(), 'title' => $full_address,
'price' => '$' . number_format($price), 'price' => '$' . number_format($property->list_price),
'address' => $address . ', ' . $city, 'address' => $full_address,
'url' => get_permalink(), 'url' => home_url('/properties/?listing=' . urlencode($property->listing_key)),
'beds' => $property->bedrooms_total,
'baths' => $property->bathrooms_total,
'sqft' => $property->living_area,
'status' => $property->standard_status,
'photo' => null, // Placeholder - photos will be added later
); );
endwhile; }
wp_reset_postdata(); }
endif;
?> ?>
<!-- Leaflet CSS --> <!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
+58 -116
View File
@@ -11,7 +11,7 @@ if (!defined('ABSPATH')) {
} }
/** /**
* Handle AJAX property filter requests * Handle AJAX property filter requests (MLS-based)
*/ */
function homeproz_ajax_filter_properties() { function homeproz_ajax_filter_properties() {
// Verify nonce // Verify nonce
@@ -19,6 +19,11 @@ function homeproz_ajax_filter_properties() {
wp_send_json_error('Invalid nonce'); wp_send_json_error('Invalid nonce');
} }
// Check if MLS plugin is available
if (!function_exists('mls_get_properties')) {
wp_send_json_error('MLS plugin not available');
}
// Get filter values // Get filter values
$property_type = isset($_POST['property_type']) ? sanitize_text_field($_POST['property_type']) : ''; $property_type = isset($_POST['property_type']) ? sanitize_text_field($_POST['property_type']) : '';
$property_status = isset($_POST['property_status']) ? sanitize_text_field($_POST['property_status']) : ''; $property_status = isset($_POST['property_status']) ? sanitize_text_field($_POST['property_status']) : '';
@@ -29,100 +34,40 @@ function homeproz_ajax_filter_properties() {
$sort = isset($_POST['sort']) ? sanitize_text_field($_POST['sort']) : 'newest'; $sort = isset($_POST['sort']) ? sanitize_text_field($_POST['sort']) : 'newest';
$paged = isset($_POST['paged']) ? intval($_POST['paged']) : 1; $paged = isset($_POST['paged']) ? intval($_POST['paged']) : 1;
// Build query args // Build MLS query args
$args = array( $per_page = 12;
'post_type' => 'property', $mls_args = array(
'posts_per_page' => 12, 'status' => $property_status ?: 'Active',
'paged' => $paged, 'limit' => 1000, // Get all for counting, then paginate
'orderby' => 'modification_timestamp',
'order' => 'DESC',
); );
// Taxonomy filters // Map filter values to MLS query args
$tax_query = array();
if ($property_type) { if ($property_type) {
$tax_query[] = array( $mls_args['property_type'] = $property_type;
'taxonomy' => 'property_type',
'field' => 'slug',
'terms' => $property_type,
);
} }
if ($property_status) {
$tax_query[] = array(
'taxonomy' => 'property_status',
'field' => 'slug',
'terms' => $property_status,
);
}
if ($property_location) { if ($property_location) {
$tax_query[] = array( $mls_args['city'] = $property_location;
'taxonomy' => 'property_location',
'field' => 'slug',
'terms' => $property_location,
);
} }
if (!empty($tax_query)) {
$args['tax_query'] = $tax_query;
if (count($tax_query) > 1) {
$args['tax_query']['relation'] = 'AND';
}
}
// Meta query for price and bedrooms
$meta_query = array();
if ($min_price) { if ($min_price) {
$meta_query[] = array( $mls_args['min_price'] = $min_price;
'key' => 'property_price',
'value' => $min_price,
'type' => 'NUMERIC',
'compare' => '>=',
);
} }
if ($max_price) { if ($max_price) {
$meta_query[] = array( $mls_args['max_price'] = $max_price;
'key' => 'property_price',
'value' => $max_price,
'type' => 'NUMERIC',
'compare' => '<=',
);
} }
if ($beds) { if ($beds) {
$meta_query[] = array( $mls_args['min_beds'] = $beds;
'key' => 'bedrooms',
'value' => $beds,
'type' => 'NUMERIC',
'compare' => '>=',
);
} }
if (!empty($meta_query)) { // Fetch all matching properties
$args['meta_query'] = $meta_query; $all_properties = mls_get_properties($mls_args);
if (count($meta_query) > 1) {
$args['meta_query']['relation'] = 'AND';
}
}
// Fetch all matching properties for status-based sorting
$args['posts_per_page'] = -1;
$args['orderby'] = 'modified';
$args['order'] = 'DESC';
$all_properties = get_posts($args);
// Sort by status (Active > Pending > Sold) then by modified date
$sorted_properties = homeproz_sort_properties_by_status($all_properties);
// Handle pagination manually // Handle pagination manually
$per_page = 12; $total = count($all_properties);
$total = count($sorted_properties);
$max_pages = ceil($total / $per_page); $max_pages = ceil($total / $per_page);
$offset = ($paged - 1) * $per_page; $offset = ($paged - 1) * $per_page;
$paged_properties = array_slice($sorted_properties, $offset, $per_page); $paged_properties = array_slice($all_properties, $offset, $per_page);
ob_start(); ob_start();
@@ -142,13 +87,11 @@ function homeproz_ajax_filter_properties() {
<?php if (!empty($paged_properties)) : ?> <?php if (!empty($paged_properties)) : ?>
<div class="properties-grid"> <div class="properties-grid">
<?php <?php
global $post; foreach ($paged_properties as $property) :
foreach ($paged_properties as $property_post) : // Pass MLS property to card template
$post = $property_post; set_query_var('mls_property', $property);
setup_postdata($post); get_template_part('template-parts/property/property-card-mls');
get_template_part('template-parts/property/property-card');
endforeach; endforeach;
wp_reset_postdata();
?> ?>
</div> </div>
@@ -197,43 +140,42 @@ function homeproz_ajax_filter_properties() {
<?php <?php
$html = ob_get_clean(); $html = ob_get_clean();
// Build markers data for map view // Build markers data for map view from MLS properties
$markers = array(); $markers = array();
$city_coords = array(
'Albert Lea' => array(43.6480, -93.3685),
'Austin' => array(43.6666, -92.9746),
'Glenville' => array(43.5733, -93.2779),
'Emmons' => array(43.5013, -93.4896),
'Clarks Grove' => array(43.7627, -93.3196),
'Alden' => array(43.6719, -93.5768),
'Hartland' => array(43.8030, -93.4846),
'Geneva' => array(43.8255, -93.2682),
'Owatonna' => array(44.0838, -93.2260),
'Faribault' => array(44.2949, -93.2688),
'Rochester' => array(44.0234, -92.4699),
'Mankato' => array(44.1636, -93.9994),
);
foreach ($sorted_properties as $prop) { foreach ($all_properties as $property) {
$city = get_field('city', $prop->ID); // Skip properties without coordinates
$price = get_field('property_price', $prop->ID); if (empty($property->latitude) || empty($property->longitude)) {
$address = get_field('street_address', $prop->ID); continue;
}
// Get coords for city, default to Albert Lea // Format address
$coords = isset($city_coords[$city]) ? $city_coords[$city] : $city_coords['Albert Lea']; $address_parts = array();
// Add small random offset to prevent overlapping markers (seeded by post ID for consistency) if ($property->street_number) {
srand($prop->ID); $address_parts[] = $property->street_number;
$lat = $coords[0] + (rand(-50, 50) / 10000); }
$lng = $coords[1] + (rand(-50, 50) / 10000); if ($property->street_name) {
$address_parts[] = $property->street_name;
}
if ($property->street_suffix) {
$address_parts[] = $property->street_suffix;
}
$street = implode(' ', $address_parts);
$full_address = $street ? $street . ', ' . $property->city : $property->city;
$markers[] = array( $markers[] = array(
'id' => $prop->ID, 'id' => $property->listing_key,
'lat' => $lat, 'lat' => (float) $property->latitude,
'lng' => $lng, 'lng' => (float) $property->longitude,
'title' => $prop->post_title, 'title' => $full_address,
'price' => '$' . number_format($price), 'price' => '$' . number_format($property->list_price),
'address' => $address . ', ' . $city, 'address' => $full_address,
'url' => get_permalink($prop->ID), 'url' => home_url('/properties/?listing=' . urlencode($property->listing_key)),
'beds' => $property->bedrooms_total,
'baths' => $property->bathrooms_total,
'sqft' => $property->living_area,
'status' => $property->standard_status,
'photo' => null, // Placeholder - photos will be added later
); );
} }
@@ -0,0 +1,136 @@
<?php
/**
* MLS Property Card Template Part
*
* Displays an MLS property in card format for archive views
*
* @package HomeProz
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
// Get MLS property data from query var
$property = get_query_var('mls_property');
if (!$property) {
return;
}
// Extract property data
$listing_key = $property->listing_key;
$price = $property->list_price;
$bedrooms = $property->bedrooms_total;
$bathrooms = $property->bathrooms_total;
$square_feet = $property->living_area;
$status = $property->standard_status;
$public_remarks = $property->public_remarks;
// Format address
$address_parts = array();
if ($property->street_number) {
$address_parts[] = $property->street_number;
}
if ($property->street_name) {
$address_parts[] = $property->street_name;
}
if ($property->street_suffix) {
$address_parts[] = $property->street_suffix;
}
$street = implode(' ', $address_parts);
$full_address = $street;
if ($property->city) {
$full_address .= ', ' . $property->city;
}
if ($property->state_or_province) {
$full_address .= ', ' . $property->state_or_province;
}
// Property URL (will be updated when single property view is implemented)
$property_url = home_url('/properties/?listing=' . urlencode($listing_key));
// Status class mapping
$status_class = 'badge-active';
if ($status === 'Pending') {
$status_class = 'badge-pending';
} elseif ($status === 'Closed' || $status === 'Sold') {
$status_class = 'badge-sold';
}
?>
<article id="property-<?php echo esc_attr($listing_key); ?>" data-property-id="<?php echo esc_attr($listing_key); ?>" class="property-card card mls-property">
<a href="<?php echo esc_url($property_url); ?>" class="property-card-link-overlay" aria-hidden="true" tabindex="-1"></a>
<div class="property-card-image">
<!-- Photo placeholder - will be implemented later -->
<div class="property-card-placeholder">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<polyline points="9 22 9 12 15 12 15 22"/>
</svg>
</div>
<?php if ($status) : ?>
<span class="property-card-badge badge <?php echo esc_attr($status_class); ?>">
<?php echo esc_html($status); ?>
</span>
<?php endif; ?>
</div>
<div class="property-card-content">
<div class="property-card-price">
<?php echo esc_html('$' . number_format($price)); ?>
</div>
<h3 class="property-card-title">
<?php echo esc_html($full_address ?: 'Property ' . $listing_key); ?>
</h3>
<?php if ($bedrooms || $bathrooms || $square_feet) : ?>
<ul class="property-card-specs">
<?php if ($bedrooms) : ?>
<li class="spec-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M3 7v11h18V7M3 7V4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v3M3 7h18M7 11h4v4H7zM14 11h3"/>
</svg>
<span><?php echo esc_html($bedrooms); ?> <?php echo $bedrooms == 1 ? 'Bed' : 'Beds'; ?></span>
</li>
<?php endif; ?>
<?php if ($bathrooms) : ?>
<li class="spec-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M4 12h16M4 12v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7M4 12V6a2 2 0 0 1 2-2h3v2a1 1 0 0 0 1 1h1a1 1 0 0 0 1-1V4"/>
</svg>
<span><?php echo esc_html($bathrooms); ?> <?php echo $bathrooms == 1 ? 'Bath' : 'Baths'; ?></span>
</li>
<?php endif; ?>
<?php if ($square_feet) : ?>
<li class="spec-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<path d="M3 9h18M9 3v18"/>
</svg>
<span><?php echo esc_html(number_format($square_feet)); ?> sqft</span>
</li>
<?php endif; ?>
</ul>
<?php endif; ?>
<?php if ($public_remarks) : ?>
<p class="property-card-excerpt">
<?php echo esc_html(wp_trim_words($public_remarks, 15, '...')); ?>
</p>
<?php endif; ?>
<span class="property-card-link">
View Details
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</span>
</div>
</article>
@@ -2,7 +2,7 @@
/** /**
* Property Results Template Part * Property Results Template Part
* *
* Displays property results for archive/search * Displays MLS property results for archive/search
* *
* @package HomeProz * @package HomeProz
*/ */
@@ -12,140 +12,70 @@ if (!defined('ABSPATH')) {
exit; exit;
} }
// Get filter values // Check if MLS plugin is available
if (!function_exists('mls_get_properties')) {
?>
<div class="no-properties">
<h3>Properties Unavailable</h3>
<p>Property listings are temporarily unavailable. Please try again later.</p>
</div>
<?php
return;
}
// Get filter values from URL
$current_type = isset($_GET['property_type']) ? sanitize_text_field($_GET['property_type']) : ''; $current_type = isset($_GET['property_type']) ? sanitize_text_field($_GET['property_type']) : '';
$current_status = isset($_GET['property_status']) ? sanitize_text_field($_GET['property_status']) : ''; $current_status = isset($_GET['property_status']) ? sanitize_text_field($_GET['property_status']) : 'Active';
$current_location = isset($_GET['property_location']) ? sanitize_text_field($_GET['property_location']) : ''; $current_location = isset($_GET['property_location']) ? sanitize_text_field($_GET['property_location']) : '';
$current_min_price = isset($_GET['min_price']) ? intval($_GET['min_price']) : ''; $current_min_price = isset($_GET['min_price']) ? intval($_GET['min_price']) : '';
$current_max_price = isset($_GET['max_price']) ? intval($_GET['max_price']) : ''; $current_max_price = isset($_GET['max_price']) ? intval($_GET['max_price']) : '';
$current_beds = isset($_GET['beds']) ? intval($_GET['beds']) : ''; $current_beds = isset($_GET['beds']) ? intval($_GET['beds']) : '';
$current_sort = isset($_GET['sort']) ? sanitize_text_field($_GET['sort']) : 'newest';
// Build query args // Pagination
$paged = get_query_var('paged') ? get_query_var('paged') : 1; $paged = get_query_var('paged') ? get_query_var('paged') : 1;
$args = array( $per_page = 12;
'post_type' => 'property',
'posts_per_page' => 12, // Build MLS query args
'paged' => $paged, $mls_args = array(
'status' => $current_status ?: 'Active',
'limit' => 1000, // Get all for counting, then paginate
'orderby' => 'modification_timestamp',
'order' => 'DESC',
); );
// Taxonomy filters // Map filter values to MLS query args
$tax_query = array();
if ($current_type) { if ($current_type) {
$tax_query[] = array( $mls_args['property_type'] = $current_type;
'taxonomy' => 'property_type',
'field' => 'slug',
'terms' => $current_type,
);
} }
if ($current_status) {
$tax_query[] = array(
'taxonomy' => 'property_status',
'field' => 'slug',
'terms' => $current_status,
);
}
if ($current_location) { if ($current_location) {
$tax_query[] = array( $mls_args['city'] = $current_location;
'taxonomy' => 'property_location',
'field' => 'slug',
'terms' => $current_location,
);
} }
if (!empty($tax_query)) {
$args['tax_query'] = $tax_query;
if (count($tax_query) > 1) {
$args['tax_query']['relation'] = 'AND';
}
}
// Meta query for price and bedrooms
$meta_query = array();
if ($current_min_price) { if ($current_min_price) {
$meta_query[] = array( $mls_args['min_price'] = $current_min_price;
'key' => 'property_price',
'value' => $current_min_price,
'type' => 'NUMERIC',
'compare' => '>=',
);
} }
if ($current_max_price) { if ($current_max_price) {
$meta_query[] = array( $mls_args['max_price'] = $current_max_price;
'key' => 'property_price',
'value' => $current_max_price,
'type' => 'NUMERIC',
'compare' => '<=',
);
} }
if ($current_beds) { if ($current_beds) {
$meta_query[] = array( $mls_args['min_beds'] = $current_beds;
'key' => 'bedrooms',
'value' => $current_beds,
'type' => 'NUMERIC',
'compare' => '>=',
);
} }
if (!empty($meta_query)) { // Fetch all matching properties
$args['meta_query'] = $meta_query; $all_properties = mls_get_properties($mls_args);
if (count($meta_query) > 1) {
$args['meta_query']['relation'] = 'AND';
}
}
// For status-based sorting, we need to fetch all matching properties and sort in PHP
// This is efficient for real estate sites with < 1000 properties
$args['posts_per_page'] = -1;
$args['orderby'] = 'modified';
$args['order'] = 'DESC';
$all_properties = get_posts($args);
// Sort by status (Active > Pending > Sold) then by modified date
$sorted_properties = homeproz_sort_properties_by_status($all_properties);
// Handle pagination manually // Handle pagination manually
$per_page = 12; $total = count($all_properties);
$total = count($sorted_properties);
$max_pages = ceil($total / $per_page); $max_pages = ceil($total / $per_page);
$offset = ($paged - 1) * $per_page; $offset = ($paged - 1) * $per_page;
$paged_properties = array_slice($sorted_properties, $offset, $per_page); $paged_properties = array_slice($all_properties, $offset, $per_page);
// Create a fake WP_Query-like object for compatibility
$properties = (object) array(
'posts' => $paged_properties,
'found_posts' => $total,
'max_num_pages' => $max_pages,
);
// Helper function to check if we have posts
$properties->have_posts = function() use (&$paged_properties) {
static $index = 0;
if ($index < count($paged_properties)) {
return true;
}
$index = 0;
return false;
};
// Loop through properties manually
global $post;
$property_index = 0;
?> ?>
<!-- Results Meta --> <!-- Results Meta -->
<div class="properties-meta"> <div class="properties-meta">
<p class="properties-count"> <p class="properties-count">
<?php if ($properties->found_posts > 0) : ?> <?php if ($total > 0) : ?>
Showing <strong><?php echo esc_html($properties->found_posts); ?></strong> Showing <strong><?php echo esc_html($total); ?></strong>
<?php echo $properties->found_posts === 1 ? 'property' : 'properties'; ?> <?php echo $total === 1 ? 'property' : 'properties'; ?>
<?php else : ?> <?php else : ?>
No properties found No properties found
<?php endif; ?> <?php endif; ?>
@@ -155,12 +85,11 @@ $property_index = 0;
<?php if (!empty($paged_properties)) : ?> <?php if (!empty($paged_properties)) : ?>
<div class="properties-grid"> <div class="properties-grid">
<?php <?php
foreach ($paged_properties as $property_post) : foreach ($paged_properties as $property) :
$post = $property_post; // Pass MLS property to card template
setup_postdata($post); set_query_var('mls_property', $property);
get_template_part('template-parts/property/property-card'); get_template_part('template-parts/property/property-card-mls');
endforeach; endforeach;
wp_reset_postdata();
?> ?>
</div> </div>
@@ -195,5 +124,3 @@ $property_index = 0;
<a href="<?php echo esc_url(get_post_type_archive_link('property')); ?>" class="btn btn-primary">View All Properties</a> <a href="<?php echo esc_url(get_post_type_archive_link('property')); ?>" class="btn btn-primary">View All Properties</a>
</div> </div>
<?php endif; ?> <?php endif; ?>
<?php wp_reset_postdata(); ?>