mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
Merge branch 'main' into feature/abri_table
This commit is contained in:
commit
8016cc6f26
13 changed files with 4436 additions and 158 deletions
23
package-lock.json
generated
23
package-lock.json
generated
|
|
@ -28,6 +28,7 @@
|
|||
"@radix-ui/react-toast": "^1.2.2",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@remixicon/react": "^4.2.0",
|
||||
"@tanstack/match-sorter-utils": "^8.19.4",
|
||||
"@tanstack/react-query": "^4.29.12",
|
||||
"@tanstack/react-table": "^8.9.3",
|
||||
"@tremor/react": "^3.18.7",
|
||||
|
|
@ -5549,6 +5550,22 @@
|
|||
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/match-sorter-utils": {
|
||||
"version": "8.19.4",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.19.4.tgz",
|
||||
"integrity": "sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"remove-accents": "0.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "4.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.40.0.tgz",
|
||||
|
|
@ -12985,6 +13002,12 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/remove-accents": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz",
|
||||
"integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/request-progress": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
"@radix-ui/react-toast": "^1.2.2",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@remixicon/react": "^4.2.0",
|
||||
"@tanstack/match-sorter-utils": "^8.19.4",
|
||||
"@tanstack/react-query": "^4.29.12",
|
||||
"@tanstack/react-table": "^8.9.3",
|
||||
"@tremor/react": "^3.18.7",
|
||||
|
|
|
|||
|
|
@ -8,7 +8,11 @@ import {
|
|||
HoverCardTrigger,
|
||||
} from "@/app/shadcn_components/ui/hover-card";
|
||||
|
||||
type ExtendedStatus = (typeof PortfolioStatus)[number] | "ECO4" | "GBIS";
|
||||
type ExtendedStatus =
|
||||
| (typeof PortfolioStatus)[number]
|
||||
| "ECO4"
|
||||
| "GBIS"
|
||||
| "NONE";
|
||||
|
||||
export default function StatusBadge({
|
||||
status,
|
||||
|
|
@ -18,6 +22,7 @@ export default function StatusBadge({
|
|||
isProperty?: boolean;
|
||||
}) {
|
||||
const statusConfig = statusColor[status];
|
||||
console.log("status", status, statusConfig);
|
||||
|
||||
return (
|
||||
<HoverCard>
|
||||
|
|
@ -129,4 +134,10 @@ const statusColor: {
|
|||
hoverText: "This property is funded under the GBIS scheme",
|
||||
propertyHoverText: "This property is funded under the GBIS scheme",
|
||||
},
|
||||
NONE: {
|
||||
class: "bg-gray-400 hover:bg-gray-400",
|
||||
text: "No Funding",
|
||||
hoverText: "This property has no funding scheme applied",
|
||||
propertyHoverText: "This property has no funding scheme applied",
|
||||
},
|
||||
};
|
||||
|
|
|
|||
1
src/app/db/migrations/0123_cloudy_gambit.sql
Normal file
1
src/app/db/migrations/0123_cloudy_gambit.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TYPE "public"."type" ADD VALUE 'secondary_glazing' BEFORE 'trickle_vent';
|
||||
4190
src/app/db/migrations/meta/0123_snapshot.json
Normal file
4190
src/app/db/migrations/meta/0123_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -862,6 +862,13 @@
|
|||
"when": 1761587998488,
|
||||
"tag": "0122_yielding_morlocks",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 123,
|
||||
"version": "7",
|
||||
"when": 1761660543815,
|
||||
"tag": "0123_cloudy_gambit",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -42,6 +42,7 @@ export const MaterialType: [string, ...string[]] = [
|
|||
"flat_roof_waterproofing",
|
||||
// Windows
|
||||
"windows_glazing",
|
||||
"secondary_glazing",
|
||||
// vents
|
||||
"trickle_vent",
|
||||
"door_undercut",
|
||||
|
|
|
|||
|
|
@ -257,25 +257,20 @@ export interface PropertyToRecommendation {
|
|||
sapPoints?: number | null;
|
||||
}
|
||||
|
||||
export interface PropertyWithRelations {
|
||||
status: string | null;
|
||||
id: bigint;
|
||||
portfolioId: bigint;
|
||||
creationStatus: string;
|
||||
export interface PropertyWithRelations extends Record<string, unknown> {
|
||||
id: number | string | bigint;
|
||||
portfolioId: number | string | bigint;
|
||||
address: string | null;
|
||||
postcode: string | null;
|
||||
target: { epc?: string | null; heatDemand?: number | null };
|
||||
recommendations: PropertyToRecommendation[];
|
||||
cost?: number | null;
|
||||
status: string | null;
|
||||
creationStatus: string | null;
|
||||
currentEpcRating: string | null;
|
||||
currentSapPoints: number | null;
|
||||
plans: {
|
||||
id: bigint;
|
||||
isDefault?: boolean;
|
||||
fundingPackage?: {
|
||||
scheme: string | null;
|
||||
} | null;
|
||||
}[];
|
||||
targetEpc: string | null;
|
||||
planId: number | null;
|
||||
fundingScheme: string | null;
|
||||
totalRecommendationSapPoints: number | null;
|
||||
totalRecommendationCost: number | null;
|
||||
}
|
||||
|
||||
export type NonIntrusiveSurveyNotes = InferModel<
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { cache } from "react";
|
|||
import { Inter } from "next/font/google";
|
||||
import { Toaster } from "@/app/shadcn_components/ui/toaster";
|
||||
import { SpeedInsights } from "@vercel/speed-insights/next";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
// If loading a variable font, you don't need to specify the font weight
|
||||
const inter = Inter({
|
||||
|
|
|
|||
|
|
@ -73,11 +73,14 @@ export default async function Page(props: {
|
|||
];
|
||||
}
|
||||
|
||||
// Time how long this takes
|
||||
console.time("getProperties3");
|
||||
const properties: PropertyWithRelations[] = await getProperties(
|
||||
portfolioId,
|
||||
1000,
|
||||
0
|
||||
);
|
||||
console.timeEnd("getProperties3");
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -25,6 +25,15 @@ import { DataTablePagination } from "./propertyTablePagination";
|
|||
import React from "react";
|
||||
import { Input } from "@/app/shadcn_components/ui/input";
|
||||
import { PropertyWithRelations } from "@/app/db/schema/property";
|
||||
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, TValue> {
|
||||
columns: ColumnDef<PropertyWithRelations>[];
|
||||
|
|
@ -49,6 +58,7 @@ export default function DataTable<TData, TValue>({
|
|||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
[]
|
||||
);
|
||||
const [globalFilter, setGlobalFilter] = React.useState("");
|
||||
|
||||
// add page change handlers for DataTablePagination
|
||||
const loadPaginatedData = () => {
|
||||
|
|
@ -72,24 +82,36 @@ export default function DataTable<TData, TValue>({
|
|||
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-4">
|
||||
<Input
|
||||
placeholder="Filter address"
|
||||
value={(table.getColumn("address")?.getFilterValue() as string) ?? ""}
|
||||
onChange={(event) =>
|
||||
table.getColumn("address")?.setFilterValue(event.target.value)
|
||||
}
|
||||
className="max-w-sm"
|
||||
/>
|
||||
<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>
|
||||
|
|
@ -98,7 +120,7 @@ export default function DataTable<TData, TValue>({
|
|||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id} className="h-14 py-4">
|
||||
<TableHead key={header.id} className="h-14 py-2">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
|
|
|
|||
|
|
@ -17,10 +17,8 @@ import { FunnelIcon } from "@heroicons/react/24/outline";
|
|||
import { formatNumber, getEpcColorClass, sapToEpc } from "@/app/utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { PortfolioStatus } from "@/app/db/schema/portfolio";
|
||||
import {
|
||||
PropertyToRecommendation,
|
||||
PropertyWithRelations,
|
||||
} from "@/app/db/schema/property";
|
||||
import { PropertyWithRelations } from "@/app/db/schema/property";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
interface DataTableColumnHeaderProps<TData, TValue>
|
||||
extends React.HTMLAttributes<HTMLDivElement> {
|
||||
|
|
@ -44,38 +42,65 @@ export function DataTableFilterHeader<TData, TValue>({
|
|||
column,
|
||||
title,
|
||||
className,
|
||||
}: DataTableColumnHeaderProps<TData, TValue>) {
|
||||
if (!column.getCanSort()) {
|
||||
return <div className={cn(className)}>{title}</div>;
|
||||
}
|
||||
options,
|
||||
renderOption,
|
||||
}: DataTableColumnHeaderProps<TData, TValue> & {
|
||||
options: string[];
|
||||
renderOption?: (opt: string) => React.ReactNode;
|
||||
}) {
|
||||
const currentValue = column.getFilterValue() as string | undefined;
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center space-x-2", className)}>
|
||||
<div className={cn("flex items-center space-x-1", className)}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant={currentValue ? "secondary" : "ghost"}
|
||||
size="sm"
|
||||
className="-ml-3 h-8 data-[state=open]:bg-accent"
|
||||
className={cn(
|
||||
"-ml-2 h-8",
|
||||
currentValue && "text-accent-foreground bg-accent"
|
||||
)}
|
||||
>
|
||||
<span>{title}</span>
|
||||
<FunnelIcon className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
{[...PortfolioStatus, "ECO4", "GBIS"].map((status) => (
|
||||
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
className="max-h-80 overflow-y-auto min-w-[10rem]"
|
||||
>
|
||||
{options.map((opt) => (
|
||||
<DropdownMenuItem
|
||||
key={status}
|
||||
onClick={() => {
|
||||
console.log("status filter:", status);
|
||||
column.setFilterValue(status);
|
||||
}}
|
||||
key={opt}
|
||||
onClick={() =>
|
||||
column.setFilterValue(currentValue === opt ? undefined : opt)
|
||||
}
|
||||
className={cn(
|
||||
"cursor-pointer flex items-center gap-2 px-2 py-1.5",
|
||||
currentValue === opt && "bg-accent"
|
||||
)}
|
||||
>
|
||||
{<StatusBadge status={status} isProperty={false} />}
|
||||
{renderOption ? renderOption(opt) : opt}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{currentValue && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"h-6 w-6 text-gray-500 hover:text-gray-800 transition-opacity duration-150",
|
||||
currentValue ? "opacity-100" : "opacity-0 pointer-events-none"
|
||||
)}
|
||||
onClick={() => column.setFilterValue(undefined)}
|
||||
title="Clear filter"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -83,6 +108,7 @@ export function DataTableFilterHeader<TData, TValue>({
|
|||
export const columns: ColumnDef<PropertyWithRelations>[] = [
|
||||
{
|
||||
accessorKey: "address",
|
||||
enableGlobalFilter: true,
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
|
|
@ -96,7 +122,6 @@ export const columns: ColumnDef<PropertyWithRelations>[] = [
|
|||
},
|
||||
cell: ({ row }) => {
|
||||
const address = String(row.getValue("address"));
|
||||
const postcode = String(row.original.postcode);
|
||||
const propertyId = row.original.id;
|
||||
const portfolioId = row.original.portfolioId;
|
||||
|
||||
|
|
@ -110,39 +135,84 @@ export const columns: ColumnDef<PropertyWithRelations>[] = [
|
|||
>
|
||||
{address}
|
||||
</a>
|
||||
<a className="text-sm text-gray-500">{postcode}</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "postcode",
|
||||
header: ({ column }) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
>
|
||||
Postcode
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="text-gray-700 font-medium flex justify-center">
|
||||
{row.original.postcode}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
// header: () => <div className="flex justify-center">Status</div>,
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<DataTableFilterHeader column={column} title="Status" />
|
||||
<DataTableFilterHeader
|
||||
column={column}
|
||||
title="Status"
|
||||
options={PortfolioStatus}
|
||||
renderOption={(status) => (
|
||||
<StatusBadge status={status} isProperty={false} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const status = row.getValue("status") ?? "";
|
||||
const plans = row.original.plans || [];
|
||||
// Check if any plan has an ECO4 or GBIS funding package
|
||||
const fundingScheme = plans.find((p) => {
|
||||
const scheme = p?.fundingPackage?.scheme;
|
||||
return scheme && ["ECO4", "GBIS"].includes(scheme.toUpperCase());
|
||||
})?.fundingPackage?.scheme;
|
||||
|
||||
const effectiveStatus = fundingScheme
|
||||
? fundingScheme.toUpperCase()
|
||||
: status;
|
||||
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
{effectiveStatus && (
|
||||
<StatusBadge status={String(effectiveStatus)} isProperty={true} />
|
||||
{status && <StatusBadge status={String(status)} isProperty={true} />}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "fundingScheme",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<DataTableFilterHeader
|
||||
column={column}
|
||||
title="Funding Scheme"
|
||||
options={["ECO4", "GBIS", "NONE"]}
|
||||
renderOption={(status) => (
|
||||
// handle status being null or undefined
|
||||
<StatusBadge status={status ?? "none"} isProperty={false} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
// if the funding scheme is "none" we display nothing
|
||||
const fundingScheme = row.getValue("fundingScheme") || "";
|
||||
// Check if any plan has an ECO4 or GBIS funding package
|
||||
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
{fundingScheme && fundingScheme !== "none" && (
|
||||
<StatusBadge
|
||||
status={String(fundingScheme).toUpperCase()}
|
||||
isProperty={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -163,27 +233,11 @@ export const columns: ColumnDef<PropertyWithRelations>[] = [
|
|||
accessorKey: "targetEpc",
|
||||
header: () => <div className="flex justify-center">Expected EPC</div>,
|
||||
cell: ({ row }) => {
|
||||
const recommendations = row.original.recommendations;
|
||||
const currentSapPoints = row.original.currentSapPoints || 0;
|
||||
|
||||
const currentSapPoints = row.original.currentSapPoints;
|
||||
const expectedSapPoints = row.original.totalRecommendationSapPoints || 0;
|
||||
|
||||
const expectedapPoints = recommendations.reduce(
|
||||
(acc: number, rec: PropertyToRecommendation) => {
|
||||
if (rec.sapPoints === null || rec.sapPoints === undefined) {
|
||||
return acc;
|
||||
}
|
||||
return acc + rec.sapPoints;
|
||||
},
|
||||
0
|
||||
);
|
||||
if (currentSapPoints === null || currentSapPoints === undefined) {
|
||||
return (
|
||||
<div className="text-gray-700 font-medium flex justify-center">
|
||||
{""}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const expectedEpc = sapToEpc(currentSapPoints + expectedapPoints);
|
||||
const expectedEpc = sapToEpc(currentSapPoints + expectedSapPoints);
|
||||
|
||||
return (
|
||||
<div className="text-gray-700 font-medium flex justify-center">
|
||||
|
|
@ -196,17 +250,7 @@ export const columns: ColumnDef<PropertyWithRelations>[] = [
|
|||
accessorKey: "cost",
|
||||
header: () => <div className="flex justify-center">Cost</div>,
|
||||
cell: ({ row }) => {
|
||||
const recommendations = row.original.recommendations;
|
||||
|
||||
const cost = recommendations.reduce(
|
||||
(acc: number, rec: PropertyToRecommendation) => {
|
||||
if (rec.estimatedCost === null || rec.estimatedCost === undefined) {
|
||||
return acc;
|
||||
}
|
||||
return acc + rec.estimatedCost;
|
||||
},
|
||||
0
|
||||
);
|
||||
const cost = row.original.totalRecommendationCost;
|
||||
|
||||
const creationStatus = row.original.creationStatus;
|
||||
if (creationStatus === "LOADING") {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
scenario,
|
||||
ScenarioSelect,
|
||||
} from "@/app/db/schema/recommendations";
|
||||
import { sql } from "drizzle-orm";
|
||||
|
||||
export interface PortfolioSettingsType {
|
||||
name: string;
|
||||
|
|
@ -418,77 +419,55 @@ export async function getProperties(
|
|||
offset: number = 0
|
||||
): Promise<PropertyWithRelations[]> {
|
||||
// We need to perform the query like this because the nested query is not supported in the ORM right now
|
||||
const data: PropertyWithRelations[] = await db.query.property.findMany({
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
columns: {
|
||||
id: true,
|
||||
portfolioId: true,
|
||||
address: true,
|
||||
postcode: true,
|
||||
status: true,
|
||||
creationStatus: true,
|
||||
currentEpcRating: true,
|
||||
currentSapPoints: true,
|
||||
},
|
||||
where: eq(property.portfolioId, BigInt(portfolioId)),
|
||||
with: {
|
||||
target: {
|
||||
columns: {
|
||||
epc: true,
|
||||
},
|
||||
},
|
||||
recommendations: {
|
||||
columns: {
|
||||
id: true,
|
||||
estimatedCost: true,
|
||||
sapPoints: true,
|
||||
},
|
||||
where: and(
|
||||
eq(recommendation.default, true),
|
||||
inArray(
|
||||
recommendation.id,
|
||||
db
|
||||
.select({
|
||||
recommendationId: planRecommendations.recommendationId,
|
||||
})
|
||||
.from(planRecommendations)
|
||||
.innerJoin(plan, eq(plan.id, planRecommendations.planId))
|
||||
.where(eq(plan.isDefault, true))
|
||||
)
|
||||
),
|
||||
},
|
||||
plans: {
|
||||
columns: {
|
||||
id: true,
|
||||
},
|
||||
where: eq(plan.isDefault, true),
|
||||
// Associate the funding information
|
||||
with: {
|
||||
fundingPackage: {
|
||||
columns: {
|
||||
scheme: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// override status to reflect ECO4/GBIS if present
|
||||
const updated = data.map((p) => {
|
||||
const fundingScheme = p.plans.find((pl) => {
|
||||
const scheme = pl?.fundingPackage?.scheme;
|
||||
return scheme && ["ECO4", "GBIS"].includes(scheme.toUpperCase());
|
||||
})?.fundingPackage?.scheme;
|
||||
const result =
|
||||
await db.execute<PropertyWithRelations>(sql<PropertyWithRelations>`
|
||||
SELECT
|
||||
p.id AS id,
|
||||
p.portfolio_id AS "portfolioId",
|
||||
p.address AS address,
|
||||
p.postcode AS postcode,
|
||||
p.status AS status,
|
||||
p.creation_status AS "creationStatus",
|
||||
p.current_epc_rating AS "currentEpcRating",
|
||||
p.current_sap_points AS "currentSapPoints",
|
||||
t.epc AS "targetEpc",
|
||||
pl.id AS "planId",
|
||||
fp.scheme AS "fundingScheme",
|
||||
COALESCE(SUM(r.sap_points), 0) AS "totalRecommendationSapPoints",
|
||||
COALESCE(SUM(r.estimated_cost), 0) AS "totalRecommendationCost"
|
||||
FROM property p
|
||||
LEFT JOIN property_targets t
|
||||
ON t.property_id = p.id
|
||||
LEFT JOIN plan pl
|
||||
ON pl.property_id = p.id
|
||||
AND pl.is_default = true
|
||||
LEFT JOIN funding_package fp
|
||||
ON fp.plan_id = pl.id
|
||||
LEFT JOIN plan_recommendations pr
|
||||
ON pr.plan_id = pl.id
|
||||
LEFT JOIN recommendation r
|
||||
ON r.id = pr.recommendation_id
|
||||
AND r.default = true
|
||||
WHERE p.portfolio_id = ${portfolioId}
|
||||
GROUP BY
|
||||
p.id,
|
||||
p.portfolio_id,
|
||||
p.address,
|
||||
p.postcode,
|
||||
p.status,
|
||||
p.creation_status,
|
||||
p.current_epc_rating,
|
||||
p.current_sap_points,
|
||||
t.epc,
|
||||
pl.id,
|
||||
fp.scheme
|
||||
LIMIT ${limit} OFFSET ${offset};
|
||||
`);
|
||||
|
||||
return {
|
||||
...p,
|
||||
status: fundingScheme ? fundingScheme.toUpperCase() : p.status,
|
||||
};
|
||||
});
|
||||
const data: PropertyWithRelations[] = result.rows;
|
||||
|
||||
return updated;
|
||||
return data;
|
||||
}
|
||||
|
||||
interface UnaggregatedPortfolioPlanRecommendation {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue