mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
This commit is contained in:
parent
c70a34f174
commit
8c9cceef77
1 changed files with 167 additions and 79 deletions
|
|
@ -1,11 +1,11 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useMemo, useRef } from "react";
|
||||
import { useState, useMemo, useRef, useEffect, useCallback } from "react";
|
||||
import { useProperties } from "./useProperties";
|
||||
import DataTable from "./dataTable";
|
||||
import PropertyFilters from "./PropertyFilters";
|
||||
import { FilterGroups } from "@/app/utils/propertyFilters";
|
||||
import { HomeIcon, FunnelIcon } from "@heroicons/react/24/outline";
|
||||
import { HomeIcon, FunnelIcon, ChevronDownIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import { columns } from "@/app/portfolio/[slug]/components/propertyTableColumns";
|
||||
|
||||
import {
|
||||
|
|
@ -49,6 +49,117 @@ function LoadingOverlay() {
|
|||
);
|
||||
}
|
||||
|
||||
/* ----------------------------------------
|
||||
Quick filter dropdown button
|
||||
----------------------------------------- */
|
||||
type QuickFilterKey = "address" | "postcode" | "propertyRef";
|
||||
|
||||
interface QuickFilterDropdownProps {
|
||||
label: string;
|
||||
placeholder: string;
|
||||
committedValue: string;
|
||||
isOpen: boolean;
|
||||
onOpen: () => void;
|
||||
onCommit: (value: string) => void;
|
||||
onClear: () => void;
|
||||
inputWidth?: string;
|
||||
}
|
||||
|
||||
function QuickFilterDropdown({
|
||||
label,
|
||||
placeholder,
|
||||
committedValue,
|
||||
isOpen,
|
||||
onOpen,
|
||||
onCommit,
|
||||
onClear,
|
||||
inputWidth = "w-52",
|
||||
}: QuickFilterDropdownProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [draft, setDraft] = useState(committedValue);
|
||||
|
||||
// Sync draft when dropdown opens
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setDraft(committedValue);
|
||||
setTimeout(() => inputRef.current?.focus(), 0);
|
||||
}
|
||||
}, [isOpen, committedValue]);
|
||||
|
||||
const commit = useCallback(() => {
|
||||
onCommit(draft.trim());
|
||||
}, [draft, onCommit]);
|
||||
|
||||
// Close + commit on outside click
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
function handleMouseDown(e: MouseEvent) {
|
||||
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
||||
commit();
|
||||
}
|
||||
}
|
||||
document.addEventListener("mousedown", handleMouseDown);
|
||||
return () => document.removeEventListener("mousedown", handleMouseDown);
|
||||
}, [isOpen, commit]);
|
||||
|
||||
const isActive = Boolean(committedValue);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="relative">
|
||||
<button
|
||||
onClick={onOpen}
|
||||
className={[
|
||||
"flex items-center gap-1.5 h-9 px-3 rounded-md border text-sm transition shrink-0",
|
||||
isActive
|
||||
? "border-gray-800 bg-gray-800 text-white"
|
||||
: "border-gray-300 text-gray-600 hover:bg-gray-50",
|
||||
].join(" ")}
|
||||
>
|
||||
<span className="font-medium">{label}</span>
|
||||
{isActive ? (
|
||||
<>
|
||||
<span className="opacity-75 max-w-[120px] truncate">: {committedValue}</span>
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={(e) => { e.stopPropagation(); onClear(); }}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { e.stopPropagation(); onClear(); } }}
|
||||
className="ml-0.5 rounded-full hover:opacity-75"
|
||||
>
|
||||
<XMarkIcon className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<ChevronDownIcon className="h-3.5 w-3.5 opacity-50" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute left-0 top-full mt-1 z-30 bg-white border border-gray-200 rounded-md shadow-md p-2 flex gap-1.5 items-center">
|
||||
<input
|
||||
ref={inputRef}
|
||||
className={`h-8 rounded border border-gray-300 px-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-black/10 ${inputWidth}`}
|
||||
placeholder={placeholder}
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") commit();
|
||||
if (e.key === "Escape") { setDraft(committedValue); onCommit(committedValue); }
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={commit}
|
||||
className="h-8 px-2.5 rounded bg-gray-800 text-white text-sm hover:bg-gray-700 transition whitespace-nowrap"
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ----------------------------------------
|
||||
Main table
|
||||
----------------------------------------- */
|
||||
|
|
@ -59,63 +170,51 @@ export default function PropertyTable({
|
|||
}) {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||
|
||||
// Quick filter display state (updates immediately)
|
||||
const [quickAddress, setQuickAddress] = useState("");
|
||||
const [quickPostcode, setQuickPostcode] = useState("");
|
||||
const [quickPropertyRef, setQuickPropertyRef] = useState("");
|
||||
// Committed quick filter values (drives the query)
|
||||
const [committedAddress, setCommittedAddress] = useState("");
|
||||
const [committedPostcode, setCommittedPostcode] = useState("");
|
||||
const [committedPropertyRef, setCommittedPropertyRef] = useState("");
|
||||
|
||||
// Debounced quick filter values (drives the query)
|
||||
const [debouncedAddress, setDebouncedAddress] = useState("");
|
||||
const [debouncedPostcode, setDebouncedPostcode] = useState("");
|
||||
const [debouncedPropertyRef, setDebouncedPropertyRef] = useState("");
|
||||
// Which quick filter dropdown is open
|
||||
const [openFilter, setOpenFilter] = useState<QuickFilterKey | null>(null);
|
||||
|
||||
// Advanced filter groups from the sidebar
|
||||
const [filterGroups, setFilterGroups] = useState<FilterGroups>([]);
|
||||
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
function handleQuickFilter(
|
||||
field: "address" | "postcode" | "propertyRef",
|
||||
value: string
|
||||
) {
|
||||
if (debounceRef.current) clearTimeout(debounceRef.current);
|
||||
debounceRef.current = setTimeout(() => {
|
||||
if (field === "address") setDebouncedAddress(value);
|
||||
if (field === "postcode") setDebouncedPostcode(value);
|
||||
if (field === "propertyRef") setDebouncedPropertyRef(value);
|
||||
}, 300);
|
||||
function commitFilter(field: QuickFilterKey, value: string) {
|
||||
if (field === "address") setCommittedAddress(value);
|
||||
if (field === "postcode") setCommittedPostcode(value);
|
||||
if (field === "propertyRef") setCommittedPropertyRef(value);
|
||||
setOpenFilter(null);
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
if (debounceRef.current) clearTimeout(debounceRef.current);
|
||||
setQuickAddress("");
|
||||
setQuickPostcode("");
|
||||
setQuickPropertyRef("");
|
||||
setDebouncedAddress("");
|
||||
setDebouncedPostcode("");
|
||||
setDebouncedPropertyRef("");
|
||||
setCommittedAddress("");
|
||||
setCommittedPostcode("");
|
||||
setCommittedPropertyRef("");
|
||||
setOpenFilter(null);
|
||||
setFilterGroups([]);
|
||||
}
|
||||
|
||||
const allFilterGroups = useMemo((): FilterGroups => {
|
||||
const quick: FilterGroups = [];
|
||||
if (debouncedAddress)
|
||||
if (committedAddress)
|
||||
quick.push({
|
||||
id: "qa",
|
||||
conditions: [{ id: "qa-c", field: "address", operator: "contains", value: debouncedAddress }],
|
||||
conditions: [{ id: "qa-c", field: "address", operator: "contains", value: committedAddress }],
|
||||
});
|
||||
if (debouncedPostcode)
|
||||
if (committedPostcode)
|
||||
quick.push({
|
||||
id: "qp",
|
||||
conditions: [{ id: "qp-c", field: "postcode", operator: "starts_with", value: debouncedPostcode }],
|
||||
conditions: [{ id: "qp-c", field: "postcode", operator: "starts_with", value: committedPostcode }],
|
||||
});
|
||||
if (debouncedPropertyRef)
|
||||
if (committedPropertyRef)
|
||||
quick.push({
|
||||
id: "qr",
|
||||
conditions: [{ id: "qr-c", field: "propertyRef", operator: "contains", value: debouncedPropertyRef }],
|
||||
conditions: [{ id: "qr-c", field: "propertyRef", operator: "contains", value: committedPropertyRef }],
|
||||
});
|
||||
return [...quick, ...filterGroups];
|
||||
}, [debouncedAddress, debouncedPostcode, debouncedPropertyRef, filterGroups]);
|
||||
}, [committedAddress, committedPostcode, committedPropertyRef, filterGroups]);
|
||||
|
||||
const hasActiveFilters = allFilterGroups.length > 0;
|
||||
|
||||
|
|
@ -139,13 +238,10 @@ export default function PropertyTable({
|
|||
const [previewLoading] = useState(false);
|
||||
const [previewError] = useState<string | null>(null);
|
||||
|
||||
const inputClass =
|
||||
"h-9 rounded-md border border-gray-300 px-3 text-sm focus:outline-none focus:ring-2 focus:ring-black/10 bg-white";
|
||||
|
||||
return (
|
||||
<div className="px-4 py-3">
|
||||
{/* Quick filters row */}
|
||||
<div className="flex items-end gap-3 mb-3 flex-wrap">
|
||||
<div className="flex items-center gap-2 mb-3 flex-wrap">
|
||||
<button
|
||||
onClick={() => setSidebarOpen((o) => !o)}
|
||||
className="flex items-center gap-1.5 h-9 px-3 rounded-md border border-gray-300 text-sm text-gray-600 hover:bg-gray-50 transition shrink-0"
|
||||
|
|
@ -155,51 +251,43 @@ export default function PropertyTable({
|
|||
{sidebarOpen ? "Hide filters" : "Filters"}
|
||||
</button>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-gray-600">Address</label>
|
||||
<input
|
||||
className={`${inputClass} w-52`}
|
||||
placeholder="Contains…"
|
||||
value={quickAddress}
|
||||
onChange={(e) => {
|
||||
setQuickAddress(e.target.value);
|
||||
handleQuickFilter("address", e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<QuickFilterDropdown
|
||||
label="Address"
|
||||
placeholder="Contains…"
|
||||
committedValue={committedAddress}
|
||||
isOpen={openFilter === "address"}
|
||||
onOpen={() => setOpenFilter(openFilter === "address" ? null : "address")}
|
||||
onCommit={(v) => commitFilter("address", v)}
|
||||
onClear={() => { setCommittedAddress(""); setOpenFilter(null); }}
|
||||
inputWidth="w-52"
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-gray-600">Postcode</label>
|
||||
<input
|
||||
className={`${inputClass} w-32`}
|
||||
placeholder="e.g. E17"
|
||||
value={quickPostcode}
|
||||
onChange={(e) => {
|
||||
setQuickPostcode(e.target.value);
|
||||
handleQuickFilter("postcode", e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<QuickFilterDropdown
|
||||
label="Postcode"
|
||||
placeholder="e.g. E17"
|
||||
committedValue={committedPostcode}
|
||||
isOpen={openFilter === "postcode"}
|
||||
onOpen={() => setOpenFilter(openFilter === "postcode" ? null : "postcode")}
|
||||
onCommit={(v) => commitFilter("postcode", v)}
|
||||
onClear={() => { setCommittedPostcode(""); setOpenFilter(null); }}
|
||||
inputWidth="w-32"
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-gray-600">
|
||||
Property Ref
|
||||
</label>
|
||||
<input
|
||||
className={`${inputClass} w-40`}
|
||||
placeholder="Landlord ref…"
|
||||
value={quickPropertyRef}
|
||||
onChange={(e) => {
|
||||
setQuickPropertyRef(e.target.value);
|
||||
handleQuickFilter("propertyRef", e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<QuickFilterDropdown
|
||||
label="Property Ref"
|
||||
placeholder="Landlord ref…"
|
||||
committedValue={committedPropertyRef}
|
||||
isOpen={openFilter === "propertyRef"}
|
||||
onOpen={() => setOpenFilter(openFilter === "propertyRef" ? null : "propertyRef")}
|
||||
onCommit={(v) => commitFilter("propertyRef", v)}
|
||||
onClear={() => { setCommittedPropertyRef(""); setOpenFilter(null); }}
|
||||
inputWidth="w-40"
|
||||
/>
|
||||
|
||||
{hasActiveFilters && (
|
||||
<button
|
||||
onClick={clearAll}
|
||||
className="self-end h-9 px-3 text-sm text-gray-500 hover:text-gray-700 underline"
|
||||
className="h-9 px-3 text-sm text-gray-500 hover:text-gray-700 underline"
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue