mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-07-22 08:48:34 +00:00
feat(portfolio): separate properties without a UPRN into their own section
The portfolio landing page listed every property in one table, mixing in the ones that never matched to an Ordnance Survey UPRN. Those unmatched properties need a different workflow (fix the address/postcode, then re-run the address→ UPRN search), so surface them separately above the main table. This change does the separation only: - getUnmatchedProperties(portfolioId): portfolio properties with uprn IS NULL (excluding soft-deleted), falling back to the user-inputted address/postcode when the canonical fields are empty. - UnmatchedProperties: an amber "needs attention" card listing address + postcode per row, with a placeholder "Search UPRN" action where the advanced ARA search will hang off. Renders nothing when every property is matched, so healthy portfolios stay uncluttered. - Portfolio page renders the section above the existing PropertyTable, which is left exactly as-is. Follow-up (the actual feature): make address/postcode editable per row and wire the "Search UPRN" button to the advanced ARA search. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c861d4a0b0
commit
4fefec7cca
3 changed files with 138 additions and 0 deletions
|
|
@ -1,4 +1,6 @@
|
|||
import PropertyTable from "../components/PropertyTable";
|
||||
import UnmatchedProperties from "../components/UnmatchedProperties";
|
||||
import { getUnmatchedProperties } from "@/lib/properties/unmatched";
|
||||
|
||||
export const revalidate = 60;
|
||||
|
||||
|
|
@ -11,8 +13,11 @@ export default async function Page(props: {
|
|||
const params = await props.params;
|
||||
const portfolioId = params.slug;
|
||||
|
||||
const unmatched = await getUnmatchedProperties(portfolioId);
|
||||
|
||||
return (
|
||||
<>
|
||||
<UnmatchedProperties properties={unmatched} />
|
||||
<PropertyTable portfolioId={portfolioId} />
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
86
src/app/portfolio/[slug]/components/UnmatchedProperties.tsx
Normal file
86
src/app/portfolio/[slug]/components/UnmatchedProperties.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import {
|
||||
ExclamationTriangleIcon,
|
||||
MagnifyingGlassIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import type { UnmatchedProperty } from "@/lib/properties/unmatched";
|
||||
|
||||
// The "needs attention" workspace that separates properties with no UPRN from
|
||||
// the matched portfolio table. Read-only for now; the per-row action is where
|
||||
// the advanced ARA search (edit address/postcode → search UPRN) will hang off.
|
||||
// Renders nothing when every property is matched, so a healthy portfolio stays
|
||||
// uncluttered.
|
||||
export default function UnmatchedProperties({
|
||||
properties,
|
||||
}: {
|
||||
properties: UnmatchedProperty[];
|
||||
}) {
|
||||
if (properties.length === 0) return null;
|
||||
|
||||
const count = properties.length;
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label="Properties without a UPRN"
|
||||
className="mb-6 overflow-hidden rounded-2xl border border-amber-200 bg-amber-50 shadow-sm"
|
||||
>
|
||||
<div className="flex items-start gap-3 border-b border-amber-100 px-6 py-4">
|
||||
<ExclamationTriangleIcon className="mt-0.5 h-5 w-5 shrink-0 text-amber-500" />
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-sm font-bold text-amber-900">
|
||||
Properties without a UPRN
|
||||
</h2>
|
||||
<span className="inline-flex items-center rounded-full bg-amber-200/70 px-2 py-0.5 text-xs font-semibold text-amber-800">
|
||||
{count}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-0.5 max-w-xl text-xs text-amber-700">
|
||||
{count === 1 ? "This property" : "These properties"} couldn't be
|
||||
matched to an Ordnance Survey address automatically. Check the address
|
||||
and postcode, then run an advanced search to find the right match.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white">
|
||||
<div className="grid grid-cols-12 gap-3 border-b border-gray-100 bg-gray-50/80 px-6 py-2 text-[11px] font-semibold uppercase tracking-wider text-gray-500">
|
||||
<div className="col-span-6">Address</div>
|
||||
<div className="col-span-3">Postcode</div>
|
||||
<div className="col-span-3 text-right">Action</div>
|
||||
</div>
|
||||
|
||||
<ul className="divide-y divide-gray-50">
|
||||
{properties.map((p) => (
|
||||
<li
|
||||
key={p.id}
|
||||
className="grid grid-cols-12 items-center gap-3 px-6 py-3"
|
||||
>
|
||||
<p
|
||||
className="col-span-6 truncate text-sm text-gray-800"
|
||||
title={p.address ?? undefined}
|
||||
>
|
||||
{p.address ?? (
|
||||
<span className="italic text-gray-400">No address</span>
|
||||
)}
|
||||
</p>
|
||||
<p className="col-span-3 text-sm text-gray-600">
|
||||
{p.postcode ?? <span className="text-gray-400">—</span>}
|
||||
</p>
|
||||
<div className="col-span-3 flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
disabled
|
||||
title="Advanced ARA search — coming soon"
|
||||
className="inline-flex cursor-not-allowed items-center gap-1.5 rounded-lg border border-gray-200 bg-white px-3 py-1.5 text-xs font-semibold text-gray-400"
|
||||
>
|
||||
<MagnifyingGlassIcon className="h-3.5 w-3.5" />
|
||||
Search UPRN
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
47
src/lib/properties/unmatched.ts
Normal file
47
src/lib/properties/unmatched.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { db } from "@/app/db/db";
|
||||
import { property } from "@/app/db/schema/property";
|
||||
import { and, asc, eq, isNull } from "drizzle-orm";
|
||||
|
||||
export interface UnmatchedProperty {
|
||||
id: string;
|
||||
address: string | null;
|
||||
postcode: string | null;
|
||||
}
|
||||
|
||||
// Properties in a portfolio that have no UPRN — i.e. were never matched to an
|
||||
// Ordnance Survey address record. We surface these separately from the main
|
||||
// property table so the customer can correct the address/postcode and re-run
|
||||
// the address→UPRN search (the "advanced ARA search", wired up in a follow-up).
|
||||
// `markedForDeletion` rows are excluded so soft-deleted properties don't show.
|
||||
export async function getUnmatchedProperties(
|
||||
portfolioId: string,
|
||||
): Promise<UnmatchedProperty[]> {
|
||||
if (!/^\d+$/.test(portfolioId)) return [];
|
||||
const pid = BigInt(portfolioId);
|
||||
|
||||
const rows = await db
|
||||
.select({
|
||||
id: property.id,
|
||||
address: property.address,
|
||||
postcode: property.postcode,
|
||||
userInputtedAddress: property.userInputtedAddress,
|
||||
userInputtedPostcode: property.userInputtedPostcode,
|
||||
})
|
||||
.from(property)
|
||||
.where(
|
||||
and(
|
||||
eq(property.portfolioId, pid),
|
||||
isNull(property.uprn),
|
||||
eq(property.markedForDeletion, false),
|
||||
),
|
||||
)
|
||||
.orderBy(asc(property.address));
|
||||
|
||||
// Prefer the canonical address; fall back to whatever the user typed on
|
||||
// import so the row is still identifiable while it awaits a match.
|
||||
return rows.map((r) => ({
|
||||
id: String(r.id),
|
||||
address: r.address ?? r.userInputtedAddress ?? null,
|
||||
postcode: r.postcode ?? r.userInputtedPostcode ?? null,
|
||||
}));
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue