diff --git a/src/app/api/live-tracking/property-documents/route.ts b/src/app/api/live-tracking/property-documents/route.ts new file mode 100644 index 0000000..912dba8 --- /dev/null +++ b/src/app/api/live-tracking/property-documents/route.ts @@ -0,0 +1,56 @@ +import { NextResponse } from "next/server"; +import { eq } from "drizzle-orm"; +import { db } from "@/app/db/db"; +import { uploadedFiles } from "@/app/db/schema/uploaded_files"; + +export async function GET(req: Request) { + const { searchParams } = new URL(req.url); + const uprnParam = searchParams.get("uprn"); + const landlordPropertyIdParam = searchParams.get("landlordPropertyId"); + + if (!uprnParam && !landlordPropertyIdParam) { + return NextResponse.json( + { error: "uprn or landlordPropertyId is required" }, + { status: 400 }, + ); + } + + try { + // Prefer UPRN — it's more selective and avoids an OR full-table scan. + // Only fall back to landlordPropertyId when no UPRN is available. + const condition = uprnParam + ? eq(uploadedFiles.uprn, BigInt(uprnParam)) + : eq(uploadedFiles.landlordPropertyId, landlordPropertyIdParam!); + + const rows = await db + .select({ + id: uploadedFiles.id, + s3FileKey: uploadedFiles.s3FileKey, + s3FileBucket: uploadedFiles.s3FileBucket, + s3UploadTimestamp: uploadedFiles.s3UploadTimestamp, + fileType: uploadedFiles.fileType, + uprn: uploadedFiles.uprn, + landlordPropertyId: uploadedFiles.landlordPropertyId, + }) + .from(uploadedFiles) + .where(condition); + + const documents = rows.map((row) => ({ + id: String(row.id), + s3FileKey: row.s3FileKey, + s3FileBucket: row.s3FileBucket, + docType: row.fileType ?? "unknown", + s3UploadTimestamp: row.s3UploadTimestamp.toISOString(), + uprn: row.uprn !== null ? String(row.uprn) : null, + landlordPropertyId: row.landlordPropertyId, + })); + + return NextResponse.json(documents); + } catch (error) { + console.error("Error fetching property documents:", error); + return NextResponse.json( + { error: "Failed to fetch documents" }, + { status: 500 }, + ); + } +} diff --git a/src/app/api/organisations/route.ts b/src/app/api/organisations/route.ts new file mode 100644 index 0000000..8f2f752 --- /dev/null +++ b/src/app/api/organisations/route.ts @@ -0,0 +1,24 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getServerSession } from "next-auth"; +import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { db } from "@/app/db/db"; +import { organisation } from "@/app/db/schema/organisation"; +import { asc } from "drizzle-orm"; + +export async function GET(_req: NextRequest) { + const session = await getServerSession(AuthOptions); + if (!session?.user?.email?.endsWith("@domna.homes")) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + const rows = await db + .select({ + id: organisation.id, + name: organisation.name, + hubspotCompanyId: organisation.hubspotCompanyId, + }) + .from(organisation) + .orderBy(asc(organisation.name)); + + return NextResponse.json(rows); +} diff --git a/src/app/api/portfolio/[portfolioId]/organisation/route.ts b/src/app/api/portfolio/[portfolioId]/organisation/route.ts new file mode 100644 index 0000000..f6bf22f --- /dev/null +++ b/src/app/api/portfolio/[portfolioId]/organisation/route.ts @@ -0,0 +1,94 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getServerSession } from "next-auth"; +import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { eq } from "drizzle-orm"; +import { db } from "@/app/db/db"; +import { portfolioOrganisation } from "@/app/db/schema/portfolio_organisation"; +import { organisation } from "@/app/db/schema/organisation"; + +function isDomnaUser(email: string | null | undefined): boolean { + return !!email?.endsWith("@domna.homes"); +} + +// GET — fetch the current linked organisation for this portfolio +export async function GET( + _req: NextRequest, + { params }: { params: Promise<{ portfolioId: string }> }, +) { + const { portfolioId } = await params; + + const rows = await db + .select({ + id: organisation.id, + name: organisation.name, + hubspotCompanyId: organisation.hubspotCompanyId, + }) + .from(portfolioOrganisation) + .innerJoin(organisation, eq(portfolioOrganisation.organisationId, organisation.id)) + .where(eq(portfolioOrganisation.portfolioId, BigInt(portfolioId))) + .limit(1); + + return NextResponse.json(rows[0] ?? null); +} + +// POST — connect an organisation to this portfolio (Domna only) +export async function POST( + req: NextRequest, + { params }: { params: Promise<{ portfolioId: string }> }, +) { + const session = await getServerSession(AuthOptions); + if (!isDomnaUser(session?.user?.email)) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + const { portfolioId } = await params; + const body = await req.json(); + const { organisationId } = body as { organisationId: string }; + + if (!organisationId) { + return NextResponse.json({ error: "organisationId required" }, { status: 400 }); + } + + // Upsert: delete any existing link then insert fresh + await db + .delete(portfolioOrganisation) + .where(eq(portfolioOrganisation.portfolioId, BigInt(portfolioId))); + + await db.insert(portfolioOrganisation).values({ + portfolioId: BigInt(portfolioId), + organisationId, + }); + + // Return the newly linked org + const rows = await db + .select({ + id: organisation.id, + name: organisation.name, + hubspotCompanyId: organisation.hubspotCompanyId, + }) + .from(portfolioOrganisation) + .innerJoin(organisation, eq(portfolioOrganisation.organisationId, organisation.id)) + .where(eq(portfolioOrganisation.portfolioId, BigInt(portfolioId))) + .limit(1); + + return NextResponse.json(rows[0] ?? null); +} + +// DELETE — disconnect the organisation from this portfolio (Domna only) +export async function DELETE( + _req: NextRequest, + { params }: { params: Promise<{ portfolioId: string }> }, +) { + const session = await getServerSession(AuthOptions); + if (!isDomnaUser(session?.user?.email)) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + const { portfolioId } = await params; + + await db + .delete(portfolioOrganisation) + .where(eq(portfolioOrganisation.portfolioId, BigInt(portfolioId))); + + return NextResponse.json({ success: true }); +} diff --git a/src/app/api/portfolio/[portfolioId]/tasks/route.ts b/src/app/api/portfolio/[portfolioId]/tasks/route.ts new file mode 100644 index 0000000..5d432e1 --- /dev/null +++ b/src/app/api/portfolio/[portfolioId]/tasks/route.ts @@ -0,0 +1,53 @@ +import { db } from "@/app/db/db"; +import { tasks } from "@/app/db/schema/tasks/tasks"; +import { subTasks } from "@/app/db/schema/tasks/subtask"; +import { eq, desc, count, sql } from "drizzle-orm"; +import { NextRequest, NextResponse } from "next/server"; + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ portfolioId: string }> } +) { + try { + const { portfolioId } = await params; + const { searchParams } = new URL(request.url); + const limit = parseInt(searchParams.get("limit") || "20"); + const offset = parseInt(searchParams.get("offset") || "0"); + + const rows = await db + .select({ + id: tasks.id, + taskSource: tasks.taskSource, + jobStarted: tasks.jobStarted, + jobCompleted: tasks.jobCompleted, + status: tasks.status, + service: tasks.service, + updatedAt: tasks.updatedAt, + totalSubtasks: count(subTasks.id), + completedSubtasks: sql`count(case when lower(${subTasks.status}) in ('completed', 'complete') then 1 end)::int`, + failedSubtasks: sql`count(case when lower(${subTasks.status}) in ('failed', 'failure', 'error') then 1 end)::int`, + }) + .from(tasks) + .leftJoin(subTasks, eq(subTasks.taskId, tasks.id)) + .where(eq(tasks.sourceId, portfolioId)) + .groupBy(tasks.id) + .orderBy(desc(tasks.updatedAt)) + .limit(limit) + .offset(offset); + + const countResult = await db + .select({ count: count() }) + .from(tasks) + .where(eq(tasks.sourceId, portfolioId)); + + const total = countResult[0].count; + + return NextResponse.json({ tasks: rows, total, limit, offset }); + } catch (error) { + console.error("Error fetching portfolio tasks:", error); + return NextResponse.json( + { error: "Failed to fetch portfolio tasks" }, + { status: 500 } + ); + } +} diff --git a/src/app/api/sign-document-url/route.ts b/src/app/api/sign-document-url/route.ts new file mode 100644 index 0000000..f273e00 --- /dev/null +++ b/src/app/api/sign-document-url/route.ts @@ -0,0 +1,38 @@ +import { NextResponse } from "next/server"; +import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"; +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; + +const energyAssessmentsS3 = new S3Client({ + region: process.env.PRESIGN_AWS_REGION, + credentials: { + accessKeyId: process.env.RETROFIT_ENERGY_ASSESSMENTS_AWS_ACCESS_KEY!, + secretAccessKey: process.env.ENERGY_ASSESSMENTS_AWS_SECRET!, + }, +}); + +const retrofitDataS3 = new S3Client({ + region: process.env.RETROFIT_DATA_DEV_REGION, + credentials: { + accessKeyId: process.env.RETROFIT_DATA_DEV_ACCESS_KEY!, + secretAccessKey: process.env.RETROFIT_DATA_DEV_SECRET_KEY!, + }, +}); + +export async function POST(req: Request) { + try { + const { key, bucket } = await req.json(); + if (!key || !bucket) + return NextResponse.json({ error: "Missing key or bucket" }, { status: 400 }); + + const isEnergyAssessments = bucket === process.env.RETROFIT_ENERGY_ASSESSMENTS_BUCKET; + const s3Client = isEnergyAssessments ? energyAssessmentsS3 : retrofitDataS3; + + const command = new GetObjectCommand({ Bucket: bucket, Key: key }); + const signedUrl = await getSignedUrl(s3Client, command, { expiresIn: 1800 }); + + return NextResponse.json({ url: signedUrl }); + } catch (error) { + console.error("Error generating signed URL:", error); + return NextResponse.json({ error: "Failed to sign URL" }, { status: 500 }); + } +} diff --git a/src/app/api/tasks/[taskId]/route.ts b/src/app/api/tasks/[taskId]/route.ts new file mode 100644 index 0000000..8c43f4f --- /dev/null +++ b/src/app/api/tasks/[taskId]/route.ts @@ -0,0 +1,26 @@ +import { db } from "@/app/db/db"; +import { subTasks } from "@/app/db/schema/tasks/subtask"; +import { eq } from "drizzle-orm"; +import { NextRequest, NextResponse } from "next/server"; + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ taskId: string }> } +) { + try { + const { taskId } = await params; + const taskSubTasks = await db + .select() + .from(subTasks) + .where(eq(subTasks.taskId, taskId)) + .orderBy(subTasks.updatedAt); + + return NextResponse.json(taskSubTasks); + } catch (error) { + console.error("Error fetching subtasks:", error); + return NextResponse.json( + { error: "Failed to fetch subtasks" }, + { status: 500 } + ); + } +} diff --git a/src/app/api/tasks/route.ts b/src/app/api/tasks/route.ts new file mode 100644 index 0000000..d11a239 --- /dev/null +++ b/src/app/api/tasks/route.ts @@ -0,0 +1,37 @@ +import { db } from "@/app/db/db"; +import { tasks } from "@/app/db/schema/tasks/tasks"; +import { desc, count } from "drizzle-orm"; +import { NextRequest, NextResponse } from "next/server"; + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const limit = parseInt(searchParams.get("limit") || "20"); + const offset = parseInt(searchParams.get("offset") || "0"); + + const allTasks = await db + .select() + .from(tasks) + .orderBy(desc(tasks.updatedAt)) + .limit(limit) + .offset(offset); + + const countResult = await db + .select({ count: count() }) + .from(tasks); + const total = countResult[0].count; + + return NextResponse.json({ + tasks: allTasks, + total, + limit, + offset, + }); + } catch (error) { + console.error("Error fetching tasks:", error); + return NextResponse.json( + { error: "Failed to fetch tasks" }, + { status: 500 } + ); + } +} diff --git a/src/app/db/db.ts b/src/app/db/db.ts index fc9d118..52b10c1 100644 --- a/src/app/db/db.ts +++ b/src/app/db/db.ts @@ -10,6 +10,11 @@ import * as EnergyAssessmentsSchema from "@/app/db/schema/energy_assessments"; import * as FundingSchema from "@/app/db/schema/funding"; import * as Relations from "@/app/db/schema/relations"; import * as Users from "@/app/db/schema/users"; +import { tasks } from "@/app/db/schema/tasks/tasks"; +import { subTasks } from "@/app/db/schema/tasks/subtask"; +import * as CrmSchema from "@/app/db/schema/crm/hubspot_deal_table"; +import * as UploadedFilesSchema from "@/app/db/schema/uploaded_files"; +import * as PortfolioOrgSchema from "@/app/db/schema/portfolio_organisation"; export const pool = new Pool({ host: process.env.DB_HOST, @@ -31,6 +36,11 @@ const schema = { ...EnergyAssessmentsSchema, ...FundingSchema, ...Users, + tasks, + subTasks, + ...CrmSchema, + ...UploadedFilesSchema, + ...PortfolioOrgSchema, }; export const db = drizzle(pool, { diff --git a/src/app/db/migrations/0164_high_sumo.sql b/src/app/db/migrations/0164_high_sumo.sql new file mode 100644 index 0000000..3031759 --- /dev/null +++ b/src/app/db/migrations/0164_high_sumo.sql @@ -0,0 +1,11 @@ +CREATE TABLE "portfolio_organisation" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "portfolio_id" bigint NOT NULL, + "organisation_id" uuid NOT NULL, + "created_at" timestamp (6) with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp (6) with time zone DEFAULT now() NOT NULL, + CONSTRAINT "portfolio_organisation_portfolio_id_unique" UNIQUE("portfolio_id") +); +--> statement-breakpoint +ALTER TABLE "portfolio_organisation" ADD CONSTRAINT "portfolio_organisation_portfolio_id_portfolio_id_fk" FOREIGN KEY ("portfolio_id") REFERENCES "public"."portfolio"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "portfolio_organisation" ADD CONSTRAINT "portfolio_organisation_organisation_id_organisation_id_fk" FOREIGN KEY ("organisation_id") REFERENCES "public"."organisation"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/src/app/db/migrations/0165_small_khan.sql b/src/app/db/migrations/0165_small_khan.sql new file mode 100644 index 0000000..bef95d7 --- /dev/null +++ b/src/app/db/migrations/0165_small_khan.sql @@ -0,0 +1,3 @@ +ALTER TYPE "public"."file_source" ADD VALUE 'ecmk';--> statement-breakpoint +ALTER TYPE "public"."file_type" ADD VALUE 'ecmk_site_note';--> statement-breakpoint +ALTER TYPE "public"."file_type" ADD VALUE 'ecmk_rd_sap_site_note'; \ No newline at end of file diff --git a/src/app/db/migrations/meta/0163_snapshot.json b/src/app/db/migrations/meta/0163_snapshot.json index 7f1ac30..e54a534 100644 --- a/src/app/db/migrations/meta/0163_snapshot.json +++ b/src/app/db/migrations/meta/0163_snapshot.json @@ -48,9 +48,7 @@ "postcode_search_postcode_unique": { "name": "postcode_search_postcode_unique", "nullsNotDistinct": false, - "columns": [ - "postcode" - ] + "columns": ["postcode"] } }, "policies": {}, @@ -123,12 +121,8 @@ "name": "aspect_condition_element_id_element_id_fk", "tableFrom": "aspect_condition", "tableTo": "element", - "columnsFrom": [ - "element_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["element_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -175,12 +169,8 @@ "name": "element_survey_id_property_condition_survey_id_fk", "tableFrom": "element", "tableTo": "property_condition_survey", - "columnsFrom": [ - "survey_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["survey_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -604,12 +594,8 @@ "name": "property_status_tracker_property_id_property_id_fk", "tableFrom": "property_status_tracker", "tableTo": "property", - "columnsFrom": [ - "property_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["property_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -617,12 +603,8 @@ "name": "property_status_tracker_portfolio_id_portfolio_id_fk", "tableFrom": "property_status_tracker", "tableTo": "portfolio", - "columnsFrom": [ - "portfolio_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["portfolio_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1397,12 +1379,8 @@ "name": "energy_assessment_documents_energy_assessment_id_energy_assessments_id_fk", "tableFrom": "energy_assessment_documents", "tableTo": "energy_assessments", - "columnsFrom": [ - "energy_assessment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["energy_assessment_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -1410,12 +1388,8 @@ "name": "energy_assessment_documents_scenario_id_energy_assessment_scenarios_id_fk", "tableFrom": "energy_assessment_documents", "tableTo": "energy_assessment_scenarios", - "columnsFrom": [ - "scenario_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["scenario_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1455,12 +1429,8 @@ "name": "energy_assessment_scenarios_energy_assessment_id_energy_assessments_id_fk", "tableFrom": "energy_assessment_scenarios", "tableTo": "energy_assessments", - "columnsFrom": [ - "energy_assessment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["energy_assessment_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1585,12 +1555,8 @@ "name": "files_from_surveyor_portfolio_id_portfolio_id_fk", "tableFrom": "files_from_surveyor", "tableTo": "portfolio", - "columnsFrom": [ - "portfolio_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["portfolio_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -1598,12 +1564,8 @@ "name": "files_from_surveyor_property_id_property_id_fk", "tableFrom": "files_from_surveyor", "tableTo": "property", - "columnsFrom": [ - "property_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["property_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1681,12 +1643,8 @@ "name": "funding_package_plan_id_plan_id_fk", "tableFrom": "funding_package", "tableTo": "plan", - "columnsFrom": [ - "plan_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["plan_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1751,12 +1709,8 @@ "name": "funding_package_measures_funding_package_id_funding_package_id_fk", "tableFrom": "funding_package_measures", "tableTo": "funding_package", - "columnsFrom": [ - "funding_package_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["funding_package_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -1764,12 +1718,8 @@ "name": "funding_package_measures_material_id_material_id_fk", "tableFrom": "funding_package_measures", "tableTo": "material", - "columnsFrom": [ - "material_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["material_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1905,12 +1855,8 @@ "name": "inspections_property_id_property_id_fk", "tableFrom": "inspections", "tableTo": "property", - "columnsFrom": [ - "property_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["property_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -2407,12 +2353,8 @@ "name": "portfolioUsers_user_id_user_id_fk", "tableFrom": "portfolioUsers", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -2420,12 +2362,8 @@ "name": "portfolioUsers_portfolio_id_portfolio_id_fk", "tableFrom": "portfolioUsers", "tableTo": "portfolio", - "columnsFrom": [ - "portfolio_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["portfolio_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -2508,12 +2446,8 @@ "name": "non_intrusive_survey_notes_survey_id_non_intrusive_survey_id_fk", "tableFrom": "non_intrusive_survey_notes", "tableTo": "non_intrusive_survey", - "columnsFrom": [ - "survey_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["survey_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -2733,12 +2667,8 @@ "name": "property_portfolio_id_portfolio_id_fk", "tableFrom": "property", "tableTo": "portfolio", - "columnsFrom": [ - "portfolio_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["portfolio_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -3165,12 +3095,8 @@ "name": "property_details_epc_property_id_property_id_fk", "tableFrom": "property_details_epc", "tableTo": "property", - "columnsFrom": [ - "property_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["property_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -3178,12 +3104,8 @@ "name": "property_details_epc_portfolio_id_portfolio_id_fk", "tableFrom": "property_details_epc", "tableTo": "portfolio", - "columnsFrom": [ - "portfolio_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["portfolio_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -3381,12 +3303,8 @@ "name": "property_targets_property_id_property_id_fk", "tableFrom": "property_targets", "tableTo": "property", - "columnsFrom": [ - "property_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["property_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -3394,12 +3312,8 @@ "name": "property_targets_portfolio_id_portfolio_id_fk", "tableFrom": "property_targets", "tableTo": "portfolio", - "columnsFrom": [ - "portfolio_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["portfolio_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -3768,12 +3682,8 @@ "name": "plan_portfolio_id_portfolio_id_fk", "tableFrom": "plan", "tableTo": "portfolio", - "columnsFrom": [ - "portfolio_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["portfolio_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -3781,12 +3691,8 @@ "name": "plan_property_id_property_id_fk", "tableFrom": "plan", "tableTo": "property", - "columnsFrom": [ - "property_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["property_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -3794,12 +3700,8 @@ "name": "plan_scenario_id_scenario_id_fk", "tableFrom": "plan", "tableTo": "scenario", - "columnsFrom": [ - "scenario_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["scenario_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -3876,12 +3778,8 @@ "name": "plan_recommendations_plan_id_plan_id_fk", "tableFrom": "plan_recommendations", "tableTo": "plan", - "columnsFrom": [ - "plan_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["plan_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -3889,12 +3787,8 @@ "name": "plan_recommendations_recommendation_id_recommendation_id_fk", "tableFrom": "plan_recommendations", "tableTo": "recommendation", - "columnsFrom": [ - "recommendation_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["recommendation_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -4104,12 +3998,8 @@ "name": "recommendation_property_id_property_id_fk", "tableFrom": "recommendation", "tableTo": "property", - "columnsFrom": [ - "property_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["property_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -4197,12 +4087,8 @@ "name": "recommendation_materials_recommendation_id_recommendation_id_fk", "tableFrom": "recommendation_materials", "tableTo": "recommendation", - "columnsFrom": [ - "recommendation_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["recommendation_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4210,12 +4096,8 @@ "name": "recommendation_materials_material_id_material_id_fk", "tableFrom": "recommendation_materials", "tableTo": "material", - "columnsFrom": [ - "material_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["material_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -4481,12 +4363,8 @@ "name": "scenario_portfolio_id_portfolio_id_fk", "tableFrom": "scenario", "tableTo": "portfolio", - "columnsFrom": [ - "portfolio_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["portfolio_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -4644,12 +4522,8 @@ "name": "solar_scenario_solar_id_solar_id_fk", "tableFrom": "solar_scenario", "tableTo": "solar", - "columnsFrom": [ - "solar_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["solar_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -4728,12 +4602,8 @@ "name": "sub_task_task_id_tasks_id_fk", "tableFrom": "sub_task", "tableTo": "tasks", - "columnsFrom": [ - "task_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["task_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4859,12 +4729,8 @@ "name": "team_org_id_organisation_id_fk", "tableFrom": "team", "tableTo": "organisation", - "columnsFrom": [ - "org_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["org_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -4919,12 +4785,8 @@ "name": "team_members_user_id_user_id_fk", "tableFrom": "team_members", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -4932,12 +4794,8 @@ "name": "team_members_team_id_team_id_fk", "tableFrom": "team_members", "tableTo": "team", - "columnsFrom": [ - "team_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["team_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -4999,12 +4857,8 @@ "name": "team_portfolio_permissions_team_id_team_id_fk", "tableFrom": "team_portfolio_permissions", "tableTo": "team", - "columnsFrom": [ - "team_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["team_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -5012,12 +4866,8 @@ "name": "team_portfolio_permissions_portfolio_id_portfolio_id_fk", "tableFrom": "team_portfolio_permissions", "tableTo": "portfolio", - "columnsFrom": [ - "portfolio_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["portfolio_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -5174,12 +5024,8 @@ "name": "account_userId_user_id_fk", "tableFrom": "account", "tableTo": "user", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5187,10 +5033,7 @@ "compositePrimaryKeys": { "account_provider_providerAccountId_pk": { "name": "account_provider_providerAccountId_pk", - "columns": [ - "provider", - "providerAccountId" - ] + "columns": ["provider", "providerAccountId"] } }, "uniqueConstraints": {}, @@ -5227,12 +5070,8 @@ "name": "session_userId_user_id_fk", "tableFrom": "session", "tableTo": "user", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5324,9 +5163,7 @@ "user_email_unique": { "name": "user_email_unique", "nullsNotDistinct": false, - "columns": [ - "email" - ] + "columns": ["email"] } }, "policies": {}, @@ -5441,12 +5278,8 @@ "name": "user_profiles_user_id_user_id_fk", "tableFrom": "user_profiles", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5485,10 +5318,7 @@ "compositePrimaryKeys": { "verificationToken_identifier_token_pk": { "name": "verificationToken_identifier_token_pk", - "columns": [ - "identifier", - "token" - ] + "columns": ["identifier", "token"] } }, "uniqueConstraints": {}, @@ -5782,12 +5612,7 @@ "public.scheme": { "name": "scheme", "schema": "public", - "values": [ - "eco4", - "gbis", - "whlg", - "none" - ] + "values": ["eco4", "gbis", "whlg", "none"] }, "public.inspection_archetype_2": { "name": "inspection_archetype_2", @@ -5804,22 +5629,12 @@ "public.inspection_archetype": { "name": "inspection_archetype", "schema": "public", - "values": [ - "Bungalow", - "Flat", - "Maisonette", - "House", - "non-domestic" - ] + "values": ["Bungalow", "Flat", "Maisonette", "House", "non-domestic"] }, "public.inspection_borescoped": { "name": "inspection_borescoped", "schema": "public", - "values": [ - "yes", - "no", - "refused" - ] + "values": ["yes", "no", "refused"] }, "public.inspections_access_issues": { "name": "inspections_access_issues", @@ -5901,11 +5716,7 @@ "public.inspections_tile_hung": { "name": "inspections_tile_hung", "schema": "public", - "values": [ - "yes", - "no", - "first floor flats are tile hung" - ] + "values": ["yes", "no", "first floor flats are tile hung"] }, "public.inspections_wall_construction": { "name": "inspections_wall_construction", @@ -5941,19 +5752,12 @@ "public.cost_unit": { "name": "cost_unit", "schema": "public", - "values": [ - "gbp_sq_meter", - "gbp_per_unit", - "gbp_per_m2", - "gbp_per_m" - ] + "values": ["gbp_sq_meter", "gbp_per_unit", "gbp_per_m2", "gbp_per_m"] }, "public.depth_unit": { "name": "depth_unit", "schema": "public", - "values": [ - "mm" - ] + "values": ["mm"] }, "public.type": { "name": "type", @@ -6006,26 +5810,17 @@ "public.r_value_unit": { "name": "r_value_unit", "schema": "public", - "values": [ - "square_meter_kelvin_per_watt" - ] + "values": ["square_meter_kelvin_per_watt"] }, "public.size_unit": { "name": "size_unit", "schema": "public", - "values": [ - "kWp", - "kW", - "watt", - "storey" - ] + "values": ["kWp", "kW", "watt", "storey"] }, "public.thermal_conductivity_unit": { "name": "thermal_conductivity_unit", "schema": "public", - "values": [ - "watt_per_meter_kelvin" - ] + "values": ["watt_per_meter_kelvin"] }, "public.goal": { "name": "goal", @@ -6041,12 +5836,7 @@ "public.role": { "name": "role", "schema": "public", - "values": [ - "creator", - "admin", - "read", - "write" - ] + "values": ["creator", "admin", "read", "write"] }, "public.status": { "name": "status", @@ -6067,32 +5857,17 @@ "public.epc": { "name": "epc", "schema": "public", - "values": [ - "A", - "B", - "C", - "D", - "E", - "F", - "G" - ] + "values": ["A", "B", "C", "D", "E", "F", "G"] }, "public.creation_status": { "name": "creation_status", "schema": "public", - "values": [ - "LOADING", - "READY", - "ERROR" - ] + "values": ["LOADING", "READY", "ERROR"] }, "public.housing_type": { "name": "housing_type", "schema": "public", - "values": [ - "Private", - "Social" - ] + "values": ["Private", "Social"] }, "public.measure_type": { "name": "measure_type", @@ -6138,35 +5913,22 @@ "public.unit_quantity": { "name": "unit_quantity", "schema": "public", - "values": [ - "m2", - "part", - "kwp" - ] + "values": ["m2", "part", "kwp"] }, "public.scenario_type": { "name": "scenario_type", "schema": "public", - "values": [ - "unit", - "building" - ] + "values": ["unit", "building"] }, "public.source": { "name": "source", "schema": "public", - "values": [ - "portfolio_id" - ] + "values": ["portfolio_id"] }, "public.file_source": { "name": "file_source", "schema": "public", - "values": [ - "pas hub", - "sharepoint", - "hubspot" - ] + "values": ["pas hub", "sharepoint", "hubspot"] }, "public.file_type": { "name": "file_type", @@ -6233,4 +5995,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/src/app/db/migrations/meta/0164_snapshot.json b/src/app/db/migrations/meta/0164_snapshot.json new file mode 100644 index 0000000..392efb8 --- /dev/null +++ b/src/app/db/migrations/meta/0164_snapshot.json @@ -0,0 +1,6317 @@ +{ + "id": "bd9ab2b2-e925-4e12-9cbf-68b917607d83", + "prevId": "4d768d67-dc33-438a-b9a1-8fdb75008554", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.postcode_search": { + "name": "postcode_search", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "postcode": { + "name": "postcode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "result_data": { + "name": "result_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_updated_at": { + "name": "last_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "postcode_search_postcode_unique": { + "name": "postcode_search_postcode_unique", + "nullsNotDistinct": false, + "columns": [ + "postcode" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.aspect_condition": { + "name": "aspect_condition", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "element_id": { + "name": "element_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "aspect_type": { + "name": "aspect_type", + "type": "aspect_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "aspect_instance": { + "name": "aspect_instance", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "install_date": { + "name": "install_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "renewal_year": { + "name": "renewal_year", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "comments": { + "name": "comments", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "aspect_condition_element_id_element_id_fk": { + "name": "aspect_condition_element_id_element_id_fk", + "tableFrom": "aspect_condition", + "tableTo": "element", + "columnsFrom": [ + "element_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.element": { + "name": "element", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "survey_id": { + "name": "survey_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "element_type": { + "name": "element_type", + "type": "element_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "element_instance": { + "name": "element_instance", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "element_survey_id_property_condition_survey_id_fk": { + "name": "element_survey_id_property_condition_survey_id_fk", + "tableFrom": "element", + "tableTo": "property_condition_survey", + "columnsFrom": [ + "survey_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property_condition_survey": { + "name": "property_condition_survey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hubspot_company_data": { + "name": "hubspot_company_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_name": { + "name": "company_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hubspot_deal_data": { + "name": "hubspot_deal_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "deal_id": { + "name": "deal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dealname": { + "name": "dealname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dealstage": { + "name": "dealstage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_code": { + "name": "project_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "landlord_property_id": { + "name": "landlord_property_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "listing_id": { + "name": "listing_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uprn": { + "name": "uprn", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome_notes": { + "name": "outcome_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "major_condition_issue_description": { + "name": "major_condition_issue_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "major_condition_issue_photos": { + "name": "major_condition_issue_photos", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "major_condition_issue_evidence_s3_url": { + "name": "major_condition_issue_evidence_s3_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "coordination_status": { + "name": "coordination_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "design_status": { + "name": "design_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pashub_link": { + "name": "pashub_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sharepoint_link": { + "name": "sharepoint_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dampmould_growth": { + "name": "dampmould_growth", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pre_sap": { + "name": "pre_sap", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "coordinator": { + "name": "coordinator", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mtp_completion_date": { + "name": "mtp_completion_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "mtp_re_model_completion_date": { + "name": "mtp_re_model_completion_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "ioe_v3_completion_date": { + "name": "ioe_v3_completion_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "proposed_measures": { + "name": "proposed_measures", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_package": { + "name": "approved_package", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "designer": { + "name": "designer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "design_type": { + "name": "design_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "design_completion_date": { + "name": "design_completion_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "actual_measures_installed": { + "name": "actual_measures_installed", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installer": { + "name": "installer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installer_handover": { + "name": "installer_handover", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lodgement_status": { + "name": "lodgement_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "measures_lodgement_date": { + "name": "measures_lodgement_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "lodgement_date": { + "name": "lodgement_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "expected_commencement_date": { + "name": "expected_commencement_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "surveyor": { + "name": "surveyor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "damp_mould_and_repairs_comments": { + "name": "damp_mould_and_repairs_comments", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmed_survey_date": { + "name": "confirmed_survey_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "confirmed_survey_time": { + "name": "confirmed_survey_time", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "surveyed_date": { + "name": "surveyed_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property_status_tracker": { + "name": "property_status_tracker", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "hubspot_deal_id": { + "name": "hubspot_deal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "property_status_tracker_property_id_property_id_fk": { + "name": "property_status_tracker_property_id_property_id_fk", + "tableFrom": "property_status_tracker", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "property_status_tracker_portfolio_id_portfolio_id_fk": { + "name": "property_status_tracker_portfolio_id_portfolio_id_fk", + "tableFrom": "property_status_tracker", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.energy_assessments": { + "name": "energy_assessments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "uprn_source": { + "name": "uprn_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "property_type": { + "name": "property_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "building_reference_number": { + "name": "building_reference_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_energy_efficiency": { + "name": "current_energy_efficiency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_energy_rating": { + "name": "current_energy_rating", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address1": { + "name": "address1", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address2": { + "name": "address2", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address3": { + "name": "address3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "posttown": { + "name": "posttown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "postcode": { + "name": "postcode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "county": { + "name": "county", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "constituency": { + "name": "constituency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "constituency_label": { + "name": "constituency_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "low_energy_fixed_light_count": { + "name": "low_energy_fixed_light_count", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "construction_age_band": { + "name": "construction_age_band", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mainheat_energy_eff": { + "name": "mainheat_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "windows_env_eff": { + "name": "windows_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lighting_energy_eff": { + "name": "lighting_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_impact_potential": { + "name": "environment_impact_potential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mainheatcont_description": { + "name": "mainheatcont_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sheating_energy_eff": { + "name": "sheating_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "local_authority": { + "name": "local_authority", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "local_authority_label": { + "name": "local_authority_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fixed_lighting_outlets_count": { + "name": "fixed_lighting_outlets_count", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "energy_tariff": { + "name": "energy_tariff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mechanical_ventilation": { + "name": "mechanical_ventilation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "solar_water_heating_flag": { + "name": "solar_water_heating_flag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "co2_emissions_potential": { + "name": "co2_emissions_potential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number_heated_rooms": { + "name": "number_heated_rooms", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "floor_description": { + "name": "floor_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "energy_consumption_potential": { + "name": "energy_consumption_potential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "built_form": { + "name": "built_form", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number_open_fireplaces": { + "name": "number_open_fireplaces", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "windows_description": { + "name": "windows_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "glazed_area": { + "name": "glazed_area", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inspection_date": { + "name": "inspection_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true + }, + "mains_gas_flag": { + "name": "mains_gas_flag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "co2_emiss_curr_per_floor_area": { + "name": "co2_emiss_curr_per_floor_area", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "heat_loss_corridor": { + "name": "heat_loss_corridor", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "unheated_corridor_length": { + "name": "unheated_corridor_length", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "flat_storey_count": { + "name": "flat_storey_count", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "roof_energy_eff": { + "name": "roof_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_floor_area": { + "name": "total_floor_area", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_impact_current": { + "name": "environment_impact_current", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "roof_description": { + "name": "roof_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "floor_energy_eff": { + "name": "floor_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number_habitable_rooms": { + "name": "number_habitable_rooms", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hot_water_env_eff": { + "name": "hot_water_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mainheatc_energy_eff": { + "name": "mainheatc_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "main_fuel": { + "name": "main_fuel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lighting_env_eff": { + "name": "lighting_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "windows_energy_eff": { + "name": "windows_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "floor_env_eff": { + "name": "floor_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sheating_env_eff": { + "name": "sheating_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lighting_description": { + "name": "lighting_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "roof_env_eff": { + "name": "roof_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "walls_energy_eff": { + "name": "walls_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "photo_supply": { + "name": "photo_supply", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lighting_cost_potential": { + "name": "lighting_cost_potential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mainheat_env_eff": { + "name": "mainheat_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "multi_glaze_proportion": { + "name": "multi_glaze_proportion", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "main_heating_controls": { + "name": "main_heating_controls", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "flat_top_storey": { + "name": "flat_top_storey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secondheat_description": { + "name": "secondheat_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "walls_env_eff": { + "name": "walls_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "transaction_type": { + "name": "transaction_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "extension_count": { + "name": "extension_count", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mainheatc_env_eff": { + "name": "mainheatc_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lmk_key": { + "name": "lmk_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wind_turbine_count": { + "name": "wind_turbine_count", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenure": { + "name": "tenure", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "floor_level": { + "name": "floor_level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "potential_energy_efficiency": { + "name": "potential_energy_efficiency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "potential_energy_rating": { + "name": "potential_energy_rating", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hot_water_energy_eff": { + "name": "hot_water_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "low_energy_lighting": { + "name": "low_energy_lighting", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "walls_description": { + "name": "walls_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hotwater_description": { + "name": "hotwater_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "co2_emissions_current": { + "name": "co2_emissions_current", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "heating_cost_current": { + "name": "heating_cost_current", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "heating_cost_potential": { + "name": "heating_cost_potential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hot_water_cost_current": { + "name": "hot_water_cost_current", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hot_water_cost_potential": { + "name": "hot_water_cost_potential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lighting_cost_current": { + "name": "lighting_cost_current", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "energy_consumption_current": { + "name": "energy_consumption_current", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lodgement_date": { + "name": "lodgement_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "lodgement_datetime": { + "name": "lodgement_datetime", + "type": "timestamp (6)", + "primaryKey": false, + "notNull": true + }, + "mainheat_description": { + "name": "mainheat_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "floor_height": { + "name": "floor_height", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "glazed_type": { + "name": "glazed_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_location": { + "name": "file_location", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "surveyor_name": { + "name": "surveyor_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "surveyor_company": { + "name": "surveyor_company", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "space_heating_kwh": { + "name": "space_heating_kwh", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "water_heating_kwh": { + "name": "water_heating_kwh", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number_of_doors": { + "name": "number_of_doors", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "number_of_insulated_doors": { + "name": "number_of_insulated_doors", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "number_of_floors": { + "name": "number_of_floors", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "insulation_wall_area": { + "name": "insulation_wall_area", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "heat_loss_perimeter": { + "name": "heat_loss_perimeter", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "party_wall_length": { + "name": "party_wall_length", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "perimeter": { + "name": "perimeter", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "rooms_with_bath_and_or_shower": { + "name": "rooms_with_bath_and_or_shower", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rooms_with_mixer_shower_no_bath": { + "name": "rooms_with_mixer_shower_no_bath", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "room_with_bath_and_mixer_shower": { + "name": "room_with_bath_and_mixer_shower", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "percent_draftproofed": { + "name": "percent_draftproofed", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "has_hot_water_cylinder": { + "name": "has_hot_water_cylinder", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cylinder_insulation_type": { + "name": "cylinder_insulation_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cylinder_insulation_thickness": { + "name": "cylinder_insulation_thickness", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cylinder_thermostat": { + "name": "cylinder_thermostat", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "main_dwelling_ground_floor_area": { + "name": "main_dwelling_ground_floor_area", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "number_of_windows": { + "name": "number_of_windows", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "windows_area": { + "name": "windows_area", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.energy_assessment_documents": { + "name": "energy_assessment_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "energy_assessment_id": { + "name": "energy_assessment_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "document_type": { + "name": "document_type", + "type": "document_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "document_location": { + "name": "document_location", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "scenario_id": { + "name": "scenario_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "energy_assessment_documents_energy_assessment_id_energy_assessments_id_fk": { + "name": "energy_assessment_documents_energy_assessment_id_energy_assessments_id_fk", + "tableFrom": "energy_assessment_documents", + "tableTo": "energy_assessments", + "columnsFrom": [ + "energy_assessment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "energy_assessment_documents_scenario_id_energy_assessment_scenarios_id_fk": { + "name": "energy_assessment_documents_scenario_id_energy_assessment_scenarios_id_fk", + "tableFrom": "energy_assessment_documents", + "tableTo": "energy_assessment_scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.energy_assessment_scenarios": { + "name": "energy_assessment_scenarios", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "scenario_name": { + "name": "scenario_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "energy_assessment_id": { + "name": "energy_assessment_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "energy_assessment_scenarios_energy_assessment_id_energy_assessments_id_fk": { + "name": "energy_assessment_scenarios_energy_assessment_id_energy_assessments_id_fk", + "tableFrom": "energy_assessment_scenarios", + "tableTo": "energy_assessments", + "columnsFrom": [ + "energy_assessment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.epc_store": { + "name": "epc_store", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "epc_api_created_at": { + "name": "epc_api_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "epc_api": { + "name": "epc_api", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "epc_page_created_at": { + "name": "epc_page_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "epc_page": { + "name": "epc_page", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "epc_page_rrn": { + "name": "epc_page_rrn", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "uq_epc_store_uprn": { + "name": "uq_epc_store_uprn", + "columns": [ + { + "expression": "uprn", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.files_from_surveyor": { + "name": "files_from_surveyor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "s3_json_url": { + "name": "s3_json_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "files_from_surveyor_portfolio_id_portfolio_id_fk": { + "name": "files_from_surveyor_portfolio_id_portfolio_id_fk", + "tableFrom": "files_from_surveyor", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_from_surveyor_property_id_property_id_fk": { + "name": "files_from_surveyor_property_id_property_id_fk", + "tableFrom": "files_from_surveyor", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.funding_package": { + "name": "funding_package", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "scheme": { + "name": "scheme", + "type": "scheme", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "project_funding": { + "name": "project_funding", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "total_uplift": { + "name": "total_uplift", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "full_project_score": { + "name": "full_project_score", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "partial_project_score": { + "name": "partial_project_score", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "uplift_project_score": { + "name": "uplift_project_score", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "funding_package_plan_id_plan_id_fk": { + "name": "funding_package_plan_id_plan_id_fk", + "tableFrom": "funding_package", + "tableTo": "plan", + "columnsFrom": [ + "plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.funding_package_measures": { + "name": "funding_package_measures", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "funding_package_id": { + "name": "funding_package_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "measure": { + "name": "measure", + "type": "type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "material_id": { + "name": "material_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "innovation_uplift": { + "name": "innovation_uplift", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "partial_project_score": { + "name": "partial_project_score", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "uplift_project_score": { + "name": "uplift_project_score", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "funding_package_measures_funding_package_id_funding_package_id_fk": { + "name": "funding_package_measures_funding_package_id_funding_package_id_fk", + "tableFrom": "funding_package_measures", + "tableTo": "funding_package", + "columnsFrom": [ + "funding_package_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "funding_package_measures_material_id_material_id_fk": { + "name": "funding_package_measures_material_id_material_id_fk", + "tableFrom": "funding_package_measures", + "tableTo": "material", + "columnsFrom": [ + "material_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inspections": { + "name": "inspections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "archetype": { + "name": "archetype", + "type": "inspection_archetype", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "archetype_2": { + "name": "archetype_2", + "type": "inspection_archetype_2", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "wall_construction": { + "name": "wall_construction", + "type": "inspections_wall_construction", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "insulation": { + "name": "insulation", + "type": "inspections_wall_insulation", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "insulation_material": { + "name": "insulation_material", + "type": "inspections_insulation_material", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "borescoped": { + "name": "borescoped", + "type": "inspection_borescoped", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "roof_orientation": { + "name": "roof_orientation", + "type": "inspections_roof_orientation", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "tile_hung": { + "name": "tile_hung", + "type": "inspections_tile_hung", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "rendered": { + "name": "rendered", + "type": "inspections_rendered", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "cladding": { + "name": "cladding", + "type": "inspections_cladding", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "access_issues": { + "name": "access_issues", + "type": "inspections_access_issues", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "surveyor_name": { + "name": "surveyor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "inspections_property_id_property_id_fk": { + "name": "inspections_property_id_property_id_fk", + "tableFrom": "inspections", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.material": { + "name": "material", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "depth": { + "name": "depth", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "depth_unit": { + "name": "depth_unit", + "type": "depth_unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "cost_unit": { + "name": "cost_unit", + "type": "cost_unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "r_value_per_mm": { + "name": "r_value_per_mm", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "r_value_unit": { + "name": "r_value_unit", + "type": "r_value_unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "thermal_conductivity": { + "name": "thermal_conductivity", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "thermal_conductivity_unit": { + "name": "thermal_conductivity_unit", + "type": "thermal_conductivity_unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "link": { + "name": "link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "prime_material_cost": { + "name": "prime_material_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "material_cost": { + "name": "material_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "labour_cost": { + "name": "labour_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "labour_hours_per_unit": { + "name": "labour_hours_per_unit", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "plant_cost": { + "name": "plant_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "total_cost": { + "name": "total_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_installer_quote": { + "name": "is_installer_quote", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "innovation_rate": { + "name": "innovation_rate", + "type": "real", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "size": { + "name": "size", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "size_unit": { + "name": "size_unit", + "type": "size_unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "includes_scaffolding": { + "name": "includes_scaffolding", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "includes_battery": { + "name": "includes_battery", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "battery_size": { + "name": "battery_size", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organisation": { + "name": "organisation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "hubspot_company_id": { + "name": "hubspot_company_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.portfolio_organisation": { + "name": "portfolio_organisation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "portfolio_organisation_portfolio_id_portfolio_id_fk": { + "name": "portfolio_organisation_portfolio_id_portfolio_id_fk", + "tableFrom": "portfolio_organisation", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "portfolio_organisation_organisation_id_organisation_id_fk": { + "name": "portfolio_organisation_organisation_id_organisation_id_fk", + "tableFrom": "portfolio_organisation", + "tableTo": "organisation", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "portfolio_organisation_portfolio_id_unique": { + "name": "portfolio_organisation_portfolio_id_unique", + "nullsNotDistinct": false, + "columns": [ + "portfolio_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.portfolio": { + "name": "portfolio", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "budget": { + "name": "budget", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "goal": { + "name": "goal", + "type": "goal", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "number_of_properties": { + "name": "number_of_properties", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "co2_equivalent_savings": { + "name": "co2_equivalent_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_savings": { + "name": "energy_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_cost_savings": { + "name": "energy_cost_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "property_valuation_increase": { + "name": "property_valuation_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "rental_yield_increase": { + "name": "rental_yield_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "total_work_hours": { + "name": "total_work_hours", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "labour_days": { + "name": "labour_days", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "epc_breakdown_pre_retrofit": { + "name": "epc_breakdown_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "epc_breakdown_post_retrofit": { + "name": "epc_breakdown_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "n_units_to_retrofit": { + "name": "n_units_to_retrofit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "co2_per_unit_pre_retrofit": { + "name": "co2_per_unit_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "co2_per_unit_post_retrofit": { + "name": "co2_per_unit_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_bill_per_unit_pre_retrofit": { + "name": "energy_bill_per_unit_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_bill_per_unit_post_retrofit": { + "name": "energy_bill_per_unit_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_consumption_per_unit_pre_retrofit": { + "name": "energy_consumption_per_unit_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_consumption_per_unit_post_retrofit": { + "name": "energy_consumption_per_unit_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "valuation_improvement_per_unit": { + "name": "valuation_improvement_per_unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_per_unit": { + "name": "cost_per_unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_per_co2_saved": { + "name": "cost_per_co2_saved", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_per_sap_point": { + "name": "cost_per_sap_point", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "valuation_return_on_investment": { + "name": "valuation_return_on_investment", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.portfolioUsers": { + "name": "portfolioUsers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "portfolioUsers_user_id_user_id_fk": { + "name": "portfolioUsers_user_id_user_id_fk", + "tableFrom": "portfolioUsers", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "portfolioUsers_portfolio_id_portfolio_id_fk": { + "name": "portfolioUsers_portfolio_id_portfolio_id_fk", + "tableFrom": "portfolioUsers", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.non_intrusive_survey": { + "name": "non_intrusive_survey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "survey_date": { + "name": "survey_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "surveyor": { + "name": "surveyor", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.non_intrusive_survey_notes": { + "name": "non_intrusive_survey_notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "survey_id": { + "name": "survey_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "non_intrusive_survey_notes_survey_id_non_intrusive_survey_id_fk": { + "name": "non_intrusive_survey_notes_survey_id_non_intrusive_survey_id_fk", + "tableFrom": "non_intrusive_survey_notes", + "tableTo": "non_intrusive_survey", + "columnsFrom": [ + "survey_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property": { + "name": "property", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "creation_status": { + "name": "creation_status", + "type": "creation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "landlord_property_id": { + "name": "landlord_property_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "building_reference_number": { + "name": "building_reference_number", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postcode": { + "name": "postcode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_pre_condition_report": { + "name": "has_pre_condition_report", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "has_recommendations": { + "name": "has_recommendations", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "property_type": { + "name": "property_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "built_form": { + "name": "built_form", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_authority": { + "name": "local_authority", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "constituency": { + "name": "constituency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number_of_rooms": { + "name": "number_of_rooms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "year_built": { + "name": "year_built", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenure": { + "name": "tenure", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_epc_rating": { + "name": "current_epc_rating", + "type": "epc", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "current_sap_points": { + "name": "current_sap_points", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "current_valuation": { + "name": "current_valuation", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "installed_measures_sap_point_adjustment": { + "name": "installed_measures_sap_point_adjustment", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "is_sap_points_adjusted_for_installed_measures": { + "name": "is_sap_points_adjusted_for_installed_measures", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "original_sap_points": { + "name": "original_sap_points", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "lodged_sap_points": { + "name": "lodged_sap_points", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "lodged_epc_rating": { + "name": "lodged_epc_rating", + "type": "epc", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "uq_property_portfolio_uprn": { + "name": "uq_property_portfolio_uprn", + "columns": [ + { + "expression": "portfolio_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "uprn", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"property\".\"uprn\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "property_portfolio_id_portfolio_id_fk": { + "name": "property_portfolio_id_portfolio_id_fk", + "tableFrom": "property", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property_details_epc": { + "name": "property_details_epc", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "full_address": { + "name": "full_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lodgement_date": { + "name": "lodgement_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_expired": { + "name": "is_expired", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "total_floor_area": { + "name": "total_floor_area", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "walls": { + "name": "walls", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "walls_rating": { + "name": "walls_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "roof": { + "name": "roof", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "roof_rating": { + "name": "roof_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "floor": { + "name": "floor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "floor_rating": { + "name": "floor_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "windows": { + "name": "windows", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "windows_rating": { + "name": "windows_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "heating": { + "name": "heating", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heating_rating": { + "name": "heating_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "heating_controls": { + "name": "heating_controls", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heating_controls_rating": { + "name": "heating_controls_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "hot_water": { + "name": "hot_water", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hot_water_rating": { + "name": "hot_water_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "lighting": { + "name": "lighting", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lighting_rating": { + "name": "lighting_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "mainfuel": { + "name": "mainfuel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ventilation": { + "name": "ventilation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "solar_pv": { + "name": "solar_pv", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "solar_hot_water": { + "name": "solar_hot_water", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "wind_turbine": { + "name": "wind_turbine", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "floor_height": { + "name": "floor_height", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "number_heated_rooms": { + "name": "number_heated_rooms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "heat_loss_corridor": { + "name": "heat_loss_corridor", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "unheated_corridor_length": { + "name": "unheated_corridor_length", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "number_of_open_fireplaces": { + "name": "number_of_open_fireplaces", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "number_of_extensions": { + "name": "number_of_extensions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "number_of_storeys": { + "name": "number_of_storeys", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "mains_gas": { + "name": "mains_gas", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "energy_tariff": { + "name": "energy_tariff", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "primary_energy_consumption": { + "name": "primary_energy_consumption", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "co2_emissions": { + "name": "co2_emissions", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "current_energy_demand": { + "name": "current_energy_demand", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "current_energy_demand_heating_hotwater": { + "name": "current_energy_demand_heating_hotwater", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "estimated": { + "name": "estimated", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "sap_05_overwritten": { + "name": "sap_05_overwritten", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "sap_05_score": { + "name": "sap_05_score", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "sap_05_epc_rating": { + "name": "sap_05_epc_rating", + "type": "epc", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "heating_cost_current": { + "name": "heating_cost_current", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "hot_water_cost_current": { + "name": "hot_water_cost_current", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "lighting_cost_current": { + "name": "lighting_cost_current", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "appliances_cost_current": { + "name": "appliances_cost_current", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "gas_standing_charge": { + "name": "gas_standing_charge", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "electricity_standing_charge": { + "name": "electricity_standing_charge", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "original_co2_emissions": { + "name": "original_co2_emissions", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "original_primary_energy_consumption": { + "name": "original_primary_energy_consumption", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "original_current_energy_demand": { + "name": "original_current_energy_demand", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "original_current_energy_demand_heating_hotwater": { + "name": "original_current_energy_demand_heating_hotwater", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "installed_measures_co2_adjustment": { + "name": "installed_measures_co2_adjustment", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "installed_measures_energy_demand_adjustment": { + "name": "installed_measures_energy_demand_adjustment", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "installed_measures_total_energy_bill_adjustment": { + "name": "installed_measures_total_energy_bill_adjustment", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "installed_measures_heat_demand_adjustment": { + "name": "installed_measures_heat_demand_adjustment", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "is_epc_adjusted_for_installed_measures": { + "name": "is_epc_adjusted_for_installed_measures", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "lodged_co2_emissions": { + "name": "lodged_co2_emissions", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "lodged_heat_demand": { + "name": "lodged_heat_demand", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "has_been_remodelled": { + "name": "has_been_remodelled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": { + "uq_property_details_epc_property_portfolio": { + "name": "uq_property_details_epc_property_portfolio", + "columns": [ + { + "expression": "property_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "portfolio_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "property_details_epc_property_id_property_id_fk": { + "name": "property_details_epc_property_id_property_id_fk", + "tableFrom": "property_details_epc", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "property_details_epc_portfolio_id_portfolio_id_fk": { + "name": "property_details_epc_portfolio_id_portfolio_id_fk", + "tableFrom": "property_details_epc", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property_details_meter": { + "name": "property_details_meter", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "energy_supplier": { + "name": "energy_supplier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gas_supplier": { + "name": "gas_supplier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "meter_reading_total": { + "name": "meter_reading_total", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "meter_reading_electricity": { + "name": "meter_reading_electricity", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "meter_reading_gas": { + "name": "meter_reading_gas", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property_details_spatial": { + "name": "property_details_spatial", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "x_coordinate": { + "name": "x_coordinate", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "y_coordinate": { + "name": "y_coordinate", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "latitude": { + "name": "latitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "longitude": { + "name": "longitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "conservation_status": { + "name": "conservation_status", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "is_listed_building": { + "name": "is_listed_building", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "is_heritage_building": { + "name": "is_heritage_building", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "uq_property_details_spatial_uprn": { + "name": "uq_property_details_spatial_uprn", + "columns": [ + { + "expression": "uprn", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property_targets": { + "name": "property_targets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "epc": { + "name": "epc", + "type": "epc", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "heat_demand": { + "name": "heat_demand", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "property_targets_property_id_property_id_fk": { + "name": "property_targets_property_id_property_id_fk", + "tableFrom": "property_targets", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "property_targets_portfolio_id_portfolio_id_fk": { + "name": "property_targets_portfolio_id_portfolio_id_fk", + "tableFrom": "property_targets", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.installed_measure": { + "name": "installed_measure", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "measure_type": { + "name": "measure_type", + "type": "measure_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "installed_at": { + "name": "installed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "sap_points": { + "name": "sap_points", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "carbon_savings": { + "name": "carbon_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "kwh_savings": { + "name": "kwh_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "bill_savings": { + "name": "bill_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "heat_demand_savings": { + "name": "heat_demand_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "idx_installed_measure_uprn": { + "name": "idx_installed_measure_uprn", + "columns": [ + { + "expression": "uprn", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_installed_measure_uprn_active": { + "name": "idx_installed_measure_uprn_active", + "columns": [ + { + "expression": "uprn", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"installed_measure\".\"is_active\" = true", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_installed_measure_measure_type": { + "name": "idx_installed_measure_measure_type", + "columns": [ + { + "expression": "measure_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_installed_measure_uprn_measure": { + "name": "idx_installed_measure_uprn_measure", + "columns": [ + { + "expression": "uprn", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "measure_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"installed_measure\".\"is_active\" = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plan": { + "name": "plan", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "scenario_id": { + "name": "scenario_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "valuation_increase_lower_bound": { + "name": "valuation_increase_lower_bound", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "valuation_increase_upper_bound": { + "name": "valuation_increase_upper_bound", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "valuation_increase_average": { + "name": "valuation_increase_average", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "post_sap_points": { + "name": "post_sap_points", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "post_epc_rating": { + "name": "post_epc_rating", + "type": "epc", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "post_co2_emissions": { + "name": "post_co2_emissions", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "co2_savings": { + "name": "co2_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "post_energy_bill": { + "name": "post_energy_bill", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_bill_savings": { + "name": "energy_bill_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "post_energy_consumption": { + "name": "post_energy_consumption", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_consumption_savings": { + "name": "energy_consumption_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "valuation_post_retrofit": { + "name": "valuation_post_retrofit", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "valuation_increase": { + "name": "valuation_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "cost_of_works": { + "name": "cost_of_works", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "contingency_cost": { + "name": "contingency_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "plan_type": { + "name": "plan_type", + "type": "plan_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_plan_portfolio_scenario": { + "name": "idx_plan_portfolio_scenario", + "columns": [ + { + "expression": "portfolio_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scenario_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_plan_latest_per_property": { + "name": "idx_plan_latest_per_property", + "columns": [ + { + "expression": "portfolio_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scenario_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "property_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plan_portfolio_id_portfolio_id_fk": { + "name": "plan_portfolio_id_portfolio_id_fk", + "tableFrom": "plan", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "plan_property_id_property_id_fk": { + "name": "plan_property_id_property_id_fk", + "tableFrom": "plan", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "plan_scenario_id_scenario_id_fk": { + "name": "plan_scenario_id_scenario_id_fk", + "tableFrom": "plan", + "tableTo": "scenario", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plan_recommendations": { + "name": "plan_recommendations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "recommendation_id": { + "name": "recommendation_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_plan_recommendations_plan_id": { + "name": "idx_plan_recommendations_plan_id", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_plan_recommendations_plan_rec": { + "name": "idx_plan_recommendations_plan_rec", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recommendation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plan_recommendations_plan_id_plan_id_fk": { + "name": "plan_recommendations_plan_id_plan_id_fk", + "tableFrom": "plan_recommendations", + "tableTo": "plan", + "columnsFrom": [ + "plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "plan_recommendations_recommendation_id_recommendation_id_fk": { + "name": "plan_recommendations_recommendation_id_recommendation_id_fk", + "tableFrom": "plan_recommendations", + "tableTo": "recommendation", + "columnsFrom": [ + "recommendation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recommendation": { + "name": "recommendation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "measure_type": { + "name": "measure_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "estimated_cost": { + "name": "estimated_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "contingency_cost": { + "name": "contingency_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "default": { + "name": "default", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "starting_u_value": { + "name": "starting_u_value", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "new_u_value": { + "name": "new_u_value", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "sap_points": { + "name": "sap_points", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "heat_demand": { + "name": "heat_demand", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "kwh_savings": { + "name": "kwh_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "co2_equivalent_savings": { + "name": "co2_equivalent_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_savings": { + "name": "energy_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_cost_savings": { + "name": "energy_cost_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "property_valuation_increase": { + "name": "property_valuation_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "rental_yield_increase": { + "name": "rental_yield_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "total_work_hours": { + "name": "total_work_hours", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "labour_days": { + "name": "labour_days", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "already_installed": { + "name": "already_installed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": { + "recommendation_property_id_idx": { + "name": "recommendation_property_id_idx", + "columns": [ + { + "expression": "property_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_recommendation_active_defaults": { + "name": "idx_recommendation_active_defaults", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"recommendation\".\"default\" = true AND \"recommendation\".\"already_installed\" = false", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_recommendation_active_id_property": { + "name": "idx_recommendation_active_id_property", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "property_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"recommendation\".\"default\" = true AND \"recommendation\".\"already_installed\" = false", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recommendation_property_id_property_id_fk": { + "name": "recommendation_property_id_property_id_fk", + "tableFrom": "recommendation", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recommendation_materials": { + "name": "recommendation_materials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "recommendation_id": { + "name": "recommendation_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "material_id": { + "name": "material_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "depth": { + "name": "depth", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "quantity_unit": { + "name": "quantity_unit", + "type": "unit_quantity", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "estimated_cost": { + "name": "estimated_cost", + "type": "real", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "recommendation_materials_recommendation_id_idx": { + "name": "recommendation_materials_recommendation_id_idx", + "columns": [ + { + "expression": "recommendation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recommendation_materials_recommendation_id_recommendation_id_fk": { + "name": "recommendation_materials_recommendation_id_recommendation_id_fk", + "tableFrom": "recommendation_materials", + "tableTo": "recommendation", + "columnsFrom": [ + "recommendation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recommendation_materials_material_id_material_id_fk": { + "name": "recommendation_materials_material_id_material_id_fk", + "tableFrom": "recommendation_materials", + "tableTo": "material", + "columnsFrom": [ + "material_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scenario": { + "name": "scenario", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "budget": { + "name": "budget", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "housing_type": { + "name": "housing_type", + "type": "housing_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "goal": { + "name": "goal", + "type": "goal", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "goal_value": { + "name": "goal_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ashp_cop": { + "name": "ashp_cop", + "type": "real", + "primaryKey": false, + "notNull": false, + "default": 2.8 + }, + "trigger_file_path": { + "name": "trigger_file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "already_installed_file_path": { + "name": "already_installed_file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "patches_file_path": { + "name": "patches_file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "non_invasive_recommendations_file_path": { + "name": "non_invasive_recommendations_file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "exclusions": { + "name": "exclusions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "multi_plan": { + "name": "multi_plan", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "contingency": { + "name": "contingency", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "funding": { + "name": "funding", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "total_work_hours": { + "name": "total_work_hours", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_savings": { + "name": "energy_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "co2_equivalent_savings": { + "name": "co2_equivalent_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_cost_savings": { + "name": "energy_cost_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "property_valuation_increase": { + "name": "property_valuation_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "labour_days": { + "name": "labour_days", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "epc_breakdown_pre_retrofit": { + "name": "epc_breakdown_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "epc_breakdown_post_retrofit": { + "name": "epc_breakdown_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number_of_properties": { + "name": "number_of_properties", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "n_units_to_retrofit": { + "name": "n_units_to_retrofit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "co2_per_unit_pre_retrofit": { + "name": "co2_per_unit_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "co2_per_unit_post_retrofit": { + "name": "co2_per_unit_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_bill_per_unit_pre_retrofit": { + "name": "energy_bill_per_unit_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_bill_per_unit_post_retrofit": { + "name": "energy_bill_per_unit_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_consumption_per_unit_pre_retrofit": { + "name": "energy_consumption_per_unit_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_consumption_per_unit_post_retrofit": { + "name": "energy_consumption_per_unit_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "valuation_improvement_per_unit": { + "name": "valuation_improvement_per_unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_per_unit": { + "name": "cost_per_unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_per_co2_saved": { + "name": "cost_per_co2_saved", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_per_sap_point": { + "name": "cost_per_sap_point", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "valuation_return_on_investment": { + "name": "valuation_return_on_investment", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "scenario_portfolio_id_portfolio_id_fk": { + "name": "scenario_portfolio_id_portfolio_id_fk", + "tableFrom": "scenario", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.solar": { + "name": "solar", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "longitude": { + "name": "longitude", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "latitude": { + "name": "latitude", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "google_api_response": { + "name": "google_api_response", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.solar_scenario": { + "name": "solar_scenario", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "solar_id": { + "name": "solar_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "scenario_type": { + "name": "scenario_type", + "type": "scenario_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "number_panels": { + "name": "number_panels", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "array_kwhp": { + "name": "array_kwhp", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "lifetime_dc_kwh": { + "name": "lifetime_dc_kwh", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "yearly_dc_kwh": { + "name": "yearly_dc_kwh", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "lifetime_ac_kwh": { + "name": "lifetime_ac_kwh", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "yearly_ac_kwh": { + "name": "yearly_ac_kwh", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "expected_payback_years": { + "name": "expected_payback_years", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "panelled_roof_area": { + "name": "panelled_roof_area", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "solar_scenario_solar_id_solar_id_fk": { + "name": "solar_scenario_solar_id_solar_id_fk", + "tableFrom": "solar_scenario", + "tableTo": "solar", + "columnsFrom": [ + "solar_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sub_task": { + "name": "sub_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "task_id": { + "name": "task_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "job_started": { + "name": "job_started", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "job_completed": { + "name": "job_completed", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'In Progress'" + }, + "inputs": { + "name": "inputs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cloud_logs_url": { + "name": "cloud_logs_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sub_task_task_id_tasks_id_fk": { + "name": "sub_task_task_id_tasks_id_fk", + "tableFrom": "sub_task", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tasks": { + "name": "tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "task_source": { + "name": "task_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "job_started": { + "name": "job_started", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "job_completed": { + "name": "job_completed", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'In Progress'" + }, + "service": { + "name": "service", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "source", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team": { + "name": "team", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_org_id_organisation_id_fk": { + "name": "team_org_id_organisation_id_fk", + "tableFrom": "team", + "tableTo": "organisation", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_members": { + "name": "team_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_members_user_id_user_id_fk": { + "name": "team_members_user_id_user_id_fk", + "tableFrom": "team_members", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "team_members_team_id_team_id_fk": { + "name": "team_members_team_id_team_id_fk", + "tableFrom": "team_members", + "tableTo": "team", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_portfolio_permissions": { + "name": "team_portfolio_permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_portfolio_permissions_team_id_team_id_fk": { + "name": "team_portfolio_permissions_team_id_team_id_fk", + "tableFrom": "team_portfolio_permissions", + "tableTo": "team", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "team_portfolio_permissions_portfolio_id_portfolio_id_fk": { + "name": "team_portfolio_permissions_portfolio_id_portfolio_id_fk", + "tableFrom": "team_portfolio_permissions", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.uploaded_files": { + "name": "uploaded_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "s3_file_bucket": { + "name": "s3_file_bucket", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "s3_file_key": { + "name": "s3_file_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "s3_upload_timestamp": { + "name": "s3_upload_timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "landlord_property_id": { + "name": "landlord_property_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "hubspot_listing_id": { + "name": "hubspot_listing_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "file_type": { + "name": "file_type", + "type": "file_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "file_source": { + "name": "file_source", + "type": "file_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "name": "account_provider_providerAccountId_pk", + "columns": [ + "provider", + "providerAccountId" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "firstName": { + "name": "firstName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "oauth_id": { + "name": "oauth_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_provider": { + "name": "oauth_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "onboarded": { + "name": "onboarded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "last_login": { + "name": "last_login", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_profiles": { + "name": "user_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_type": { + "name": "user_type", + "type": "user_profiles_user_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "property_count": { + "name": "property_count", + "type": "user_profiles_property_count", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "goals": { + "name": "goals", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "referral_source": { + "name": "referral_source", + "type": "user_profiles_referral_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "nrla_membership_id": { + "name": "nrla_membership_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "accepted_privacy": { + "name": "accepted_privacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "accepted_privacy_at": { + "name": "accepted_privacy_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "marketing_opt_in": { + "name": "marketing_opt_in", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "marketing_opt_in_at": { + "name": "marketing_opt_in_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_profiles_user_id_user_id_fk": { + "name": "user_profiles_user_id_user_id_fk", + "tableFrom": "user_profiles", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verificationToken": { + "name": "verificationToken", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "name": "verificationToken_identifier_token_pk", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.whlg": { + "name": "whlg", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "postcode": { + "name": "postcode", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.aspect_type": { + "name": "aspect_type", + "schema": "public", + "values": [ + "material", + "condition", + "type", + "area", + "configuration", + "presence", + "risk", + "severity", + "location", + "finish", + "insulation", + "pointing", + "spalling", + "lintels", + "cladding", + "category", + "quantity", + "adequacy", + "rating", + "strategy", + "extent", + "distribution", + "structure", + "covering", + "fire_rating", + "external_decoration", + "work_required", + "age_band", + "construction_type", + "classification", + "system" + ] + }, + "public.element_type": { + "name": "element_type", + "schema": "public", + "values": [ + "property", + "property_construction_type", + "property_classification", + "property_age_band", + "storey_count", + "floor_level", + "floor_level_front_door", + "accessible_housing_register", + "asbestos", + "quality_standard", + "ccu", + "passenger_lift", + "stairlift", + "disabled_hoist_tracking", + "disabled_facilities", + "steps_to_front_door", + "roof", + "pitched_roof_covering", + "flat_roof_covering", + "rainwater_goods", + "loft_insulation", + "porch_canopy", + "chimney", + "fascia", + "soffit", + "fascia_soffit_bargeboards", + "gutters", + "store_roof", + "garage_roof", + "garage_and_store_roof", + "external_wall", + "external_noise_insulation", + "primary_wall", + "secondary_wall", + "downpipes", + "external_decoration", + "cladding", + "spandrel_panels", + "garage_walls", + "party_wall_fire_break", + "external_brickwork_pointing", + "internal_downpipes_external_area", + "external_windows", + "communal_windows", + "secondary_glazing", + "store_windows", + "garage_windows", + "garage_and_store_windows", + "external_door", + "front_door", + "rear_door", + "store_door", + "garage_door", + "garage_and_store_door", + "communal_entrance_door", + "main_door", + "block_entrance_door", + "lintel", + "patio_french_door", + "door_entry_handset", + "paths_and_hardstandings", + "parking_areas", + "boundary_walls", + "front_fencing", + "rear_fencing", + "side_fencing", + "rear_gate", + "front_gate", + "gates", + "retaining_walls", + "private_balcony", + "balcony_balustrade", + "outbuildings", + "garage_structure", + "paving", + "roads", + "soil_and_vent", + "solar_thermals", + "drop_kerb", + "outbuilding_overhaul", + "external_structural_defects", + "access_ramp", + "kitchen", + "kitchen_space_layout", + "tenant_installed_kitchen", + "kitchen_extractor_fan", + "bathroom", + "secondary_bathroom", + "secondary_toilet", + "bathroom_extractor_fan", + "additional_wc_or_whb", + "bathroom_remaining_life_source", + "kitchen_remaining_life_source", + "central_heating", + "heating_boiler", + "heating_distribution", + "secondary_heating", + "hot_water_system", + "cold_water_storage", + "heating_system", + "boiler_fuel", + "water_heating", + "programmable_heating", + "community_heating", + "gas_available", + "heat_recovery_units", + "heating_improvements", + "electrical_wiring", + "consumer_unit", + "smoke_detection", + "heat_detection", + "carbon_monoxide_detection", + "fire_door_rating", + "fire_risk_assessment", + "internal_wiring", + "electrics", + "communal_heating", + "communal_boiler", + "communal_electrics", + "communal_fire_alarm", + "communal_emergency_lighting", + "communal_door_entry", + "communal_cctv", + "communal_bin_store", + "communal_bin_store_doors", + "communal_bin_store_walls", + "communal_bin_store_roof", + "communal_refuse_chute", + "communal_floor_covering", + "communal_kitchen", + "communal_bathroom", + "communal_toilets", + "communal_gates", + "communal_lift", + "communal_passenger_lift", + "communal_balcony_walkway", + "communal_entrance", + "communal_internal_decorations", + "communal_internal_floor", + "communal_walkways", + "communal_external_doors", + "communal_stairs", + "communal_aerial", + "communal_aov", + "communal_internal_doors", + "communal_lateral_mains", + "communal_lighting", + "communal_lighting_conductor", + "communal_store_roof", + "communal_store_walls", + "communal_store_doors", + "communal_warden_call_system", + "communal_bms", + "communal_booster_pump", + "communal_dry_riser", + "communal_wet_riser", + "communal_cold_water_storage", + "communal_sprinkler", + "communal_plug_sockets", + "communal_circulation_space", + "ffhh_damp", + "ffhh_hold_and_cold_water", + "ffhh_drainage_lavatories", + "ffhh_neglected", + "ffhh_natural_light", + "ffhh_ventilation", + "ffhh_food_prep_and_washup", + "ffhh_unsafe_layout", + "ffhh_unstable_building", + "hhsrs_damp_and_mould", + "hhsrs_excess_cold", + "hhsrs_excess_heat", + "hhsrs_asbestos_and_mmf", + "hhsrs_biocides", + "hhsrs_carbon_monoxide", + "hhsrs_lead", + "hhsrs_radiation", + "hhsrs_uncombusted_fuel_gas", + "hhsrs_volatile_organic_compounds", + "hhsrs_crowding_and_space", + "hhsrs_entry_by_intruders", + "hhsrs_lighting", + "hhsrs_noise", + "hhsrs_domestic_hygiene_pests_refuse", + "hhsrs_food_safety", + "hhsrs_personal_hygiene_sanitation", + "hhsrs_water_supply", + "hhsrs_falls_associated_with_baths", + "hhsrs_falls_on_level_surfaces", + "hhsrs_falls_on_stairs", + "hhsrs_falls_between_levels", + "hhsrs_electrical_hazards", + "hhsrs_fire", + "hhsrs_flames_hot_surfaces", + "hhsrs_collision_and_entrapment", + "hhsrs_collision_hazards_low_headroom", + "hhsrs_explosions", + "hhsrs_ergonomics", + "hhsrs_structural_collapse", + "hhsrs_amenities" + ] + }, + "public.document_type": { + "name": "document_type", + "schema": "public", + "values": [ + "EPR", + "Condition Report", + "Evidence Report", + "Summary Information", + "Floor Plan", + "Scenario Draft EPC", + "Scenario Site Notes" + ] + }, + "public.scheme": { + "name": "scheme", + "schema": "public", + "values": [ + "eco4", + "gbis", + "whlg", + "none" + ] + }, + "public.inspection_archetype_2": { + "name": "inspection_archetype_2", + "schema": "public", + "values": [ + "detached", + "mid-terrace", + "enclosed mid-terrace", + "end-terrace", + "enclosed end-terrace", + "semi-detached" + ] + }, + "public.inspection_archetype": { + "name": "inspection_archetype", + "schema": "public", + "values": [ + "Bungalow", + "Flat", + "Maisonette", + "House", + "non-domestic" + ] + }, + "public.inspection_borescoped": { + "name": "inspection_borescoped", + "schema": "public", + "values": [ + "yes", + "no", + "refused" + ] + }, + "public.inspections_access_issues": { + "name": "inspections_access_issues", + "schema": "public", + "values": [ + "see notes", + "damp issues", + "foliage on walls", + "bushes against wall", + "trees around/anove property", + "high rise block flats/maisonettes", + "conservatory", + "lean-to", + "garage", + "extension", + "decking", + "shed against wall" + ] + }, + "public.inspections_cladding": { + "name": "inspections_cladding", + "schema": "public", + "values": [ + "none", + "cladded with “sufficient space to fill the wall”", + "cladded with “insufficient space to fill the wall”" + ] + }, + "public.inspections_insulation_material": { + "name": "inspections_insulation_material", + "schema": "public", + "values": [ + "empty 50-90", + "empty 100+", + "empty 30-40", + "empty less than 30", + "loose fibre/wool", + "eps/celo/king", + "fibre batts - with cavity", + "fibre batts - no cavity", + "loose bead", + "glued bead", + "formaldehyde", + "bubble wrap", + "poly chunks" + ] + }, + "public.inspections_rendered": { + "name": "inspections_rendered", + "schema": "public", + "values": [ + "no render", + "rendered with “insufficient” space between dpc and render", + "rendered with “sufficient” space between dpc and render" + ] + }, + "public.inspections_roof_orientation": { + "name": "inspections_roof_orientation", + "schema": "public", + "values": [ + "north", + "east", + "south", + "west", + "north-east", + "north-west", + "south-east", + "south-west", + "n/s split", + "e/w split", + "ne/sw split", + "nw/se split", + "flat roof", + "no roof", + "roof too small", + "already has solar pv" + ] + }, + "public.inspections_tile_hung": { + "name": "inspections_tile_hung", + "schema": "public", + "values": [ + "yes", + "no", + "first floor flats are tile hung" + ] + }, + "public.inspections_wall_construction": { + "name": "inspections_wall_construction", + "schema": "public", + "values": [ + "cavity", + "solid", + "system built", + "timber framed", + "steel framed", + "re-walled cavity", + "mansard pre-fab", + "mansard ewi", + "mansard re-walled" + ] + }, + "public.inspections_wall_insulation": { + "name": "inspections_wall_insulation", + "schema": "public", + "values": [ + "empty cavity", + "filled at build", + "partial", + "retro drilled", + "ewi", + "iwi", + "solid non-cavity", + "system built", + "timber framed", + "steel framed" + ] + }, + "public.cost_unit": { + "name": "cost_unit", + "schema": "public", + "values": [ + "gbp_sq_meter", + "gbp_per_unit", + "gbp_per_m2", + "gbp_per_m" + ] + }, + "public.depth_unit": { + "name": "depth_unit", + "schema": "public", + "values": [ + "mm" + ] + }, + "public.type": { + "name": "type", + "schema": "public", + "values": [ + "suspended_floor_insulation", + "solid_floor_insulation", + "external_wall_insulation", + "internal_wall_insulation", + "cavity_wall_insulation", + "mechanical_ventilation", + "loft_insulation", + "exposed_floor_insulation", + "flat_roof_insulation", + "room_roof_insulation", + "cavity_wall_extraction", + "iwi_wall_demolition", + "iwi_vapour_barrier", + "iwi_redecoration", + "suspended_floor_demolition", + "suspended_floor_redecoration", + "suspended_floor_vapour_barrier", + "solid_floor_demolition", + "solid_floor_preparation", + "solid_floor_vapour_barrier", + "solid_floor_redecoration", + "ewi_wall_demolition", + "ewi_wall_preparation", + "ewi_wall_redecoration", + "low_energy_lighting_installation", + "flat_roof_preparation", + "flat_roof_vapour_barrier", + "flat_roof_waterproofing", + "windows_glazing", + "secondary_glazing", + "double_glazing", + "trickle_vent", + "door_undercut", + "solar_pv", + "solar_battery", + "scaffolding", + "high_heat_retention_storage_heaters", + "air_source_heat_pump", + "boiler_upgrade", + "roomstat_programmer_trvs", + "time_temperature_zone_control", + "sealing_fireplace" + ] + }, + "public.r_value_unit": { + "name": "r_value_unit", + "schema": "public", + "values": [ + "square_meter_kelvin_per_watt" + ] + }, + "public.size_unit": { + "name": "size_unit", + "schema": "public", + "values": [ + "kWp", + "kW", + "watt", + "storey" + ] + }, + "public.thermal_conductivity_unit": { + "name": "thermal_conductivity_unit", + "schema": "public", + "values": [ + "watt_per_meter_kelvin" + ] + }, + "public.goal": { + "name": "goal", + "schema": "public", + "values": [ + "Valuation Improvement", + "Increasing EPC", + "Reducing CO2 emissions", + "Energy Savings", + "None" + ] + }, + "public.role": { + "name": "role", + "schema": "public", + "values": [ + "creator", + "admin", + "read", + "write" + ] + }, + "public.status": { + "name": "status", + "schema": "public", + "values": [ + "scoping", + "survey", + "assessment", + "tendering", + "project underway", + "completion; status: on track", + "completion; status: delayed", + "completion; status: at risk", + "completion; status: completed", + "needs review" + ] + }, + "public.epc": { + "name": "epc", + "schema": "public", + "values": [ + "A", + "B", + "C", + "D", + "E", + "F", + "G" + ] + }, + "public.creation_status": { + "name": "creation_status", + "schema": "public", + "values": [ + "LOADING", + "READY", + "ERROR" + ] + }, + "public.housing_type": { + "name": "housing_type", + "schema": "public", + "values": [ + "Private", + "Social" + ] + }, + "public.measure_type": { + "name": "measure_type", + "schema": "public", + "values": [ + "air_source_heat_pump", + "boiler_upgrade", + "high_heat_retention_storage_heaters", + "secondary_heating", + "roomstat_programmer_trvs", + "time_temperature_zone_control", + "cylinder_thermostat", + "cavity_wall_insulation", + "extension_cavity_wall_insulation", + "external_wall_insulation", + "internal_wall_insulation", + "loft_insulation", + "flat_roof_insulation", + "room_roof_insulation", + "solid_floor_insulation", + "suspended_floor_insulation", + "double_glazing", + "secondary_glazing", + "draught_proofing", + "mechanical_ventilation", + "low_energy_lighting", + "solar_pv", + "hot_water_tank_insulation", + "sealing_open_fireplace" + ] + }, + "public.plan_type": { + "name": "plan_type", + "schema": "public", + "values": [ + "solar_eco4", + "solar_hhrsh_eco4", + "empty_cavity_eco", + "partial_cavity_eco", + "extraction_eco" + ] + }, + "public.unit_quantity": { + "name": "unit_quantity", + "schema": "public", + "values": [ + "m2", + "part", + "kwp" + ] + }, + "public.scenario_type": { + "name": "scenario_type", + "schema": "public", + "values": [ + "unit", + "building" + ] + }, + "public.source": { + "name": "source", + "schema": "public", + "values": [ + "portfolio_id" + ] + }, + "public.file_source": { + "name": "file_source", + "schema": "public", + "values": [ + "pas hub", + "sharepoint", + "hubspot" + ] + }, + "public.file_type": { + "name": "file_type", + "schema": "public", + "values": [ + "photo_pack", + "site_note", + "rd_sap_site_note", + "pas_2023_ventilation", + "pas_2023_condition", + "pas_significance", + "par_photo_pack", + "pas_2023_property", + "pas_2023_occupancy" + ] + }, + "public.user_profiles_property_count": { + "name": "user_profiles_property_count", + "schema": "public", + "values": [ + "1", + "2–5", + "6–20", + "21+", + "1–50", + "51–100", + "101–300", + "301–1000", + "1000+" + ] + }, + "public.user_profiles_referral_source": { + "name": "user_profiles_referral_source", + "schema": "public", + "values": [ + "search", + "social_media", + "NRLA", + "partner", + "word_of_mouth", + "other" + ] + }, + "public.user_profiles_user_type": { + "name": "user_profiles_user_type", + "schema": "public", + "values": [ + "private_landlord", + "private_tenant", + "social_landlord", + "social_tenant", + "homeowner", + "other" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/src/app/db/migrations/meta/0165_snapshot.json b/src/app/db/migrations/meta/0165_snapshot.json new file mode 100644 index 0000000..62e818c --- /dev/null +++ b/src/app/db/migrations/meta/0165_snapshot.json @@ -0,0 +1,6320 @@ +{ + "id": "9b3c34c0-f8c9-4c83-8318-16bdb2ab50f6", + "prevId": "bd9ab2b2-e925-4e12-9cbf-68b917607d83", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.postcode_search": { + "name": "postcode_search", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "postcode": { + "name": "postcode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "result_data": { + "name": "result_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_updated_at": { + "name": "last_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "postcode_search_postcode_unique": { + "name": "postcode_search_postcode_unique", + "nullsNotDistinct": false, + "columns": [ + "postcode" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.aspect_condition": { + "name": "aspect_condition", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "element_id": { + "name": "element_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "aspect_type": { + "name": "aspect_type", + "type": "aspect_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "aspect_instance": { + "name": "aspect_instance", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "install_date": { + "name": "install_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "renewal_year": { + "name": "renewal_year", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "comments": { + "name": "comments", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "aspect_condition_element_id_element_id_fk": { + "name": "aspect_condition_element_id_element_id_fk", + "tableFrom": "aspect_condition", + "tableTo": "element", + "columnsFrom": [ + "element_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.element": { + "name": "element", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "survey_id": { + "name": "survey_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "element_type": { + "name": "element_type", + "type": "element_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "element_instance": { + "name": "element_instance", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "element_survey_id_property_condition_survey_id_fk": { + "name": "element_survey_id_property_condition_survey_id_fk", + "tableFrom": "element", + "tableTo": "property_condition_survey", + "columnsFrom": [ + "survey_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property_condition_survey": { + "name": "property_condition_survey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hubspot_company_data": { + "name": "hubspot_company_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_name": { + "name": "company_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hubspot_deal_data": { + "name": "hubspot_deal_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "deal_id": { + "name": "deal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dealname": { + "name": "dealname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dealstage": { + "name": "dealstage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_code": { + "name": "project_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "landlord_property_id": { + "name": "landlord_property_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "listing_id": { + "name": "listing_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uprn": { + "name": "uprn", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome_notes": { + "name": "outcome_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "major_condition_issue_description": { + "name": "major_condition_issue_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "major_condition_issue_photos": { + "name": "major_condition_issue_photos", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "major_condition_issue_evidence_s3_url": { + "name": "major_condition_issue_evidence_s3_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "coordination_status": { + "name": "coordination_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "design_status": { + "name": "design_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pashub_link": { + "name": "pashub_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sharepoint_link": { + "name": "sharepoint_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dampmould_growth": { + "name": "dampmould_growth", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pre_sap": { + "name": "pre_sap", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "coordinator": { + "name": "coordinator", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mtp_completion_date": { + "name": "mtp_completion_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "mtp_re_model_completion_date": { + "name": "mtp_re_model_completion_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "ioe_v3_completion_date": { + "name": "ioe_v3_completion_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "proposed_measures": { + "name": "proposed_measures", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_package": { + "name": "approved_package", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "designer": { + "name": "designer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "design_type": { + "name": "design_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "design_completion_date": { + "name": "design_completion_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "actual_measures_installed": { + "name": "actual_measures_installed", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installer": { + "name": "installer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installer_handover": { + "name": "installer_handover", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lodgement_status": { + "name": "lodgement_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "measures_lodgement_date": { + "name": "measures_lodgement_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "lodgement_date": { + "name": "lodgement_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "expected_commencement_date": { + "name": "expected_commencement_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "surveyor": { + "name": "surveyor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "damp_mould_and_repairs_comments": { + "name": "damp_mould_and_repairs_comments", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmed_survey_date": { + "name": "confirmed_survey_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "confirmed_survey_time": { + "name": "confirmed_survey_time", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "surveyed_date": { + "name": "surveyed_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property_status_tracker": { + "name": "property_status_tracker", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "hubspot_deal_id": { + "name": "hubspot_deal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "property_status_tracker_property_id_property_id_fk": { + "name": "property_status_tracker_property_id_property_id_fk", + "tableFrom": "property_status_tracker", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "property_status_tracker_portfolio_id_portfolio_id_fk": { + "name": "property_status_tracker_portfolio_id_portfolio_id_fk", + "tableFrom": "property_status_tracker", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.energy_assessments": { + "name": "energy_assessments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "uprn_source": { + "name": "uprn_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "property_type": { + "name": "property_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "building_reference_number": { + "name": "building_reference_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_energy_efficiency": { + "name": "current_energy_efficiency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_energy_rating": { + "name": "current_energy_rating", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address1": { + "name": "address1", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address2": { + "name": "address2", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address3": { + "name": "address3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "posttown": { + "name": "posttown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "postcode": { + "name": "postcode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "county": { + "name": "county", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "constituency": { + "name": "constituency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "constituency_label": { + "name": "constituency_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "low_energy_fixed_light_count": { + "name": "low_energy_fixed_light_count", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "construction_age_band": { + "name": "construction_age_band", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mainheat_energy_eff": { + "name": "mainheat_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "windows_env_eff": { + "name": "windows_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lighting_energy_eff": { + "name": "lighting_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_impact_potential": { + "name": "environment_impact_potential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mainheatcont_description": { + "name": "mainheatcont_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sheating_energy_eff": { + "name": "sheating_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "local_authority": { + "name": "local_authority", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "local_authority_label": { + "name": "local_authority_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fixed_lighting_outlets_count": { + "name": "fixed_lighting_outlets_count", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "energy_tariff": { + "name": "energy_tariff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mechanical_ventilation": { + "name": "mechanical_ventilation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "solar_water_heating_flag": { + "name": "solar_water_heating_flag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "co2_emissions_potential": { + "name": "co2_emissions_potential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number_heated_rooms": { + "name": "number_heated_rooms", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "floor_description": { + "name": "floor_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "energy_consumption_potential": { + "name": "energy_consumption_potential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "built_form": { + "name": "built_form", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number_open_fireplaces": { + "name": "number_open_fireplaces", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "windows_description": { + "name": "windows_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "glazed_area": { + "name": "glazed_area", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inspection_date": { + "name": "inspection_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true + }, + "mains_gas_flag": { + "name": "mains_gas_flag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "co2_emiss_curr_per_floor_area": { + "name": "co2_emiss_curr_per_floor_area", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "heat_loss_corridor": { + "name": "heat_loss_corridor", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "unheated_corridor_length": { + "name": "unheated_corridor_length", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "flat_storey_count": { + "name": "flat_storey_count", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "roof_energy_eff": { + "name": "roof_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_floor_area": { + "name": "total_floor_area", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_impact_current": { + "name": "environment_impact_current", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "roof_description": { + "name": "roof_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "floor_energy_eff": { + "name": "floor_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number_habitable_rooms": { + "name": "number_habitable_rooms", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hot_water_env_eff": { + "name": "hot_water_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mainheatc_energy_eff": { + "name": "mainheatc_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "main_fuel": { + "name": "main_fuel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lighting_env_eff": { + "name": "lighting_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "windows_energy_eff": { + "name": "windows_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "floor_env_eff": { + "name": "floor_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sheating_env_eff": { + "name": "sheating_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lighting_description": { + "name": "lighting_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "roof_env_eff": { + "name": "roof_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "walls_energy_eff": { + "name": "walls_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "photo_supply": { + "name": "photo_supply", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lighting_cost_potential": { + "name": "lighting_cost_potential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mainheat_env_eff": { + "name": "mainheat_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "multi_glaze_proportion": { + "name": "multi_glaze_proportion", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "main_heating_controls": { + "name": "main_heating_controls", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "flat_top_storey": { + "name": "flat_top_storey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secondheat_description": { + "name": "secondheat_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "walls_env_eff": { + "name": "walls_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "transaction_type": { + "name": "transaction_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "extension_count": { + "name": "extension_count", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mainheatc_env_eff": { + "name": "mainheatc_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lmk_key": { + "name": "lmk_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wind_turbine_count": { + "name": "wind_turbine_count", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenure": { + "name": "tenure", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "floor_level": { + "name": "floor_level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "potential_energy_efficiency": { + "name": "potential_energy_efficiency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "potential_energy_rating": { + "name": "potential_energy_rating", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hot_water_energy_eff": { + "name": "hot_water_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "low_energy_lighting": { + "name": "low_energy_lighting", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "walls_description": { + "name": "walls_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hotwater_description": { + "name": "hotwater_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "co2_emissions_current": { + "name": "co2_emissions_current", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "heating_cost_current": { + "name": "heating_cost_current", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "heating_cost_potential": { + "name": "heating_cost_potential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hot_water_cost_current": { + "name": "hot_water_cost_current", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hot_water_cost_potential": { + "name": "hot_water_cost_potential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lighting_cost_current": { + "name": "lighting_cost_current", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "energy_consumption_current": { + "name": "energy_consumption_current", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lodgement_date": { + "name": "lodgement_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "lodgement_datetime": { + "name": "lodgement_datetime", + "type": "timestamp (6)", + "primaryKey": false, + "notNull": true + }, + "mainheat_description": { + "name": "mainheat_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "floor_height": { + "name": "floor_height", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "glazed_type": { + "name": "glazed_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_location": { + "name": "file_location", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "surveyor_name": { + "name": "surveyor_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "surveyor_company": { + "name": "surveyor_company", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "space_heating_kwh": { + "name": "space_heating_kwh", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "water_heating_kwh": { + "name": "water_heating_kwh", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number_of_doors": { + "name": "number_of_doors", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "number_of_insulated_doors": { + "name": "number_of_insulated_doors", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "number_of_floors": { + "name": "number_of_floors", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "insulation_wall_area": { + "name": "insulation_wall_area", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "heat_loss_perimeter": { + "name": "heat_loss_perimeter", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "party_wall_length": { + "name": "party_wall_length", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "perimeter": { + "name": "perimeter", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "rooms_with_bath_and_or_shower": { + "name": "rooms_with_bath_and_or_shower", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rooms_with_mixer_shower_no_bath": { + "name": "rooms_with_mixer_shower_no_bath", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "room_with_bath_and_mixer_shower": { + "name": "room_with_bath_and_mixer_shower", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "percent_draftproofed": { + "name": "percent_draftproofed", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "has_hot_water_cylinder": { + "name": "has_hot_water_cylinder", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cylinder_insulation_type": { + "name": "cylinder_insulation_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cylinder_insulation_thickness": { + "name": "cylinder_insulation_thickness", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cylinder_thermostat": { + "name": "cylinder_thermostat", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "main_dwelling_ground_floor_area": { + "name": "main_dwelling_ground_floor_area", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "number_of_windows": { + "name": "number_of_windows", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "windows_area": { + "name": "windows_area", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.energy_assessment_documents": { + "name": "energy_assessment_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "energy_assessment_id": { + "name": "energy_assessment_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "document_type": { + "name": "document_type", + "type": "document_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "document_location": { + "name": "document_location", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "scenario_id": { + "name": "scenario_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "energy_assessment_documents_energy_assessment_id_energy_assessments_id_fk": { + "name": "energy_assessment_documents_energy_assessment_id_energy_assessments_id_fk", + "tableFrom": "energy_assessment_documents", + "tableTo": "energy_assessments", + "columnsFrom": [ + "energy_assessment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "energy_assessment_documents_scenario_id_energy_assessment_scenarios_id_fk": { + "name": "energy_assessment_documents_scenario_id_energy_assessment_scenarios_id_fk", + "tableFrom": "energy_assessment_documents", + "tableTo": "energy_assessment_scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.energy_assessment_scenarios": { + "name": "energy_assessment_scenarios", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "scenario_name": { + "name": "scenario_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "energy_assessment_id": { + "name": "energy_assessment_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "energy_assessment_scenarios_energy_assessment_id_energy_assessments_id_fk": { + "name": "energy_assessment_scenarios_energy_assessment_id_energy_assessments_id_fk", + "tableFrom": "energy_assessment_scenarios", + "tableTo": "energy_assessments", + "columnsFrom": [ + "energy_assessment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.epc_store": { + "name": "epc_store", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "epc_api_created_at": { + "name": "epc_api_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "epc_api": { + "name": "epc_api", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "epc_page_created_at": { + "name": "epc_page_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "epc_page": { + "name": "epc_page", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "epc_page_rrn": { + "name": "epc_page_rrn", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "uq_epc_store_uprn": { + "name": "uq_epc_store_uprn", + "columns": [ + { + "expression": "uprn", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.files_from_surveyor": { + "name": "files_from_surveyor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "s3_json_url": { + "name": "s3_json_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "files_from_surveyor_portfolio_id_portfolio_id_fk": { + "name": "files_from_surveyor_portfolio_id_portfolio_id_fk", + "tableFrom": "files_from_surveyor", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_from_surveyor_property_id_property_id_fk": { + "name": "files_from_surveyor_property_id_property_id_fk", + "tableFrom": "files_from_surveyor", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.funding_package": { + "name": "funding_package", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "scheme": { + "name": "scheme", + "type": "scheme", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "project_funding": { + "name": "project_funding", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "total_uplift": { + "name": "total_uplift", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "full_project_score": { + "name": "full_project_score", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "partial_project_score": { + "name": "partial_project_score", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "uplift_project_score": { + "name": "uplift_project_score", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "funding_package_plan_id_plan_id_fk": { + "name": "funding_package_plan_id_plan_id_fk", + "tableFrom": "funding_package", + "tableTo": "plan", + "columnsFrom": [ + "plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.funding_package_measures": { + "name": "funding_package_measures", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "funding_package_id": { + "name": "funding_package_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "measure": { + "name": "measure", + "type": "type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "material_id": { + "name": "material_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "innovation_uplift": { + "name": "innovation_uplift", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "partial_project_score": { + "name": "partial_project_score", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "uplift_project_score": { + "name": "uplift_project_score", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "funding_package_measures_funding_package_id_funding_package_id_fk": { + "name": "funding_package_measures_funding_package_id_funding_package_id_fk", + "tableFrom": "funding_package_measures", + "tableTo": "funding_package", + "columnsFrom": [ + "funding_package_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "funding_package_measures_material_id_material_id_fk": { + "name": "funding_package_measures_material_id_material_id_fk", + "tableFrom": "funding_package_measures", + "tableTo": "material", + "columnsFrom": [ + "material_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inspections": { + "name": "inspections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "archetype": { + "name": "archetype", + "type": "inspection_archetype", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "archetype_2": { + "name": "archetype_2", + "type": "inspection_archetype_2", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "wall_construction": { + "name": "wall_construction", + "type": "inspections_wall_construction", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "insulation": { + "name": "insulation", + "type": "inspections_wall_insulation", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "insulation_material": { + "name": "insulation_material", + "type": "inspections_insulation_material", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "borescoped": { + "name": "borescoped", + "type": "inspection_borescoped", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "roof_orientation": { + "name": "roof_orientation", + "type": "inspections_roof_orientation", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "tile_hung": { + "name": "tile_hung", + "type": "inspections_tile_hung", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "rendered": { + "name": "rendered", + "type": "inspections_rendered", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "cladding": { + "name": "cladding", + "type": "inspections_cladding", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "access_issues": { + "name": "access_issues", + "type": "inspections_access_issues", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "surveyor_name": { + "name": "surveyor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "inspections_property_id_property_id_fk": { + "name": "inspections_property_id_property_id_fk", + "tableFrom": "inspections", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.material": { + "name": "material", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "depth": { + "name": "depth", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "depth_unit": { + "name": "depth_unit", + "type": "depth_unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "cost_unit": { + "name": "cost_unit", + "type": "cost_unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "r_value_per_mm": { + "name": "r_value_per_mm", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "r_value_unit": { + "name": "r_value_unit", + "type": "r_value_unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "thermal_conductivity": { + "name": "thermal_conductivity", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "thermal_conductivity_unit": { + "name": "thermal_conductivity_unit", + "type": "thermal_conductivity_unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "link": { + "name": "link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "prime_material_cost": { + "name": "prime_material_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "material_cost": { + "name": "material_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "labour_cost": { + "name": "labour_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "labour_hours_per_unit": { + "name": "labour_hours_per_unit", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "plant_cost": { + "name": "plant_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "total_cost": { + "name": "total_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_installer_quote": { + "name": "is_installer_quote", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "innovation_rate": { + "name": "innovation_rate", + "type": "real", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "size": { + "name": "size", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "size_unit": { + "name": "size_unit", + "type": "size_unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "includes_scaffolding": { + "name": "includes_scaffolding", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "includes_battery": { + "name": "includes_battery", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "battery_size": { + "name": "battery_size", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organisation": { + "name": "organisation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "hubspot_company_id": { + "name": "hubspot_company_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.portfolio_organisation": { + "name": "portfolio_organisation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "portfolio_organisation_portfolio_id_portfolio_id_fk": { + "name": "portfolio_organisation_portfolio_id_portfolio_id_fk", + "tableFrom": "portfolio_organisation", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "portfolio_organisation_organisation_id_organisation_id_fk": { + "name": "portfolio_organisation_organisation_id_organisation_id_fk", + "tableFrom": "portfolio_organisation", + "tableTo": "organisation", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "portfolio_organisation_portfolio_id_unique": { + "name": "portfolio_organisation_portfolio_id_unique", + "nullsNotDistinct": false, + "columns": [ + "portfolio_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.portfolio": { + "name": "portfolio", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "budget": { + "name": "budget", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "goal": { + "name": "goal", + "type": "goal", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "number_of_properties": { + "name": "number_of_properties", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "co2_equivalent_savings": { + "name": "co2_equivalent_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_savings": { + "name": "energy_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_cost_savings": { + "name": "energy_cost_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "property_valuation_increase": { + "name": "property_valuation_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "rental_yield_increase": { + "name": "rental_yield_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "total_work_hours": { + "name": "total_work_hours", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "labour_days": { + "name": "labour_days", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "epc_breakdown_pre_retrofit": { + "name": "epc_breakdown_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "epc_breakdown_post_retrofit": { + "name": "epc_breakdown_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "n_units_to_retrofit": { + "name": "n_units_to_retrofit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "co2_per_unit_pre_retrofit": { + "name": "co2_per_unit_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "co2_per_unit_post_retrofit": { + "name": "co2_per_unit_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_bill_per_unit_pre_retrofit": { + "name": "energy_bill_per_unit_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_bill_per_unit_post_retrofit": { + "name": "energy_bill_per_unit_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_consumption_per_unit_pre_retrofit": { + "name": "energy_consumption_per_unit_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_consumption_per_unit_post_retrofit": { + "name": "energy_consumption_per_unit_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "valuation_improvement_per_unit": { + "name": "valuation_improvement_per_unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_per_unit": { + "name": "cost_per_unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_per_co2_saved": { + "name": "cost_per_co2_saved", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_per_sap_point": { + "name": "cost_per_sap_point", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "valuation_return_on_investment": { + "name": "valuation_return_on_investment", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.portfolioUsers": { + "name": "portfolioUsers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "portfolioUsers_user_id_user_id_fk": { + "name": "portfolioUsers_user_id_user_id_fk", + "tableFrom": "portfolioUsers", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "portfolioUsers_portfolio_id_portfolio_id_fk": { + "name": "portfolioUsers_portfolio_id_portfolio_id_fk", + "tableFrom": "portfolioUsers", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.non_intrusive_survey": { + "name": "non_intrusive_survey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "survey_date": { + "name": "survey_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "surveyor": { + "name": "surveyor", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.non_intrusive_survey_notes": { + "name": "non_intrusive_survey_notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "survey_id": { + "name": "survey_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "non_intrusive_survey_notes_survey_id_non_intrusive_survey_id_fk": { + "name": "non_intrusive_survey_notes_survey_id_non_intrusive_survey_id_fk", + "tableFrom": "non_intrusive_survey_notes", + "tableTo": "non_intrusive_survey", + "columnsFrom": [ + "survey_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property": { + "name": "property", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "creation_status": { + "name": "creation_status", + "type": "creation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "landlord_property_id": { + "name": "landlord_property_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "building_reference_number": { + "name": "building_reference_number", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postcode": { + "name": "postcode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_pre_condition_report": { + "name": "has_pre_condition_report", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "has_recommendations": { + "name": "has_recommendations", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "property_type": { + "name": "property_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "built_form": { + "name": "built_form", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_authority": { + "name": "local_authority", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "constituency": { + "name": "constituency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number_of_rooms": { + "name": "number_of_rooms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "year_built": { + "name": "year_built", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenure": { + "name": "tenure", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_epc_rating": { + "name": "current_epc_rating", + "type": "epc", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "current_sap_points": { + "name": "current_sap_points", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "current_valuation": { + "name": "current_valuation", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "installed_measures_sap_point_adjustment": { + "name": "installed_measures_sap_point_adjustment", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "is_sap_points_adjusted_for_installed_measures": { + "name": "is_sap_points_adjusted_for_installed_measures", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "original_sap_points": { + "name": "original_sap_points", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "lodged_sap_points": { + "name": "lodged_sap_points", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "lodged_epc_rating": { + "name": "lodged_epc_rating", + "type": "epc", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "uq_property_portfolio_uprn": { + "name": "uq_property_portfolio_uprn", + "columns": [ + { + "expression": "portfolio_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "uprn", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"property\".\"uprn\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "property_portfolio_id_portfolio_id_fk": { + "name": "property_portfolio_id_portfolio_id_fk", + "tableFrom": "property", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property_details_epc": { + "name": "property_details_epc", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "full_address": { + "name": "full_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lodgement_date": { + "name": "lodgement_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_expired": { + "name": "is_expired", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "total_floor_area": { + "name": "total_floor_area", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "walls": { + "name": "walls", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "walls_rating": { + "name": "walls_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "roof": { + "name": "roof", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "roof_rating": { + "name": "roof_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "floor": { + "name": "floor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "floor_rating": { + "name": "floor_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "windows": { + "name": "windows", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "windows_rating": { + "name": "windows_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "heating": { + "name": "heating", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heating_rating": { + "name": "heating_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "heating_controls": { + "name": "heating_controls", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heating_controls_rating": { + "name": "heating_controls_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "hot_water": { + "name": "hot_water", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hot_water_rating": { + "name": "hot_water_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "lighting": { + "name": "lighting", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lighting_rating": { + "name": "lighting_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "mainfuel": { + "name": "mainfuel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ventilation": { + "name": "ventilation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "solar_pv": { + "name": "solar_pv", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "solar_hot_water": { + "name": "solar_hot_water", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "wind_turbine": { + "name": "wind_turbine", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "floor_height": { + "name": "floor_height", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "number_heated_rooms": { + "name": "number_heated_rooms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "heat_loss_corridor": { + "name": "heat_loss_corridor", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "unheated_corridor_length": { + "name": "unheated_corridor_length", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "number_of_open_fireplaces": { + "name": "number_of_open_fireplaces", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "number_of_extensions": { + "name": "number_of_extensions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "number_of_storeys": { + "name": "number_of_storeys", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "mains_gas": { + "name": "mains_gas", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "energy_tariff": { + "name": "energy_tariff", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "primary_energy_consumption": { + "name": "primary_energy_consumption", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "co2_emissions": { + "name": "co2_emissions", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "current_energy_demand": { + "name": "current_energy_demand", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "current_energy_demand_heating_hotwater": { + "name": "current_energy_demand_heating_hotwater", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "estimated": { + "name": "estimated", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "sap_05_overwritten": { + "name": "sap_05_overwritten", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "sap_05_score": { + "name": "sap_05_score", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "sap_05_epc_rating": { + "name": "sap_05_epc_rating", + "type": "epc", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "heating_cost_current": { + "name": "heating_cost_current", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "hot_water_cost_current": { + "name": "hot_water_cost_current", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "lighting_cost_current": { + "name": "lighting_cost_current", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "appliances_cost_current": { + "name": "appliances_cost_current", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "gas_standing_charge": { + "name": "gas_standing_charge", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "electricity_standing_charge": { + "name": "electricity_standing_charge", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "original_co2_emissions": { + "name": "original_co2_emissions", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "original_primary_energy_consumption": { + "name": "original_primary_energy_consumption", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "original_current_energy_demand": { + "name": "original_current_energy_demand", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "original_current_energy_demand_heating_hotwater": { + "name": "original_current_energy_demand_heating_hotwater", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "installed_measures_co2_adjustment": { + "name": "installed_measures_co2_adjustment", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "installed_measures_energy_demand_adjustment": { + "name": "installed_measures_energy_demand_adjustment", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "installed_measures_total_energy_bill_adjustment": { + "name": "installed_measures_total_energy_bill_adjustment", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "installed_measures_heat_demand_adjustment": { + "name": "installed_measures_heat_demand_adjustment", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "is_epc_adjusted_for_installed_measures": { + "name": "is_epc_adjusted_for_installed_measures", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "lodged_co2_emissions": { + "name": "lodged_co2_emissions", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "lodged_heat_demand": { + "name": "lodged_heat_demand", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "has_been_remodelled": { + "name": "has_been_remodelled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": { + "uq_property_details_epc_property_portfolio": { + "name": "uq_property_details_epc_property_portfolio", + "columns": [ + { + "expression": "property_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "portfolio_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "property_details_epc_property_id_property_id_fk": { + "name": "property_details_epc_property_id_property_id_fk", + "tableFrom": "property_details_epc", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "property_details_epc_portfolio_id_portfolio_id_fk": { + "name": "property_details_epc_portfolio_id_portfolio_id_fk", + "tableFrom": "property_details_epc", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property_details_meter": { + "name": "property_details_meter", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "energy_supplier": { + "name": "energy_supplier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gas_supplier": { + "name": "gas_supplier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "meter_reading_total": { + "name": "meter_reading_total", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "meter_reading_electricity": { + "name": "meter_reading_electricity", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "meter_reading_gas": { + "name": "meter_reading_gas", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property_details_spatial": { + "name": "property_details_spatial", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "x_coordinate": { + "name": "x_coordinate", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "y_coordinate": { + "name": "y_coordinate", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "latitude": { + "name": "latitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "longitude": { + "name": "longitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "conservation_status": { + "name": "conservation_status", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "is_listed_building": { + "name": "is_listed_building", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "is_heritage_building": { + "name": "is_heritage_building", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "uq_property_details_spatial_uprn": { + "name": "uq_property_details_spatial_uprn", + "columns": [ + { + "expression": "uprn", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property_targets": { + "name": "property_targets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "epc": { + "name": "epc", + "type": "epc", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "heat_demand": { + "name": "heat_demand", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "property_targets_property_id_property_id_fk": { + "name": "property_targets_property_id_property_id_fk", + "tableFrom": "property_targets", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "property_targets_portfolio_id_portfolio_id_fk": { + "name": "property_targets_portfolio_id_portfolio_id_fk", + "tableFrom": "property_targets", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.installed_measure": { + "name": "installed_measure", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "measure_type": { + "name": "measure_type", + "type": "measure_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "installed_at": { + "name": "installed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "sap_points": { + "name": "sap_points", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "carbon_savings": { + "name": "carbon_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "kwh_savings": { + "name": "kwh_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "bill_savings": { + "name": "bill_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "heat_demand_savings": { + "name": "heat_demand_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "idx_installed_measure_uprn": { + "name": "idx_installed_measure_uprn", + "columns": [ + { + "expression": "uprn", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_installed_measure_uprn_active": { + "name": "idx_installed_measure_uprn_active", + "columns": [ + { + "expression": "uprn", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"installed_measure\".\"is_active\" = true", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_installed_measure_measure_type": { + "name": "idx_installed_measure_measure_type", + "columns": [ + { + "expression": "measure_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_installed_measure_uprn_measure": { + "name": "idx_installed_measure_uprn_measure", + "columns": [ + { + "expression": "uprn", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "measure_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"installed_measure\".\"is_active\" = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plan": { + "name": "plan", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "scenario_id": { + "name": "scenario_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "valuation_increase_lower_bound": { + "name": "valuation_increase_lower_bound", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "valuation_increase_upper_bound": { + "name": "valuation_increase_upper_bound", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "valuation_increase_average": { + "name": "valuation_increase_average", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "post_sap_points": { + "name": "post_sap_points", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "post_epc_rating": { + "name": "post_epc_rating", + "type": "epc", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "post_co2_emissions": { + "name": "post_co2_emissions", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "co2_savings": { + "name": "co2_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "post_energy_bill": { + "name": "post_energy_bill", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_bill_savings": { + "name": "energy_bill_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "post_energy_consumption": { + "name": "post_energy_consumption", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_consumption_savings": { + "name": "energy_consumption_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "valuation_post_retrofit": { + "name": "valuation_post_retrofit", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "valuation_increase": { + "name": "valuation_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "cost_of_works": { + "name": "cost_of_works", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "contingency_cost": { + "name": "contingency_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "plan_type": { + "name": "plan_type", + "type": "plan_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_plan_portfolio_scenario": { + "name": "idx_plan_portfolio_scenario", + "columns": [ + { + "expression": "portfolio_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scenario_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_plan_latest_per_property": { + "name": "idx_plan_latest_per_property", + "columns": [ + { + "expression": "portfolio_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scenario_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "property_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plan_portfolio_id_portfolio_id_fk": { + "name": "plan_portfolio_id_portfolio_id_fk", + "tableFrom": "plan", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "plan_property_id_property_id_fk": { + "name": "plan_property_id_property_id_fk", + "tableFrom": "plan", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "plan_scenario_id_scenario_id_fk": { + "name": "plan_scenario_id_scenario_id_fk", + "tableFrom": "plan", + "tableTo": "scenario", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plan_recommendations": { + "name": "plan_recommendations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "recommendation_id": { + "name": "recommendation_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_plan_recommendations_plan_id": { + "name": "idx_plan_recommendations_plan_id", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_plan_recommendations_plan_rec": { + "name": "idx_plan_recommendations_plan_rec", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recommendation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plan_recommendations_plan_id_plan_id_fk": { + "name": "plan_recommendations_plan_id_plan_id_fk", + "tableFrom": "plan_recommendations", + "tableTo": "plan", + "columnsFrom": [ + "plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "plan_recommendations_recommendation_id_recommendation_id_fk": { + "name": "plan_recommendations_recommendation_id_recommendation_id_fk", + "tableFrom": "plan_recommendations", + "tableTo": "recommendation", + "columnsFrom": [ + "recommendation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recommendation": { + "name": "recommendation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "measure_type": { + "name": "measure_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "estimated_cost": { + "name": "estimated_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "contingency_cost": { + "name": "contingency_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "default": { + "name": "default", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "starting_u_value": { + "name": "starting_u_value", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "new_u_value": { + "name": "new_u_value", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "sap_points": { + "name": "sap_points", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "heat_demand": { + "name": "heat_demand", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "kwh_savings": { + "name": "kwh_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "co2_equivalent_savings": { + "name": "co2_equivalent_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_savings": { + "name": "energy_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_cost_savings": { + "name": "energy_cost_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "property_valuation_increase": { + "name": "property_valuation_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "rental_yield_increase": { + "name": "rental_yield_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "total_work_hours": { + "name": "total_work_hours", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "labour_days": { + "name": "labour_days", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "already_installed": { + "name": "already_installed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": { + "recommendation_property_id_idx": { + "name": "recommendation_property_id_idx", + "columns": [ + { + "expression": "property_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_recommendation_active_defaults": { + "name": "idx_recommendation_active_defaults", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"recommendation\".\"default\" = true AND \"recommendation\".\"already_installed\" = false", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_recommendation_active_id_property": { + "name": "idx_recommendation_active_id_property", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "property_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"recommendation\".\"default\" = true AND \"recommendation\".\"already_installed\" = false", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recommendation_property_id_property_id_fk": { + "name": "recommendation_property_id_property_id_fk", + "tableFrom": "recommendation", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recommendation_materials": { + "name": "recommendation_materials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "recommendation_id": { + "name": "recommendation_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "material_id": { + "name": "material_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "depth": { + "name": "depth", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "quantity_unit": { + "name": "quantity_unit", + "type": "unit_quantity", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "estimated_cost": { + "name": "estimated_cost", + "type": "real", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "recommendation_materials_recommendation_id_idx": { + "name": "recommendation_materials_recommendation_id_idx", + "columns": [ + { + "expression": "recommendation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recommendation_materials_recommendation_id_recommendation_id_fk": { + "name": "recommendation_materials_recommendation_id_recommendation_id_fk", + "tableFrom": "recommendation_materials", + "tableTo": "recommendation", + "columnsFrom": [ + "recommendation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recommendation_materials_material_id_material_id_fk": { + "name": "recommendation_materials_material_id_material_id_fk", + "tableFrom": "recommendation_materials", + "tableTo": "material", + "columnsFrom": [ + "material_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scenario": { + "name": "scenario", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "budget": { + "name": "budget", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "housing_type": { + "name": "housing_type", + "type": "housing_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "goal": { + "name": "goal", + "type": "goal", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "goal_value": { + "name": "goal_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ashp_cop": { + "name": "ashp_cop", + "type": "real", + "primaryKey": false, + "notNull": false, + "default": 2.8 + }, + "trigger_file_path": { + "name": "trigger_file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "already_installed_file_path": { + "name": "already_installed_file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "patches_file_path": { + "name": "patches_file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "non_invasive_recommendations_file_path": { + "name": "non_invasive_recommendations_file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "exclusions": { + "name": "exclusions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "multi_plan": { + "name": "multi_plan", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "contingency": { + "name": "contingency", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "funding": { + "name": "funding", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "total_work_hours": { + "name": "total_work_hours", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_savings": { + "name": "energy_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "co2_equivalent_savings": { + "name": "co2_equivalent_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_cost_savings": { + "name": "energy_cost_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "property_valuation_increase": { + "name": "property_valuation_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "labour_days": { + "name": "labour_days", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "epc_breakdown_pre_retrofit": { + "name": "epc_breakdown_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "epc_breakdown_post_retrofit": { + "name": "epc_breakdown_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number_of_properties": { + "name": "number_of_properties", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "n_units_to_retrofit": { + "name": "n_units_to_retrofit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "co2_per_unit_pre_retrofit": { + "name": "co2_per_unit_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "co2_per_unit_post_retrofit": { + "name": "co2_per_unit_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_bill_per_unit_pre_retrofit": { + "name": "energy_bill_per_unit_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_bill_per_unit_post_retrofit": { + "name": "energy_bill_per_unit_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_consumption_per_unit_pre_retrofit": { + "name": "energy_consumption_per_unit_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_consumption_per_unit_post_retrofit": { + "name": "energy_consumption_per_unit_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "valuation_improvement_per_unit": { + "name": "valuation_improvement_per_unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_per_unit": { + "name": "cost_per_unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_per_co2_saved": { + "name": "cost_per_co2_saved", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_per_sap_point": { + "name": "cost_per_sap_point", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "valuation_return_on_investment": { + "name": "valuation_return_on_investment", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "scenario_portfolio_id_portfolio_id_fk": { + "name": "scenario_portfolio_id_portfolio_id_fk", + "tableFrom": "scenario", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.solar": { + "name": "solar", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "longitude": { + "name": "longitude", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "latitude": { + "name": "latitude", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "google_api_response": { + "name": "google_api_response", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.solar_scenario": { + "name": "solar_scenario", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "solar_id": { + "name": "solar_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "scenario_type": { + "name": "scenario_type", + "type": "scenario_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "number_panels": { + "name": "number_panels", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "array_kwhp": { + "name": "array_kwhp", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "lifetime_dc_kwh": { + "name": "lifetime_dc_kwh", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "yearly_dc_kwh": { + "name": "yearly_dc_kwh", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "lifetime_ac_kwh": { + "name": "lifetime_ac_kwh", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "yearly_ac_kwh": { + "name": "yearly_ac_kwh", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "expected_payback_years": { + "name": "expected_payback_years", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "panelled_roof_area": { + "name": "panelled_roof_area", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "solar_scenario_solar_id_solar_id_fk": { + "name": "solar_scenario_solar_id_solar_id_fk", + "tableFrom": "solar_scenario", + "tableTo": "solar", + "columnsFrom": [ + "solar_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sub_task": { + "name": "sub_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "task_id": { + "name": "task_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "job_started": { + "name": "job_started", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "job_completed": { + "name": "job_completed", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'In Progress'" + }, + "inputs": { + "name": "inputs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cloud_logs_url": { + "name": "cloud_logs_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sub_task_task_id_tasks_id_fk": { + "name": "sub_task_task_id_tasks_id_fk", + "tableFrom": "sub_task", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tasks": { + "name": "tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "task_source": { + "name": "task_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "job_started": { + "name": "job_started", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "job_completed": { + "name": "job_completed", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'In Progress'" + }, + "service": { + "name": "service", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "source", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team": { + "name": "team", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_org_id_organisation_id_fk": { + "name": "team_org_id_organisation_id_fk", + "tableFrom": "team", + "tableTo": "organisation", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_members": { + "name": "team_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_members_user_id_user_id_fk": { + "name": "team_members_user_id_user_id_fk", + "tableFrom": "team_members", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "team_members_team_id_team_id_fk": { + "name": "team_members_team_id_team_id_fk", + "tableFrom": "team_members", + "tableTo": "team", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_portfolio_permissions": { + "name": "team_portfolio_permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_portfolio_permissions_team_id_team_id_fk": { + "name": "team_portfolio_permissions_team_id_team_id_fk", + "tableFrom": "team_portfolio_permissions", + "tableTo": "team", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "team_portfolio_permissions_portfolio_id_portfolio_id_fk": { + "name": "team_portfolio_permissions_portfolio_id_portfolio_id_fk", + "tableFrom": "team_portfolio_permissions", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.uploaded_files": { + "name": "uploaded_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "s3_file_bucket": { + "name": "s3_file_bucket", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "s3_file_key": { + "name": "s3_file_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "s3_upload_timestamp": { + "name": "s3_upload_timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "landlord_property_id": { + "name": "landlord_property_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "hubspot_listing_id": { + "name": "hubspot_listing_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "file_type": { + "name": "file_type", + "type": "file_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "file_source": { + "name": "file_source", + "type": "file_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "name": "account_provider_providerAccountId_pk", + "columns": [ + "provider", + "providerAccountId" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "firstName": { + "name": "firstName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "oauth_id": { + "name": "oauth_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_provider": { + "name": "oauth_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "onboarded": { + "name": "onboarded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "last_login": { + "name": "last_login", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_profiles": { + "name": "user_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_type": { + "name": "user_type", + "type": "user_profiles_user_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "property_count": { + "name": "property_count", + "type": "user_profiles_property_count", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "goals": { + "name": "goals", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "referral_source": { + "name": "referral_source", + "type": "user_profiles_referral_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "nrla_membership_id": { + "name": "nrla_membership_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "accepted_privacy": { + "name": "accepted_privacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "accepted_privacy_at": { + "name": "accepted_privacy_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "marketing_opt_in": { + "name": "marketing_opt_in", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "marketing_opt_in_at": { + "name": "marketing_opt_in_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_profiles_user_id_user_id_fk": { + "name": "user_profiles_user_id_user_id_fk", + "tableFrom": "user_profiles", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verificationToken": { + "name": "verificationToken", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "name": "verificationToken_identifier_token_pk", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.whlg": { + "name": "whlg", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "postcode": { + "name": "postcode", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.aspect_type": { + "name": "aspect_type", + "schema": "public", + "values": [ + "material", + "condition", + "type", + "area", + "configuration", + "presence", + "risk", + "severity", + "location", + "finish", + "insulation", + "pointing", + "spalling", + "lintels", + "cladding", + "category", + "quantity", + "adequacy", + "rating", + "strategy", + "extent", + "distribution", + "structure", + "covering", + "fire_rating", + "external_decoration", + "work_required", + "age_band", + "construction_type", + "classification", + "system" + ] + }, + "public.element_type": { + "name": "element_type", + "schema": "public", + "values": [ + "property", + "property_construction_type", + "property_classification", + "property_age_band", + "storey_count", + "floor_level", + "floor_level_front_door", + "accessible_housing_register", + "asbestos", + "quality_standard", + "ccu", + "passenger_lift", + "stairlift", + "disabled_hoist_tracking", + "disabled_facilities", + "steps_to_front_door", + "roof", + "pitched_roof_covering", + "flat_roof_covering", + "rainwater_goods", + "loft_insulation", + "porch_canopy", + "chimney", + "fascia", + "soffit", + "fascia_soffit_bargeboards", + "gutters", + "store_roof", + "garage_roof", + "garage_and_store_roof", + "external_wall", + "external_noise_insulation", + "primary_wall", + "secondary_wall", + "downpipes", + "external_decoration", + "cladding", + "spandrel_panels", + "garage_walls", + "party_wall_fire_break", + "external_brickwork_pointing", + "internal_downpipes_external_area", + "external_windows", + "communal_windows", + "secondary_glazing", + "store_windows", + "garage_windows", + "garage_and_store_windows", + "external_door", + "front_door", + "rear_door", + "store_door", + "garage_door", + "garage_and_store_door", + "communal_entrance_door", + "main_door", + "block_entrance_door", + "lintel", + "patio_french_door", + "door_entry_handset", + "paths_and_hardstandings", + "parking_areas", + "boundary_walls", + "front_fencing", + "rear_fencing", + "side_fencing", + "rear_gate", + "front_gate", + "gates", + "retaining_walls", + "private_balcony", + "balcony_balustrade", + "outbuildings", + "garage_structure", + "paving", + "roads", + "soil_and_vent", + "solar_thermals", + "drop_kerb", + "outbuilding_overhaul", + "external_structural_defects", + "access_ramp", + "kitchen", + "kitchen_space_layout", + "tenant_installed_kitchen", + "kitchen_extractor_fan", + "bathroom", + "secondary_bathroom", + "secondary_toilet", + "bathroom_extractor_fan", + "additional_wc_or_whb", + "bathroom_remaining_life_source", + "kitchen_remaining_life_source", + "central_heating", + "heating_boiler", + "heating_distribution", + "secondary_heating", + "hot_water_system", + "cold_water_storage", + "heating_system", + "boiler_fuel", + "water_heating", + "programmable_heating", + "community_heating", + "gas_available", + "heat_recovery_units", + "heating_improvements", + "electrical_wiring", + "consumer_unit", + "smoke_detection", + "heat_detection", + "carbon_monoxide_detection", + "fire_door_rating", + "fire_risk_assessment", + "internal_wiring", + "electrics", + "communal_heating", + "communal_boiler", + "communal_electrics", + "communal_fire_alarm", + "communal_emergency_lighting", + "communal_door_entry", + "communal_cctv", + "communal_bin_store", + "communal_bin_store_doors", + "communal_bin_store_walls", + "communal_bin_store_roof", + "communal_refuse_chute", + "communal_floor_covering", + "communal_kitchen", + "communal_bathroom", + "communal_toilets", + "communal_gates", + "communal_lift", + "communal_passenger_lift", + "communal_balcony_walkway", + "communal_entrance", + "communal_internal_decorations", + "communal_internal_floor", + "communal_walkways", + "communal_external_doors", + "communal_stairs", + "communal_aerial", + "communal_aov", + "communal_internal_doors", + "communal_lateral_mains", + "communal_lighting", + "communal_lighting_conductor", + "communal_store_roof", + "communal_store_walls", + "communal_store_doors", + "communal_warden_call_system", + "communal_bms", + "communal_booster_pump", + "communal_dry_riser", + "communal_wet_riser", + "communal_cold_water_storage", + "communal_sprinkler", + "communal_plug_sockets", + "communal_circulation_space", + "ffhh_damp", + "ffhh_hold_and_cold_water", + "ffhh_drainage_lavatories", + "ffhh_neglected", + "ffhh_natural_light", + "ffhh_ventilation", + "ffhh_food_prep_and_washup", + "ffhh_unsafe_layout", + "ffhh_unstable_building", + "hhsrs_damp_and_mould", + "hhsrs_excess_cold", + "hhsrs_excess_heat", + "hhsrs_asbestos_and_mmf", + "hhsrs_biocides", + "hhsrs_carbon_monoxide", + "hhsrs_lead", + "hhsrs_radiation", + "hhsrs_uncombusted_fuel_gas", + "hhsrs_volatile_organic_compounds", + "hhsrs_crowding_and_space", + "hhsrs_entry_by_intruders", + "hhsrs_lighting", + "hhsrs_noise", + "hhsrs_domestic_hygiene_pests_refuse", + "hhsrs_food_safety", + "hhsrs_personal_hygiene_sanitation", + "hhsrs_water_supply", + "hhsrs_falls_associated_with_baths", + "hhsrs_falls_on_level_surfaces", + "hhsrs_falls_on_stairs", + "hhsrs_falls_between_levels", + "hhsrs_electrical_hazards", + "hhsrs_fire", + "hhsrs_flames_hot_surfaces", + "hhsrs_collision_and_entrapment", + "hhsrs_collision_hazards_low_headroom", + "hhsrs_explosions", + "hhsrs_ergonomics", + "hhsrs_structural_collapse", + "hhsrs_amenities" + ] + }, + "public.document_type": { + "name": "document_type", + "schema": "public", + "values": [ + "EPR", + "Condition Report", + "Evidence Report", + "Summary Information", + "Floor Plan", + "Scenario Draft EPC", + "Scenario Site Notes" + ] + }, + "public.scheme": { + "name": "scheme", + "schema": "public", + "values": [ + "eco4", + "gbis", + "whlg", + "none" + ] + }, + "public.inspection_archetype_2": { + "name": "inspection_archetype_2", + "schema": "public", + "values": [ + "detached", + "mid-terrace", + "enclosed mid-terrace", + "end-terrace", + "enclosed end-terrace", + "semi-detached" + ] + }, + "public.inspection_archetype": { + "name": "inspection_archetype", + "schema": "public", + "values": [ + "Bungalow", + "Flat", + "Maisonette", + "House", + "non-domestic" + ] + }, + "public.inspection_borescoped": { + "name": "inspection_borescoped", + "schema": "public", + "values": [ + "yes", + "no", + "refused" + ] + }, + "public.inspections_access_issues": { + "name": "inspections_access_issues", + "schema": "public", + "values": [ + "see notes", + "damp issues", + "foliage on walls", + "bushes against wall", + "trees around/anove property", + "high rise block flats/maisonettes", + "conservatory", + "lean-to", + "garage", + "extension", + "decking", + "shed against wall" + ] + }, + "public.inspections_cladding": { + "name": "inspections_cladding", + "schema": "public", + "values": [ + "none", + "cladded with “sufficient space to fill the wall”", + "cladded with “insufficient space to fill the wall”" + ] + }, + "public.inspections_insulation_material": { + "name": "inspections_insulation_material", + "schema": "public", + "values": [ + "empty 50-90", + "empty 100+", + "empty 30-40", + "empty less than 30", + "loose fibre/wool", + "eps/celo/king", + "fibre batts - with cavity", + "fibre batts - no cavity", + "loose bead", + "glued bead", + "formaldehyde", + "bubble wrap", + "poly chunks" + ] + }, + "public.inspections_rendered": { + "name": "inspections_rendered", + "schema": "public", + "values": [ + "no render", + "rendered with “insufficient” space between dpc and render", + "rendered with “sufficient” space between dpc and render" + ] + }, + "public.inspections_roof_orientation": { + "name": "inspections_roof_orientation", + "schema": "public", + "values": [ + "north", + "east", + "south", + "west", + "north-east", + "north-west", + "south-east", + "south-west", + "n/s split", + "e/w split", + "ne/sw split", + "nw/se split", + "flat roof", + "no roof", + "roof too small", + "already has solar pv" + ] + }, + "public.inspections_tile_hung": { + "name": "inspections_tile_hung", + "schema": "public", + "values": [ + "yes", + "no", + "first floor flats are tile hung" + ] + }, + "public.inspections_wall_construction": { + "name": "inspections_wall_construction", + "schema": "public", + "values": [ + "cavity", + "solid", + "system built", + "timber framed", + "steel framed", + "re-walled cavity", + "mansard pre-fab", + "mansard ewi", + "mansard re-walled" + ] + }, + "public.inspections_wall_insulation": { + "name": "inspections_wall_insulation", + "schema": "public", + "values": [ + "empty cavity", + "filled at build", + "partial", + "retro drilled", + "ewi", + "iwi", + "solid non-cavity", + "system built", + "timber framed", + "steel framed" + ] + }, + "public.cost_unit": { + "name": "cost_unit", + "schema": "public", + "values": [ + "gbp_sq_meter", + "gbp_per_unit", + "gbp_per_m2", + "gbp_per_m" + ] + }, + "public.depth_unit": { + "name": "depth_unit", + "schema": "public", + "values": [ + "mm" + ] + }, + "public.type": { + "name": "type", + "schema": "public", + "values": [ + "suspended_floor_insulation", + "solid_floor_insulation", + "external_wall_insulation", + "internal_wall_insulation", + "cavity_wall_insulation", + "mechanical_ventilation", + "loft_insulation", + "exposed_floor_insulation", + "flat_roof_insulation", + "room_roof_insulation", + "cavity_wall_extraction", + "iwi_wall_demolition", + "iwi_vapour_barrier", + "iwi_redecoration", + "suspended_floor_demolition", + "suspended_floor_redecoration", + "suspended_floor_vapour_barrier", + "solid_floor_demolition", + "solid_floor_preparation", + "solid_floor_vapour_barrier", + "solid_floor_redecoration", + "ewi_wall_demolition", + "ewi_wall_preparation", + "ewi_wall_redecoration", + "low_energy_lighting_installation", + "flat_roof_preparation", + "flat_roof_vapour_barrier", + "flat_roof_waterproofing", + "windows_glazing", + "secondary_glazing", + "double_glazing", + "trickle_vent", + "door_undercut", + "solar_pv", + "solar_battery", + "scaffolding", + "high_heat_retention_storage_heaters", + "air_source_heat_pump", + "boiler_upgrade", + "roomstat_programmer_trvs", + "time_temperature_zone_control", + "sealing_fireplace" + ] + }, + "public.r_value_unit": { + "name": "r_value_unit", + "schema": "public", + "values": [ + "square_meter_kelvin_per_watt" + ] + }, + "public.size_unit": { + "name": "size_unit", + "schema": "public", + "values": [ + "kWp", + "kW", + "watt", + "storey" + ] + }, + "public.thermal_conductivity_unit": { + "name": "thermal_conductivity_unit", + "schema": "public", + "values": [ + "watt_per_meter_kelvin" + ] + }, + "public.goal": { + "name": "goal", + "schema": "public", + "values": [ + "Valuation Improvement", + "Increasing EPC", + "Reducing CO2 emissions", + "Energy Savings", + "None" + ] + }, + "public.role": { + "name": "role", + "schema": "public", + "values": [ + "creator", + "admin", + "read", + "write" + ] + }, + "public.status": { + "name": "status", + "schema": "public", + "values": [ + "scoping", + "survey", + "assessment", + "tendering", + "project underway", + "completion; status: on track", + "completion; status: delayed", + "completion; status: at risk", + "completion; status: completed", + "needs review" + ] + }, + "public.epc": { + "name": "epc", + "schema": "public", + "values": [ + "A", + "B", + "C", + "D", + "E", + "F", + "G" + ] + }, + "public.creation_status": { + "name": "creation_status", + "schema": "public", + "values": [ + "LOADING", + "READY", + "ERROR" + ] + }, + "public.housing_type": { + "name": "housing_type", + "schema": "public", + "values": [ + "Private", + "Social" + ] + }, + "public.measure_type": { + "name": "measure_type", + "schema": "public", + "values": [ + "air_source_heat_pump", + "boiler_upgrade", + "high_heat_retention_storage_heaters", + "secondary_heating", + "roomstat_programmer_trvs", + "time_temperature_zone_control", + "cylinder_thermostat", + "cavity_wall_insulation", + "extension_cavity_wall_insulation", + "external_wall_insulation", + "internal_wall_insulation", + "loft_insulation", + "flat_roof_insulation", + "room_roof_insulation", + "solid_floor_insulation", + "suspended_floor_insulation", + "double_glazing", + "secondary_glazing", + "draught_proofing", + "mechanical_ventilation", + "low_energy_lighting", + "solar_pv", + "hot_water_tank_insulation", + "sealing_open_fireplace" + ] + }, + "public.plan_type": { + "name": "plan_type", + "schema": "public", + "values": [ + "solar_eco4", + "solar_hhrsh_eco4", + "empty_cavity_eco", + "partial_cavity_eco", + "extraction_eco" + ] + }, + "public.unit_quantity": { + "name": "unit_quantity", + "schema": "public", + "values": [ + "m2", + "part", + "kwp" + ] + }, + "public.scenario_type": { + "name": "scenario_type", + "schema": "public", + "values": [ + "unit", + "building" + ] + }, + "public.source": { + "name": "source", + "schema": "public", + "values": [ + "portfolio_id" + ] + }, + "public.file_source": { + "name": "file_source", + "schema": "public", + "values": [ + "pas hub", + "sharepoint", + "hubspot", + "ecmk" + ] + }, + "public.file_type": { + "name": "file_type", + "schema": "public", + "values": [ + "photo_pack", + "site_note", + "rd_sap_site_note", + "pas_2023_ventilation", + "pas_2023_condition", + "pas_significance", + "par_photo_pack", + "pas_2023_property", + "pas_2023_occupancy", + "ecmk_site_note", + "ecmk_rd_sap_site_note" + ] + }, + "public.user_profiles_property_count": { + "name": "user_profiles_property_count", + "schema": "public", + "values": [ + "1", + "2–5", + "6–20", + "21+", + "1–50", + "51–100", + "101–300", + "301–1000", + "1000+" + ] + }, + "public.user_profiles_referral_source": { + "name": "user_profiles_referral_source", + "schema": "public", + "values": [ + "search", + "social_media", + "NRLA", + "partner", + "word_of_mouth", + "other" + ] + }, + "public.user_profiles_user_type": { + "name": "user_profiles_user_type", + "schema": "public", + "values": [ + "private_landlord", + "private_tenant", + "social_landlord", + "social_tenant", + "homeowner", + "other" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/src/app/db/migrations/meta/_journal.json b/src/app/db/migrations/meta/_journal.json index e65e26e..5bea76c 100644 --- a/src/app/db/migrations/meta/_journal.json +++ b/src/app/db/migrations/meta/_journal.json @@ -1149,6 +1149,20 @@ "when": 1775123235194, "tag": "0163_cultured_madripoor", "breakpoints": true + }, + { + "idx": 164, + "version": "7", + "when": 1775310006908, + "tag": "0164_high_sumo", + "breakpoints": true + }, + { + "idx": 165, + "version": "7", + "when": 1775577933185, + "tag": "0165_small_khan", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/app/db/schema/portfolio_organisation.ts b/src/app/db/schema/portfolio_organisation.ts new file mode 100644 index 0000000..246e3e4 --- /dev/null +++ b/src/app/db/schema/portfolio_organisation.ts @@ -0,0 +1,24 @@ +import { pgTable, bigint, uuid, timestamp } from "drizzle-orm/pg-core"; +import { portfolio } from "./portfolio"; +import { organisation } from "./organisation"; +import { InferModel } from "drizzle-orm"; + +export const portfolioOrganisation = pgTable("portfolio_organisation", { + id: uuid("id").defaultRandom().primaryKey(), + portfolioId: bigint("portfolio_id", { mode: "bigint" }) + .notNull() + .references(() => portfolio.id, { onDelete: "cascade" }) + .unique(), // one organisation per portfolio + organisationId: uuid("organisation_id") + .notNull() + .references(() => organisation.id, { onDelete: "cascade" }), + createdAt: timestamp("created_at", { precision: 6, withTimezone: true }) + .defaultNow() + .notNull(), + updatedAt: timestamp("updated_at", { precision: 6, withTimezone: true }) + .defaultNow() + .notNull(), +}); + +export type PortfolioOrganisation = InferModel; +export type NewPortfolioOrganisation = InferModel; diff --git a/src/app/db/schema/uploaded_files.ts b/src/app/db/schema/uploaded_files.ts index 9f949fd..88e84ff 100644 --- a/src/app/db/schema/uploaded_files.ts +++ b/src/app/db/schema/uploaded_files.ts @@ -9,13 +9,16 @@ export const fileType = pgEnum("file_type", [ "pas_significance", "par_photo_pack", "pas_2023_property", - "pas_2023_occupancy" + "pas_2023_occupancy", + "ecmk_site_note", + "ecmk_rd_sap_site_note" ]); export const fileSource = pgEnum("file_source", [ "pas hub", "sharepoint", - "hubspot" + "hubspot", + "ecmk" ]); export const uploadedFiles = pgTable( diff --git a/src/app/portfolio/[slug]/(portfolio)/settings/OrganisationLinkCard.tsx b/src/app/portfolio/[slug]/(portfolio)/settings/OrganisationLinkCard.tsx new file mode 100644 index 0000000..176047c --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/settings/OrganisationLinkCard.tsx @@ -0,0 +1,280 @@ +"use client"; + +import { useState, useMemo } from "react"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { Building2, CheckCircle2, Link2, Link2Off, AlertTriangle, Search } from "lucide-react"; +import { Button } from "@/app/shadcn_components/ui/button"; +import { Input } from "@/app/shadcn_components/ui/input"; +import { + Dialog, + DialogContent, + DialogTitle, + DialogFooter, +} from "@/app/shadcn_components/ui/dialog"; + +type OrgSummary = { + id: string; + name: string | null; + hubspotCompanyId: string | null; +}; + +async function fetchCurrentOrg(portfolioId: string): Promise { + const res = await fetch(`/api/portfolio/${portfolioId}/organisation`); + if (!res.ok) throw new Error("Failed to fetch linked organisation"); + return res.json(); +} + +async function fetchAllOrgs(): Promise { + const res = await fetch("/api/organisations"); + if (!res.ok) throw new Error("Failed to fetch organisations"); + return res.json(); +} + +export default function OrganisationLinkCard({ portfolioId }: { portfolioId: string }) { + const queryClient = useQueryClient(); + + const [connectOpen, setConnectOpen] = useState(false); + const [disconnectOpen, setDisconnectOpen] = useState(false); + const [selectedOrgId, setSelectedOrgId] = useState(null); + const [searchQuery, setSearchQuery] = useState(""); + const [confirmed, setConfirmed] = useState(false); + + // Current linked org + const { data: currentOrg, isLoading: loadingCurrent } = useQuery({ + queryKey: ["portfolio-org", portfolioId], + queryFn: () => fetchCurrentOrg(portfolioId), + }); + + // All orgs — only fetched when connect modal is open + const { data: allOrgs = [], isLoading: loadingOrgs } = useQuery({ + queryKey: ["all-organisations"], + queryFn: fetchAllOrgs, + enabled: connectOpen, + }); + + const connectMutation = useMutation({ + mutationFn: async (organisationId: string) => { + const res = await fetch(`/api/portfolio/${portfolioId}/organisation`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ organisationId }), + }); + if (!res.ok) throw new Error("Failed to connect organisation"); + return res.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["portfolio-org", portfolioId] }); + setConnectOpen(false); + setSelectedOrgId(null); + setConfirmed(false); + setSearchQuery(""); + }, + }); + + const disconnectMutation = useMutation({ + mutationFn: async () => { + const res = await fetch(`/api/portfolio/${portfolioId}/organisation`, { + method: "DELETE", + }); + if (!res.ok) throw new Error("Failed to disconnect organisation"); + return res.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["portfolio-org", portfolioId] }); + setDisconnectOpen(false); + }, + }); + + const filteredOrgs = useMemo( + () => + allOrgs.filter((o) => + (o.name ?? "").toLowerCase().includes(searchQuery.toLowerCase()), + ), + [allOrgs, searchQuery], + ); + + const selectedOrg = allOrgs.find((o) => o.id === selectedOrgId) ?? null; + + return ( +
+ {/* Header */} +
+
+ +
+
+

Organisation Link

+

+ Connect this portfolio to an organisation to enable live project tracking +

+
+
+ + {/* Body */} +
+ {loadingCurrent ? ( +
+ ) : currentOrg ? ( +
+
+ +
+

{currentOrg.name ?? "Unnamed organisation"}

+

+ Connected · HubSpot ID: {currentOrg.hubspotCompanyId ?? "—"} +

+
+
+
+ + +
+
+ ) : ( +
+
+
+ +
+

No organisation linked

+
+ +
+ )} +
+ + {/* ── Connect modal ─────────────────────────────────────────────── */} + { setConnectOpen(v); if (!v) { setSelectedOrgId(null); setConfirmed(false); setSearchQuery(""); } }}> + + Connect Organisation + + {/* Search */} +
+ + setSearchQuery(e.target.value)} + placeholder="Search organisations…" + className="pl-9 h-9 text-sm border-gray-200" + /> +
+ + {/* Org list */} +
+ {loadingOrgs ? ( +
Loading…
+ ) : filteredOrgs.length === 0 ? ( +
No organisations found
+ ) : ( + filteredOrgs.map((org) => ( + + )) + )} +
+ + {/* Warning */} +
+ +

+ Viewers of this portfolio will be able to see live project tracking data associated with the selected organisation. +

+
+ + {/* Confirmation checkbox */} + + + + + + +
+
+ + {/* ── Disconnect confirm dialog ──────────────────────────────────── */} + + + Disconnect organisation? +

+ Are you sure you want to disconnect{" "} + {currentOrg?.name ?? "this organisation"}? + Live project tracking data will no longer be visible to portfolio viewers. +

+ + + + +
+
+
+ ); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/settings/PortfolioSettings.tsx b/src/app/portfolio/[slug]/(portfolio)/settings/PortfolioSettings.tsx deleted file mode 100644 index b493531..0000000 --- a/src/app/portfolio/[slug]/(portfolio)/settings/PortfolioSettings.tsx +++ /dev/null @@ -1,544 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { useMutation } from "@tanstack/react-query"; -import { PortfolioSettingsType } from "../../utils"; -import { Button } from "@/app/shadcn_components/ui/button"; -import { Input } from "@/app/shadcn_components/ui/input"; -import { useRouter } from "next/navigation"; -import { handleNumericKeyDown } from "@/app/utils"; -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectLabel, - SelectTrigger, - SelectValue, -} from "@/app/shadcn_components/ui/select"; -import { - Dialog, - DialogContent, - DialogTitle, - DialogFooter, -} from "@/app/shadcn_components/ui/dialog"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/app/shadcn_components/ui/table"; -import { PortfolioStatus as PortfolioStatusOptions } from "@/app/db/schema/portfolio"; -import { PortfolioGoal as PortfolioGoalOptions } from "@/app/db/schema/portfolio"; -import { useSession } from "next-auth/react"; -import PortfolioPlanTable from "@/app/components/portfolio/measures/PlanTable"; -import { UsersPermissionsCard } from "./UsersPermissionsCard"; - -// dropdown selection component for both goal and status - -export function SettingsDropdown({ - startingValue, - options, - setOption, - className, -}: { - startingValue: string; - options: string[]; - setOption: (option: string) => void; - className?: string; -}) { - function handleValueChange(newValue: string) { - setOption(newValue); - } - - return ( - - ); -} - -type updateSettingsArgs = { - userId: bigint; - portfolioId: string; - name: string | null; - budget: number | string | undefined | null; - goal: (typeof PortfolioGoalOptions)[number] | null; - status: (typeof PortfolioStatusOptions)[number] | null; -}; - -type bodyType = { - name?: string; - budget?: number | string; - goal?: string; - status?: string; -}; - -const updateSettings = async ({ - userId, - portfolioId, - name, - budget, - goal, - status, -}: updateSettingsArgs) => { - const permissionsReponse = await fetch( - `/api/portfolio/${portfolioId}/permissions`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - userId: userId.toString(), - action: "update", - }), - }, - ); - - const permissionsData = await permissionsReponse.json(); - const permitted = permissionsData.permitted; - console.log("USER IS PERMITTED TO DO THIS!!!!"); - // If the user is not permitted to delete the portfolio, we'll throw an error - if (!permitted) { - throw new Error("User is not permitted to update this portfolio"); - } - // We convert the the bigint to a string since big ints are not serialisable and we don't want to loose precision - - // We will create a js object with the starting values - // We will then update the values that are not null - - const body: bodyType = {}; - - if (name) { - body.name = name; - } - - if (budget) { - body.budget = budget; - } - - if (goal) { - body.goal = goal; - } - - if (status) { - body.status = status; - } - - const requestBody = JSON.stringify(body); - - const response = await fetch(`/api/portfolio/${portfolioId}`, { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: requestBody, - }); - - if (!response.ok) { - throw new Error("Network response was not ok"); - } - - return response.json(); -}; - -async function deletePortfolio({ - userId, - portfolioId, -}: { - userId: bigint; - portfolioId: string; -}) { - try { - console.log("Attempting to DELETE portfolio by calling API:", { - userId, - portfolioId, - }); - - // We'll check if the user is authorized to delete this portfolio - const permissionsReponse = await fetch( - `/api/portfolio/${portfolioId}/permissions`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - userId: userId.toString(), - action: "delete", - }), - }, - ); - - const permissionsData = await permissionsReponse.json(); - const permitted = permissionsData.permitted; - - // If the user is not permitted to delete the portfolio, we'll throw an error - if (!permitted) { - throw new Error("User is not permitted to delete this portfolio"); - } - - const response = await fetch(`/api/portfolio/${portfolioId}`, { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - throw new Error( - "deletePortfolio has been called into action but utterly failed to do the API handoff", - ); - } - - return await response.json(); - } catch (error) { - console.error("Error after failing to the try to get a response:", error); - throw error; - } -} - -export default function PortfolioSettings({ - portfolioId, - portfolioSettingsData, -}: { - portfolioId: string; - portfolioSettingsData: PortfolioSettingsType; -}) { - // This is a client component so we can access the session directly - const session = useSession(); - const router = useRouter(); - - const { mutate, isLoading } = useMutation(updateSettings, { - onSuccess: () => { - router.refresh(); - }, - onError: (error) => { - // handle error - console.log(error); - }, - }); - - const { mutate: mutateDelete } = useMutation(deletePortfolio, { - onSuccess: () => { - setIsDeleteModalOpen(false); - router.push("/home"); - }, - onError: (error) => { - console.error( - "Because the API hand off failed, we're right back here at the mutation station", - error, - ); - }, - }); - - const [portfolioName, setPortfolioName] = useState( - portfolioSettingsData.name, - ); - - const [portfolioBudget, setPortfolioBudget] = useState< - number | string | null - >(portfolioSettingsData.budget); - - const [portfolioGoal, setPortfolioGoal] = useState( - portfolioSettingsData.goal, - ); - - const [portfolioStatus, setPortfolioStatus] = useState( - portfolioSettingsData.status, - ); - - // Set up state for deleteModal and deleteConfirmation - - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - - const [deleteConfirmationByName, setDeleteConfirmationByName] = useState(""); - - if (session.status === "loading") { - // You can return a loading spinner or placeholder here - return
Loading...
; - } - - if (!session.data) { - // The user is not logged in, redirect them to sign in - return null; - } - - const userId = session.data.user.dbId; - - function handleOpenDeleteModal() { - setDeleteConfirmationByName(""); - setIsDeleteModalOpen(true); - } - - async function handleDeleteConfirmation() { - if (deleteConfirmationByName !== portfolioSettingsData.name) { - console.warn("Delete confirmation name does not match"); - return; - } - - try { - console.log("[DELETE] starting delete mutation"); - - await mutateDelete({ - userId, - portfolioId, - }); - - console.log("[DELETE] mutation completed successfully"); - // Refresh table / page data - router.refresh(); - } catch (err) { - console.error("[DELETE] mutation failed", err); - } - } - - // Change NAME functionality - changing state - - function handlePortfolioNameChange(e: React.ChangeEvent) { - setPortfolioName(e.target.value); - } - - // The onClick function called to update the NAME in the DB - - function handleRename() { - mutate({ - userId, - portfolioId, - name: portfolioName, - budget: null, - goal: null, - status: null, - }); - } - - // BUDGET CHANGING FUNCTIONS - - // Change BUDGET functionality - changing state - - function handlePortfolioBudgetUpdate(e: React.ChangeEvent) { - setPortfolioBudget(Number(e.target.value)); - } - - // The onClick function called to update the BUDGET in the DB - - function handleBudgetUpdate() { - mutate({ - userId, - portfolioId, - name: null, - budget: portfolioBudget, - goal: null, - status: null, - }); - } - - // CHANGING GOAL AND STATUS FUNCTIONALITY - - // The onClick function called to update the GOAL in the DB - - function handleGoalUpdate() { - mutate({ - userId, - portfolioId, - name: null, - budget: null, - goal: portfolioGoal, - status: null, - }); - } - - // The onClick function called to update the BUDGET in the DB - - function handleStatusUpdate() { - mutate({ - userId, - portfolioId, - name: null, - budget: null, - goal: null, - status: portfolioStatus, - }); - } - - // HTML to render the page - - // TODO: 1) Set up the useMutate hook - // 2) Set up the api functions - // 3) add the call to mutate() so that when we submit the form, the data is updated in the DB - // 4) Create the API - - return ( -
-
- - - - - Rename the Portfolio: -

- Permanently change the name of your portfolio -

-
- - - - - - -
- - - Change the Portfolio Budget: -

- The total budget across ALL properties. Works aim to stay - within this budget -

-
- - handleNumericKeyDown(e)} - /> - - - - -
- - - Change the Portfolio Goal: -

- Adjust the overall aim of the works conducted on this - portfolio -

-
- - - - - - -
- - - Change the Status of the Portfolio: -

- Adjust where the portfolio stands in the works pipeline -

-
- - - - - - -
-
-
-
- -
- - - - - Danger Zone: - - - - - - - - Delete the Portfolio: -

- Permanently delete the portfolio and all property data - assigned to this portfolio -

-
- - - - -
-
-
- - - Are you sure? -

- To confirm, please type the name of the portfolio ( - {portfolioSettingsData.name}) -

- setDeleteConfirmationByName(e.target.value)} - placeholder="Type portfolio name" - /> - - - - -
-
-
-
- ); -} diff --git a/src/app/portfolio/[slug]/(portfolio)/settings/SettingsSidebarLink.tsx b/src/app/portfolio/[slug]/(portfolio)/settings/SettingsSidebarLink.tsx new file mode 100644 index 0000000..1ce8079 --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/settings/SettingsSidebarLink.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { usePathname, useRouter } from "next/navigation"; +import { useTransition } from "react"; +import { cn } from "@/lib/utils"; + +export function SettingsSidebarLink({ + href, + icon, + children, +}: { + href: string; + icon: React.ReactNode; + children: React.ReactNode; +}) { + const pathname = usePathname(); + const router = useRouter(); + const isActive = pathname === href; + const [isPending, startTransition] = useTransition(); + + function handleClick(e: React.MouseEvent) { + e.preventDefault(); + if (isActive) return; + startTransition(() => router.push(href)); + } + + return ( + + ); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/settings/UsersPermissionsCard.tsx b/src/app/portfolio/[slug]/(portfolio)/settings/UsersPermissionsCard.tsx index 4695ca5..dc2d5cf 100644 --- a/src/app/portfolio/[slug]/(portfolio)/settings/UsersPermissionsCard.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/settings/UsersPermissionsCard.tsx @@ -11,7 +11,7 @@ import { import { Input } from "@/app/shadcn_components/ui/input"; import { Button } from "@/app/shadcn_components/ui/button"; -import { useState, useEffect } from "react"; +import { useState } from "react"; import { Role, RoleDropdown, Collaborator } from "@/app/portfolio/[slug]/(portfolio)/settings/roles"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; diff --git a/src/app/portfolio/[slug]/(portfolio)/settings/connected-organisation/page.tsx b/src/app/portfolio/[slug]/(portfolio)/settings/connected-organisation/page.tsx new file mode 100644 index 0000000..ac31647 --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/settings/connected-organisation/page.tsx @@ -0,0 +1,22 @@ +import { redirect } from "next/navigation"; +import { getServerSession } from "next-auth"; +import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import OrganisationLinkCard from "../OrganisationLinkCard"; + +export default async function ConnectedOrganisationPage(props: { + params: Promise<{ slug: string }>; +}) { + const { slug } = await props.params; + const session = await getServerSession(AuthOptions); + const isDomnaUser = !!session?.user?.email?.endsWith("@domna.homes"); + + if (!isDomnaUser) { + redirect(`/portfolio/${slug}/settings/general`); + } + + return ( +
+ +
+ ); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/settings/general/DangerZone.tsx b/src/app/portfolio/[slug]/(portfolio)/settings/general/DangerZone.tsx new file mode 100644 index 0000000..971d416 --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/settings/general/DangerZone.tsx @@ -0,0 +1,151 @@ +"use client"; + +import { useState } from "react"; +import { useMutation } from "@tanstack/react-query"; +import { Button } from "@/app/shadcn_components/ui/button"; +import { useRouter } from "next/navigation"; +import { + Dialog, + DialogContent, + DialogTitle, + DialogFooter, +} from "@/app/shadcn_components/ui/dialog"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/app/shadcn_components/ui/table"; +import { useSession } from "next-auth/react"; + +async function deletePortfolio({ + userId, + portfolioId, +}: { + userId: bigint; + portfolioId: string; +}) { + const permissionsReponse = await fetch( + `/api/portfolio/${portfolioId}/permissions`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ userId: userId.toString(), action: "delete" }), + }, + ); + + const permissionsData = await permissionsReponse.json(); + if (!permissionsData.permitted) { + throw new Error("User is not permitted to delete this portfolio"); + } + + const response = await fetch(`/api/portfolio/${portfolioId}`, { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + }); + + if (!response.ok) { + throw new Error("Failed to delete portfolio"); + } + + return await response.json(); +} + +export default function DangerZone({ + portfolioId, + portfolioName, +}: { + portfolioId: string; + portfolioName: string; +}) { + const session = useSession(); + const router = useRouter(); + + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [deleteConfirmationByName, setDeleteConfirmationByName] = useState(""); + + const { mutate: mutateDelete } = useMutation(deletePortfolio, { + onSuccess: () => { + setIsDeleteModalOpen(false); + router.push("/home"); + }, + onError: (error) => console.error("Delete failed", error), + }); + + if (session.status === "loading") return null; + if (!session.data) return null; + + const userId = session.data.user.dbId; + + async function handleDeleteConfirmation() { + if (deleteConfirmationByName !== portfolioName) return; + mutateDelete({ userId, portfolioId }); + } + + return ( +
+ + + + + Danger Zone: + + + + + + + Delete the Portfolio: +

+ Permanently delete the portfolio and all property data assigned to this portfolio +

+
+ + + +
+
+
+ + + Are you sure? +

+ To confirm, please type the name of the portfolio ( + {portfolioName}) +

+ setDeleteConfirmationByName(e.target.value)} + placeholder="Type portfolio name" + /> + + + + +
+
+
+ ); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/settings/general/GeneralSettingsForm.tsx b/src/app/portfolio/[slug]/(portfolio)/settings/general/GeneralSettingsForm.tsx new file mode 100644 index 0000000..44bef4e --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/settings/general/GeneralSettingsForm.tsx @@ -0,0 +1,238 @@ +"use client"; + +import { useState } from "react"; +import { useMutation } from "@tanstack/react-query"; +import { PortfolioSettingsType } from "../../../utils"; +import { Button } from "@/app/shadcn_components/ui/button"; +import { Input } from "@/app/shadcn_components/ui/input"; +import { useRouter } from "next/navigation"; +import { handleNumericKeyDown } from "@/app/utils"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/app/shadcn_components/ui/select"; +import { PortfolioStatus as PortfolioStatusOptions } from "@/app/db/schema/portfolio"; +import { PortfolioGoal as PortfolioGoalOptions } from "@/app/db/schema/portfolio"; +import { useSession } from "next-auth/react"; + +function SettingsDropdown({ + startingValue, + options, + setOption, +}: { + startingValue: string; + options: string[]; + setOption: (option: string) => void; +}) { + return ( + + ); +} + +type updateSettingsArgs = { + userId: bigint; + portfolioId: string; + name: string | null; + budget: number | string | undefined | null; + goal: (typeof PortfolioGoalOptions)[number] | null; + status: (typeof PortfolioStatusOptions)[number] | null; +}; + +type bodyType = { + name?: string; + budget?: number | string; + goal?: string; + status?: string; +}; + +const updateSettings = async ({ + userId, + portfolioId, + name, + budget, + goal, + status, +}: updateSettingsArgs) => { + const permissionsReponse = await fetch( + `/api/portfolio/${portfolioId}/permissions`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ userId: userId.toString(), action: "update" }), + }, + ); + + const permissionsData = await permissionsReponse.json(); + if (!permissionsData.permitted) { + throw new Error("User is not permitted to update this portfolio"); + } + + const body: bodyType = {}; + if (name) body.name = name; + if (budget) body.budget = budget; + if (goal) body.goal = goal; + if (status) body.status = status; + + const response = await fetch(`/api/portfolio/${portfolioId}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + + if (!response.ok) throw new Error("Network response was not ok"); + return response.json(); +}; + +function SettingRow({ + label, + description, + children, +}: { + label: string; + description: string; + children: React.ReactNode; +}) { + return ( +
+
+
+

{label}

+

{description}

+
+
{children}
+
+
+ ); +} + +export default function GeneralSettingsForm({ + portfolioId, + portfolioSettingsData, +}: { + portfolioId: string; + portfolioSettingsData: PortfolioSettingsType; +}) { + const session = useSession(); + const router = useRouter(); + + const { mutate } = useMutation(updateSettings, { + onSuccess: () => router.refresh(), + onError: (error) => console.log(error), + }); + + const [portfolioName, setPortfolioName] = useState(portfolioSettingsData.name); + const [portfolioBudget, setPortfolioBudget] = useState( + portfolioSettingsData.budget, + ); + const [portfolioGoal, setPortfolioGoal] = useState(portfolioSettingsData.goal); + const [portfolioStatus, setPortfolioStatus] = useState(portfolioSettingsData.status); + + if (session.status === "loading") return
Loading...
; + if (!session.data) return null; + + const userId = session.data.user.dbId; + + return ( +
+

General

+

Manage your portfolio settings.

+ +
+ + setPortfolioName(e.target.value)} + className="w-48" + /> + + + + + setPortfolioBudget(Number(e.target.value))} + onKeyDown={(e) => handleNumericKeyDown(e)} + className="w-48" + /> + + + + + + + + + + + + +
+
+ ); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/settings/general/page.tsx b/src/app/portfolio/[slug]/(portfolio)/settings/general/page.tsx new file mode 100644 index 0000000..282e572 --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/settings/general/page.tsx @@ -0,0 +1,20 @@ +import { getPortfolioSettings } from "../../../utils"; +import GeneralSettingsForm from "./GeneralSettingsForm"; +import DangerZone from "./DangerZone"; + +export default async function GeneralSettingsPage(props: { + params: Promise<{ slug: string }>; +}) { + const { slug } = await props.params; + const portfolioSettingsData = await getPortfolioSettings(slug); + + return ( +
+ + +
+ ); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/settings/layout.tsx b/src/app/portfolio/[slug]/(portfolio)/settings/layout.tsx new file mode 100644 index 0000000..db1a38b --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/settings/layout.tsx @@ -0,0 +1,58 @@ +import { getServerSession } from "next-auth"; +import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { SettingsSidebarLink } from "./SettingsSidebarLink"; +import { Settings2, Users, Building2, ScrollText } from "lucide-react"; + +export default async function SettingsLayout({ + children, + params, +}: { + children: React.ReactNode; + params: Promise<{ slug: string }>; +}) { + const { slug } = await params; + const session = await getServerSession(AuthOptions); + const isDomnaUser = !!session?.user?.email?.endsWith("@domna.homes"); + + return ( +
+ + +
{children}
+
+ ); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/settings/logs/PortfolioLogs.tsx b/src/app/portfolio/[slug]/(portfolio)/settings/logs/PortfolioLogs.tsx new file mode 100644 index 0000000..62596c3 --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/settings/logs/PortfolioLogs.tsx @@ -0,0 +1,120 @@ +"use client"; + +import { useState } from "react"; +import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; +import PortfolioTaskList from "./PortfolioTaskList"; +import PortfolioSubtaskDetails from "./PortfolioSubtaskDetails"; + +export interface Task { + id: string; + taskSource: string; + jobStarted: string | null; + jobCompleted: string | null; + status: string; + service: string | null; + updatedAt: string; +} + +export interface PortfolioTask extends Task { + totalSubtasks: number; + completedSubtasks: number; + failedSubtasks: number; +} + +export interface SubTask { + id: string; + taskId: string; + jobStarted: string | null; + jobCompleted: string | null; + status: string; + inputs: string | null; + outputs: string | null; + cloudLogsURL: string | null; + updatedAt: string; +} + +interface TasksResponse { + tasks: PortfolioTask[]; + total: number; + limit: number; + offset: number; +} + +export default function PortfolioLogs({ portfolioId }: { portfolioId: string }) { + const [selectedTaskId, setSelectedTaskId] = useState(null); + + const { + data: tasksData, + isLoading, + isFetchingNextPage, + isError, + error: tasksError, + fetchNextPage, + refetch, + } = useInfiniteQuery({ + queryKey: ["portfolioTasks", portfolioId], + queryFn: async ({ pageParam = 0 }) => { + const response = await fetch( + `/api/portfolio/${portfolioId}/tasks?limit=20&offset=${pageParam}` + ); + if (!response.ok) throw new Error("Failed to fetch tasks"); + return response.json(); + }, + getNextPageParam: (lastPage) => { + const nextOffset = lastPage.offset + lastPage.tasks.length; + return nextOffset < lastPage.total ? nextOffset : undefined; + }, + enabled: !!portfolioId, + }); + + const tasks = tasksData?.pages.flatMap((p) => p.tasks) ?? []; + const total = tasksData?.pages[0]?.total ?? 0; + const errorMessage = isError + ? (tasksError instanceof Error ? tasksError.message : "An error occurred") + : null; + + const { data: subtasks = [] } = useQuery({ + queryKey: ["taskSubtasks", selectedTaskId], + queryFn: async () => { + const response = await fetch(`/api/tasks/${selectedTaskId}`); + if (!response.ok) throw new Error("Failed to fetch subtasks"); + return response.json(); + }, + enabled: !!selectedTaskId, + }); + + const selectedTask = tasks.find((t) => t.id === selectedTaskId); + + return ( +
+ {/* Left sidebar - Task list */} +
+ fetchNextPage()} + onRefresh={() => refetch()} + /> +
+ + {/* Right side - Subtask details */} +
+ {selectedTaskId ? ( + + ) : ( +
+ Select a task to view its subtasks +
+ )} +
+
+ ); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/settings/logs/PortfolioSubtaskDetails.tsx b/src/app/portfolio/[slug]/(portfolio)/settings/logs/PortfolioSubtaskDetails.tsx new file mode 100644 index 0000000..7b9a334 --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/settings/logs/PortfolioSubtaskDetails.tsx @@ -0,0 +1,305 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { SubTask, PortfolioTask } from "./PortfolioLogs"; +import { ScrollArea } from "@/app/shadcn_components/ui/scroll-area"; +import { Card } from "@/app/shadcn_components/ui/card"; +import { Button } from "@/app/shadcn_components/ui/button"; +import { ChevronDown, AlertTriangle } from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface PortfolioSubtaskDetailsProps { + subtasks: SubTask[]; + task?: PortfolioTask; +} + +function StatusPill({ status }: { status: string }) { + const s = status.toLowerCase(); + const isComplete = s === "completed" || s === "complete"; + const isInProgress = s === "in progress"; + const isFailed = s === "failed" || s === "failure" || s === "error"; + + return ( + + {status} + + ); +} + +function formatJson(jsonString: string | null): string { + if (!jsonString) return "N/A"; + try { + return JSON.stringify(JSON.parse(jsonString), null, 2); + } catch { + return jsonString; + } +} + +function CopyableCodeBlock({ content, label }: { content: string; label: string }) { + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(content); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + console.error("Failed to copy:", err); + } + }; + + return ( +
+
+

{label}

+ +
+
+        {content}
+      
+
+ ); +} + +function ExpandableSubtaskTile({ + subtask, + index, + isExpanded, + onToggle, +}: { + subtask: SubTask; + index: number; + isExpanded: boolean; + onToggle: () => void; +}) { + const isFailed = subtask.status.toLowerCase() === "failed"; + + return ( + + + + {isExpanded && ( +
+ {/* Failure callout */} + {isFailed && ( +
+ +
+

This subtask failed.

+ {subtask.cloudLogsURL && ( + + View error logs → + + )} +
+
+ )} + + {/* Timeline */} + {(subtask.jobStarted || subtask.jobCompleted) && ( +
+ {subtask.jobStarted && ( +
+

Started

+

+ {new Date(subtask.jobStarted).toLocaleString()} +

+
+ )} + {subtask.jobCompleted && ( +
+

Completed

+

+ {new Date(subtask.jobCompleted).toLocaleString()} +

+
+ )} +
+ )} + + {/* Inputs */} + {subtask.inputs && ( + + )} + + {/* Outputs */} + {subtask.outputs && ( + + )} + + {/* Cloud Logs (for non-failed subtasks) */} + {subtask.cloudLogsURL && !isFailed && ( + + )} + +

+ Updated: {new Date(subtask.updatedAt).toLocaleString()} +

+
+ )} +
+ ); +} + +export default function PortfolioSubtaskDetails({ + subtasks, + task, +}: PortfolioSubtaskDetailsProps) { + const [expandedSubtasks, setExpandedSubtasks] = useState>({}); + + useEffect(() => { + setExpandedSubtasks({}); + }, [task?.id]); + + const toggleSubtask = (subtaskId: string) => { + setExpandedSubtasks((prev) => ({ ...prev, [subtaskId]: !prev[subtaskId] })); + }; + + const total = Number(task?.totalSubtasks ?? 0); + const completed = Number(task?.completedSubtasks ?? 0); + const failed = Number(task?.failedSubtasks ?? 0); + const completionPct = total > 0 ? Math.round((completed / total) * 100) : 0; + const remainingCount = total - completed; + const isAllDone = total > 0 && completed === total; + + return ( +
+ {/* Task Header */} + {task && ( +
+
+
+

{task.taskSource}

+ +
+ + {/* Enriched stats */} + {total > 0 && ( + <> +
+
0 ? "bg-red-500" : "bg-blue-500" + )} + style={{ width: `${completionPct}%` }} + /> +
+
+ + {completionPct}% complete + + · + {remainingCount} remaining + {failed > 0 && ( + <> + · + + + {failed} failed + + + )} +
+ + )} + +
+
+

Task ID

+ {task.id} +
+ {task.service && ( +
+

Service

+

{task.service}

+
+ )} + {task.jobStarted && ( +
+

Job Started

+

+ {new Date(task.jobStarted).toLocaleString()} +

+
+ )} + {task.jobCompleted && ( +
+

Job Completed

+

+ {new Date(task.jobCompleted).toLocaleString()} +

+
+ )} +
+
+
+ )} + + {/* Subtasks List */} + +
+

+ Subtasks ({subtasks.length}) +

+ + {subtasks.length === 0 && ( +
No subtasks found
+ )} + +
+ {subtasks.map((subtask, index) => ( + toggleSubtask(subtask.id)} + /> + ))} +
+
+
+
+ ); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/settings/logs/PortfolioTaskList.tsx b/src/app/portfolio/[slug]/(portfolio)/settings/logs/PortfolioTaskList.tsx new file mode 100644 index 0000000..0204cea --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/settings/logs/PortfolioTaskList.tsx @@ -0,0 +1,339 @@ +"use client"; + +import { useState, useMemo } from "react"; +import { PortfolioTask } from "./PortfolioLogs"; +import { cn } from "@/lib/utils"; +import { Button } from "@/app/shadcn_components/ui/button"; +import { Input } from "@/app/shadcn_components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/app/shadcn_components/ui/select"; + +interface PortfolioTaskListProps { + tasks: PortfolioTask[]; + selectedTaskId: string | null; + onSelectTask: (taskId: string) => void; + loading: boolean; + loadingMore: boolean; + error: string | null; + total: number; + onLoadMore: () => void; + onRefresh: () => void; +} + +type SortOption = "recent" | "oldest" | "status" | "service"; + +function StatusPill({ status }: { status: string }) { + const s = status.toLowerCase(); + const isComplete = s === "completed" || s === "complete"; + const isInProgress = s === "in progress"; + const isFailed = s === "failed" || s === "failure" || s === "error"; + + return ( + + {status} + + ); +} + +export default function PortfolioTaskList({ + tasks, + selectedTaskId, + onSelectTask, + loading, + loadingMore, + error, + total, + onLoadMore, + onRefresh, +}: PortfolioTaskListProps) { + const [searchQuery, setSearchQuery] = useState(""); + const [statusFilter, setStatusFilter] = useState("all"); + const [serviceFilter, setServiceFilter] = useState("all"); + const [sortBy, setSortBy] = useState("recent"); + + const uniqueStatuses = useMemo( + () => Array.from(new Set(tasks.map((t) => t.status))).sort(), + [tasks], + ); + const uniqueServices = useMemo( + () => + Array.from( + new Set(tasks.map((t) => t.service).filter(Boolean)), + ).sort() as string[], + [tasks], + ); + + const filteredTasks = useMemo(() => { + let result = tasks; + + if (statusFilter !== "all") { + result = result.filter((t) => t.status === statusFilter); + } + if (serviceFilter !== "all") { + result = result.filter((t) => t.service === serviceFilter); + } + if (searchQuery) { + const query = searchQuery.toLowerCase(); + result = result.filter( + (t) => + t.id.toLowerCase().includes(query) || + t.taskSource.toLowerCase().includes(query) || + (t.service?.toLowerCase().includes(query) ?? false), + ); + } + + const sorted = [...result]; + switch (sortBy) { + case "recent": + sorted.sort( + (a, b) => + new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(), + ); + break; + case "oldest": + sorted.sort( + (a, b) => + new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime(), + ); + break; + case "status": + sorted.sort((a, b) => a.status.localeCompare(b.status)); + break; + case "service": + sorted.sort((a, b) => (a.service ?? "").localeCompare(b.service ?? "")); + break; + } + + return sorted; + }, [tasks, statusFilter, serviceFilter, searchQuery, sortBy]); + + return ( +
+ {/* Header */} +
+
+
+

Tasks

+

+ {filteredTasks.length} of {tasks.length} (Total: {total}) +

+
+ +
+
+ + {/* Filters */} +
+ setSearchQuery(e.target.value)} + className="text-xs h-8" + /> + + {uniqueStatuses.length > 0 && ( + + )} + {uniqueServices.length > 0 && ( + + )} + {(searchQuery || statusFilter !== "all" || serviceFilter !== "all") && ( + + )} +
+ + {/* Content */} +
+ {error && ( +
+ {error} +
+ )} + {loading && ( +
+ Loading tasks... +
+ )} + {!loading && !error && tasks.length === 0 && ( +
+ No tasks found for this portfolio +
+ )} + +
+ {filteredTasks.map((task) => { + const total = Number(task.totalSubtasks); + const completed = Number(task.completedSubtasks); + const failed = Number(task.failedSubtasks); + const completionPct = + total > 0 ? Math.round((completed / total) * 100) : 0; + const remainingCount = total - completed; + const isAllDone = total > 0 && completed === total; + + return ( + + ); + })} + + {tasks.length < total && ( +
+ +
+ )} +
+
+
+ ); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/settings/logs/page.tsx b/src/app/portfolio/[slug]/(portfolio)/settings/logs/page.tsx new file mode 100644 index 0000000..cf5ae8a --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/settings/logs/page.tsx @@ -0,0 +1,22 @@ +import { redirect } from "next/navigation"; +import { getServerSession } from "next-auth"; +import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import PortfolioLogs from "./PortfolioLogs"; + +export default async function LogsPage(props: { + params: Promise<{ slug: string }>; +}) { + const { slug } = await props.params; + const session = await getServerSession(AuthOptions); + const isDomnaUser = !!session?.user?.email?.endsWith("@domna.homes"); + + if (!isDomnaUser) { + redirect(`/portfolio/${slug}/settings/general`); + } + + return ( +
+ +
+ ); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/settings/page.tsx b/src/app/portfolio/[slug]/(portfolio)/settings/page.tsx index 67ae43f..8d30a54 100644 --- a/src/app/portfolio/[slug]/(portfolio)/settings/page.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/settings/page.tsx @@ -1,23 +1,8 @@ -import { getPortfolioSettings } from "../../utils"; -import PortfolioSettings from "./PortfolioSettings"; +import { redirect } from "next/navigation"; -export default async function PortfolioSettingsPage( - props: { - params: Promise<{ slug: string }>; - } -) { - const params = await props.params; - const portfolioId = params.slug; - const portfolioSettingsData = await getPortfolioSettings(portfolioId); - - return ( - <> -
- -
- - ); +export default async function SettingsRootPage(props: { + params: Promise<{ slug: string }>; +}) { + const { slug } = await props.params; + redirect(`/portfolio/${slug}/settings/general`); } diff --git a/src/app/portfolio/[slug]/(portfolio)/settings/user-access/page.tsx b/src/app/portfolio/[slug]/(portfolio)/settings/user-access/page.tsx new file mode 100644 index 0000000..5d749ab --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/settings/user-access/page.tsx @@ -0,0 +1,13 @@ +import { UsersPermissionsCard } from "../UsersPermissionsCard"; + +export default async function UserAccessPage(props: { + params: Promise<{ slug: string }>; +}) { + const { slug } = await props.params; + + return ( +
+ +
+ ); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/AnalyticsView.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/AnalyticsView.tsx new file mode 100644 index 0000000..73b3dec --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/AnalyticsView.tsx @@ -0,0 +1,411 @@ +"use client"; + +import { useState } from "react"; +import { motion } from "framer-motion"; +import { Home, AlertTriangle, ToggleLeft, ToggleRight } from "lucide-react"; +import { Card, CardContent } from "@/app/shadcn_components/ui/card"; +import SurveyedResultsPieChart from "./SurveyedResultsPieChart"; +import DampMouldRiskPanel from "./DampMouldRiskPanel"; +import CompletionTrendsChart from "./CompletionTrendsChart"; +import SurveyIssuesPanel from "./SurveyIssuesPanel"; +import { STAGE_COLORS, STAGE_ORDER } from "./types"; +import type { + ProjectData, + ClassifiedDeal, + TableModal, + FunnelStage, + DisplayStage, +} from "./types"; + +// ----------------------------------------------------------------------- +// Stat card (reused from original LiveTracker) +// ----------------------------------------------------------------------- +function StatCard({ + icon: Icon, + title, + value, + subtitle, + onClick, + accent = "brandblue", +}: { + icon: React.ElementType; + title: string; + value: string | number; + subtitle?: string; + onClick: () => void; + accent?: "brandblue" | "red" | "bright-red"; +}) { + const accentConfig = { + brandblue: { + gradient: "from-brandlightblue/30 to-brandlightblue/10", + border: "border-brandblue/20", + text: "text-brandblue", + value: "text-brandblue", + hover: "hover:border-brandblue/40 hover:shadow-lg", + icon: "text-brandblue", + }, + red: { + gradient: "from-red-100/30 to-red-50/20", + border: "border-red-300/40", + text: "text-red-500", + value: "text-red-500", + hover: "hover:border-red-300/60 hover:shadow-lg", + icon: "text-red-500", + }, + "bright-red": { + gradient: "from-red-100 to-red-50", + border: "border-red-500", + text: "text-red-700", + value: "text-red-900", + hover: "hover:border-red-600 hover:shadow-lg", + icon: "text-red-700", + }, + }; + const config = accentConfig[accent]; + + return ( + +
+
+

+ {title} +

+

+ {value} + {subtitle && ( + + {subtitle} + + )} +

+
+ +
+
+ ); +} + +// ----------------------------------------------------------------------- +// Per-stage column config for the drill-down table +// ----------------------------------------------------------------------- +type StageTableConfig = { + cols: (keyof ClassifiedDeal)[]; + labels: Partial>; +}; + +const STAGE_TABLE_CONFIG: Record = { + "Booking in Progress": { + cols: ["dealname", "landlordPropertyId", "confirmedSurveyDate", "ioeV1Date"], + labels: { + dealname: "Address", + landlordPropertyId: "Ref", + confirmedSurveyDate: "Confirmed Survey Date", + ioeV1Date: "Expected Commencement", + }, + }, + "Assessment in Progress": { + cols: ["dealname", "landlordPropertyId", "confirmedSurveyDate", "ioeV1Date", "outcome", "coordinator"], + labels: { + dealname: "Address", + landlordPropertyId: "Ref", + confirmedSurveyDate: "Confirmed Survey Date", + ioeV1Date: "Expected Commencement", + outcome: "Outcome", + coordinator: "Surveyor", + }, + }, + "Coordination in Progress": { + cols: ["dealname", "landlordPropertyId", "coordinator", "preSapScore", "coordinationStatus"], + labels: { + dealname: "Address", + landlordPropertyId: "Ref", + coordinator: "Coordinator", + preSapScore: "Pre-SAP Score", + coordinationStatus: "Coordination Status", + }, + }, + "Design in Progress": { + cols: ["dealname", "landlordPropertyId", "designer", "proposedMeasures", "designType"], + labels: { + dealname: "Address", + landlordPropertyId: "Ref", + designer: "Designer", + proposedMeasures: "Proposed Measures", + designType: "Design Type", + }, + }, + _default: { + cols: ["dealname", "landlordPropertyId", "displayStage", "installer"], + labels: { + dealname: "Address", + landlordPropertyId: "Ref", + displayStage: "Stage", + installer: "Installer", + }, + }, +}; + +// ----------------------------------------------------------------------- +// Pipeline Funnel — rich card rows +// ----------------------------------------------------------------------- +function PipelineFunnel({ + funnelStages, + allDeals, + onOpenTable, +}: { + funnelStages: FunnelStage[]; + allDeals: ClassifiedDeal[]; + onOpenTable: ( + stage: string, + deals: ClassifiedDeal[], + columns?: (keyof ClassifiedDeal)[], + columnLabels?: Partial>, + breakdown?: Record, + title?: string, + description?: string, + reason?: string, + ) => void; +}) { + const [mode, setMode] = useState<"current" | "cumulative">("current"); + + const ALWAYS_VISIBLE: DisplayStage[] = ["At Lodgement", "Project Complete"]; + const visibleStages = funnelStages.filter( + (s) => s.currentCount > 0 || s.cumulativeCount > 0 || ALWAYS_VISIBLE.includes(s.stage), + ); + + const maxCount = Math.max( + ...visibleStages.map((s) => + mode === "current" ? s.currentCount : s.cumulativeCount, + ), + 1, + ); + + return ( + + +
+
+

+ Pipeline Overview +

+

+ {mode === "cumulative" + ? "Properties that have reached each stage or beyond" + : "Properties currently at each stage"} +

+
+ +
+ +
+ {visibleStages.map((s) => { + const count = + mode === "current" ? s.currentCount : s.cumulativeCount; + const pct = mode === "current" ? s.currentPct : s.cumulativePct; + const pastCount = s.cumulativeCount - s.currentCount; + const barWidth = maxCount > 0 ? (count / maxCount) * 100 : 0; + const c = STAGE_COLORS[s.stage]; + + const deals = allDeals.filter((d) => + mode === "current" + ? d.displayStage === s.stage + : STAGE_ORDER.indexOf(d.displayStage) >= + STAGE_ORDER.indexOf(s.stage), + ); + + return ( + { + const { cols, labels } = STAGE_TABLE_CONFIG[s.stage] ?? STAGE_TABLE_CONFIG._default; + onOpenTable(`Pipeline — ${s.stage}`, deals, cols, labels); + }} + className={`w-full text-left rounded-xl border ${c.border} ${c.bg} p-4 shadow-sm hover:shadow-md transition-shadow`} + type="button" + > + {/* Header row: dot + name + pct badge */} +
+
+ + + {s.stage} + +
+ + {pct.toFixed(0)}% + +
+ + {/* Progress bar */} +
+ 0 ? "0.5rem" : 0 }} + /> +
+ + {/* Stats row */} +
+
+ {count} + + {mode === "current" ? "here now" : "reached stage"} + +
+ {mode === "cumulative" && pastCount > 0 && ( +
+ {pastCount} + {" past this stage"} +
+ )} +
+
+ ); + })} +
+
+
+ ); +} + +// ----------------------------------------------------------------------- +// AnalyticsView — props +// ----------------------------------------------------------------------- +interface AnalyticsViewProps { + projects: { projectCode: string }[]; + currentProject: ProjectData; + currentProjectCode: string; + onProjectChange: (code: string) => void; + onOpenTable: ( + stage: string, + deals: ClassifiedDeal[], + columns?: (keyof ClassifiedDeal)[], + columnLabels?: Partial>, + breakdown?: Record, + ) => void; + majorConditionDeals: ClassifiedDeal[]; + totalDeals: number; +} + +export default function AnalyticsView({ + projects, + currentProject, + currentProjectCode, + onProjectChange, + onOpenTable, + majorConditionDeals, + totalDeals, +}: AnalyticsViewProps) { + return ( +
+ {/* Row 1: project selector + stat card (Properties in project) */} +
+ {/* Project selector */} + +
+

+ Select Project +

+
+ +
+
+
+ + {/* Properties in project */} + + onOpenTable( + currentProjectCode === "__ALL__" ? "All Properties" : `${currentProjectCode} — All Properties`, + currentProject.allDeals, + ["dealname", "landlordPropertyId"], + { dealname: "Address Ref.", landlordPropertyId: "Property Ref." }, + ) + } + accent="brandblue" + /> +
+ + {/* Row 1.5: Completion trends chart */} + + + {/* Row 2: section header */} +
+

+ Project Insights —{" "} + + {currentProjectCode === "__ALL__" ? "All Projects" : currentProjectCode} + +

+
+ + {/* Row 4: Pipeline Funnel */} + + + {/* Row 5: Damp & Mould Risk (moved up) */} + + + {/* Row 6: Survey Issues */} + +
+ ); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/CompletionTrendsChart.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/CompletionTrendsChart.tsx new file mode 100644 index 0000000..d869e97 --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/CompletionTrendsChart.tsx @@ -0,0 +1,480 @@ +"use client"; + +import { useState } from "react"; +import { AlertCircle } from "lucide-react"; +import { Card, Title } from "@tremor/react"; +import { + BarChart as RechartsBarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + LabelList, + ResponsiveContainer, + Legend as RechartsLegend, +} from "recharts"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, +} from "@/app/shadcn_components/ui/select"; +import type { ClassifiedDeal } from "./types"; + +interface CompletionTrendsChartProps { + deals: ClassifiedDeal[]; + projectCode?: string; + onOpenTable?: ( + stage: string, + deals: ClassifiedDeal[], + columns?: (keyof ClassifiedDeal)[], + columnLabels?: Partial>, + ) => void; +} + +const METRICS = [ + { key: "bookings", label: "Bookings", dateField: "confirmedSurveyDate" }, + { + key: "assessments", + label: "Completed Assessments", + dateField: "surveyedDate", + }, + { + key: "coordination", + label: "Completed Coordination", + dateField: "ioeV1Date", + }, + { key: "design", label: "Completed Designs", dateField: "designDate" }, + { + key: "lodgement", + label: "Completed Lodgements", + dateField: "fullLodgementDate", + }, +]; + +// Brand colour palette +const C = { + blue: "#5d6be0", + midblue: "#3943b7", + lightblue: "#8b96e9", + paleblue: "#b8bef4", + brown: "#c4a47c", +}; + +function ChartTooltip({ + payload, + active, + label, +}: { + payload?: { name: string; value: number; color: string }[]; + active?: boolean; + label?: string; +}) { + if (!active || !payload?.length) return null; + // Filter out the internal _total key + const visible = payload.filter((p) => p.name !== "_total"); + if (!visible.length) return null; + return ( +
+

{label}

+ {visible.map((item, i) => ( +
+
+ + {item.name} +
+ {item.value} +
+ ))} +
+ ); +} + +// Client-facing design type labels +const DESIGN_TYPE_LABELS: Record = { + "Archetype (Complex)": "Bespoke (Complex)", + "Archetype (Simple)": "Bespoke (Simple)", + "Repetitive (Complex)": "Standard (Complex)", + "Repetitive (Simple)": "Standard (Simple)", +}; +const DESIGN_TYPE_ORDER = [ + "Bespoke (Complex)", + "Bespoke (Simple)", + "Standard (Complex)", + "Standard (Simple)", +]; + +function getMondayOfWeek(date: Date): string { + const d = new Date(date); + const day = d.getDay(); + d.setDate(d.getDate() - (day === 0 ? 6 : day - 1)); + d.setHours(0, 0, 0, 0); + return d.toISOString().split("T")[0]; +} + +function formatMonday(isoDate: string): string { + return new Date(isoDate).toLocaleDateString("en-GB", { + day: "numeric", + month: "short", + year: "numeric", + }); +} + +function fillWeekGaps(keys: string[]): string[] { + if (keys.length === 0) return []; + const sorted = [...keys].sort(); + const result: string[] = []; + const current = new Date(sorted[0]); + const end = new Date(sorted[sorted.length - 1]); + while (current <= end) { + result.push(current.toISOString().split("T")[0]); + current.setDate(current.getDate() + 7); + } + return result; +} + +function aggregateByWeek( + deals: ClassifiedDeal[], + dateField: string, + filter?: (deal: ClassifiedDeal) => boolean, +) { + const weekCounts: Record = {}; + for (const deal of deals) { + if (filter && !filter(deal)) continue; + const date = deal[dateField as keyof ClassifiedDeal] as string | Date | null; + if (!date) continue; + const d = new Date(date); + if (isNaN(d.getTime())) continue; + const key = getMondayOfWeek(d); + weekCounts[key] = (weekCounts[key] || 0) + 1; + } + const allKeys = fillWeekGaps(Object.keys(weekCounts)); + return allKeys.map((isoKey) => ({ + week: formatMonday(isoKey), + value: weekCounts[isoKey] ?? 0, + })); +} + +function aggregateCoordinationByWeek( + deals: ClassifiedDeal[], +): Array<{ week: string; "V1 (MTP)": number; "V2 (Re-model)": number; _total: number }> { + const v1Counts: Record = {}; + const v2Counts: Record = {}; + + for (const deal of deals) { + const status = (deal.coordinationStatus ?? "").toUpperCase(); + if (status.includes("(V1) IOE/MTP COMPLETE") && deal.ioeV1Date) { + const d = new Date(deal.ioeV1Date); + if (!isNaN(d.getTime())) { + const key = getMondayOfWeek(d); + v1Counts[key] = (v1Counts[key] || 0) + 1; + } + } + if (status.includes("(V2) IOE/MTP COMPLETE") && deal.ioeV2Date) { + const d = new Date(deal.ioeV2Date); + if (!isNaN(d.getTime())) { + const key = getMondayOfWeek(d); + v2Counts[key] = (v2Counts[key] || 0) + 1; + } + } + } + + const allKeys = fillWeekGaps( + Array.from(new Set([...Object.keys(v1Counts), ...Object.keys(v2Counts)])), + ); + + return allKeys.map((isoKey) => { + const v1 = v1Counts[isoKey] ?? 0; + const v2 = v2Counts[isoKey] ?? 0; + return { week: formatMonday(isoKey), "V1 (MTP)": v1, "V2 (Re-model)": v2, _total: v1 + v2 }; + }); +} + +function aggregateAssessmentsByWeek( + deals: ClassifiedDeal[], +): Array<{ week: string; "Retrofit Assessment": number; EPC: number; _total: number }> { + const retrofitCounts: Record = {}; + const epcCounts: Record = {}; + + for (const deal of deals) { + const o = deal.outcome ?? ""; + const isRetrofit = o === "Surveyed" || o === "Surveyed - Pending Upload"; + const isEpc = o === "EPC Completed"; + if (!isRetrofit && !isEpc) continue; + if (!deal.surveyedDate) continue; + const d = new Date(deal.surveyedDate); + if (isNaN(d.getTime())) continue; + const key = getMondayOfWeek(d); + if (isRetrofit) retrofitCounts[key] = (retrofitCounts[key] || 0) + 1; + if (isEpc) epcCounts[key] = (epcCounts[key] || 0) + 1; + } + + const allKeys = fillWeekGaps( + Array.from(new Set([...Object.keys(retrofitCounts), ...Object.keys(epcCounts)])), + ); + + return allKeys.map((isoKey) => { + const r = retrofitCounts[isoKey] ?? 0; + const e = epcCounts[isoKey] ?? 0; + return { week: formatMonday(isoKey), "Retrofit Assessment": r, EPC: e, _total: r + e }; + }); +} + +function aggregateLodgementsByWeek( + deals: ClassifiedDeal[], +): Array<{ week: string; "Stage 1 Lodgement": number; "Lodged Measures": number; _total: number }> { + const stageCounts: Record = {}; + const measuresCounts: Record = {}; + + for (const deal of deals) { + if (deal.fullLodgementDate) { + const d = new Date(deal.fullLodgementDate); + if (!isNaN(d.getTime())) { + const key = getMondayOfWeek(d); + stageCounts[key] = (stageCounts[key] || 0) + 1; + } + } + if (deal.measuresLodgementDate) { + const d = new Date(deal.measuresLodgementDate); + if (!isNaN(d.getTime())) { + const key = getMondayOfWeek(d); + measuresCounts[key] = (measuresCounts[key] || 0) + 1; + } + } + } + + const allKeys = fillWeekGaps( + Array.from(new Set([...Object.keys(stageCounts), ...Object.keys(measuresCounts)])), + ); + + return allKeys.map((isoKey) => { + const s = stageCounts[isoKey] ?? 0; + const m = measuresCounts[isoKey] ?? 0; + return { week: formatMonday(isoKey), "Stage 1 Lodgement": s, "Lodged Measures": m, _total: s + m }; + }); +} + +function aggregateDesignsByWeek( + deals: ClassifiedDeal[], +): Array> { + const counts: Record> = {}; + + for (const deal of deals) { + if (deal.designStatus?.toUpperCase() !== "UPLOADED") continue; + if (!deal.designDate) continue; + const d = new Date(deal.designDate); + if (isNaN(d.getTime())) continue; + const key = getMondayOfWeek(d); + const rawType = deal.designType ?? "Unknown"; + const label = DESIGN_TYPE_LABELS[rawType] ?? rawType; + if (!counts[key]) counts[key] = {}; + counts[key][label] = (counts[key][label] || 0) + 1; + } + + const allKeys = fillWeekGaps(Object.keys(counts)); + return allKeys.map((isoKey) => { + const entry: Record = { week: formatMonday(isoKey) }; + let total = 0; + for (const label of DESIGN_TYPE_ORDER) { + const v = counts[isoKey]?.[label] ?? 0; + entry[label] = v; + total += v; + } + entry._total = total; + return entry; + }); +} + +// Compute total completed count for metrics that support it +function computeTotalCompleted( + metric: string, + chartData: Record[], + categories: string[], +): number | null { + if (!["bookings", "assessments", "coordination", "design"].includes(metric)) return null; + return chartData.reduce((sum, row) => { + return sum + categories.reduce((s, cat) => s + ((row[cat] as number) || 0), 0); + }, 0); +} + +export default function CompletionTrendsChart({ + deals, + onOpenTable, +}: CompletionTrendsChartProps) { + const [metric, setMetric] = useState(METRICS[0].key); + + const selectedMetric = METRICS.find((m) => m.key === metric)!; + const isCoordination = metric === "coordination"; + const isAssessments = metric === "assessments"; + const isLodgement = metric === "lodgement"; + const isDesign = metric === "design"; + const isStacked = isCoordination || isAssessments || isLodgement || isDesign; + + // External assessments with no date + const undatedAssessments = isAssessments + ? deals.filter((d) => { + const o = d.outcome ?? ""; + return (o === "Surveyed" || o === "Surveyed - Pending Upload") && !d.surveyedDate; + }) + : []; + + // Build chart data + let chartData: Record[]; + let categories: string[]; + let colors: string[]; + + if (isCoordination) { + chartData = aggregateCoordinationByWeek(deals); + categories = ["V1 (MTP)", "V2 (Re-model)"]; + colors = [C.blue, C.midblue]; + } else if (isAssessments) { + chartData = aggregateAssessmentsByWeek(deals); + categories = ["Retrofit Assessment", "EPC"]; + colors = [C.blue, C.midblue]; + } else if (isLodgement) { + chartData = aggregateLodgementsByWeek(deals); + categories = ["Stage 1 Lodgement", "Lodged Measures"]; + colors = [C.blue, C.lightblue]; + } else if (isDesign) { + chartData = aggregateDesignsByWeek(deals); + categories = DESIGN_TYPE_ORDER; + colors = [C.midblue, C.blue, C.lightblue, C.paleblue]; + } else { + const singleData = aggregateByWeek(deals, selectedMetric.dateField); + chartData = singleData.map((d) => ({ + week: d.week, + [selectedMetric.label]: d.value, + })); + categories = [selectedMetric.label]; + colors = [C.blue]; + } + + const totalCompleted = computeTotalCompleted(metric, chartData, categories); + + return ( + + {/* Header row */} +
+
+ + Trends Over Time + + {totalCompleted !== null && ( +
+ {totalCompleted} + + {metric === "bookings" ? "booked to date" : "completed to date"} + + +
+ )} +
+
+ +
+
+ + {/* Undated external assessments — shown above the chart */} + {isAssessments && undatedAssessments.length > 0 && ( +
+
+ + + {undatedAssessments.length}{" "} + external assessment{undatedAssessments.length !== 1 ? "s" : ""} have no date recorded + +
+ {onOpenTable && ( + + )} +
+ )} + + {/* Chart */} + + + + + + } cursor={{ fill: "rgba(89,107,224,0.06)" }} /> + {categories.map((cat, i) => ( + + {/* For stacked bars: show total on the top (last) bar only via _total. + For non-stacked: show each bar's own value. */} + {i === categories.length - 1 && ( + (v === 0 ? "" : v)} + /> + )} + + ))} + + + + {/* Legend for stacked charts */} + {isStacked && ( + ({ + value: cat, + type: "square" as const, + color: colors[i], + }))} + /> + )} +
+ ); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DampMouldRiskPanel.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DampMouldRiskPanel.tsx new file mode 100644 index 0000000..4b25709 --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DampMouldRiskPanel.tsx @@ -0,0 +1,222 @@ +"use client"; + +import { motion } from "framer-motion"; +import { Droplets, AlertTriangle, ShieldAlert } from "lucide-react"; +import { Card, CardContent } from "@/app/shadcn_components/ui/card"; +import type { DampMouldRiskData, ClassifiedDeal } from "./types"; + +interface DampMouldRiskPanelProps { + risk: DampMouldRiskData; + onOpenTable: ( + stage: string, + deals: ClassifiedDeal[], + columns?: (keyof ClassifiedDeal)[], + columnLabels?: Partial> + ) => void; +} + +function RiskStatCard({ + label, + subtitle, + count, + total, + icon: Icon, + color, + onClick, +}: { + label: string; + subtitle: string; + count: number; + total: number; + icon: React.ElementType; + color: "amber" | "orange" | "red"; + onClick: () => void; +}) { + const pct = total > 0 ? ((count / total) * 100).toFixed(1) : "0.0"; + + const styles = { + amber: { + gradient: "from-amber-50 to-amber-50/30", + border: "border-amber-200", + hover: "hover:border-amber-300 hover:shadow-md", + icon: "text-amber-500", + badge: "bg-amber-100 text-amber-700", + bar: "bg-amber-400", + value: "text-amber-700", + }, + orange: { + gradient: "from-orange-50 to-orange-50/30", + border: "border-orange-200", + hover: "hover:border-orange-300 hover:shadow-md", + icon: "text-orange-500", + badge: "bg-orange-100 text-orange-700", + bar: "bg-orange-400", + value: "text-orange-700", + }, + red: { + gradient: "from-red-50 to-red-50/30", + border: "border-red-300", + hover: "hover:border-red-400 hover:shadow-md", + icon: "text-red-500", + badge: "bg-red-100 text-red-700", + bar: "bg-red-500", + value: "text-red-700", + }, + }; + + const s = styles[color]; + + return ( + +
+
+ +
+ + {pct}% + +
+ +

{count}

+

{label}

+

{subtitle}

+ + {/* Mini progress bar */} +
+
+
+ + ); +} + +export default function DampMouldRiskPanel({ + risk, + onOpenTable, +}: DampMouldRiskPanelProps) { + const { totalDeals } = risk; + + const surveyColumns: (keyof ClassifiedDeal)[] = [ + "dealname", + "landlordPropertyId", + "majorConditionIssueDescription", + "majorConditionIssuePhotosS3", + ]; + + const surveyLabels: Partial> = { + dealname: "Address", + landlordPropertyId: "Property Ref", + majorConditionIssueDescription: "Surveyor Notes", + majorConditionIssuePhotosS3: "Photo Evidence", + }; + + const coordColumns: (keyof ClassifiedDeal)[] = [ + "dealname", + "landlordPropertyId", + "dampMouldFlag", + "coordinator", + ]; + + const coordLabels: Partial> = { + dealname: "Address", + landlordPropertyId: "Property Ref", + dampMouldFlag: "Coordinator Flag", + coordinator: "Coordinator", + }; + + const noRisk = + risk.surveyFlagCount === 0 && + risk.coordinatorFlagCount === 0; + + return ( + + + {/* Header */} +
+
+ +
+
+

+ Awaab's Law — Damp & Mould Risk +

+

+ Comparison of flags raised at survey vs coordination stage +

+
+
+ + {noRisk ? ( +
+
+ +
+

+ No damp or mould flags recorded for this project. +

+
+ ) : ( + <> +
+ + onOpenTable( + "Damp & Mould — Survey Stage Flags", + risk.surveyFlagDeals, + surveyColumns, + surveyLabels + ) + } + /> + + onOpenTable( + "Damp & Mould — Coordination Stage Flags", + risk.coordinatorFlagDeals, + coordColumns, + coordLabels + ) + } + /> +
+ + {/* Missed risk callout */} + {risk.coordinatorFlagCount > risk.surveyFlagCount && ( +
+ +

+ + {risk.coordinatorFlagCount - risk.surveyFlagCount} additional{" "} + {risk.coordinatorFlagCount - risk.surveyFlagCount === 1 ? "property was" : "properties were"}{" "} + + flagged for damp & mould at the coordination stage that{" "} + {risk.coordinatorFlagCount - risk.surveyFlagCount === 1 ? "was" : "were"} not + identified during the initial survey. +

+
+ )} + + )} +
+
+ ); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DocumentTable.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DocumentTable.tsx new file mode 100644 index 0000000..b89ba9b --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DocumentTable.tsx @@ -0,0 +1,289 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { + useReactTable, + getCoreRowModel, + getFilteredRowModel, + getSortedRowModel, + getPaginationRowModel, + flexRender, + type SortingState, + type PaginationState, +} from "@tanstack/react-table"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/app/shadcn_components/ui/table"; +import { Input } from "@/app/shadcn_components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, +} from "@/app/shadcn_components/ui/select"; +import { Search, ChevronLeft, ChevronRight, Download } from "lucide-react"; +import { createDocumentTableColumns } from "./DocumentTableColumns"; +import type { ClassifiedDeal, DocStatusMap } from "./types"; + +type SurveyStatusFilter = "all" | "none" | "partial" | "complete"; + +interface DocumentTableProps { + data: ClassifiedDeal[]; + onOpenDrawer: (uprn: string | null, landlordPropertyId: string | null, dealname: string | null) => void; + docStatusMap: DocStatusMap; +} + +function escapeCell(value: unknown): string { + if (value === null || value === undefined) return ""; + const str = + value instanceof Date + ? value.toLocaleDateString("en-GB") + : String(value); + return str.includes(",") || str.includes('"') || str.includes("\n") + ? `"${str.replace(/"/g, '""')}"` + : str; +} + +export default function DocumentTable({ data, onOpenDrawer, docStatusMap }: DocumentTableProps) { + const [globalFilter, setGlobalFilter] = useState(""); + const [surveyStatusFilter, setSurveyStatusFilter] = useState("all"); + const [sorting, setSorting] = useState([]); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 25, + }); + + const filteredData = useMemo(() => { + if (surveyStatusFilter === "all") return data; + return data.filter((d) => { + const status = d.uprn ? docStatusMap[d.uprn] : undefined; + if (surveyStatusFilter === "none") return !status || !status.hasDocs; + if (surveyStatusFilter === "partial") return !!status?.hasDocs && !status.isComplete; + if (surveyStatusFilter === "complete") return !!status?.isComplete; + return true; + }); + }, [data, surveyStatusFilter, docStatusMap]); + + const columns = useMemo( + () => createDocumentTableColumns(onOpenDrawer, docStatusMap), + [onOpenDrawer, docStatusMap], + ); + + const table = useReactTable({ + data: filteredData, + columns, + state: { globalFilter, sorting, pagination }, + onGlobalFilterChange: setGlobalFilter, + onSortingChange: setSorting, + onPaginationChange: setPagination, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + globalFilterFn: "includesString", + }); + + const downloadCsv = () => { + const rows = table.getFilteredRowModel().rows; + const header = "Address,Landlord ID,Survey Status"; + const body = rows + .map((row) => { + const status = row.original.uprn ? docStatusMap[row.original.uprn] : undefined; + const surveyStatus = status?.isComplete + ? "Complete" + : status?.hasDocs + ? "Partial" + : "No Docs"; + return [ + escapeCell(row.original.dealname), + escapeCell(row.original.landlordPropertyId), + surveyStatus, + ].join(","); + }) + .join("\n"); + const blob = new Blob([header + "\n" + body], { type: "text/csv;charset=utf-8;" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "document-management.csv"; + a.click(); + URL.revokeObjectURL(url); + }; + + const pageCount = table.getPageCount(); + const currentPage = table.getState().pagination.pageIndex + 1; + const totalFiltered = table.getFilteredRowModel().rows.length; + + const surveyStatusLabel: Record = { + all: "All statuses", + none: "No Survey Docs", + partial: "Partial Survey Docs", + complete: "Complete Survey Docs", + }; + + return ( +
+ {/* Toolbar */} +
+ {/* Search */} +
+ + { + setGlobalFilter(e.target.value); + setPagination((p) => ({ ...p, pageIndex: 0 })); + }} + placeholder="Search address, landlord ID…" + className="pl-9 h-9 text-sm border-gray-200 focus:border-brandblue/40 focus:ring-brandblue/20" + /> +
+ + {/* Survey status filter */} + + + {/* Download CSV */} + +
+ + {/* Result count */} +

+ Showing{" "} + + {Math.min( + table.getState().pagination.pageSize, + totalFiltered - table.getState().pagination.pageIndex * table.getState().pagination.pageSize, + )} + {" "} + of{" "} + {totalFiltered}{" "} + {surveyStatusFilter !== "all" ? `(${surveyStatusLabel[surveyStatusFilter].toLowerCase()}) ` : ""} + propert{totalFiltered === 1 ? "y" : "ies"} +

+ + {/* Table */} +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} + + ))} + + ))} + + + {table.getRowModel().rows.length ? ( + table.getRowModel().rows.map((row, i) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No properties match the current filters. + + + )} + +
+
+
+ + {/* Pagination */} + {pageCount > 1 && ( +
+
+ Rows per page: + +
+ +
+ + Page {currentPage} of {pageCount} + +
+ + +
+
+
+ )} +
+ ); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DocumentTableColumns.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DocumentTableColumns.tsx new file mode 100644 index 0000000..f88514d --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DocumentTableColumns.tsx @@ -0,0 +1,147 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; +import { ArrowUpDown, CheckCircle2, AlertCircle, FileX } from "lucide-react"; +import type { ClassifiedDeal, DocStatusMap, DocStatus } from "./types"; + +function SortableHeader({ + label, + column, +}: { + label: string; + column: { toggleSorting: (desc: boolean) => void; getIsSorted: () => false | "asc" | "desc" }; +}) { + return ( + + ); +} + +function SurveyStatusBadge({ status }: { status: DocStatus | undefined }) { + if (status?.isComplete) { + return ( + + + Complete + + ); + } + if (status?.hasDocs) { + return ( + + + Partial + + ); + } + return ( + + + No Docs + + ); +} + +export function createDocumentTableColumns( + onOpenDrawer: (uprn: string | null, landlordPropertyId: string | null, dealname: string | null) => void, + docStatusMap: DocStatusMap = {}, +): ColumnDef[] { + return [ + // ── Address ────────────────────────────────────────────────────────── + { + accessorKey: "dealname", + id: "dealname", + header: ({ column }) => , + cell: ({ row }) => ( +
+

+ {row.original.dealname ?? "—"} +

+
+ ), + enableHiding: false, + }, + + // ── Landlord ID ────────────────────────────────────────────────────── + { + accessorKey: "landlordPropertyId", + id: "landlordPropertyId", + header: ({ column }) => , + cell: ({ row }) => ( + + {row.original.landlordPropertyId ?? "—"} + + ), + enableHiding: false, + }, + + // ── Survey Status ───────────────────────────────────────────────────── + { + id: "surveyStatus", + accessorFn: (row) => { + const status = row.uprn ? docStatusMap[row.uprn] : undefined; + if (status?.isComplete) return 2; + if (status?.hasDocs) return 1; + return 0; + }, + header: ({ column }) => , + cell: ({ row }) => { + const status = row.original.uprn ? docStatusMap[row.original.uprn] : undefined; + return ; + }, + enableHiding: false, + }, + + // ── Documents button ───────────────────────────────────────────────── + { + id: "documents", + header: () => ( + Docs + ), + cell: ({ row }) => { + const uprn = row.original.uprn ?? ""; + const status = uprn ? docStatusMap[uprn] : undefined; + + let icon: React.ReactNode; + let className: string; + + if (status?.isComplete) { + icon = ; + className = + "inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs font-medium border border-emerald-200 text-emerald-700 bg-emerald-50 hover:bg-emerald-100 hover:border-emerald-300 transition-all duration-150 whitespace-nowrap"; + } else if (status?.hasDocs) { + icon = ; + className = + "inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs font-medium border border-amber-200 text-amber-700 bg-amber-50 hover:bg-amber-100 hover:border-amber-300 transition-all duration-150 whitespace-nowrap"; + } else { + icon = ; + className = + "inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs font-medium border border-gray-200 text-gray-400 bg-gray-50 hover:bg-gray-100 hover:border-gray-300 transition-all duration-150 whitespace-nowrap"; + } + + return ( + + ); + }, + enableSorting: false, + enableHiding: false, + }, + ]; +} diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DrillDownTable.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DrillDownTable.tsx new file mode 100644 index 0000000..7adf28e --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DrillDownTable.tsx @@ -0,0 +1,304 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { useMutation } from "@tanstack/react-query"; +import { + useReactTable, + getCoreRowModel, + getFilteredRowModel, + getSortedRowModel, + getPaginationRowModel, + flexRender, + type SortingState, + type PaginationState, + type ColumnDef, +} from "@tanstack/react-table"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/app/shadcn_components/ui/table"; +import { Input } from "@/app/shadcn_components/ui/input"; +import { Search, Download, ChevronLeft, ChevronRight } from "lucide-react"; +import type { ClassifiedDeal, HubspotDeal } from "./types"; + +interface DrillDownTableProps { + data: ClassifiedDeal[]; + columns?: (keyof HubspotDeal)[]; + columnLabels?: Partial>; +} + +function escapeCell(value: unknown): string { + if (value === null || value === undefined) return ""; + const str = + value instanceof Date ? value.toLocaleDateString("en-GB") : String(value); + return str.includes(",") || str.includes('"') || str.includes("\n") + ? `"${str.replace(/"/g, '""')}"` + : str; +} + +function PhotoDownloadButton({ url }: { url: string }) { + const { mutate: download, isPending } = useMutation({ + mutationFn: async () => { + const key = url.split(".amazonaws.com/")[1]; + if (!key) throw new Error("Invalid S3 key"); + const res = await fetch("/api/sign-s3-url", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ key }), + }); + if (!res.ok) throw new Error("Failed to get signed URL"); + const data = await res.json(); + return data.url as string; + }, + onSuccess: (signedUrl) => { + window.open(signedUrl, "_blank"); + }, + }); + + return ( + + ); +} + +function PhotoDownloadCell({ value }: { value: unknown }) { + let urls: string[] = []; + + if (typeof value === "string") { + try { + const parsed = JSON.parse(value); + urls = Array.isArray(parsed) ? parsed : [value]; + } catch { + urls = value.split(/[\s,]+/).filter((u) => u.startsWith("http")); + } + } else if (Array.isArray(value)) { + urls = value as string[]; + } + + if (urls.length === 0) return No photos; + + return ( +
+ {urls.map((url, idx) => ( + + ))} +
+ ); +} + +export default function DrillDownTable({ + data, + columns: columnKeys, + columnLabels, +}: DrillDownTableProps) { + const [globalFilter, setGlobalFilter] = useState(""); + const [sorting, setSorting] = useState([]); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 25, + }); + + const visibleKeys: (keyof HubspotDeal)[] = columnKeys?.length + ? columnKeys + : (Object.keys(data?.[0] || {}) as (keyof HubspotDeal)[]); + + const columns = useMemo[]>( + () => + visibleKeys.map((key) => ({ + accessorKey: key as string, + id: key as string, + header: () => ( + + {columnLabels?.[key] ?? (key as string)} + + ), + cell: ({ row }) => { + const value = row.original[key as keyof ClassifiedDeal]; + if (key === "majorConditionIssuePhotosS3") { + return ; + } + return ( + + {value != null ? String(value) : ( + + )} + + ); + }, + })), + // eslint-disable-next-line react-hooks/exhaustive-deps + [visibleKeys.join(","), columnLabels], + ); + + const table = useReactTable({ + data, + columns, + state: { globalFilter, sorting, pagination }, + onGlobalFilterChange: setGlobalFilter, + onSortingChange: setSorting, + onPaginationChange: setPagination, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + globalFilterFn: "includesString", + }); + + const downloadCsv = () => { + const rows = table.getFilteredRowModel().rows; + const exportKeys = visibleKeys.filter((k) => k !== "majorConditionIssuePhotosS3"); + const header = exportKeys + .map((k) => columnLabels?.[k] ?? (k as string)) + .join(","); + const body = rows + .map((row) => + exportKeys.map((k) => escapeCell(row.original[k as keyof ClassifiedDeal])).join(","), + ) + .join("\n"); + const blob = new Blob([header + "\n" + body], { + type: "text/csv;charset=utf-8;", + }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "data.csv"; + a.click(); + URL.revokeObjectURL(url); + }; + + const pageCount = table.getPageCount(); + const currentPage = table.getState().pagination.pageIndex + 1; + const totalFiltered = table.getFilteredRowModel().rows.length; + + return ( +
+ {/* Toolbar */} +
+
+ + { + setGlobalFilter(e.target.value); + setPagination((p) => ({ ...p, pageIndex: 0 })); + }} + placeholder="Search…" + className="pl-9 h-9 text-sm border-gray-200 focus:border-brandblue/40 focus:ring-brandblue/20" + /> +
+ +
+ + {/* Row count */} +

+ Showing{" "} + {totalFiltered}{" "} + {totalFiltered === 1 ? "row" : "rows"} +

+ + {/* Table */} +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ))} + + ))} + + + {table.getRowModel().rows.length ? ( + table.getRowModel().rows.map((row, i) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + No results found. + + + )} + +
+
+
+ + {/* Pagination */} + {pageCount > 1 && ( +
+ + Page {currentPage} of {pageCount} + +
+ + +
+
+ )} +
+ ); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx index 2765db0..502bd71 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx @@ -1,61 +1,85 @@ "use client"; import { useState } from "react"; -import ProgressOverview from "./ProgressOverview"; -import SurveyedResultsPieChart from "./SurveyedResultsPieChart"; -import TableViewer from "./TableViewer"; -import { Card, CardContent } from "@/app/shadcn_components/ui/card"; -import { Home, AlertTriangle } from "lucide-react"; import { motion } from "framer-motion"; -import type { LiveTrackerProps, TableModal, ClassifiedDeal, HubspotDeal } from "./types"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/app/shadcn_components/ui/tabs"; +import { Card, CardContent } from "@/app/shadcn_components/ui/card"; +import { BarChart2, Table2, FolderOpen } from "lucide-react"; +import DrillDownTable from "./DrillDownTable"; +import PropertyTable from "./PropertyTable"; +import DocumentTable from "./DocumentTable"; +import type { HubspotDeal } from "./types"; +import PropertyDrawer from "./PropertyDrawer"; +import PropertyDetailDrawer from "./PropertyDetailDrawer"; +import AnalyticsView from "./AnalyticsView"; +import type { + LiveTrackerProps, + TableModal, + ClassifiedDeal, + DocumentDrawerState, + DocStatusMap, +} from "./types"; export default function LiveTracker({ projects, totalDeals, majorConditionDeals, + docStatusMap, }: LiveTrackerProps) { - // UI State: which table modal is open - const [openTable, setOpenTable] = useState(null); + // ── Tab state ──────────────────────────────────────────────────────── + const [activeTab, setActiveTab] = useState<"analytics" | "properties" | "documents">( + "analytics", + ); - // UI State: which project tab is selected + // ── Project selector (shared across both tabs) ─────────────────────── const projectCodes = projects.map((p) => p.projectCode); const [currentProjectCode, setCurrentProjectCode] = useState(projectCodes[0]); const currentProject = projects.find( - (p) => p.projectCode === currentProjectCode + (p) => p.projectCode === currentProjectCode, ); - // Compute minor stuff inline (not data processing) - const majorIssues = majorConditionDeals.length; - const majorPercent = ((majorIssues / totalDeals) * 100).toFixed(1); - const hasSurveyData = (currentProject?.outcomePieSlices.length ?? 0) > 0; + // ── Drill-down table modal (used by AnalyticsView) ─────────────────── + const [openTable, setOpenTable] = useState(null); - // Group allDeals by outcome for pie chart click handler - const dealsByOutcome: Record = {}; - for (const deal of currentProject?.allDeals ?? []) { - if (deal.outcome) { - (dealsByOutcome[deal.outcome] ??= []).push(deal); - } - } + // ── Document drawer (used by PropertyTable) ────────────────────────── + const [drawerState, setDrawerState] = useState({ + open: false, + uprn: null, + landlordPropertyId: null, + dealname: null, + }); + + // ── Property detail drawer ─────────────────────────────────────────── + const [detailDeal, setDetailDeal] = useState(null); const handleOpenTable = ( stage: string, filteredDeals: ClassifiedDeal[], - columns?: (keyof HubspotDeal)[], - columnLabels?: Partial>, - breakdown?: Record + columns?: (keyof ClassifiedDeal)[], + columnLabels?: Partial>, + breakdown?: Record, ) => { setOpenTable({ stage, data: filteredDeals, - columns: columns || ["dealname", "landlordPropertyId"], - columnLabels: columnLabels || { + columns: (columns || ["dealname", "landlordPropertyId"]) as (keyof ClassifiedDeal)[], + columnLabels: (columnLabels || { dealname: "Address Ref.", landlordPropertyId: "Property Ref.", - }, + }) as Partial>, breakdown, }); }; + const handleOpenDrawer = (uprn: string | null, landlordPropertyId: string | null, dealname: string | null) => { + setDrawerState({ open: true, uprn, landlordPropertyId, dealname }); + }; + if (!totalDeals) { return ( @@ -67,123 +91,125 @@ export default function LiveTracker({ } return ( -
- {/* 🌍 Global Overview */} -
- {/* Project Selector */} - -
-

- Select Project -

-
- -
-
-
- - {/* Total Properties per Project */} - - handleOpenTable( - `${currentProjectCode} — All Properties`, - currentProject?.allDeals ?? [], - ["dealname", "landlordPropertyId"], - { - dealname: "Address Ref.", - landlordPropertyId: "Property Ref.", - } - ) - } - accent="brandblue" - /> - - {/* Major Issues */} - - handleOpenTable( - "Awaab's Law Reporting", - majorConditionDeals, - [ - "dealname", - "landlordPropertyId", - "majorConditionIssueDescription", - "majorConditionIssuePhotosS3", - ], - { - dealname: "Address Ref.", - landlordPropertyId: "Property Ref.", - majorConditionIssueDescription: "Surveyor's Notes", - majorConditionIssuePhotosS3: "Photo Evidence", - } - ) - } - accent={majorIssues > 0 ? "bright-red" : "red"} - /> -
- - {/* 📊 Project Insights */} - {currentProject && ( -
-
-

- Project-Level Insights —{" "} - {currentProjectCode} -

-
- -
+ setActiveTab(v as "analytics" | "properties" | "documents")} + > + {/* Tab bar */} + + - - - + + Analytics + + + + Properties + + + + Document Management + + - {hasSurveyData && ( - - - + {/* Analytics tab */} + + {currentProject && ( + + )} + + + {/* Properties tab */} + +
+ {/* Project selector — mirrors analytics tab */} + {projects.length > 1 && ( +
+ Project: + +
)} -
-
- )} - {/* 🔹 Table Modal */} + +
+ + {/* Document Management tab */} + +
+ {projects.length > 1 && ( +
+ Project: + +
+ )} + +
+
+ + + {/* ── Drill-down table modal ─────────────────────────────────────── */} {openTable && (
- {/* Breakdown Stats */} {openTable.breakdown && (
- {Object.entries(openTable.breakdown).map(([category, items]) => { - const isCompleted = category.includes("Completed"); - const bgColor = isCompleted - ? "bg-gradient-to-br from-brandblue/25 to-brandblue/15" - : "bg-gradient-to-br from-amber-100/40 to-amber-50/30"; - const borderColor = isCompleted - ? "border-brandblue/40" - : "border-amber-200/50"; - const textColor = isCompleted - ? "text-brandblue" - : "text-amber-600"; - const labelColor = isCompleted - ? "text-brandblue" - : "text-amber-600/70"; + {Object.entries(openTable.breakdown).map( + ([category, items]) => { + const isCompleted = category.includes("Complete"); + const bgColor = isCompleted + ? "bg-gradient-to-br from-brandblue/25 to-brandblue/15" + : "bg-gradient-to-br from-amber-100/40 to-amber-50/30"; + const borderColor = isCompleted + ? "border-brandblue/40" + : "border-amber-200/50"; + const textColor = isCompleted + ? "text-brandblue" + : "text-amber-600"; + const labelColor = isCompleted + ? "text-brandblue" + : "text-amber-600/70"; - return ( -
-

- {category} -

-

- {items.length} -

-

- {( - ((items.length / openTable.data.length) * 100) | - 0 - )} - % of total -

-
- ); - })} +

+ {category} +

+

+ {items.length} +

+

+ {((items.length / openTable.data.length) * 100) | 0} + % of total +

+
+ ); + }, + )}
)}
-
- +
@@ -276,83 +296,23 @@ export default function LiveTracker({
)} + + {/* ── Document drawer ────────────────────────────────────────────── */} + + setDrawerState({ open: false, uprn: null, landlordPropertyId: null, dealname: null }) + } + /> + + {/* ── Property detail drawer ─────────────────────────────────────── */} + setDetailDeal(null)} + />
); } - -/** 🔸Small stat card component */ -function StatCard({ - icon: Icon, - title, - value, - subtitle, - onClick, - accent = "brandblue", -}: { - icon: any; - title: string; - value: string | number; - subtitle?: string; - onClick: () => void; - accent?: "brandblue" | "red" | "bright-red"; -}) { - const accentConfig = { - brandblue: { - gradient: "from-brandlightblue/30 to-brandlightblue/10", - border: "border-brandblue/20", - text: "text-brandblue", - value: "text-brandblue", - hover: "hover:border-brandblue/40 hover:shadow-lg", - icon: "text-brandblue", - }, - red: { - gradient: "from-red-100/30 to-red-50/20", - border: "border-red-300/40", - text: "text-red-500", - value: "text-red-500", - hover: "hover:border-red-300/60 hover:shadow-lg", - icon: "text-red-500", - }, - "bright-red": { - gradient: "from-red-100 to-red-50", - border: "border-red-500", - text: "text-red-700", - value: "text-red-900", - hover: "hover:border-red-600 hover:shadow-lg", - icon: "text-red-700", - }, - }; - - const config = accentConfig[accent]; - - return ( - -
-
-

- {title} -

-

- {value} - {subtitle && ( - - {subtitle} - - )} -

-
- -
-
- ); -} diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ProgressOverview.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ProgressOverview.tsx index d745c4c..92113c7 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ProgressOverview.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ProgressOverview.tsx @@ -1,19 +1,81 @@ "use client"; -import { Card } from "@tremor/react"; -import { AlertCircle } from "lucide-react"; import { motion } from "framer-motion"; -import ExpandableCountBar from "./ExpandableCountBar"; -import type { ProjectProgressData, ClassifiedDeal, HubspotDeal } from "./types"; +import { CheckCircle2, ArrowRight } from "lucide-react"; +import { Card, CardContent } from "@/app/shadcn_components/ui/card"; +import { STAGE_COLORS } from "./types"; +import type { + ProjectProgressData, + ClassifiedDeal, +} from "./types"; +const EARLY_COLUMNS: (keyof ClassifiedDeal)[] = [ + "dealname", + "landlordPropertyId", + "displayStage", + "preSapScore", + "outcome", +]; +const EARLY_LABELS: Partial> = { + dealname: "Address", + landlordPropertyId: "Ref", + displayStage: "Current Stage", + preSapScore: "Pre-SAP Score", + outcome: "Survey Outcome", +}; + +// ----------------------------------------------------------------------- +// Circular progress ring (SVG) +// ----------------------------------------------------------------------- +function RingProgress({ + pct, + color = "#14163d", + size = 80, +}: { + pct: number; + color?: string; + size?: number; +}) { + const r = 34; + const circ = 2 * Math.PI * r; + const offset = circ - (Math.min(pct, 100) / 100) * circ; + return ( + + + + + ); +} + +// ----------------------------------------------------------------------- +// Main component +// ----------------------------------------------------------------------- interface ProgressOverviewProps { data: ProjectProgressData; onOpenTable?: ( stage: string, deals: ClassifiedDeal[], - columns?: (keyof HubspotDeal)[], - columnLabels?: Partial>, - breakdown?: Record + columns?: (keyof ClassifiedDeal)[], + columnLabels?: Partial>, + breakdown?: Record, ) => void; } @@ -21,319 +83,125 @@ export default function ProgressOverview({ data, onOpenTable, }: ProgressOverviewProps) { - // Pre-computed values from props const { completedPercentage, completedCount, - totalDeals, - queriesDeals, - coordination, - design, + nonQueryTotal, + stageProgress, } = data; - // SVG circle calculations (pure, no memo needed) - const radius = 40; - const circumference = 2 * Math.PI * radius; - const strokeDashoffset = circumference - (completedPercentage / 100) * circumference; - - const handleCompletedClick = () => { - if (onOpenTable) { - onOpenTable( - "Completed Properties", - data.completedDeals, - ["dealname", "landlordPropertyId"], - { - dealname: "Address Ref.", - landlordPropertyId: "Property Ref.", - } - ); - } - }; - - const handleCoordinationClick = () => { - if (onOpenTable) { - const coordinationBreakdown = { - "Coordination Completed": coordination.completedDeals, - "Coordination in Progress": coordination.inProgressDeals, - }; - const allCoordDeals = [ - ...coordination.completedDeals, - ...coordination.inProgressDeals, - ]; - onOpenTable( - "Coordination Status", - allCoordDeals, - undefined, - undefined, - coordinationBreakdown - ); - } - }; - - const handleDesignClick = () => { - if (onOpenTable) { - const designBreakdown = { - "Design Completed": design.completedDeals, - "Design in Progress": design.inProgressDeals, - }; - const allDesignDeals = [ - ...design.completedDeals, - ...design.inProgressDeals, - ]; - onOpenTable( - "Design Status", - allDesignDeals, - undefined, - undefined, - designBreakdown - ); - } - }; - - const handleQueriesClick = () => { - if (onOpenTable && queriesDeals.length > 0) { - onOpenTable( - "Properties Needing Attention", - queriesDeals, - ["dealname", "landlordPropertyId", "coordinationStatus"], - { - dealname: "Address Ref.", - landlordPropertyId: "Property Ref.", - coordinationStatus: "Issue", - } - ); - } - }; + // Early-stage rows (scope / booking / assessment) + const earlyStages = [ + "Scope & Planning", + "Booking in Progress", + "Assessment in Progress", + ]; + const earlyItems = stageProgress.filter( + (s) => earlyStages.includes(s.stage) && s.count > 0, + ); return ( -
- {/* Work Completed - Full Width Overview at Top */} - - -
- {/* Header with Circular Progress */} -
-
-

+ + + {/* ── Completion header ──────────────────────────────────────────── */} + + onOpenTable?.( + "Completed Properties", + data.completedDeals, + [ + "dealname", + "landlordPropertyId", + "displayStage", + "actualMeasuresInstalled", + "fullLodgementDate", + ], + { + dealname: "Address", + landlordPropertyId: "Ref", + displayStage: "Stage", + actualMeasuresInstalled: "Measures Installed", + fullLodgementDate: "Lodgement Date", + }, + ) + } + className="group w-full text-left rounded-xl border border-emerald-200 bg-gradient-to-r from-emerald-50 to-white p-5 hover:border-emerald-300 hover:shadow-md transition-all duration-200" + > +

+
+ +
+ + {completedPercentage.toFixed(0)}% + +
+
+
+
+ + Work Completed -

-

- End-to-end project overview -

-
- - {/* Circular Progress */} -
- - {/* Background circle */} - - {/* Progress circle */} - - - - - - - - - {/* Center text */} -
- - {completedPercentage.toFixed(0)}% - - - {completedCount}/{totalDeals} - -
+
+

+ {completedCount} + + / {nonQueryTotal} + +

+

+ Properties fully lodged and funded +

- - {/* CTA */} -
- View Completed Properties - -
+
- - + - {/* Early Stage Cards - Scope, Booking, Assessment */} - {(() => { - const earlyStages = [ - "Scope & Planning", - "Booking in Progress", - "Assessment in Progress", - ]; - const earlyStageItems = data.stageProgress.filter((s) => - earlyStages.includes(s.stage) - ); - - return earlyStageItems.length > 0 ? ( -
- {earlyStageItems.map((item) => ( - { - if (onOpenTable) { - onOpenTable( + {/* ── Early stage chips ─────────────────────────────────────────── */} + {earlyItems.length > 0 && ( +
+ {earlyItems.map((item) => { + const c = STAGE_COLORS[item.stage]; + return ( + + onOpenTable?.( item.stage, item.deals, - ["dealname", "landlordPropertyId"], - { - dealname: "Address Ref.", - landlordPropertyId: "Property Ref.", - } - ); + EARLY_COLUMNS, + EARLY_LABELS, + ) } - }} - whileHover={{ scale: 1.02 }} - className="group relative text-left" - > - -
-
-

- {item.stage} -

-

- {item.count} -

-
- -
-

- {item.percentage.toFixed(0)}% of total -

-
- -
- View - -
+ className={`group text-left rounded-xl border p-3 transition-all duration-200 hover:shadow-md ${c.bg} ${c.border} hover:opacity-95`} + > +
+ + + {item.stage} +
- - - ))} +

{item.count}

+

+ {item.percentage.toFixed(0)}% of total +

+ + ); + })}
- ) : null; - })()} + )} - {/* Project Summary Cards - Coordination & Design */} -
- - - -
- - {/* Queries / Attention Required Section */} - {queriesDeals.length > 0 && ( - - -
- {/* Header with Alert */} -
-
- -
-
-

- Requires Your Input -

-

- These properties need your feedback or assistance to progress -

-
-
- - {/* Count Display */} -
-

- {queriesDeals.length} -

-

- {queriesDeals.length === 1 ? "property" : "properties"}{" "} - awaiting action -

-
- - {/* CTA */} -
- Review Details - -
-
-
-
- )} -
+ + ); } diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyDetailDrawer.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyDetailDrawer.tsx new file mode 100644 index 0000000..cbe9c9d --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyDetailDrawer.tsx @@ -0,0 +1,271 @@ +"use client"; + +import { motion, AnimatePresence } from "framer-motion"; +import { X, CheckCircle2, Circle, AlertTriangle } from "lucide-react"; +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerTitle, + DrawerDescription, +} from "@/app/shadcn_components/ui/drawer"; +import { STAGE_COLORS } from "./types"; +import type { ClassifiedDeal } from "./types"; + +// ----------------------------------------------------------------------- +// Milestone definitions — ordered pipeline steps with their date fields +// ----------------------------------------------------------------------- +const MILESTONES: { label: string; field: keyof ClassifiedDeal; sublabel?: string }[] = [ + { label: "Booking Confirmed", field: "confirmedSurveyDate" }, + { label: "Assessment Completed", field: "surveyedDate" }, + { label: "Coordination (V1)", field: "ioeV1Date", sublabel: "IOE/MTP V1" }, + { label: "Coordination (V2)", field: "ioeV2Date", sublabel: "IOE/MTP V2" }, + { label: "Design Completed", field: "designDate" }, + { label: "Measures Lodged", field: "measuresLodgementDate" }, + { label: "Stage 1 Lodgement", field: "fullLodgementDate" }, +]; + +function formatDate(d: Date | string | null | undefined): string | null { + if (!d) return null; + try { + return new Date(d).toLocaleDateString("en-GB", { + day: "numeric", + month: "short", + year: "numeric", + }); + } catch { + return null; + } +} + +// ----------------------------------------------------------------------- +// Mini info row +// ----------------------------------------------------------------------- +function InfoRow({ label, value }: { label: string; value: React.ReactNode }) { + if (!value) return null; + return ( +
+ {label} + {value} +
+ ); +} + +// ----------------------------------------------------------------------- +// Stage badge +// ----------------------------------------------------------------------- +function StageBadge({ stage }: { stage: ClassifiedDeal["displayStage"] }) { + const c = STAGE_COLORS[stage] ?? STAGE_COLORS["Unknown Stage"]; + return ( + + + {stage} + + ); +} + +// ----------------------------------------------------------------------- +// Vertical milestone timeline +// ----------------------------------------------------------------------- +function MilestoneTimeline({ deal }: { deal: ClassifiedDeal }) { + const milestones = MILESTONES.map((m) => ({ + ...m, + date: formatDate(deal[m.field] as Date | string | null), + })); + + // Find last completed index + const lastCompletedIdx = milestones.reduce((acc, m, i) => (m.date ? i : acc), -1); + + return ( +
+ {milestones.map((m, i) => { + const completed = !!m.date; + const isLast = i === milestones.length - 1; + + return ( +
+ {/* Left: dot + connecting line */} +
+
+ {completed ? ( + + ) : ( + + )} +
+ {!isLast && ( +
+ )} +
+ + {/* Right: label + date */} +
+
+
+

+ {m.label} +

+ {m.sublabel && ( +

{m.sublabel}

+ )} +
+ {m.date ? ( + + {m.date} + + ) : ( + Pending + )} +
+
+
+ ); + })} +
+ ); +} + +// ----------------------------------------------------------------------- +// PropertyDetailDrawer — main component +// ----------------------------------------------------------------------- +interface PropertyDetailDrawerProps { + deal: ClassifiedDeal | null; + onClose: () => void; +} + +export default function PropertyDetailDrawer({ deal, onClose }: PropertyDetailDrawerProps) { + const open = !!deal; + + return ( + !v && onClose()} direction="right"> + +
+ + {deal && ( + <> + {/* Header */} + +
+
+ + {deal.dealname ?? "Property Details"} + +
+ + {deal.landlordPropertyId && ( + + {deal.landlordPropertyId} + + )} + {deal.projectCode && ( + + {deal.projectCode} + + )} +
+
+ + + +
+ +
+ + {/* Scrollable body */} +
+ + {/* Damp & mould alert */} + {(deal.dampMouldFlag || deal.majorConditionIssuePhotosS3) && ( +
+ +
+

Damp & Mould Flag

+ {deal.dampMouldFlag && ( +

{deal.dampMouldFlag}

+ )} + {deal.majorConditionIssueDescription && ( +

{deal.majorConditionIssueDescription}

+ )} +
+
+ )} + + {/* Key details */} +
+

Property Details

+
+ + + + {deal.preSapScore} + : null + } + /> + + {deal.outcomeNotes && ( + + )} + + + +
+
+ + {/* Measures */} + {(deal.proposedMeasures || deal.approvedPackage || deal.actualMeasuresInstalled) && ( +
+

Measures

+
+ + + + +
+
+ )} + + {/* Timeline */} +
+

Project Timeline

+ +
+
+ + {/* Footer */} +
+ {deal.uprn && ( +

UPRN: {deal.uprn}

+ )} +
+ + )} + + + ); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyDrawer.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyDrawer.tsx new file mode 100644 index 0000000..6dc83ef --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyDrawer.tsx @@ -0,0 +1,349 @@ +"use client"; + +import { useQuery, useMutation } from "@tanstack/react-query"; +import { useRef } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { + FileDown, + FileText, + FileX, + Loader2, + FolderOpen, + X, + ExternalLink, +} from "lucide-react"; +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerTitle, + DrawerDescription, +} from "@/app/shadcn_components/ui/drawer"; +import type { PropertyDocument } from "./types"; +import { EXPECTED_SURVEY_DOC_TYPES } from "./types"; + +// Human-readable labels for the main DB fileType enum values +const DOC_TYPE_LABELS: Record = { + photo_pack: "Photo Pack", + site_note: "Site Note", + rd_sap_site_note: "RdSAP Site Note", + pas_2023_ventilation: "PAS 2023 Ventilation", + pas_2023_condition: "PAS 2023 Condition Report", + pas_significance: "PAS Significance", + par_photo_pack: "PAR Photo Pack", + pas_2023_property: "PAS 2023 Property Report", + pas_2023_occupancy: "PAS 2023 Occupancy Report", +}; + +// All survey docs go under this group for now (extensible later) +function getDocCategory(_docType: string): string { + return "Survey Documents"; +} + +function formatDate(iso: string): string { + try { + return new Date(iso).toLocaleDateString("en-GB", { + day: "numeric", + month: "short", + year: "numeric", + }); + } catch { + return iso; + } +} + +// ----------------------------------------------------------------------- +// Individual document row +// ----------------------------------------------------------------------- +function DocumentRow({ doc }: { doc: PropertyDocument }) { + const label = DOC_TYPE_LABELS[doc.docType] ?? doc.docType; + + const { mutate: download, isPending: signing } = useMutation({ + mutationFn: async () => { + const res = await fetch("/api/sign-document-url", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ key: doc.s3FileKey, bucket: doc.s3FileBucket }), + }); + if (!res.ok) throw new Error("Failed to get signed URL"); + const data = await res.json(); + return data.url as string; + }, + onSuccess: (url) => { + window.open(url, "_blank"); + }, + }); + + return ( + + {/* Left: icon + label + date stacked */} +
+
+ +
+
+

{label}

+

+ {formatDate(doc.s3UploadTimestamp)} +

+
+
+ + {/* Right: download button */} + +
+ ); +} + +// ----------------------------------------------------------------------- +// PropertyDrawer — main component +// ----------------------------------------------------------------------- +interface PropertyDrawerProps { + open: boolean; + uprn: string | null; + landlordPropertyId: string | null; + dealname: string | null; + onClose: () => void; +} + +export default function PropertyDrawer({ + open, + uprn, + landlordPropertyId, + dealname, + onClose, +}: PropertyDrawerProps) { + const canQuery = !!(uprn || landlordPropertyId); + const { + data: fetchedDocuments = [], + isFetching, + isError, + } = useQuery({ + queryKey: ["property-documents", uprn, landlordPropertyId], + queryFn: async () => { + const params = new URLSearchParams(); + if (uprn) params.set("uprn", uprn); + else if (landlordPropertyId) + params.set("landlordPropertyId", landlordPropertyId); + const res = await fetch( + `/api/live-tracking/property-documents?${params}`, + ); + if (!res.ok) throw new Error("Failed to load documents"); + return res.json() as Promise; + }, + enabled: open && canQuery, + staleTime: 30_000, + }); + + // Keep the last successfully fetched result so the closing animation doesn't + // flash the empty state (the parent nulls out uprn/landlordPropertyId on close, + // which disables the query and resets fetchedDocuments to [] mid-animation). + const lastDocumentsRef = useRef([]); + if (open && !isFetching && !isError) { + lastDocumentsRef.current = fetchedDocuments as PropertyDocument[]; + } + const documents = open ? (fetchedDocuments as PropertyDocument[]) : lastDocumentsRef.current; + + // Group docs by category for display + const grouped = documents.reduce< + Record + >((acc, doc) => { + const category = getDocCategory(doc.docType); + (acc[category] ??= []).push(doc); + return acc; + }, {}); + + const hasDocuments = documents.length > 0; + + const presentTypes = new Set(documents.map((d) => d.docType)); + const missingTypes = EXPECTED_SURVEY_DOC_TYPES.filter( + (t) => !presentTypes.has(t), + ); + + return ( + !v && onClose()} direction="right"> + + {/* Remove the default drag handle */} +
+ + +
+
+ + {dealname ?? "Property Documents"} + + {uprn ? ( + + UPRN: {uprn} + + ) : landlordPropertyId ? ( + + Ref: {landlordPropertyId} + + ) : null} +
+ + + +
+ + {hasDocuments && !isFetching && ( +
+ + + {documents.length} document{documents.length !== 1 ? "s" : ""} + +
+ )} +
+ + {/* Body */} +
+ {/* Loading state */} + {isFetching && ( +
+ {[1, 2, 3].map((i) => ( +
+ ))} +
+ )} + + {/* Error state */} + {isError && !isFetching && ( +
+
+ +
+

+ Could not load documents +

+

+ Please try again later. +

+
+ )} + + {/* Empty state — shows all missing doc types */} + {!isFetching && !isError && !hasDocuments && ( +
+
+
+ +
+

+ No documents available +

+

+ All {EXPECTED_SURVEY_DOC_TYPES.length} survey documents are + outstanding. +

+
+
+

+ Missing Documents ({missingTypes.length}) +

+ {missingTypes.map((t) => ( +
+ + + {DOC_TYPE_LABELS[t] ?? t} + +
+ ))} +
+
+ )} + + {/* Document groups */} + + {!isFetching && + !isError && + hasDocuments && + Object.entries(grouped).map(([category, docs]) => ( + +

+ {category} +

+
+ {docs.map((doc) => ( + + ))} +
+
+ ))} +
+ + {/* Missing documents section — shown when some but not all docs are present */} + {!isFetching && + !isError && + hasDocuments && + missingTypes.length > 0 && ( + +

+ Missing Documents ({missingTypes.length}) +

+
+ {missingTypes.map((t) => ( +
+ + + {DOC_TYPE_LABELS[t] ?? t} + +
+ ))} +
+
+ )} +
+ + {/* Footer */} +
+

+ Download links expire after 30 minutes. Refresh to generate a new + link. +

+
+ + + ); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTable.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTable.tsx new file mode 100644 index 0000000..ac29be0 --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTable.tsx @@ -0,0 +1,421 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { + useReactTable, + getCoreRowModel, + getFilteredRowModel, + getSortedRowModel, + getPaginationRowModel, + flexRender, + type SortingState, + type VisibilityState, + type PaginationState, +} from "@tanstack/react-table"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/app/shadcn_components/ui/table"; +import { Input } from "@/app/shadcn_components/ui/input"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/app/shadcn_components/ui/dropdown-menu"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, +} from "@/app/shadcn_components/ui/select"; +import { Search, SlidersHorizontal, ChevronLeft, ChevronRight, Download } from "lucide-react"; +import { createPropertyTableColumns } from "./PropertyTableColumns"; +import { STAGE_ORDER } from "./types"; +import type { ClassifiedDeal, DocStatusMap } from "./types"; + +// Human-readable labels for toggle dropdown +const COLUMN_LABELS: Record = { + landlordPropertyId: "Property Ref", + uprn: "UPRN", + projectCode: "Project", + coordinator: "Coordinator", + designer: "Designer", + installer: "Installer", + proposedMeasures: "Proposed Measures", + approvedPackage: "Approved Package", + actualMeasuresInstalled: "Installed Measures", + preSapScore: "Pre-SAP", + lodgementStatus: "Lodgement Status", + designDate: "Design Date", + fullLodgementDate: "Lodgement Date", +}; + +type DocFilter = "all" | "has_docs" | "incomplete" | "none"; + +interface PropertyTableProps { + data: ClassifiedDeal[]; + onOpenDrawer: (uprn: string | null, landlordPropertyId: string | null, dealname: string | null) => void; + onOpenDetail?: (deal: ClassifiedDeal) => void; + showDocuments?: boolean; + docStatusMap?: DocStatusMap; +} + +const CSV_FIELDS: { key: keyof ClassifiedDeal; label: string }[] = [ + { key: "dealname", label: "Address" }, + { key: "landlordPropertyId", label: "Property Ref" }, + { key: "uprn", label: "UPRN" }, + { key: "displayStage", label: "Stage" }, + { key: "projectCode", label: "Project" }, + { key: "coordinator", label: "Coordinator" }, + { key: "designer", label: "Designer" }, + { key: "installer", label: "Installer" }, + { key: "proposedMeasures", label: "Proposed Measures" }, + { key: "approvedPackage", label: "Approved Package" }, + { key: "actualMeasuresInstalled", label: "Installed Measures" }, + { key: "preSapScore", label: "Pre-SAP" }, + { key: "lodgementStatus", label: "Lodgement Status" }, + { key: "designDate", label: "Design Date" }, + { key: "fullLodgementDate", label: "Lodgement Date" }, +]; + +function escapeCell(value: unknown): string { + if (value === null || value === undefined) return ""; + const str = + value instanceof Date + ? value.toLocaleDateString("en-GB") + : String(value); + return str.includes(",") || str.includes('"') || str.includes("\n") + ? `"${str.replace(/"/g, '""')}"` + : str; +} + +export default function PropertyTable({ data, onOpenDrawer, onOpenDetail, showDocuments = false, docStatusMap = {} }: PropertyTableProps) { + const [globalFilter, setGlobalFilter] = useState(""); + const [stageFilter, setStageFilter] = useState("all"); + const [docFilter, setDocFilter] = useState("all"); + const [sorting, setSorting] = useState([]); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 25, + }); + const [columnVisibility, setColumnVisibility] = useState({ + designer: false, + installer: false, + proposedMeasures: false, + approvedPackage: false, + actualMeasuresInstalled: false, + preSapScore: false, + lodgementStatus: false, + designDate: false, + fullLodgementDate: false, + }); + + // Pre-filter by stage and doc status before TanStack gets it + const filteredData = useMemo(() => { + let result = data; + if (stageFilter !== "all") { + result = result.filter((d) => d.displayStage === stageFilter); + } + if (docFilter !== "all") { + result = result.filter((d) => { + const status = d.uprn ? docStatusMap[d.uprn] : undefined; + if (docFilter === "none") return !status || !status.hasDocs; + if (docFilter === "has_docs") return !!status?.hasDocs; + if (docFilter === "incomplete") return !!status?.hasDocs && !status.isComplete; + return true; + }); + } + return result; + }, [data, stageFilter, docFilter, docStatusMap]); + + const columns = useMemo( + () => createPropertyTableColumns(onOpenDrawer, showDocuments, docStatusMap, onOpenDetail), + [onOpenDrawer, showDocuments, docStatusMap, onOpenDetail] + ); + + const table = useReactTable({ + data: filteredData, + columns, + state: { + globalFilter, + sorting, + pagination, + columnVisibility, + }, + onGlobalFilterChange: setGlobalFilter, + onSortingChange: setSorting, + onPaginationChange: setPagination, + onColumnVisibilityChange: setColumnVisibility, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + globalFilterFn: "includesString", + }); + + const downloadCsv = () => { + const rows = table.getFilteredRowModel().rows; + const header = CSV_FIELDS.map((f) => f.label).join(","); + const body = rows + .map((row) => + CSV_FIELDS.map((f) => escapeCell(row.original[f.key])).join(",") + ) + .join("\n"); + const blob = new Blob([header + "\n" + body], { type: "text/csv;charset=utf-8;" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "properties.csv"; + a.click(); + URL.revokeObjectURL(url); + }; + + const toggleableColumns = table + .getAllColumns() + .filter((col) => col.getCanHide() && COLUMN_LABELS[col.id]); + + const pageCount = table.getPageCount(); + const currentPage = table.getState().pagination.pageIndex + 1; + const totalFiltered = table.getFilteredRowModel().rows.length; + + return ( +
+ {/* Toolbar */} +
+ {/* Search */} +
+ + { + setGlobalFilter(e.target.value); + setPagination((p) => ({ ...p, pageIndex: 0 })); + }} + placeholder="Search address, UPRN, coordinator…" + className="pl-9 h-9 text-sm border-gray-200 focus:border-brandblue/40 focus:ring-brandblue/20" + /> +
+ + {/* Stage filter */} + + + {/* Docs filter */} + {showDocuments && ( + + )} + + {/* Download CSV */} + + + {/* Column visibility */} + + + + + + + Toggle columns + + + {toggleableColumns.map((col) => ( + col.toggleVisibility(val)} + className="text-sm" + > + {COLUMN_LABELS[col.id] ?? col.id} + + ))} + + +
+ + {/* Result count */} +

+ Showing{" "} + + {Math.min( + table.getState().pagination.pageSize, + totalFiltered - table.getState().pagination.pageIndex * table.getState().pagination.pageSize + )} + {" "} + of{" "} + {totalFiltered}{" "} + {stageFilter !== "all" ? `"${stageFilter}" ` : ""} + {docFilter !== "all" ? `(${docFilter === "has_docs" ? "has docs" : docFilter === "incomplete" ? "incomplete docs" : "no docs"}) ` : ""} + propert{totalFiltered === 1 ? "y" : "ies"} +

+ + {/* Table */} +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows.length ? ( + table.getRowModel().rows.map((row, i) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No properties match the current filters. + + + )} + +
+
+
+ + {/* Pagination */} + {pageCount > 1 && ( +
+
+ Rows per page: + +
+ +
+ + Page {currentPage} of {pageCount} + +
+ + +
+
+
+ )} +
+ ); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTableColumns.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTableColumns.tsx new file mode 100644 index 0000000..9562418 --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTableColumns.tsx @@ -0,0 +1,324 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; +import { ArrowUpDown, CheckCircle2, AlertCircle, FileX } from "lucide-react"; +import { STAGE_COLORS } from "./types"; +import type { ClassifiedDeal, DisplayStage, DocStatusMap } from "./types"; + +// ----------------------------------------------------------------------- +// Stage badge — consistent pill rendering +// ----------------------------------------------------------------------- +function StageBadge({ stage }: { stage: DisplayStage }) { + const c = STAGE_COLORS[stage] ?? STAGE_COLORS["Unknown Stage"]; + return ( + + + {stage} + + ); +} + +// Sortable column header helper +function SortableHeader({ + label, + column, +}: { + label: string; + column: { toggleSorting: (desc: boolean) => void; getIsSorted: () => false | "asc" | "desc" }; +}) { + return ( + + ); +} + +// ----------------------------------------------------------------------- +// Column factory — takes onOpenDrawer so the Documents button can trigger it +// showDocuments controls whether the Docs action column is included +// docStatusMap provides per-UPRN document status for status indicators +// ----------------------------------------------------------------------- +export function createPropertyTableColumns( + onOpenDrawer: (uprn: string | null, landlordPropertyId: string | null, dealname: string | null) => void, + showDocuments: boolean = false, + docStatusMap: DocStatusMap = {}, + onOpenDetail?: (deal: ClassifiedDeal) => void, +): ColumnDef[] { + const columns: ColumnDef[] = [ + // ── Address ────────────────────────────────────────────────────────── + { + accessorKey: "dealname", + id: "dealname", + header: ({ column }) => , + cell: ({ row }) => ( +
+ {onOpenDetail ? ( + + ) : ( +

+ {row.original.dealname ?? "—"} +

+ )} +
+ ), + enableHiding: false, + }, + + // ── Property ref ───────────────────────────────────────────────────── + { + accessorKey: "landlordPropertyId", + id: "landlordPropertyId", + header: ({ column }) => , + cell: ({ row }) => ( + + {row.original.landlordPropertyId ?? "—"} + + ), + }, + + // ── UPRN ───────────────────────────────────────────────────────────── + { + accessorKey: "uprn", + id: "uprn", + header: () => ( + UPRN + ), + cell: ({ row }) => ( + + {row.original.uprn ?? "—"} + + ), + }, + + // ── Stage badge ────────────────────────────────────────────────────── + { + accessorKey: "displayStage", + id: "displayStage", + header: ({ column }) => , + cell: ({ row }) => , + filterFn: (row, _id, filterValue: string) => + row.original.displayStage === filterValue, + enableHiding: false, + }, + + // ── Project code ───────────────────────────────────────────────────── + { + accessorKey: "projectCode", + id: "projectCode", + header: ({ column }) => , + cell: ({ row }) => ( + + {row.original.projectCode ?? "—"} + + ), + }, + + // ── Coordinator ────────────────────────────────────────────────────── + { + accessorKey: "coordinator", + id: "coordinator", + header: ({ column }) => , + cell: ({ row }) => ( + + {row.original.coordinator ?? } + + ), + }, + + // ── Designer ───────────────────────────────────────────────────────── + { + accessorKey: "designer", + id: "designer", + header: ({ column }) => , + cell: ({ row }) => ( + + {row.original.designer ?? } + + ), + }, + + // ── Installer ──────────────────────────────────────────────────────── + { + accessorKey: "installer", + id: "installer", + header: ({ column }) => , + cell: ({ row }) => ( + + {row.original.installer ?? } + + ), + }, + + // ── Proposed measures ──────────────────────────────────────────────── + { + accessorKey: "proposedMeasures", + id: "proposedMeasures", + header: () => ( + + Proposed Measures + + ), + cell: ({ row }) => ( + + {row.original.proposedMeasures ?? } + + ), + }, + + // ── Approved package ───────────────────────────────────────────────── + { + accessorKey: "approvedPackage", + id: "approvedPackage", + header: () => ( + + Approved Package + + ), + cell: ({ row }) => ( + + {row.original.approvedPackage ?? } + + ), + }, + + // ── Installed measures ─────────────────────────────────────────────── + { + accessorKey: "actualMeasuresInstalled", + id: "actualMeasuresInstalled", + header: () => ( + + Installed + + ), + cell: ({ row }) => ( + + {row.original.actualMeasuresInstalled ?? } + + ), + }, + + // ── Pre-SAP score ──────────────────────────────────────────────────── + { + accessorKey: "preSapScore", + id: "preSapScore", + header: ({ column }) => , + cell: ({ row }) => { + const score = row.original.preSapScore; + if (!score) return ; + const n = Number(score); + const colour = + n < 30 + ? "text-red-600 bg-red-50" + : n < 50 + ? "text-amber-700 bg-amber-50" + : "text-emerald-700 bg-emerald-50"; + return ( + + {score} + + ); + }, + }, + + + // ── Lodgement status ───────────────────────────────────────────────── + { + accessorKey: "lodgementStatus", + id: "lodgementStatus", + header: ({ column }) => , + cell: ({ row }) => ( + + {row.original.lodgementStatus ?? } + + ), + }, + + // ── Design date ────────────────────────────────────────────────────── + { + accessorKey: "designDate", + id: "designDate", + header: ({ column }) => , + cell: ({ row }) => { + const d = row.original.designDate; + return ( + + {d ? new Date(d).toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "2-digit" }) : } + + ); + }, + }, + + // ── Full lodgement date ────────────────────────────────────────────── + { + accessorKey: "fullLodgementDate", + id: "fullLodgementDate", + header: ({ column }) => , + cell: ({ row }) => { + const d = row.original.fullLodgementDate; + return ( + + {d ? new Date(d).toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "2-digit" }) : } + + ); + }, + }, + + ]; + + if (showDocuments) { + columns.push({ + id: "documents", + header: () => ( + Docs + ), + cell: ({ row }) => { + const uprn = row.original.uprn ?? ""; + const status = uprn ? docStatusMap[uprn] : undefined; + const isComplete = status?.isComplete; + const hasDocs = status?.hasDocs; + + let icon: React.ReactNode; + let className: string; + + if (isComplete) { + icon = ; + className = + "inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs font-medium border border-emerald-200 text-emerald-700 bg-emerald-50 hover:bg-emerald-100 hover:border-emerald-300 transition-all duration-150 whitespace-nowrap"; + } else if (hasDocs) { + icon = ; + className = + "inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs font-medium border border-amber-200 text-amber-700 bg-amber-50 hover:bg-amber-100 hover:border-amber-300 transition-all duration-150 whitespace-nowrap"; + } else { + icon = ; + className = + "inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs font-medium border border-gray-200 text-gray-400 bg-gray-50 hover:bg-gray-100 hover:border-gray-300 transition-all duration-150 whitespace-nowrap"; + } + + return ( + + ); + }, + enableSorting: false, + enableHiding: false, + }); + } + + return columns; +} diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/SurveyIssuesPanel.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/SurveyIssuesPanel.tsx new file mode 100644 index 0000000..d8c576b --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/SurveyIssuesPanel.tsx @@ -0,0 +1,107 @@ +"use client"; + +import { motion } from "framer-motion"; +import { AlertCircle } from "lucide-react"; +import { Card, CardContent } from "@/app/shadcn_components/ui/card"; +import type { ClassifiedDeal } from "./types"; + +const SUCCESSFUL_OUTCOMES = new Set(["Surveyed", "Surveyed - Pending Upload"]); + +const COLUMNS: (keyof ClassifiedDeal)[] = [ + "dealname", + "landlordPropertyId", + "outcome", + "outcomeNotes", +]; +const COLUMN_LABELS: Partial> = { + dealname: "Address", + landlordPropertyId: "Ref", + outcome: "Outcome", + outcomeNotes: "Notes", +}; + +interface SurveyIssuesPanelProps { + deals: ClassifiedDeal[]; + onOpenTable: ( + stage: string, + deals: ClassifiedDeal[], + columns?: (keyof ClassifiedDeal)[], + columnLabels?: Partial>, + breakdown?: Record, + ) => void; +} + +export default function SurveyIssuesPanel({ + deals, + onOpenTable, +}: SurveyIssuesPanelProps) { + // Filter to deals with a populated outcome that is not a success + const issueDeals = deals.filter( + (d) => d.outcome && !SUCCESSFUL_OUTCOMES.has(d.outcome), + ); + + if (issueDeals.length === 0) return null; + + // Group by outcome, sorted by count descending + const groups = new Map(); + for (const deal of issueDeals) { + const key = deal.outcome!; + if (!groups.has(key)) groups.set(key, []); + groups.get(key)!.push(deal); + } + const sortedGroups = Array.from(groups.entries()).sort( + (a, b) => b[1].length - a[1].length, + ); + + return ( + + +
+
+
+ +

+ Survey Issues +

+ + {issueDeals.length} affected + +
+

+ Properties where the survey did not result in a successful outcome +

+
+
+ +
+ {sortedGroups.map(([outcomeLabel, groupDeals]) => ( + + onOpenTable( + `Survey Issues — ${outcomeLabel}`, + groupDeals, + COLUMNS, + COLUMN_LABELS, + ) + } + className="group text-left rounded-xl border border-amber-200 bg-gradient-to-br from-amber-50 to-white p-4 hover:border-amber-300 hover:shadow-md transition-all duration-200" + > +

+ {outcomeLabel} +

+

+ {groupDeals.length} +

+

+ {((groupDeals.length / issueDeals.length) * 100).toFixed(0)}% of + issues +

+
+ ))} +
+
+
+ ); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/TableViewer.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/TableViewer.tsx deleted file mode 100644 index 680c0be..0000000 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/TableViewer.tsx +++ /dev/null @@ -1,214 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { Download } from "lucide-react"; -import type { ClassifiedDeal, HubspotDeal } from "./types"; - -interface TableViewerProps { - data: ClassifiedDeal[]; - columns?: (keyof HubspotDeal)[]; - columnLabels?: Partial>; - breakdown?: Record; -} - -export default function TableViewer({ - data, - columns, - columnLabels, - breakdown, -}: TableViewerProps) { - const [searchTerms, setSearchTerms] = useState>({}); - const visibleColumns = columns?.length - ? columns - : (Object.keys(data?.[0] || {}) as (keyof HubspotDeal)[]); - - // Helper: Get category for a row based on breakdown - const getCategoryForRow = ( - row: ClassifiedDeal, - brk: Record | undefined - ): string | undefined => { - if (!brk) return undefined; - for (const [category, items] of Object.entries(brk)) { - if (items.includes(row)) return category; - } - return undefined; - }; - - const getRowStatus = (row: ClassifiedDeal) => { - if (!breakdown) return "untouched"; - - const category = getCategoryForRow(row, breakdown); - if (category?.includes("Completed")) { - return "completed"; - } else if (category?.includes("Progress")) { - return "progress"; - } - return "untouched"; - }; - - const getRowBackgroundColor = (status: string) => { - switch (status) { - case "completed": - return "bg-white"; - case "progress": - return "bg-white"; - case "untouched": - return "bg-white"; - default: - return "bg-white"; - } - }; - - const getSortPriority = (status: string) => { - switch (status) { - case "completed": - return 0; - case "progress": - return 1; - case "untouched": - return 2; - default: - return 3; - } - }; - - // Inline filter derivation (no useMemo) - const filteredData = data.filter((row) => - visibleColumns.every((col) => { - const term = searchTerms[col]?.toLowerCase() || ""; - if (!term) return true; - const value = String(row[col as keyof ClassifiedDeal] ?? "").toLowerCase(); - return value.includes(term); - }) - ); - - // Inline sort derivation (no useMemo) - const sortedFilteredData = [...filteredData].sort((a, b) => { - const statusA = getRowStatus(a); - const statusB = getRowStatus(b); - return getSortPriority(statusA) - getSortPriority(statusB); - }); - - const renderCellContent = (col: keyof HubspotDeal, value: any) => { - if (col === "majorConditionIssuePhotosS3" && value) { - let urls: string[] = []; - - if (typeof value === "string") { - try { - const parsed = JSON.parse(value); - urls = Array.isArray(parsed) ? parsed : [value]; - } catch { - urls = value.split(/[\s,]+/).filter((u) => u.startsWith("http")); - } - } else if (Array.isArray(value)) { - urls = value; - } - - if (urls.length === 0) - return No photos; - - const handleDownload = async (rawUrl: string) => { - try { - // Extract the object key (after the bucket domain) - const key = rawUrl.split(".amazonaws.com/")[1]; - if (!key) return alert("Invalid S3 key"); - - const res = await fetch("/api/sign-s3-url", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ key }), - }); - - const data = await res.json(); - if (data.url) { - window.open(data.url, "_blank"); - } else { - alert("Failed to get signed URL"); - } - } catch (err) { - console.error(err); - alert("Error downloading file"); - } - }; - - return ( -
- {urls.map((url, idx) => ( - - ))} -
- ); - } - - return String(value ?? ""); - }; - - return ( -
- - - - {visibleColumns.map((col) => ( - - ))} - - - - {sortedFilteredData.length === 0 ? ( - - - - ) : ( - sortedFilteredData.map((row, i) => { - const status = getRowStatus(row); - return ( - - {visibleColumns.map((col) => ( - - ))} - - ); - }) - )} - -
-
- - {columnLabels?.[col] || (col as string)} - - - setSearchTerms((prev) => ({ - ...prev, - [col]: e.target.value, - })) - } - /> -
-
- No results found -
- {renderCellContent( - col, - row[col as keyof ClassifiedDeal] - )} -
-
- ); -} diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx index 53b04a4..55da823 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx @@ -1,13 +1,64 @@ import { getServerSession } from "next-auth"; import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions"; import { redirect } from "next/navigation"; -import { surveyDB } from "../../../../../db/surveyDB/connection"; -import { hubspotDealData } from "../../../../../db/schema/crm/hubspot_deal_table"; -import { hubspotCompanyData } from "@/app/db/schema/crm/hubspot_company_table"; -import { eq } from "drizzle-orm"; +import { eq, inArray } from "drizzle-orm"; import LiveTracker from "./LiveTracker"; import { computeLiveTrackerData } from "./transforms"; -import type { HubspotDeal } from "./types"; +import { db } from "@/app/db/db"; +import { hubspotDealData } from "@/app/db/schema/crm/hubspot_deal_table"; +import { uploadedFiles } from "@/app/db/schema/uploaded_files"; +import { portfolioOrganisation } from "@/app/db/schema/portfolio_organisation"; +import { organisation } from "@/app/db/schema/organisation"; +import type { HubspotDeal, DocStatusMap, DocStatus } from "./types"; +import { EXPECTED_SURVEY_DOC_TYPES } from "./types"; +import type { InferSelectModel } from "drizzle-orm"; +import { Card, CardContent } from "@/app/shadcn_components/ui/card"; +import { Building2 } from "lucide-react"; + +type DbDeal = InferSelectModel; + +function mapDbRowToHubspotDeal(row: DbDeal): HubspotDeal { + return { + id: row.id, + dealId: row.dealId, + dealname: row.dealname, + dealstage: row.dealstage, + companyId: row.companyId, + projectCode: row.projectCode, + landlordPropertyId: row.landlordPropertyId, + uprn: row.uprn, + outcome: row.outcome, + outcomeNotes: row.outcomeNotes, + majorConditionIssueDescription: row.majorConditionIssueDescription, + majorConditionIssuePhotos: row.majorConditionIssuePhotos, + majorConditionIssuePhotosS3: row.majorConditionIssuePhotosS3, + coordinationStatus: row.coordinationStatus, + designStatus: row.designStatus, + pashubLink: row.pashubLink, + sharepointLink: row.sharepointLink, + dampMouldFlag: row.dampmouldGrowth, + preSapScore: row.preSap, + coordinator: row.coordinator, + ioeV1Date: row.mtpCompletionDate, + ioeV2Date: row.mtpReModelCompletionDate, + ioeV3Date: row.ioeV3CompletionDate, + proposedMeasures: row.proposedMeasures, + approvedPackage: row.approvedPackage, + designer: row.designer, + designDate: row.designCompletionDate, + actualMeasuresInstalled: row.actualMeasuresInstalled, + installer: row.installer, + installerHandover: row.installerHandover, + lodgementStatus: row.lodgementStatus, + measuresLodgementDate: row.measuresLodgementDate, + fullLodgementDate: row.lodgementDate, + confirmedSurveyDate: row.confirmedSurveyDate, + surveyedDate: row.SurveyedDate, + designType: row.dealType, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + }; +} export default async function LiveReportingPage(props: { params: Promise<{ slug: string }>; @@ -16,61 +67,98 @@ export default async function LiveReportingPage(props: { const user = await getServerSession(AuthOptions); if (!user?.user) { - console.error("User not found"); redirect("/"); } - // 🏢 Fetch the company + // Look up the linked organisation for this portfolio + const link = await db + .select({ hubspotCompanyId: organisation.hubspotCompanyId }) + .from(portfolioOrganisation) + .innerJoin(organisation, eq(portfolioOrganisation.organisationId, organisation.id)) + .where(eq(portfolioOrganisation.portfolioId, BigInt(portfolioId))) + .limit(1); - const [company] = await surveyDB - .select() - .from(hubspotCompanyData) - .where(eq(hubspotCompanyData.groupId, portfolioId)); + const pageHeader = ( +
+
Live Projects
+

+ {`Check in on your projects' progress with real-time data updates.`} +

+
+
+ ); - if (!company) { + if (!link.length || !link[0].hubspotCompanyId) { return ( -
-
- No information to show. -
-
+
+ {pageHeader} + + +
+ +
+
+

No organisation linked

+

+ A Domna administrator needs to connect this portfolio to an organisation in{" "} + Portfolio Settings before live tracking data can be displayed. +

+
+
+
+
); } - // 💼 Fetch deals for that company - const deals = await surveyDB + const companyId = link[0].hubspotCompanyId; + + const rawDeals = await db .select() .from(hubspotDealData) - .where(eq(hubspotDealData.companyId, company.companyId)); + .where(eq(hubspotDealData.companyId, companyId)); - if (!deals || deals.length === 0) { - return ( -
-
- No information to show. -
-
- ); + const deals = rawDeals.map(mapDbRowToHubspotDeal); + const trackerData = computeLiveTrackerData(deals); + + // Fetch survey document status for all properties + const uprnList = deals + .map((d) => d.uprn) + .filter((u): u is string => !!u) + .map((u) => { + try { return BigInt(u); } catch { return null; } + }) + .filter((u): u is bigint => u !== null); + + let docStatusMap: DocStatusMap = {}; + + if (uprnList.length > 0) { + const docRows = await db + .select() + .from(uploadedFiles) + .where(inArray(uploadedFiles.uprn, uprnList)); + + const grouped: Record> = {}; + for (const row of docRows) { + if (row.uprn === null || row.fileType === null) continue; + const key = String(row.uprn); + (grouped[key] ??= new Set()).add(row.fileType); + } + + for (const [uprn, types] of Object.entries(grouped)) { + const presentTypes = Array.from(types); + const status: DocStatus = { + presentTypes, + hasDocs: presentTypes.length > 0, + isComplete: EXPECTED_SURVEY_DOC_TYPES.every((t) => types.has(t)), + }; + docStatusMap[uprn] = status; + } } - // 🔄 Transform raw deals to typed and computed data - const trackerData = computeLiveTrackerData(deals as HubspotDeal[]); - return (
-
-
- Live Projects -
-

- {`Check in on your projects' progress with real-time data updates.`} -

-
-
- - + {pageHeader} +
); } - - diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.ts b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.ts index 692b7ef..fe1da87 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.ts +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.ts @@ -12,6 +12,8 @@ import type { OutcomeSlice, LiveTrackerProps, WorkPhaseStats, + DampMouldRiskData, + FunnelStage, } from "./types"; import { @@ -25,7 +27,7 @@ import { // ----------------------------------------------------------------------- const STAGE_ID_MAP: Record = { "1617223910": "Scope & Planning", //[Ops] Backlog - "3583836399": "Scope & Planning", //[Ops] Route Planning + "3583836399": "Scope & Planning", //[Ops] Route Planning "3589581001": "Booking in Progress", // [Bookings] Ready for Bookings Team "3569878239": "Booking in Progress", //[Bookings] Send initial booking SMS "1617223911": "Booking in Progress", // [Bookings] Send Email @@ -39,13 +41,13 @@ const STAGE_ID_MAP: Record = { "1617223913": "Assessment in Progress", //[Ops] Survey in Progress "2558220518": "Assessment in Progress", // [Ops] Not attempted - needs reallocation "3474594026": "Booking in Progress", //[Ops/Bookings] Rebooked - Needs updating - "3206388924": "Assessment in Progress", //[Ops] Surveyed - Pending Upload from Surveyor (Up to Khalim + Kev as debatable) + "3206388924": "Assessment in Progress", //[Ops] Surveyed - Pending Upload from Surveyor "1617223915": "Queries", //[Ops] No Access - Need Sign Off "1617223917": "Queries", //[Ops] No Access - No Revisit "1887735998": "Queries", //[Ops] Not Viable "3061261536": "Queries", //[Sales/Tech] Major condition issue "3948185842": "AFTER_ASSESSMENT", //[Admin] Admin to check all paperwork for external comms - "1617223914": "AFTER_ASSESSMENT",// [Ops]Surveyed in Pashub, Transit Job to Co-ordination + "1617223914": "AFTER_ASSESSMENT", // [Ops] Surveyed in Pashub, Transit Job to Co-ordination "1617223916": "Queries", // [Ops] Properties to Review Manually "2628341989": "Assessment in Progress", //[Ops] Assessment needs correction "3441170637": "AFTER_ASSESSMENT", //[Ops] Awaiting PV Design @@ -54,9 +56,6 @@ const STAGE_ID_MAP: Record = { "1960060104": "Queries", //[Ops] HA Informed "1960060105": "Queries", //[Ops] HA Works Scheduled "1960060106": "AFTER_ASSESSMENT", //[Ops] HA Works Complete - // "1668803772": "", //[Ops] ERF Delivered to HA - // "1668803773": "", //[Ops] ERF Signed - // "2769407183": "", //[Ops] PV - Needs Heating Upgrade (Pre EPR D) "2769407184": "Queries", //[Ops] Talk to client, Needs Heating Upgrade (Pre EPR C) "2702650617": "AFTER_ASSESSMENT", //[Design] Ready for Design "2473886962": "AFTER_ASSESSMENT", //[Design] Design in progress @@ -65,12 +64,12 @@ const STAGE_ID_MAP: Record = { // ----------------------------------------------------------------------- // After-assessment sub-classification -// Resolves AFTER_ASSESSMENT deals based on coordinationStatus and designStatus +// Resolves AFTER_ASSESSMENT deals based on coordinationStatus + designStatus // ----------------------------------------------------------------------- function resolveAfterAssessmentStage( coordinationStatus: string | null, designStatus: string | null -): DisplayStage { +): "Coordination in Progress" | "Design in Progress" | "POST_DESIGN" | "Queries" { const coord = coordinationStatus?.toUpperCase() ?? ""; const design = designStatus?.toUpperCase() ?? ""; @@ -83,25 +82,43 @@ function resolveAfterAssessmentStage( coord.includes("(V2) IOE/MTP COMPLETE") || coord.includes("(V3) IOE/MTP COMPLETE") ) { - return design === "UPLOADED" ? "Completed" : "Design in Progress"; + return design === "UPLOADED" ? "POST_DESIGN" : "Design in Progress"; } // Default for AFTER_ASSESSMENT return "Coordination in Progress"; } +// ----------------------------------------------------------------------- +// Post-design sub-classification +// Called when design is UPLOADED — resolves install / lodgement / completed +// ----------------------------------------------------------------------- +function resolvePostDesignStage(deal: HubspotDeal): DisplayStage { + if (deal.fullLodgementDate) return "Project Complete"; + if (deal.measuresLodgementDate) return "At Post Survey"; + if (deal.lodgementStatus) return "At Lodgement"; + if (deal.actualMeasuresInstalled || deal.installerHandover) return "Installation Complete"; + return "Installation in Progress"; +} + // ----------------------------------------------------------------------- // Resolve display stage for a single deal -// Maps dealstage ID + coordinationStatus + designStatus -> DisplayStage +// Maps dealstage ID + coordination/design/install status -> DisplayStage // ----------------------------------------------------------------------- export function resolveDisplayStage(deal: HubspotDeal): DisplayStage { const raw = STAGE_ID_MAP[deal.dealstage ?? ""] ?? "AFTER_ASSESSMENT"; if (raw === "AFTER_ASSESSMENT") { - return resolveAfterAssessmentStage( + const afterAssessment = resolveAfterAssessmentStage( deal.coordinationStatus, deal.designStatus ); + + if (afterAssessment === "POST_DESIGN") { + return resolvePostDesignStage(deal); + } + + return afterAssessment; } // RA ISSUE override can apply to other stages too @@ -125,6 +142,52 @@ export function classifyDeals(deals: HubspotDeal[]): ClassifiedDeal[] { })); } +// ----------------------------------------------------------------------- +// Compute damp & mould risk — survey vs coordination stage comparison +// ----------------------------------------------------------------------- +export function computeDampMouldRisk(deals: ClassifiedDeal[]): DampMouldRiskData { + const surveyFlagDeals = deals.filter((d) => !!d.majorConditionIssuePhotosS3); + const coordinatorFlagDeals = deals.filter((d) => !!d.dampMouldFlag); + const bothFlaggedCount = surveyFlagDeals.filter((d) => !!d.dampMouldFlag).length; + + return { + surveyFlagCount: surveyFlagDeals.length, + coordinatorFlagCount: coordinatorFlagDeals.length, + bothFlaggedCount, + totalDeals: deals.length, + surveyFlagDeals, + coordinatorFlagDeals, + }; +} + +// ----------------------------------------------------------------------- +// Compute pipeline funnel — dual counts (current snapshot + cumulative) +// ----------------------------------------------------------------------- +export function computeFunnelStages(deals: ClassifiedDeal[]): FunnelStage[] { + const nonQueryDeals = deals.filter((d) => d.displayStage !== "Queries"); + const total = nonQueryDeals.length; + + return STAGE_ORDER.map((stage) => { + const stageIndex = STAGE_ORDER.indexOf(stage); + + const currentCount = nonQueryDeals.filter( + (d) => d.displayStage === stage + ).length; + + const cumulativeCount = nonQueryDeals.filter( + (d) => STAGE_ORDER.indexOf(d.displayStage) >= stageIndex + ).length; + + return { + stage, + currentCount, + currentPct: total > 0 ? (currentCount / total) * 100 : 0, + cumulativeCount, + cumulativePct: total > 0 ? (cumulativeCount / total) * 100 : 0, + }; + }); +} + // ----------------------------------------------------------------------- // Compute all ProjectProgressData for a set of already-classified deals // ----------------------------------------------------------------------- @@ -154,7 +217,7 @@ export function computeProjectProgress( } ); - const completedDeals = stageBuckets["Completed"] ?? []; + const completedDeals = stageBuckets["Project Complete"] ?? []; const completedCount = completedDeals.length; const completedPercentage = nonQueryTotal > 0 ? (completedCount / nonQueryTotal) * 100 : 0; @@ -162,12 +225,17 @@ export function computeProjectProgress( const totalDeals = deals.length; // Coordination phase: - // completed = Design in Progress + Completed (i.e. coordination is done) + // completed = Design in Progress + Installation in Progress + Installation Complete + At Lodgement + At Post Survey + Project Complete // in progress = Coordination in Progress - const coordCompletedDeals = deals.filter( - (d) => - d.displayStage === "Design in Progress" || - d.displayStage === "Completed" + const coordCompletedDeals = deals.filter((d) => + [ + "Design in Progress", + "Installation in Progress", + "Installation Complete", + "At Lodgement", + "At Post Survey", + "Project Complete", + ].includes(d.displayStage) ); const coordInProgressDeals = deals.filter( (d) => d.displayStage === "Coordination in Progress" @@ -179,33 +247,79 @@ export function computeProjectProgress( completedCount: coordCompletedDeals.length, inProgressCount: coordInProgressDeals.length, completedPercentage: - totalDeals > 0 - ? (coordCompletedDeals.length / totalDeals) * 100 - : 0, + totalDeals > 0 ? (coordCompletedDeals.length / totalDeals) * 100 : 0, inProgressPercentage: - totalDeals > 0 - ? (coordInProgressDeals.length / totalDeals) * 100 - : 0, + totalDeals > 0 ? (coordInProgressDeals.length / totalDeals) * 100 : 0, total: totalDeals, }; // Design phase: - // completed = Completed stage + // completed = Installation in Progress + Installation Complete + At Lodgement + At Post Survey + Project Complete // in progress = Design in Progress + const designCompletedDeals = deals.filter((d) => + [ + "Installation in Progress", + "Installation Complete", + "At Lodgement", + "At Post Survey", + "Project Complete", + ].includes(d.displayStage) + ); const designInProgressDeals = deals.filter( (d) => d.displayStage === "Design in Progress" ); const design: WorkPhaseStats = { - completedDeals, + completedDeals: designCompletedDeals, inProgressDeals: designInProgressDeals, - completedCount, + completedCount: designCompletedDeals.length, inProgressCount: designInProgressDeals.length, + completedPercentage: + totalDeals > 0 ? (designCompletedDeals.length / totalDeals) * 100 : 0, + inProgressPercentage: + totalDeals > 0 ? (designInProgressDeals.length / totalDeals) * 100 : 0, + total: totalDeals, + }; + + // Install phase: + // completed = At Lodgement + At Post Survey + Project Complete + // in progress = Installation Complete + const installCompletedDeals = deals.filter((d) => + ["At Lodgement", "At Post Survey", "Project Complete"].includes(d.displayStage) + ); + const installInProgressDeals = deals.filter( + (d) => d.displayStage === "Installation Complete" + ); + + const install: WorkPhaseStats = { + completedDeals: installCompletedDeals, + inProgressDeals: installInProgressDeals, + completedCount: installCompletedDeals.length, + inProgressCount: installInProgressDeals.length, + completedPercentage: + totalDeals > 0 ? (installCompletedDeals.length / totalDeals) * 100 : 0, + inProgressPercentage: + totalDeals > 0 ? (installInProgressDeals.length / totalDeals) * 100 : 0, + total: totalDeals, + }; + + // Lodgement phase: + // completed = At Post Survey + Project Complete + // in progress = At Lodgement + const lodgementInProgressDeals = deals.filter( + (d) => d.displayStage === "At Lodgement" + ); + + const lodgement: WorkPhaseStats = { + completedDeals, + inProgressDeals: lodgementInProgressDeals, + completedCount, + inProgressCount: lodgementInProgressDeals.length, completedPercentage: totalDeals > 0 ? (completedCount / totalDeals) * 100 : 0, inProgressPercentage: totalDeals > 0 - ? (designInProgressDeals.length / totalDeals) * 100 + ? (lodgementInProgressDeals.length / totalDeals) * 100 : 0, total: totalDeals, }; @@ -220,6 +334,10 @@ export function computeProjectProgress( totalDeals, coordination, design, + install, + lodgement, + dampMouldRisk: computeDampMouldRisk(deals), + funnelStages: computeFunnelStages(deals), }; } @@ -257,7 +375,7 @@ export function computeOutcomeSlices(deals: ClassifiedDeal[]): OutcomeSlice[] { // ----------------------------------------------------------------------- export function computeLiveTrackerData( rawDeals: HubspotDeal[] -): LiveTrackerProps { +): Omit { // Classify all deals (add displayStage field) const classified = classifyDeals(rawDeals); @@ -283,6 +401,16 @@ export function computeLiveTrackerData( }) ); + // When there are multiple project codes, prepend a synthetic "All Projects" entry + if (projects.length > 1) { + projects.unshift({ + projectCode: "__ALL__", + progress: computeProjectProgress(classified), + outcomePieSlices: computeOutcomeSlices(classified), + allDeals: classified, + }); + } + return { projects, totalDeals: classified.length, diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts index 229a2ed..931413c 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts @@ -5,6 +5,7 @@ // ----------------------------------------------------------------------- // Raw DB row from hubspotDealData table +// New CRM-synced fields are nullable — populated by HubSpot sync // ----------------------------------------------------------------------- export type HubspotDeal = { id: string; @@ -22,12 +23,38 @@ export type HubspotDeal = { majorConditionIssuePhotosS3: string | null; coordinationStatus: string | null; designStatus: string | null; + + // ── CRM-synced additions ────────────────────────────────────────────── + pashubLink: string | null; + sharepointLink: string | null; + dampMouldFlag: string | null; // coordinator-stage damp/mould flag + preSapScore: string | null; // kept as text (HubSpot returns strings) + coordinator: string | null; + ioeV1Date: Date | null; + ioeV2Date: Date | null; + ioeV3Date: Date | null; + proposedMeasures: string | null; + approvedPackage: string | null; + designer: string | null; + designDate: Date | null; + actualMeasuresInstalled: string | null; + installer: string | null; + installerHandover: string | null; + lodgementStatus: string | null; + measuresLodgementDate: Date | null; + fullLodgementDate: Date | null; + confirmedSurveyDate: Date | null; + surveyedDate: Date | null; + designType: string | null; + createdAt: Date; updatedAt: Date; }; // ----------------------------------------------------------------------- -// Stage classification result - human-readable display labels +// Stage classification result — human-readable display labels +// Full end-to-end pipeline: assessment → coordination → design → +// install → lodgement → completed (funded) // ----------------------------------------------------------------------- export type DisplayStage = | "Scope & Planning" @@ -35,12 +62,16 @@ export type DisplayStage = | "Assessment in Progress" | "Coordination in Progress" | "Design in Progress" - | "Completed" + | "Installation in Progress" + | "Installation Complete" + | "At Lodgement" + | "At Post Survey" + | "Project Complete" | "Queries" | "Unknown Stage"; // ----------------------------------------------------------------------- -// A classified deal - original row plus its resolved display stage +// A classified deal — original row plus its resolved display stage // ----------------------------------------------------------------------- export type ClassifiedDeal = HubspotDeal & { displayStage: DisplayStage; @@ -57,7 +88,7 @@ export type StageProgressItem = { }; // ----------------------------------------------------------------------- -// Coordination/Design summary card data +// Coordination/Design/Install/Lodgement summary card data // ----------------------------------------------------------------------- export type WorkPhaseStats = { completedDeals: ClassifiedDeal[]; @@ -69,6 +100,29 @@ export type WorkPhaseStats = { total: number; }; +// ----------------------------------------------------------------------- +// Damp & mould risk comparison (survey-stage vs coordination-stage flags) +// ----------------------------------------------------------------------- +export type DampMouldRiskData = { + surveyFlagCount: number; // majorConditionIssuePhotosS3 not null + coordinatorFlagCount: number; // dampMouldFlag not null/non-empty + bothFlaggedCount: number; // flagged at both stages (highest risk) + totalDeals: number; + surveyFlagDeals: ClassifiedDeal[]; + coordinatorFlagDeals: ClassifiedDeal[]; +}; + +// ----------------------------------------------------------------------- +// Pipeline funnel data — dual counts per stage +// ----------------------------------------------------------------------- +export type FunnelStage = { + stage: DisplayStage; + currentCount: number; // deals at exactly this stage right now + currentPct: number; // as % of non-query total + cumulativeCount: number; // deals that have reached this stage or beyond + cumulativePct: number; +}; + // ----------------------------------------------------------------------- // All computed data for the ProgressOverview component // ----------------------------------------------------------------------- @@ -82,6 +136,10 @@ export type ProjectProgressData = { totalDeals: number; coordination: WorkPhaseStats; design: WorkPhaseStats; + install: WorkPhaseStats; + lodgement: WorkPhaseStats; + dampMouldRisk: DampMouldRiskData; + funnelStages: FunnelStage[]; }; // ----------------------------------------------------------------------- @@ -110,19 +168,62 @@ export type LiveTrackerProps = { projects: ProjectData[]; totalDeals: number; majorConditionDeals: ClassifiedDeal[]; // for Awaab's Law card + docStatusMap: DocStatusMap; }; // ----------------------------------------------------------------------- // Table drill-down shape (stays in LiveTracker state) +// columns can include computed ClassifiedDeal fields (e.g. displayStage) // ----------------------------------------------------------------------- export type TableModal = { stage: string; data: ClassifiedDeal[]; - columns: (keyof HubspotDeal)[]; - columnLabels: Partial>; + columns: (keyof ClassifiedDeal)[]; + columnLabels: Partial>; breakdown?: Record; }; +// ----------------------------------------------------------------------- +// Document drawer types +// ----------------------------------------------------------------------- +export type PropertyDocument = { + id: string; + s3FileKey: string; // S3 object key — used directly for presigned URL + s3FileBucket: string; // S3 bucket name + docType: string; // fileType enum value + s3UploadTimestamp: string; // ISO string + uprn: string | null; + landlordPropertyId: string | null; +}; + +// All survey document types expected for a complete survey +export const EXPECTED_SURVEY_DOC_TYPES = [ + "photo_pack", + "site_note", + "rd_sap_site_note", + "pas_2023_ventilation", + "pas_2023_condition", + "pas_significance", + "par_photo_pack", + "pas_2023_property", + "pas_2023_occupancy", +] as const; + +export type DocStatus = { + presentTypes: string[]; + hasDocs: boolean; + isComplete: boolean; // all EXPECTED_SURVEY_DOC_TYPES present +}; + +export type DocStatusMap = Record; // keyed by UPRN string + +export type DocumentDrawerState = { + open: boolean; + uprn: string | null; + landlordPropertyId: string | null; + dealname: string | null; +}; + // ----------------------------------------------------------------------- // Surveyor outcome constants (single source of truth) // ----------------------------------------------------------------------- @@ -149,5 +250,90 @@ export const STAGE_ORDER: DisplayStage[] = [ "Assessment in Progress", "Coordination in Progress", "Design in Progress", - "Completed", + "Installation in Progress", + "Installation Complete", + "At Lodgement", + "At Post Survey", + "Project Complete", ]; + +// ----------------------------------------------------------------------- +// Stage colour mapping — used for badges (PropertyTable) and funnel bars (AnalyticsView) +// ----------------------------------------------------------------------- +export const STAGE_COLORS: Record< + DisplayStage, + { bg: string; text: string; border: string; dot: string } +> = { + "Scope & Planning": { + bg: "bg-slate-100", + text: "text-slate-700", + border: "border-slate-200", + dot: "bg-slate-400", + }, + "Booking in Progress": { + bg: "bg-sky-50", + text: "text-sky-700", + border: "border-sky-200", + dot: "bg-sky-400", + }, + "Assessment in Progress": { + bg: "bg-blue-100", + text: "text-blue-900", + border: "border-blue-400", + dot: "bg-blue-700", + }, + "Coordination in Progress": { + bg: "bg-indigo-50", + text: "text-indigo-700", + border: "border-indigo-200", + dot: "bg-indigo-400", + }, + "Design in Progress": { + bg: "bg-blue-50", + text: "text-blue-700", + border: "border-blue-200", + dot: "bg-blue-400", + }, + "Installation in Progress": { + bg: "bg-indigo-50", + text: "text-indigo-600", + border: "border-indigo-200", + dot: "bg-indigo-300", + }, + "Installation Complete": { + bg: "bg-teal-50", + text: "text-teal-700", + border: "border-teal-200", + dot: "bg-teal-400", + }, + "At Lodgement": { + bg: "bg-cyan-50", + text: "text-cyan-700", + border: "border-cyan-200", + dot: "bg-cyan-400", + }, + "At Post Survey": { + bg: "bg-violet-50", + text: "text-violet-700", + border: "border-violet-200", + dot: "bg-violet-400", + }, + "Project Complete": { + bg: "bg-emerald-50", + text: "text-emerald-700", + border: "border-emerald-200", + dot: "bg-emerald-500", + }, + Queries: { + bg: "bg-red-50", + text: "text-red-600", + border: "border-red-200", + dot: "bg-red-400", + }, + "Unknown Stage": { + bg: "bg-gray-50", + text: "text-gray-500", + border: "border-gray-100", + dot: "bg-gray-300", + }, +}; diff --git a/tailwind.config.js b/tailwind.config.js index 31dda4f..3145938 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -371,6 +371,32 @@ module.exports = { "ui-selected:bg-[#eff6fc]", "ui-selected:border-[#eff6fc]", "ui-selected:text-[#eff6fc]", + // brandbrown for Tremor charts + "bg-[#c4a47c]", + "border-[#c4a47c]", + "hover:bg-[#c4a47c]", + "hover:border-[#c4a47c]", + "hover:text-[#c4a47c]", + "fill-[#c4a47c]", + "ring-[#c4a47c]", + "stroke-[#c4a47c]", + "text-[#c4a47c]", + "ui-selected:bg-[#c4a47c]", + "ui-selected:border-[#c4a47c]", + "ui-selected:text-[#c4a47c]", + // lighter blue for Tremor charts + "bg-[#8b96e9]", + "border-[#8b96e9]", + "hover:bg-[#8b96e9]", + "hover:border-[#8b96e9]", + "hover:text-[#8b96e9]", + "fill-[#8b96e9]", + "ring-[#8b96e9]", + "stroke-[#8b96e9]", + "text-[#8b96e9]", + "ui-selected:bg-[#8b96e9]", + "ui-selected:border-[#8b96e9]", + "ui-selected:text-[#8b96e9]", // brand blues for Tremor charts "bg-[#14163d]", "border-[#14163d]", @@ -392,6 +418,12 @@ module.exports = { "fill-[#5d6be0]", "stroke-[#5d6be0]", "text-[#5d6be0]", + // pale blue (4th chart series) + "bg-[#b8bef4]", + "border-[#b8bef4]", + "fill-[#b8bef4]", + "stroke-[#b8bef4]", + "text-[#b8bef4]", "bg-[#1f3abdff]", "border-[#1f3abdff]", "fill-[#1f3abdff]",