mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-07-22 08:48:34 +00:00
feat(portfolio): move unmatched properties into a "Needs attention" tab
Reworks the separation from a card stacked above the table into a proper second tab, and takes unmatched properties out of the main list entirely: - PortfolioTabs: "Properties" | "Needs attention" tab shell with a count badge on the second tab. Both panels stay mounted (hidden with CSS) so the main table keeps its filter/pagination state across tab switches. - getProperties / getPropertiesCount now exclude uprn IS NULL, so unmatched properties no longer appear (or count) in the main table until resolved. (Both functions are only used by the main table's /api/properties route.) - UnmatchedProperties: lives in the tab; shows an "all matched" empty state instead of rendering nothing, and copy updated for the edit-only scope. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f1cef2e643
commit
7ac632a7aa
4 changed files with 113 additions and 14 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import PropertyTable from "../components/PropertyTable";
|
||||
import UnmatchedProperties from "../components/UnmatchedProperties";
|
||||
import PortfolioTabs from "../components/PortfolioTabs";
|
||||
import { getUnmatchedProperties } from "@/lib/properties/unmatched";
|
||||
|
||||
export const revalidate = 60;
|
||||
|
|
@ -16,9 +17,12 @@ export default async function Page(props: {
|
|||
const unmatched = await getUnmatchedProperties(portfolioId);
|
||||
|
||||
return (
|
||||
<>
|
||||
<UnmatchedProperties properties={unmatched} portfolioId={portfolioId} />
|
||||
<PropertyTable portfolioId={portfolioId} />
|
||||
</>
|
||||
<PortfolioTabs
|
||||
needsAttentionCount={unmatched.length}
|
||||
properties={<PropertyTable portfolioId={portfolioId} />}
|
||||
needsAttention={
|
||||
<UnmatchedProperties properties={unmatched} portfolioId={portfolioId} />
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
74
src/app/portfolio/[slug]/components/PortfolioTabs.tsx
Normal file
74
src/app/portfolio/[slug]/components/PortfolioTabs.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
// Two-tab shell for the portfolio landing view: the main properties table, and
|
||||
// a "Needs attention" tab for properties that aren't matched to a UPRN yet.
|
||||
// Both panels stay mounted (hidden with CSS) so the table keeps its filter /
|
||||
// pagination state when the user flicks between tabs.
|
||||
export default function PortfolioTabs({
|
||||
needsAttentionCount,
|
||||
properties,
|
||||
needsAttention,
|
||||
}: {
|
||||
needsAttentionCount: number;
|
||||
properties: React.ReactNode;
|
||||
needsAttention: React.ReactNode;
|
||||
}) {
|
||||
const [tab, setTab] = useState<"properties" | "needs-attention">("properties");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4 flex items-center gap-1 border-b border-gray-200">
|
||||
<TabButton
|
||||
active={tab === "properties"}
|
||||
onClick={() => setTab("properties")}
|
||||
>
|
||||
Properties
|
||||
</TabButton>
|
||||
<TabButton
|
||||
active={tab === "needs-attention"}
|
||||
onClick={() => setTab("needs-attention")}
|
||||
>
|
||||
<ExclamationTriangleIcon className="h-4 w-4" />
|
||||
Needs attention
|
||||
{needsAttentionCount > 0 && (
|
||||
<span className="inline-flex min-w-[1.25rem] items-center justify-center rounded-full bg-amber-100 px-1.5 text-[11px] font-bold text-amber-800">
|
||||
{needsAttentionCount}
|
||||
</span>
|
||||
)}
|
||||
</TabButton>
|
||||
</div>
|
||||
|
||||
<div className={tab === "properties" ? "" : "hidden"}>{properties}</div>
|
||||
<div className={tab === "needs-attention" ? "" : "hidden"}>
|
||||
{needsAttention}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TabButton({
|
||||
active,
|
||||
onClick,
|
||||
children,
|
||||
}: {
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`-mb-px flex items-center gap-1.5 border-b-2 px-4 py-2.5 text-sm font-semibold transition-colors ${
|
||||
active
|
||||
? "border-primary text-primary"
|
||||
: "border-transparent text-gray-500 hover:text-gray-800"
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||
import {
|
||||
ExclamationTriangleIcon,
|
||||
CheckCircleIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import type { UnmatchedProperty } from "@/lib/properties/unmatched";
|
||||
import EditAddress from "./EditAddress";
|
||||
|
||||
// The "needs attention" workspace that separates properties with no UPRN from
|
||||
// the matched portfolio table. Each row lets the user correct the address /
|
||||
// postcode (UPRN matching is a later step). Renders nothing when every property
|
||||
// is matched, so a healthy portfolio stays uncluttered.
|
||||
// The "Needs attention" tab: properties with no UPRN, separated out of the main
|
||||
// table until they're matched. Each row lets the user correct the address /
|
||||
// postcode (UPRN matching is a later step).
|
||||
export default function UnmatchedProperties({
|
||||
properties,
|
||||
portfolioId,
|
||||
|
|
@ -13,14 +15,26 @@ export default function UnmatchedProperties({
|
|||
properties: UnmatchedProperty[];
|
||||
portfolioId: string;
|
||||
}) {
|
||||
if (properties.length === 0) return null;
|
||||
|
||||
const count = properties.length;
|
||||
|
||||
if (count === 0) {
|
||||
return (
|
||||
<div className="rounded-2xl border border-gray-100 bg-white px-6 py-16 text-center shadow-sm">
|
||||
<CheckCircleIcon className="mx-auto h-9 w-9 text-emerald-500" />
|
||||
<p className="mt-3 text-sm font-semibold text-gray-700">
|
||||
All properties are matched
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-gray-400">
|
||||
Every property has a UPRN — nothing needs attention.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label="Properties without a UPRN"
|
||||
className="mb-6 overflow-hidden rounded-2xl border border-amber-200 bg-amber-50 shadow-sm"
|
||||
className="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" />
|
||||
|
|
@ -35,8 +49,9 @@ export default function UnmatchedProperties({
|
|||
</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.
|
||||
matched to an Ordnance Survey address automatically. Correct the
|
||||
address and postcode so {count === 1 ? "it's" : "they're"} ready to
|
||||
match — they stay out of the main list until resolved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -739,6 +739,9 @@ export async function getPropertiesCount(
|
|||
${epcJoins}
|
||||
${planJoin}
|
||||
WHERE p.portfolio_id = ${portfolioId}
|
||||
-- Unmatched (no-UPRN) properties live in the "Needs attention" tab until
|
||||
-- resolved, so they're excluded from the main table + its count.
|
||||
AND p.uprn IS NOT NULL
|
||||
${combinedWhere}
|
||||
`);
|
||||
|
||||
|
|
@ -831,6 +834,9 @@ export async function getProperties(
|
|||
ON epc.property_id = p.id
|
||||
${newApproachJoins}
|
||||
WHERE p.portfolio_id = ${portfolioId}
|
||||
-- Unmatched (no-UPRN) properties live in the "Needs attention" tab until
|
||||
-- resolved, so they're excluded from the main table.
|
||||
AND p.uprn IS NOT NULL
|
||||
${combinedWhere}
|
||||
LIMIT ${limit} OFFSET ${offset};
|
||||
`);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue