Stu Mason
Stu Mason

Activity

StuMason/cleanconnect
TidyLinker.com
TypeScript
Pull Request Opened

PR #132 opened: feat: server-side cleaner search + simplified cleaner job feed

Summary

Two pieces of stakeholder-driven work landed together because they share infrastructure (HasLocation::rankByDistance):

  1. Cleaner search overhaul + visibility backfill. Search was returning a single cleaner — confirmed via prod tinker that 16 of 18 onboarded cleaners had visibility=false (historical default). Backfill migration flips them on; onboarding completion now defaults visibility=true going forward. While in here: filtering and sorting moved server-side with proper pagination, ratings now reflect real published reviews instead of hardcoded 0, and cleaners with no coordinates are no longer silently dropped when a location is supplied.
  2. Cleaner-side job feed simplification. Dropped the quote_status dropdown (low signal). Kept the service filter. Jobs are now ranked by distance instead of filtered by it — same fix as the cleaner search side. Added a banner prompting cleaners with no postcode to set one.

Also bundled: a stakeholder-facing proposal doc (docs/plans/job-sharing-proposal.md) for the owner-controlled job sharing feature we discussed in lieu of the full removal in #131.

Why now

  • Search was effectively broken on prod (1 cleaner visible).
  • Filters were a mix of fake (always-true available), redundant (verification toggles when searchable() already gates on verification), and noisy.
  • The withDistanceFrom scope's whereNotNull made a class of bugs invisible — cleaners and jobs without coordinates just disappeared from search results.

Key technical notes

  • HasLocation::rankByDistance orders by distance ASC with NULLs last instead of filtering them out. The existing withDistanceFrom is kept for genuine radius-cutoff cases (e.g. job-match notifications).
  • CleanerProfile::withRatingStats uses Eloquent's native withAvg/withCount rather than a custom selectRaw subquery. Why: selectRaw subqueries with bindings break under paginate()'s count-clone — bindings get misaligned. withAvg/withCount survive paginate correctly.
  • SQLite cast(? as real) on the min_rating filter — without explicit casting on both sides, SQLite's PDO type coercion silently fails the >= comparison when the bound value arrives as a PHP float. Caught by the test suite, documented inline.
  • DTO drops available — was always-true, used to disable a "Book Now" button that was never disabled in practice. Cleaner-card simplified accordingly.

Test plan

  • php artisan test against Cleaner/, Listeners/, Admin/, Client/ suites — 370 passing
  • New tests cover: pagination shape, min_rating filter, max_rate filter, sort, NULL-coordinate handling, visibility default on onboarding completion
  • vendor/bin/pint --dirty clean
  • npm run format && npm run lint clean
  • TypeScript check clean (only existing tsconfig deprecation warning)
  • Manual QA on staging: confirm search now returns multiple cleaners, filters update via URL, pagination works, cleaner job feed surfaces postcode banner when missing

Migration

The visibility backfill (2026_04_28_132636_backfill_cleaner_profile_visibility) flips visibility to true for any cleaner with onboarding_completed_at IS NOT NULL and visibility = false. On prod that's 16 records. The migration has no down — flipping back would also stomp legitimately visibility=true rows.

If you know of any cleaner who is intentionally hidden (paused, in dispute, etc.), flag now so I can add an exclusion before this lands.

Out of scope

  • The job-sharing proposal doc is review/discussion material only — no code changes for that.
  • Real-rating sort behaviour with mostly-zero-review cleaners may want a tiebreaker tweak once reviews start coming in.
+836
additions
-483
deletions
17
files changed