diff --git a/src/app/portfolio/[slug]/(portfolio)/page.tsx b/src/app/portfolio/[slug]/(portfolio)/page.tsx index 4f9b0cde..d4514980 100644 --- a/src/app/portfolio/[slug]/(portfolio)/page.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/page.tsx @@ -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 ( <> + ); diff --git a/src/app/portfolio/[slug]/components/UnmatchedProperties.tsx b/src/app/portfolio/[slug]/components/UnmatchedProperties.tsx new file mode 100644 index 00000000..9d6ef33e --- /dev/null +++ b/src/app/portfolio/[slug]/components/UnmatchedProperties.tsx @@ -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 ( +
+
+ +
+
+

+ Properties without a UPRN +

+ + {count} + +
+

+ {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. +

+
+
+ +
+
+
Address
+
Postcode
+
Action
+
+ +
    + {properties.map((p) => ( +
  • +

    + {p.address ?? ( + No address + )} +

    +

    + {p.postcode ?? } +

    +
    + +
    +
  • + ))} +
+
+
+ ); +} diff --git a/src/lib/properties/unmatched.ts b/src/lib/properties/unmatched.ts new file mode 100644 index 00000000..9f8e6334 --- /dev/null +++ b/src/lib/properties/unmatched.ts @@ -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 { + 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, + })); +}