From 158d5ded92c87a8d831164dae3fde9187c4afc49 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 6 Jul 2026 15:13:49 +0000 Subject: [PATCH] fix(postcode-search): PR #355 review feedback - Order address candidates naturally so Flat 2 sorts before Flat 10 (TDD'd in the domain core; numeric locale compare). - Disable spell check / autocomplete / autocorrect on the postcode input. - Long result lists and the tray's postcode chips now scroll inside their containers instead of growing the page. - gitignore docs/wip/** and untrack the postcode-search handover doc. Co-Authored-By: Claude Fable 5 --- .gitignore | 3 + docs/wip/postcode-search-handover.md | 58 ------------------- .../properties/add/AddPropertiesClient.tsx | 10 ++++ src/lib/postcodeSearch/model.test.ts | 18 ++++++ src/lib/postcodeSearch/model.ts | 7 ++- 5 files changed, 36 insertions(+), 60 deletions(-) delete mode 100644 docs/wip/postcode-search-handover.md diff --git a/.gitignore b/.gitignore index 2d9e9175..04f701b5 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,9 @@ next-env.d.ts backlog/** +# Session working notes / handovers — not for review (PR #355 feedback) +docs/wip/** + # Personal Claude Code settings (per-developer, not shared) .claude/settings.local.json diff --git a/docs/wip/postcode-search-handover.md b/docs/wip/postcode-search-handover.md deleted file mode 100644 index abb69454..00000000 --- a/docs/wip/postcode-search-handover.md +++ /dev/null @@ -1,58 +0,0 @@ -# Handover: Add properties by postcode search - -**Branch:** `feature/add-properties-by-postcode` (2 commits pushed, tree clean). -**Context date:** 2026-07-06. **Goal journey:** add properties → create scenarios (done, PR #353) → trigger modelling (soon). - -## State - -Done and verified (`npx tsc --noEmit` clean, 329 tests pass): -- **Domain core** `src/lib/postcodeSearch/model.ts` + `model.test.ts` (9 tests): - `normalisePostcode`, `classifyOsCode`, `isCacheFresh` (30-day), `toAddressCandidates` - (DPA>LPI, UPRN-dedup, postcode-stripped title-case address), `buildWritePlan` - (property row + 0–2 override facts). -- **ADR-0007** (`docs/adr/`) — all decisions + rationale. `docs/adr/**` gitignore rule removed - (it had eaten ADRs 0004–0006; ask team if recoverable). -- **CONTEXT.md** — "Postcode search" entry; VocabularyMapping now has two producers. -- `os_places` added to `OverrideSourceValues` (schema only — **migration not yet generated**). -- **Approved design mockup** (build the page to this): https://claude.ai/code/artifact/d05a3ab0-48ed-41c3-94b4-b13106b7f372 - -## Remaining work (in order) - -1. **Enum migration**: run `./generate_migration.sh` for `override_source` + `os_places` - (no `.sql` migrations live in-repo; script is repo-root). -2. **Routes** under `src/app/api/portfolio/[portfolioId]/properties/`: - - `search/route.ts` GET `?postcode=`: normalise → `postcode_search` cache read - (`isCacheFresh`; row has unique `postcode` + jsonb `resultData`) → on miss/stale call - `lookupPostcode` (`src/app/api/postcode/LookupPostcode.ts`; **add - `parliamentary_constituency` to its result type**) + OS pages fetch - (`LookupOsPlaces.ts`) → upsert cache → return `toAddressCandidates` + geo + - `alreadyInPortfolio` flags (one query: uprns in portfolio) + cache age. - - `route.ts` POST submit `{candidates, geo}` (re-validate server-side): transaction over - `buildWritePlan` rows — insert `property` `ON CONFLICT DO NOTHING` (partial unique - `uq_property_portfolio_uprn`), upsert `landlord_property_type_overrides` / - `landlord_built_form_type_overrides` (`(portfolio_id, description)` unique, - description = OS code, source `os_places`), insert `property_overrides` snapshots - (`building_part=0`, `original_spreadsheet_description` = OS code). Return - `{added, skipped}`. Next-15 rules: `await params`, `{ error }` shape. -3. **Page** `/portfolio/[slug]/properties/add` + **"Add properties" dropdown** beside Export - in `src/app/portfolio/[slug]/components/PropertyTable.tsx` ("Search by postcode" + - "Bulk excel import — Soon"). Then retire toolbar `AddNew` dropdown. TanStack Query v4 - mutations; no useEffect/useMemo (CLAUDE.md). - -## How to verify (patterns proven this session) - -- DB: `.env.local` creds; NODE_PATH node scripts with `pg` (`ssl:{rejectUnauthorized:false}`). -- Live app: `./node_modules/.bin/next dev -p 3111`; mint a session cookie with - `next-auth/jwt` `encode` + `NEXTAUTH_SECRET` from `.env.local`, email `@domna.homes`, - cookie `next-auth.session-token`. Server-rendered data is greppable in the RSC payload. -- Test portfolio: **814** (new-approach). Searched-in properties will render blank/awaiting - modelling — that's by design (ADR-0007), not a bug. -- `OS_API_KEY` env var powers OS Places. - -## Gotchas - -- Properties inserted now are "new approach" (`updated_at >= 2026-06-01`, `epcSources.ts`) — - every read path expects an EPC graph that won't exist until modelling. Expected. -- Never per-scenario/per-property probe loops on `plan` (1.6M rows) — grouped/scoped queries - only (see `docs/wip/new-modelling-ui-handover.md` perf notes). -- PR #353 (Scenarios tab) is separate, open; its ADR is numbered 0003. diff --git a/src/app/portfolio/[slug]/properties/add/AddPropertiesClient.tsx b/src/app/portfolio/[slug]/properties/add/AddPropertiesClient.tsx index 34cfe027..1e9ed6a1 100644 --- a/src/app/portfolio/[slug]/properties/add/AddPropertiesClient.tsx +++ b/src/app/portfolio/[slug]/properties/add/AddPropertiesClient.tsx @@ -160,6 +160,9 @@ export default function AddPropertiesClient({ }} placeholder='e.g. "M20 4TF"' autoFocus + spellCheck={false} + autoComplete="off" + autoCorrect="off" className="flex-1 border border-slate-200 rounded-xl px-4 py-2.5 text-base font-semibold uppercase placeholder:normal-case placeholder:font-normal placeholder:text-sm placeholder:text-slate-400 focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/10" /> ); })} + )} {/* Selection tray */} {totalSelected > 0 && !done && (
+ {/* Many postcodes: chips scroll inside the tray instead of growing it */} +
{postcodes.map((pc) => ( ))} +
{totalSelected}{" "} {totalSelected === 1 ? "property" : "properties"} across{" "} diff --git a/src/lib/postcodeSearch/model.test.ts b/src/lib/postcodeSearch/model.test.ts index 5268f10d..3c1449eb 100644 --- a/src/lib/postcodeSearch/model.test.ts +++ b/src/lib/postcodeSearch/model.test.ts @@ -62,6 +62,24 @@ describe("toAddressCandidates", () => { }); expect(out[1]).toMatchObject({ uprn: "101", selectable: false }); }); + + it("orders addresses numerically, so Flat 2 comes before Flat 10", () => { + const flats = [ + { DPA: { UPRN: "1", ADDRESS: "FLAT 10, BIRCH COURT, M20 4TF", CLASSIFICATION_CODE: "RD06" } }, + { DPA: { UPRN: "2", ADDRESS: "FLAT 2, BIRCH COURT, M20 4TF", CLASSIFICATION_CODE: "RD06" } }, + { DPA: { UPRN: "3", ADDRESS: "FLAT 1, BIRCH COURT, M20 4TF", CLASSIFICATION_CODE: "RD06" } }, + { DPA: { UPRN: "4", ADDRESS: "3 BIRCH LANE, M20 4TF", CLASSIFICATION_CODE: "RD02" } }, + { DPA: { UPRN: "5", ADDRESS: "12 BIRCH LANE, M20 4TF", CLASSIFICATION_CODE: "RD02" } }, + ]; + const out = toAddressCandidates(flats as never, "M20 4TF"); + expect(out.map((c) => c.address)).toEqual([ + "3 Birch Lane", + "12 Birch Lane", + "Flat 1, Birch Court", + "Flat 2, Birch Court", + "Flat 10, Birch Court", + ]); + }); }); describe("buildWritePlan", () => { diff --git a/src/lib/postcodeSearch/model.ts b/src/lib/postcodeSearch/model.ts index 92bb9d09..289cdb2d 100644 --- a/src/lib/postcodeSearch/model.ts +++ b/src/lib/postcodeSearch/model.ts @@ -58,7 +58,8 @@ const titleCase = (s: string) => /** * Flatten OS Places items into unique address candidates: DPA preferred over * LPI for the same UPRN; address rendered in title case with the postcode - * suffix stripped (it lives in its own column). + * suffix stripped (it lives in its own column); ordered naturally so + * "Flat 2" sorts before "Flat 10". */ export function toAddressCandidates( items: OsItem[], @@ -90,7 +91,9 @@ export function toAddressCandidates( lng: typeof rec.LNG === "number" ? rec.LNG : null, ...cls, }; - }); + }).sort((a, b) => + a.address.localeCompare(b.address, "en", { numeric: true, sensitivity: "base" }), + ); } export interface PostcodeGeo {