mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-07-22 08:48:34 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
parent
8a0ad0c0dc
commit
158d5ded92
5 changed files with 36 additions and 60 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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"
|
||||
/>
|
||||
<button
|
||||
|
|
@ -298,6 +301,9 @@ export default function AddPropertiesClient({
|
|||
</span>
|
||||
</div>
|
||||
|
||||
{/* Long postcodes can return ~100 addresses — scroll inside the card
|
||||
rather than growing the page. */}
|
||||
<div className="max-h-[26rem] overflow-y-auto pr-1 -mr-1">
|
||||
{data.candidates.map((c) => {
|
||||
const { label, note } = describeCandidate(c);
|
||||
const disabled = !c.selectable || c.alreadyInPortfolio;
|
||||
|
|
@ -350,12 +356,15 @@ export default function AddPropertiesClient({
|
|||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Selection tray */}
|
||||
{totalSelected > 0 && !done && (
|
||||
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-40 w-[min(56rem,calc(100vw-2rem))] bg-white/95 backdrop-blur border border-slate-200 rounded-2xl shadow-2xl px-4 py-3 flex items-center gap-3 flex-wrap">
|
||||
{/* Many postcodes: chips scroll inside the tray instead of growing it */}
|
||||
<div className="flex items-center gap-2 flex-wrap max-h-16 overflow-y-auto">
|
||||
{postcodes.map((pc) => (
|
||||
<span
|
||||
key={pc}
|
||||
|
|
@ -377,6 +386,7 @@ export default function AddPropertiesClient({
|
|||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-xs text-slate-500">
|
||||
<b className="text-gray-800">{totalSelected}</b>{" "}
|
||||
{totalSelected === 1 ? "property" : "properties"} across{" "}
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue