mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-30 12:55:02 +00:00
added db filtereing
This commit is contained in:
parent
7a7c379749
commit
c9bfb0ce9c
12 changed files with 576 additions and 215 deletions
26
.vscode/settings.json
vendored
Normal file
26
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
|
||||
// Hot reload setting that needs to be in user settings
|
||||
// "jupyter.runStartupCommands": [
|
||||
// "%load_ext autoreload", "%autoreload 2"
|
||||
// ]
|
||||
// --- VIM SETTINGS ---
|
||||
// "vim.useSystemClipboard": true,
|
||||
"vim.enableNeovim": false,
|
||||
|
||||
// Allow VSCode native keybindings to override Vim when needed
|
||||
"vim.handleKeys": {
|
||||
"<C-p>": false,
|
||||
"<C-P>": false,
|
||||
"<C-S-p>": false,
|
||||
"<C-c>": false,
|
||||
"<C-v>": false,
|
||||
"<C-S-v>": false,
|
||||
"<C-S-e>": false,
|
||||
"<C-b>": false,
|
||||
"<C-j>": false,
|
||||
"<C-S-c>": false,
|
||||
"<C-k>": false
|
||||
},
|
||||
|
||||
}
|
||||
25
src/app/api/properties/route.ts
Normal file
25
src/app/api/properties/route.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getProperties } from "@/app/portfolio/[slug]/utils";
|
||||
import { PropertyFilter } from "@/app/utils/propertyFilters";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const body = await req.json();
|
||||
|
||||
const portfolioId = body.portfolioId;
|
||||
const filters: PropertyFilter[] = body.filters ?? [];
|
||||
|
||||
if (!portfolioId) {
|
||||
return NextResponse.json(
|
||||
{ error: "Missing portfolioId" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
console.log("filters", filters);
|
||||
const properties = await getProperties(
|
||||
portfolioId,
|
||||
1000,
|
||||
0,
|
||||
filters
|
||||
);
|
||||
return NextResponse.json(properties);
|
||||
}
|
||||
|
|
@ -1,27 +1,13 @@
|
|||
import { HomeIcon } from "@heroicons/react/24/outline";
|
||||
import { getPortfolio, getPortfolioPerformance, getProperties } from "../utils";
|
||||
import DataTable from "@/app/portfolio/[slug]/components/propertyTable";
|
||||
import { columns } from "@/app/portfolio/[slug]/components/propertyTableColumns";
|
||||
import DataTable from "@/app/portfolio/[slug]/components/dataTable";
|
||||
import { PropertyWithRelations } from "@/app/db/schema/property";
|
||||
import PropertyTable from "../components/PropertyTable";
|
||||
|
||||
import SummaryBox from "@/app/components/portfolio/SummaryBox";
|
||||
|
||||
// We enfore caching of data for 60 seconds
|
||||
export const revalidate = 60;
|
||||
|
||||
function EmptyPropertyState() {
|
||||
return (
|
||||
<div className="flex justify-center h-1/2">
|
||||
<div className="bg-white rounded-lg w-full">
|
||||
<p className="text-center text-gray-400 pt-6">
|
||||
Hover over "New Property" to start adding properties to your
|
||||
Portfolio
|
||||
<HomeIcon className="h-20 w-20 mx-auto mt-4 text-gray-200" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{ slug: string }>;
|
||||
searchParams: Promise<{
|
||||
|
|
@ -71,26 +57,9 @@ export default async function Page(props: {
|
|||
];
|
||||
}
|
||||
|
||||
const properties: PropertyWithRelations[] = await getProperties(
|
||||
portfolioId,
|
||||
1000,
|
||||
0,
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-center">
|
||||
<div className="grid grid-cols-11 w-full max-w-8xl">
|
||||
<div className="col-span-11 bg-white">
|
||||
{properties.length === 0 ? (
|
||||
<EmptyPropertyState />
|
||||
) : (
|
||||
<DataTable data={properties} columns={columns} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
<PropertyTable portfolioId={portfolioId}/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { ProjectProposal, DashboardSummary } from "./ProjectProposal";
|
||||
import { getPlansWithTotals } from "./utils";
|
||||
import DataTable from "@/app/portfolio/[slug]/components/propertyTable";
|
||||
import DataTable from "@/app/portfolio/[slug]/components/dataTable";
|
||||
import { planColumns } from "./ProposalColumns";
|
||||
|
||||
export default async function ProjectProposalPage(props: {
|
||||
|
|
|
|||
151
src/app/portfolio/[slug]/components/PropertyFilters.tsx
Normal file
151
src/app/portfolio/[slug]/components/PropertyFilters.tsx
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export type PropertyFilterValues = {
|
||||
address: string;
|
||||
postcode: string;
|
||||
current_epc_at_most: "" | "C" | "D" | "E" | "F" | "G";
|
||||
expected_epc_at_least: "" | "A" | "B" | "C" | "D";
|
||||
};
|
||||
|
||||
const EPC_ORDER = ["A", "B", "C", "D", "E", "F", "G"] as const;
|
||||
|
||||
const epcIndex = (epc: string) =>
|
||||
EPC_ORDER.indexOf(epc as (typeof EPC_ORDER)[number]);
|
||||
|
||||
export default function PropertyFilters({
|
||||
onApply,
|
||||
}: {
|
||||
onApply: (filters: PropertyFilterValues) => void;
|
||||
}) {
|
||||
const [address, setAddress] = useState("");
|
||||
const [postcode, setPostcode] = useState("");
|
||||
const [currentEpc, setCurrentEpc] =
|
||||
useState<PropertyFilterValues["current_epc_at_most"]>("");
|
||||
const [expectedEpc, setExpectedEpc] =
|
||||
useState<PropertyFilterValues["expected_epc_at_least"]>("");
|
||||
|
||||
/* ----------------------------------------
|
||||
Auto-fix invalid combinations
|
||||
----------------------------------------- */
|
||||
useEffect(() => {
|
||||
if (currentEpc && expectedEpc) {
|
||||
// expected must be BETTER than current
|
||||
if (epcIndex(expectedEpc) >= epcIndex(currentEpc)) {
|
||||
setExpectedEpc("");
|
||||
}
|
||||
}
|
||||
}, [currentEpc]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentEpc && expectedEpc) {
|
||||
if (epcIndex(expectedEpc) >= epcIndex(currentEpc)) {
|
||||
setCurrentEpc("");
|
||||
}
|
||||
}
|
||||
}, [expectedEpc]);
|
||||
|
||||
function apply() {
|
||||
onApply({
|
||||
address,
|
||||
postcode,
|
||||
current_epc_at_most: currentEpc,
|
||||
expected_epc_at_least: expectedEpc,
|
||||
});
|
||||
}
|
||||
|
||||
function clear() {
|
||||
setAddress("");
|
||||
setPostcode("");
|
||||
setCurrentEpc("");
|
||||
setExpectedEpc("");
|
||||
|
||||
onApply({
|
||||
address: "",
|
||||
postcode: "",
|
||||
current_epc_at_most: "",
|
||||
expected_epc_at_least: "",
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex gap-4 bg-gray-50 p-4 rounded-lg items-end">
|
||||
{/* Address */}
|
||||
<input
|
||||
className="border rounded px-3 py-2"
|
||||
placeholder="Address contains…"
|
||||
value={address}
|
||||
onChange={(e) => setAddress(e.target.value)}
|
||||
/>
|
||||
|
||||
{/* Postcode */}
|
||||
<input
|
||||
className="border rounded px-3 py-2"
|
||||
placeholder="Postcode"
|
||||
value={postcode}
|
||||
onChange={(e) => setPostcode(e.target.value)}
|
||||
/>
|
||||
|
||||
{/* Current EPC */}
|
||||
<select
|
||||
className="border rounded px-3 py-2"
|
||||
value={currentEpc}
|
||||
onChange={(e) =>
|
||||
setCurrentEpc(e.target.value as any)
|
||||
}
|
||||
>
|
||||
<option value="">Current EPC is…</option>
|
||||
{["C", "D", "E", "F", "G"].map((epc) => (
|
||||
<option
|
||||
key={epc}
|
||||
value={epc}
|
||||
disabled={
|
||||
expectedEpc &&
|
||||
epcIndex(epc) <= epcIndex(expectedEpc)
|
||||
}
|
||||
>
|
||||
{epc} or worse
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{/* Expected EPC */}
|
||||
<select
|
||||
className="border rounded px-3 py-2"
|
||||
value={expectedEpc}
|
||||
onChange={(e) =>
|
||||
setExpectedEpc(e.target.value as any)
|
||||
}
|
||||
>
|
||||
<option value="">Expected EPC at least…</option>
|
||||
{["A", "B", "C", "D"].map((epc) => (
|
||||
<option
|
||||
key={epc}
|
||||
value={epc}
|
||||
disabled={
|
||||
currentEpc &&
|
||||
epcIndex(epc) >= epcIndex(currentEpc)
|
||||
}
|
||||
>
|
||||
{epc} or better
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<button
|
||||
onClick={apply}
|
||||
className="bg-black text-white px-4 py-2 rounded"
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={clear}
|
||||
className="text-gray-500 px-2"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
109
src/app/portfolio/[slug]/components/PropertyTable.tsx
Normal file
109
src/app/portfolio/[slug]/components/PropertyTable.tsx
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useProperties } from "./useProperties";
|
||||
import DataTable from "./dataTable";
|
||||
import PropertyFilters, {
|
||||
PropertyFilterValues,
|
||||
} from "./PropertyFilters";
|
||||
import { PropertyFilter } from "@/app/utils/propertyFilters";
|
||||
import { HomeIcon } from "@heroicons/react/24/outline";
|
||||
import { columns } from "@/app/portfolio/[slug]/components/propertyTableColumns";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export function parsePropertyFilters(
|
||||
filters: PropertyFilterValues
|
||||
): PropertyFilter[] {
|
||||
const parsed: PropertyFilter[] = [];
|
||||
|
||||
if (filters.address) {
|
||||
parsed.push({
|
||||
field: "address",
|
||||
operator: "contains",
|
||||
value: filters.address,
|
||||
});
|
||||
}
|
||||
|
||||
if (filters.postcode) {
|
||||
parsed.push({
|
||||
field: "postcode",
|
||||
operator: "starts_with",
|
||||
value: filters.postcode,
|
||||
});
|
||||
}
|
||||
|
||||
if (filters.current_epc_at_most) {
|
||||
parsed.push({
|
||||
field: "currentEpc",
|
||||
operator: "epc_at_most",
|
||||
value: filters.current_epc_at_most,
|
||||
});
|
||||
}
|
||||
|
||||
if (filters.expected_epc_at_least) {
|
||||
parsed.push({
|
||||
field: "expectedEpc",
|
||||
operator: "epc_at_least",
|
||||
value: filters.expected_epc_at_least.toUpperCase(),
|
||||
});
|
||||
}
|
||||
console.log(parsed)
|
||||
return parsed;
|
||||
}
|
||||
|
||||
|
||||
function EmptyPropertyState() {
|
||||
return (
|
||||
<div className="flex justify-center h-1/2">
|
||||
<div className="bg-white rounded-lg w-full">
|
||||
<p className="text-center text-gray-400 pt-6">
|
||||
Hover over "New Property" to start adding properties to your
|
||||
Portfolio
|
||||
<HomeIcon className="h-20 w-20 mx-auto mt-4 text-gray-200" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PropertyTable({ portfolioId }: { portfolioId: string }) {
|
||||
const [filters, setFilters] = useState<PropertyFilterValues>({
|
||||
address: "",
|
||||
postcode: "",
|
||||
current_epc_at_most: "",
|
||||
expected_epc_at_least: "",
|
||||
});
|
||||
|
||||
const parsedFilters = useMemo(
|
||||
() => parsePropertyFilters(filters),
|
||||
[filters]
|
||||
);
|
||||
|
||||
const { data = [], isLoading, isFetching, isError } = useProperties({
|
||||
portfolioId,
|
||||
filters: parsedFilters,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<div className="grid grid-cols-11 w-full max-w-8xl">
|
||||
<div className="col-span-11 bg-white">
|
||||
<PropertyFilters onApply={setFilters} />
|
||||
|
||||
{isLoading ? (
|
||||
<div className="p-6 text-gray-400">Loading properties…</div>
|
||||
) : isError || data.length === 0 ? (
|
||||
<EmptyPropertyState />
|
||||
) : (
|
||||
<>
|
||||
{isFetching && (
|
||||
<div className="text-sm text-gray-400 px-4">Updating…</div>
|
||||
)}
|
||||
<DataTable data={data} columns={columns} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
137
src/app/portfolio/[slug]/components/dataTable.tsx
Normal file
137
src/app/portfolio/[slug]/components/dataTable.tsx
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
SortingState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
FilterFn,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/app/shadcn_components/ui/table";
|
||||
|
||||
import { useState } from "react";
|
||||
import { DataTablePagination } from "./propertyTablePagination";
|
||||
import { rankItem } from "@tanstack/match-sorter-utils";
|
||||
|
||||
/* ----------------------------------------
|
||||
Optional fuzzy global filter
|
||||
----------------------------------------- */
|
||||
const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => {
|
||||
const itemRank = rankItem(String(row.getValue(columnId) ?? ""), value);
|
||||
addMeta?.({ itemRank });
|
||||
return itemRank.passed;
|
||||
};
|
||||
|
||||
interface DataTableProps<TData> {
|
||||
columns: ColumnDef<TData, any>[];
|
||||
data: TData[];
|
||||
}
|
||||
|
||||
export default function DataTable<TData extends Record<string, any>>({
|
||||
data,
|
||||
columns,
|
||||
}: DataTableProps<TData>) {
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [columnFilters, setColumnFilters] =
|
||||
useState<ColumnFiltersState>([]);
|
||||
const [globalFilter, setGlobalFilter] = useState("");
|
||||
const [currentPageIndex, setCurrentPageIndex] = useState(0);
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
onGlobalFilterChange: setGlobalFilter,
|
||||
|
||||
globalFilterFn: fuzzyFilter,
|
||||
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
globalFilter,
|
||||
pagination: {
|
||||
pageIndex: currentPageIndex,
|
||||
pageSize: 7,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id} className="h-14 py-2">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{table.getRowModel().rows.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<div className="mb-2">
|
||||
<DataTablePagination
|
||||
table={table}
|
||||
currentPageIndex={currentPageIndex}
|
||||
setCurrentPageIndex={setCurrentPageIndex}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,172 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
SortingState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/app/shadcn_components/ui/table";
|
||||
import { useState } from "react";
|
||||
import { DataTablePagination } from "./propertyTablePagination";
|
||||
import React from "react";
|
||||
import { Input } from "@/app/shadcn_components/ui/input";
|
||||
import { rankItem } from "@tanstack/match-sorter-utils";
|
||||
import { FilterFn } from "@tanstack/react-table";
|
||||
|
||||
// Optional: Fuzzy global filter
|
||||
const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => {
|
||||
const itemRank = rankItem(String(row.getValue(columnId) ?? ""), value);
|
||||
addMeta?.({ itemRank });
|
||||
return itemRank.passed;
|
||||
};
|
||||
|
||||
interface DataTableProps<TData> {
|
||||
columns: ColumnDef<TData, any>[];
|
||||
data: TData[];
|
||||
}
|
||||
|
||||
function fetchData<TData>(offset: number): TData[] {
|
||||
// placeholder function for fetching
|
||||
const data: TData[] = [];
|
||||
return data;
|
||||
}
|
||||
|
||||
export default function DataTable<TData extends Record<string, any>>({
|
||||
data,
|
||||
columns,
|
||||
}: DataTableProps<TData>) {
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [tableData, setTableData] = useState(() => [...data]);
|
||||
const [offset, setOffset] = useState(0);
|
||||
const [currentPageIndex, setCurrentPageIndex] = useState(0);
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
[]
|
||||
);
|
||||
const [globalFilter, setGlobalFilter] = React.useState("");
|
||||
|
||||
// add page change handlers for DataTablePagination
|
||||
const loadPaginatedData = () => {
|
||||
const newData = fetchData<TData>(offset);
|
||||
if (newData) {
|
||||
setTableData([...tableData, ...newData]);
|
||||
setOffset(offset + 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const table = useReactTable({
|
||||
data: tableData,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onSortingChange: setSorting,
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
onGlobalFilterChange: setGlobalFilter,
|
||||
globalFilterFn: fuzzyFilter,
|
||||
state: {
|
||||
sorting,
|
||||
pagination: { pageIndex: currentPageIndex, pageSize: 7 },
|
||||
columnFilters,
|
||||
globalFilter,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center py-2">
|
||||
<div className="flex items-center py-2">
|
||||
<Input
|
||||
placeholder="Search address or postcode..."
|
||||
value={globalFilter ?? ""}
|
||||
onChange={(event) => setGlobalFilter(event.target.value)}
|
||||
className="w-64"
|
||||
/>
|
||||
{globalFilter && (
|
||||
<button
|
||||
onClick={() => setGlobalFilter("")}
|
||||
className="ml-4 text-gray-500 hover:text-gray-800"
|
||||
title="Clear search"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id} className="h-14 py-2">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="mb-2">
|
||||
<DataTablePagination
|
||||
table={table}
|
||||
loadPaginatedData={loadPaginatedData}
|
||||
currentPageIndex={currentPageIndex}
|
||||
setCurrentPageIndex={setCurrentPageIndex}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
30
src/app/portfolio/[slug]/components/useProperties.ts
Normal file
30
src/app/portfolio/[slug]/components/useProperties.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { useQuery } from "@tanstack/react-query";
|
||||
import { PropertyWithRelations } from "@/app/db/schema/property";
|
||||
import { PropertyFilter } from "@/app/utils/propertyFilters";
|
||||
|
||||
interface Params {
|
||||
portfolioId: string;
|
||||
filters: PropertyFilter[];
|
||||
}
|
||||
|
||||
export function useProperties({ portfolioId, filters }: Params) {
|
||||
return useQuery<PropertyWithRelations[]>({
|
||||
queryKey: ["properties", portfolioId, filters],
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/properties", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
portfolioId,
|
||||
filters,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error("Failed to fetch properties");
|
||||
return res.json();
|
||||
},
|
||||
keepPreviousData: true,
|
||||
});
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ import {
|
|||
} from "@/app/db/schema/recommendations";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { PropertyFilter } from "@/app/utils/propertyFilters";
|
||||
import { EPC_TO_SAP_MIN, EPC_TO_SAP_MAX } from "@/app/utils/epc";
|
||||
|
||||
export interface PortfolioSettingsType {
|
||||
name: string;
|
||||
|
|
@ -424,6 +425,67 @@ export async function getProperties(
|
|||
|
||||
const whereClauses: any[] = [];
|
||||
|
||||
for (const filter of filters) {
|
||||
switch (filter.field) {
|
||||
case "address":
|
||||
if (filter.operator === "contains") {
|
||||
whereClauses.push(
|
||||
sql`p.address ILIKE ${"%" + filter.value + "%"}`
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case "postcode":
|
||||
if (filter.operator === "starts_with") {
|
||||
whereClauses.push(
|
||||
sql`p.postcode ILIKE ${filter.value + "%"}`
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case "currentEpc": {
|
||||
console.log("EPC at most", filter.value)
|
||||
const maxSap =
|
||||
EPC_TO_SAP_MAX[filter.value as keyof typeof EPC_TO_SAP_MAX];
|
||||
if (maxSap === undefined) break;
|
||||
|
||||
if (filter.operator === "epc_at_most") {
|
||||
whereClauses.push(
|
||||
sql`p.current_sap_points <= ${maxSap}`
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "expectedEpc": {
|
||||
if (filter.operator === "epc_at_least") {
|
||||
whereClauses.push(sql`
|
||||
CASE t.epc::text
|
||||
WHEN 'A' THEN 7
|
||||
WHEN 'B' THEN 6
|
||||
WHEN 'C' THEN 5
|
||||
WHEN 'D' THEN 4
|
||||
WHEN 'E' THEN 3
|
||||
WHEN 'F' THEN 2
|
||||
WHEN 'G' THEN 1
|
||||
END
|
||||
>=
|
||||
CASE ${filter.value}
|
||||
WHEN 'A' THEN 7
|
||||
WHEN 'B' THEN 6
|
||||
WHEN 'C' THEN 5
|
||||
WHEN 'D' THEN 4
|
||||
WHEN 'E' THEN 3
|
||||
WHEN 'F' THEN 2
|
||||
WHEN 'G' THEN 1
|
||||
END
|
||||
`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const combinedWhere =
|
||||
whereClauses.length > 0
|
||||
? sql`AND (${sql.join(whereClauses, sql` AND `)})`
|
||||
|
|
|
|||
25
src/app/utils/epc.ts
Normal file
25
src/app/utils/epc.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
export const EPC_TO_SAP_MIN: Record<
|
||||
"A" | "B" | "C" | "D" | "E" | "F" | "G",
|
||||
number
|
||||
> = {
|
||||
A: 92,
|
||||
B: 81,
|
||||
C: 69,
|
||||
D: 55,
|
||||
E: 39,
|
||||
F: 21,
|
||||
G: 0,
|
||||
};
|
||||
|
||||
export const EPC_TO_SAP_MAX: Record<
|
||||
"A" | "B" | "C" | "D" | "E" | "F" | "G",
|
||||
number
|
||||
> = {
|
||||
A: 100,
|
||||
B: 91,
|
||||
C: 80,
|
||||
D: 68,
|
||||
E: 54,
|
||||
F: 38,
|
||||
G: 20,
|
||||
};
|
||||
|
|
@ -1,16 +1,15 @@
|
|||
export type FilterField =
|
||||
| "address"
|
||||
| "postcode"
|
||||
| "status"
|
||||
| "currentEpcRating"
|
||||
| "currentSapPoints";
|
||||
| "currentEpc"
|
||||
| "expectedEpc";
|
||||
|
||||
export type FilterOperator =
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "equals"
|
||||
| "gte"
|
||||
| "lte";
|
||||
| "epc_at_least"
|
||||
| "epc_at_most";
|
||||
|
||||
export interface PropertyFilter {
|
||||
field: FilterField;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue