From 2825eb1062f5958cef314c6d832f34b58b18fa66 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Sat, 21 Feb 2026 08:20:23 +0000 Subject: [PATCH 01/25] show this to khalim --- src/app/db/db.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app/db/db.ts b/src/app/db/db.ts index fc9d118..22b8138 100644 --- a/src/app/db/db.ts +++ b/src/app/db/db.ts @@ -10,6 +10,8 @@ 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"; export const pool = new Pool({ host: process.env.DB_HOST, @@ -31,6 +33,8 @@ const schema = { ...EnergyAssessmentsSchema, ...FundingSchema, ...Users, + tasks, + subTasks, }; export const db = drizzle(pool, { From c8d38c11d52350df73de3cb511df63e28bf7cab6 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Sat, 21 Feb 2026 08:24:55 +0000 Subject: [PATCH 02/25] subtasks --- src/app/api/tasks/[taskId]/route.ts | 26 +++ src/app/api/tasks/route.ts | 37 ++++ src/app/iq/IQDashboard.tsx | 146 ++++++++++++++ src/app/iq/SubtaskDetails.tsx | 237 ++++++++++++++++++++++ src/app/iq/TaskList.tsx | 293 ++++++++++++++++++++++++++++ src/app/iq/page.tsx | 5 + 6 files changed, 744 insertions(+) create mode 100644 src/app/api/tasks/[taskId]/route.ts create mode 100644 src/app/api/tasks/route.ts create mode 100644 src/app/iq/IQDashboard.tsx create mode 100644 src/app/iq/SubtaskDetails.tsx create mode 100644 src/app/iq/TaskList.tsx create mode 100644 src/app/iq/page.tsx 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/iq/IQDashboard.tsx b/src/app/iq/IQDashboard.tsx new file mode 100644 index 0000000..262a35f --- /dev/null +++ b/src/app/iq/IQDashboard.tsx @@ -0,0 +1,146 @@ +"use client"; + +import { useState, useEffect } from "react"; +import TaskList from "./TaskList"; +import SubtaskDetails from "./SubtaskDetails"; + +export interface Task { + id: string; + taskSource: string; + jobStarted: string | null; + jobCompleted: string | null; + status: string; + service: string | null; + updatedAt: string; +} + +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: Task[]; + total: number; + limit: number; + offset: number; +} + +export default function IQDashboard() { + const [tasks, setTasks] = useState([]); + const [selectedTaskId, setSelectedTaskId] = useState(null); + const [subtasks, setSubtasks] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [total, setTotal] = useState(0); + const [limit, setLimit] = useState(20); + const [offset, setOffset] = useState(0); + const [loadingMore, setLoadingMore] = useState(false); + + // Fetch tasks with pagination + const fetchTasks = async (newLimit: number, newOffset: number) => { + try { + if (newOffset === 0) setLoading(true); + else setLoadingMore(true); + + const response = await fetch( + `/api/tasks?limit=${newLimit}&offset=${newOffset}` + ); + if (!response.ok) throw new Error("Failed to fetch tasks"); + const data: TasksResponse = await response.json(); + + if (newOffset === 0) { + setTasks(data.tasks); + } else { + setTasks((prev) => [...prev, ...data.tasks]); + } + + setTotal(data.total); + setLimit(data.limit); + setOffset(newOffset + data.tasks.length); + setError(null); + } catch (err) { + setError(err instanceof Error ? err.message : "An error occurred"); + if (newOffset === 0) setTasks([]); + } finally { + setLoading(false); + setLoadingMore(false); + } + }; + + // Initial load (first 20) + useEffect(() => { + fetchTasks(20, 0); + }, []); + + const handleLoadMore = () => { + fetchTasks(20, offset); + }; + + const handleRefresh = () => { + setOffset(0); + fetchTasks(20, 0); + }; + + // Fetch subtasks when a task is selected + useEffect(() => { + if (!selectedTaskId) { + setSubtasks([]); + return; + } + + const fetchSubtasks = async () => { + try { + const response = await fetch(`/api/tasks/${selectedTaskId}`); + if (!response.ok) throw new Error("Failed to fetch subtasks"); + const data = await response.json(); + setSubtasks(data); + } catch (err) { + setSubtasks([]); + } + }; + + fetchSubtasks(); + }, [selectedTaskId]); + + return ( +
+ {/* Left sidebar - Task list */} +
+ +
+ + {/* Right side - Subtask details */} +
+ {selectedTaskId ? ( + t.id === selectedTaskId)} + /> + ) : ( +
+ Select a task to view its subtasks +
+ )} +
+
+ ); +} diff --git a/src/app/iq/SubtaskDetails.tsx b/src/app/iq/SubtaskDetails.tsx new file mode 100644 index 0000000..5d1e381 --- /dev/null +++ b/src/app/iq/SubtaskDetails.tsx @@ -0,0 +1,237 @@ +"use client"; + +import { useState } from "react"; +import { SubTask, Task } from "./IQDashboard"; +import { Badge } from "@/app/shadcn_components/ui/badge"; +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"; + +interface SubtaskDetailsProps { + selectedTaskId: string; + subtasks: SubTask[]; + task?: Task; +} + +function getStatusColor( + status: string +): "default" | "secondary" | "destructive" | "outline" { + switch (status.toLowerCase()) { + case "completed": + return "default"; + case "in progress": + return "secondary"; + case "failed": + return "destructive"; + default: + return "outline"; + } +} + +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}
+      
+
+ ); +} + +export default function SubtaskDetails({ + selectedTaskId, + subtasks, + task, +}: SubtaskDetailsProps) { + return ( +
+ {/* Task Header */} + {task && ( +
+
+
+

+ {task.taskSource} +

+ + {task.status} + +
+
+
+

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()} +

+
+ )} +
+

Updated

+

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

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

+ Subtasks ({subtasks.length}) +

+ + {subtasks.length === 0 && ( +
+ No subtasks found +
+ )} + +
+ {subtasks.map((subtask, index) => ( + +
+ {/* Header */} +
+
+

+ Subtask {index + 1} +

+ + {subtask.id} + +
+ + {subtask.status} + +
+ + {/* Timeline */} +
+ {subtask.jobStarted && ( +
+

Started

+

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

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

Completed

+

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

+
+ )} +
+ + {/* Inputs */} + {subtask.inputs && ( + + )} + + {/* Outputs */} + {subtask.outputs && ( + + )} + + {/* Cloud Logs */} + {subtask.cloudLogsURL && ( +
+

+ Cloud Logs +

+ + {subtask.cloudLogsURL} + +
+ )} + + {/* Updated */} +

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

+
+
+ ))} +
+
+
+
+
+ ); +} diff --git a/src/app/iq/TaskList.tsx b/src/app/iq/TaskList.tsx new file mode 100644 index 0000000..f87539a --- /dev/null +++ b/src/app/iq/TaskList.tsx @@ -0,0 +1,293 @@ +"use client"; + +import { useState, useMemo } from "react"; +import { Task } from "./IQDashboard"; +import { Badge } from "@/app/shadcn_components/ui/badge"; +import { ScrollArea } from "@/app/shadcn_components/ui/scroll-area"; +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 TaskListProps { + tasks: Task[]; + 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 getStatusColor( + status: string +): "default" | "secondary" | "destructive" | "outline" { + switch (status.toLowerCase()) { + case "completed": + case "in progress": + return "default"; + case "failed": + return "destructive"; + default: + return "secondary"; + } +} + +export default function TaskList({ + tasks, + selectedTaskId, + onSelectTask, + loading, + loadingMore, + error, + total, + onLoadMore, + onRefresh, +}: TaskListProps) { + const [searchQuery, setSearchQuery] = useState(""); + const [statusFilter, setStatusFilter] = useState("all"); + const [serviceFilter, setServiceFilter] = useState("all"); + const [sortBy, setSortBy] = useState("recent"); + + // Get unique statuses and services for filter options + const uniqueStatuses = useMemo( + () => [...new Set(tasks.map((t) => t.status))].sort(), + [tasks] + ); + const uniqueServices = useMemo( + () => + [...new Set(tasks.map((t) => t.service).filter(Boolean))].sort() as string[], + [tasks] + ); + + // Filter and sort tasks + const filteredTasks = useMemo(() => { + let result = tasks; + + // Status filter + if (statusFilter !== "all") { + result = result.filter((t) => t.status === statusFilter); + } + + // Service filter + if (serviceFilter !== "all") { + result = result.filter((t) => t.service === serviceFilter); + } + + // Search query + 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) + ); + } + + // Sort + 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 */} +
+ {/* Search */} + setSearchQuery(e.target.value)} + className="text-sm" + /> + + {/* Sort */} + + + {/* Status Filter */} + {uniqueStatuses.length > 0 && ( + + )} + + {/* Service Filter */} + {uniqueServices.length > 0 && ( + + )} + + {/* Reset Filters */} + {(searchQuery || statusFilter !== "all" || serviceFilter !== "all") && ( + + )} +
+ + {/* Content */} + + {error && ( +
+ {error} +
+ )} + + {loading && ( +
+ Loading tasks... +
+ )} + + {!loading && !error && tasks.length === 0 && ( +
+ No tasks found +
+ )} + +
+ {filteredTasks.map((task) => ( + + ))} + + {/* Load More Button */} + {tasks.length < total && ( +
+ +
+ )} +
+
+
+ ); +} diff --git a/src/app/iq/page.tsx b/src/app/iq/page.tsx new file mode 100644 index 0000000..4410678 --- /dev/null +++ b/src/app/iq/page.tsx @@ -0,0 +1,5 @@ +import IQDashboard from "./IQDashboard"; + +export default function IQPage() { + return ; +} From 862fcb0f19d81064a77331077aaa40c9740e65da Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Sat, 21 Feb 2026 08:29:25 +0000 Subject: [PATCH 03/25] npm build --- src/app/iq/TaskList.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/iq/TaskList.tsx b/src/app/iq/TaskList.tsx index f87539a..8be56cc 100644 --- a/src/app/iq/TaskList.tsx +++ b/src/app/iq/TaskList.tsx @@ -60,12 +60,12 @@ export default function TaskList({ // Get unique statuses and services for filter options const uniqueStatuses = useMemo( - () => [...new Set(tasks.map((t) => t.status))].sort(), + () => Array.from(new Set(tasks.map((t) => t.status))).sort(), [tasks] ); const uniqueServices = useMemo( () => - [...new Set(tasks.map((t) => t.service).filter(Boolean))].sort() as string[], + Array.from(new Set(tasks.map((t) => t.service).filter(Boolean))).sort() as string[], [tasks] ); From d2636c668ef02a8fdd2a2db23969c70a4369ee9f Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Sat, 21 Feb 2026 08:39:00 +0000 Subject: [PATCH 04/25] redeploy --- src/app/iq/SubtaskDetails.tsx | 206 +++++++++++++++++++++------------- 1 file changed, 129 insertions(+), 77 deletions(-) diff --git a/src/app/iq/SubtaskDetails.tsx b/src/app/iq/SubtaskDetails.tsx index 5d1e381..7139a1a 100644 --- a/src/app/iq/SubtaskDetails.tsx +++ b/src/app/iq/SubtaskDetails.tsx @@ -6,6 +6,7 @@ import { Badge } from "@/app/shadcn_components/ui/badge"; 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 } from "lucide-react"; interface SubtaskDetailsProps { selectedTaskId: string; @@ -76,11 +77,131 @@ function CopyableCodeBlock({ ); } +interface ExpandedSubtask { + [key: string]: boolean; +} + +function ExpandableSubtaskTile({ + subtask, + index, + isExpanded, + onToggle, +}: { + subtask: SubTask; + index: number; + isExpanded: boolean; + onToggle: () => void; +}) { + return ( + + {/* Tile Header */} + + + {/* Expanded Content */} + {isExpanded && ( +
+ {/* 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 */} + {subtask.cloudLogsURL && ( +
+

+ Cloud Logs +

+ + {subtask.cloudLogsURL} + +
+ )} + + {/* Updated */} +

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

+
+ )} +
+ ); +} + export default function SubtaskDetails({ selectedTaskId, subtasks, task, }: SubtaskDetailsProps) { + const [expandedSubtasks, setExpandedSubtasks] = useState({}); + + const toggleSubtask = (subtaskId: string) => { + setExpandedSubtasks((prev) => ({ + ...prev, + [subtaskId]: !prev[subtaskId], + })); + }; + return (
{/* Task Header */} @@ -149,84 +270,15 @@ export default function SubtaskDetails({
)} -
+
{subtasks.map((subtask, index) => ( - -
- {/* Header */} -
-
-

- Subtask {index + 1} -

- - {subtask.id} - -
- - {subtask.status} - -
- - {/* Timeline */} -
- {subtask.jobStarted && ( -
-

Started

-

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

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

Completed

-

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

-
- )} -
- - {/* Inputs */} - {subtask.inputs && ( - - )} - - {/* Outputs */} - {subtask.outputs && ( - - )} - - {/* Cloud Logs */} - {subtask.cloudLogsURL && ( -
-

- Cloud Logs -

- - {subtask.cloudLogsURL} - -
- )} - - {/* Updated */} -

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

-
-
+ toggleSubtask(subtask.id)} + /> ))}
From 6605223865324e6c13851ce9c9488e2a2d7a72c5 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 31 Mar 2026 14:17:46 +0000 Subject: [PATCH 05/25] working on reporting page --- .../your-projects/live/LiveTracker.tsx | 338 +++------ .../your-projects/live/ProgressOverview.tsx | 706 ++++++++++-------- .../(portfolio)/your-projects/live/page.tsx | 52 +- .../your-projects/live/transforms.ts | 167 ++++- .../(portfolio)/your-projects/live/types.ts | 172 ++++- 5 files changed, 829 insertions(+), 606 deletions(-) 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..ccdb2d5 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx @@ -1,41 +1,51 @@ "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 } from "lucide-react"; +import TableViewer from "./TableViewer"; +import PropertyTable from "./PropertyTable"; +import PropertyDrawer from "./PropertyDrawer"; +import AnalyticsView from "./AnalyticsView"; +import type { + LiveTrackerProps, + TableModal, + ClassifiedDeal, + HubspotDeal, + DocumentDrawerState, +} from "./types"; export default function LiveTracker({ projects, totalDeals, majorConditionDeals, }: LiveTrackerProps) { - // UI State: which table modal is open - const [openTable, setOpenTable] = useState(null); + // ── Tab state ──────────────────────────────────────────────────────── + const [activeTab, setActiveTab] = useState<"analytics" | "properties">("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 ); - // 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, + dealname: null, + }); const handleOpenTable = ( stage: string, @@ -56,6 +66,10 @@ export default function LiveTracker({ }); }; + const handleOpenDrawer = (uprn: string | null, dealname: string | null) => { + setDrawerState({ open: true, uprn, dealname }); + }; + if (!totalDeals) { return ( @@ -67,123 +81,74 @@ 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")} + > + {/* Tab bar */} + + - - - + + Analytics + + + + Properties + + - {hasSurveyData && ( - - - + {/* Analytics tab */} + + {currentProject && ( + + )} + + + {/* Properties tab */} + +
+ {/* Project selector — mirrors analytics tab */} + {projects.length > 1 && ( +
+ Project: + +
)} -
-
- )} - {/* 🔹 Table Modal */} + +
+ + + + {/* ── Drill-down table modal ─────────────────────────────────────── */} {openTable && (
- {/* Breakdown Stats */} {openTable.breakdown && (
{Object.entries(openTable.breakdown).map(([category, items]) => { @@ -219,9 +183,7 @@ export default function LiveTracker({ const borderColor = isCompleted ? "border-brandblue/40" : "border-amber-200/50"; - const textColor = isCompleted - ? "text-brandblue" - : "text-amber-600"; + const textColor = isCompleted ? "text-brandblue" : "text-amber-600"; const labelColor = isCompleted ? "text-brandblue" : "text-amber-600/70"; @@ -231,20 +193,14 @@ export default function LiveTracker({ key={category} className={`${bgColor} rounded-lg p-3 border ${borderColor}`} > -

+

{category}

{items.length}

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

); @@ -264,10 +220,7 @@ export default function LiveTracker({
)} + + {/* ── Document drawer ────────────────────────────────────────────── */} + + setDrawerState({ open: false, uprn: null, dealname: 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..3644571 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ProgressOverview.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ProgressOverview.tsx @@ -1,339 +1,429 @@ "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 { AlertCircle, CheckCircle2, ArrowRight, Users, PenLine, Hammer, FileCheck } from "lucide-react"; +import { Card, CardContent } from "@/app/shadcn_components/ui/card"; +import { STAGE_COLORS } from "./types"; +import type { ProjectProgressData, ClassifiedDeal, ClassifiedDeal as CD } from "./types"; +// ----------------------------------------------------------------------- +// Columns shown per card — enough context to understand WHY +// ----------------------------------------------------------------------- +const QUERIES_COLUMNS: (keyof CD)[] = [ + "dealname", "landlordPropertyId", "displayStage", "outcome", "coordinationStatus", "outcomeNotes", +]; +const QUERIES_LABELS: Partial> = { + dealname: "Address", + landlordPropertyId: "Ref", + displayStage: "Reason / Stage", + outcome: "Survey Outcome", + coordinationStatus: "Coordination Status", + outcomeNotes: "Notes", +}; + +const COORD_COLUMNS: (keyof CD)[] = [ + "dealname", "landlordPropertyId", "displayStage", "coordinator", "coordinationStatus", "ioeV1Date", +]; +const COORD_LABELS: Partial> = { + dealname: "Address", + landlordPropertyId: "Ref", + displayStage: "Current Stage", + coordinator: "Coordinator", + coordinationStatus: "Coordination Status", + ioeV1Date: "IOE/MTP Date", +}; + +const DESIGN_COLUMNS: (keyof CD)[] = [ + "dealname", "landlordPropertyId", "displayStage", "designer", "designStatus", "approvedPackage", "designDate", +]; +const DESIGN_LABELS: Partial> = { + dealname: "Address", + landlordPropertyId: "Ref", + displayStage: "Current Stage", + designer: "Designer", + designStatus: "Design Status", + approvedPackage: "Package", + designDate: "Design Date", +}; + +const INSTALL_COLUMNS: (keyof CD)[] = [ + "dealname", "landlordPropertyId", "displayStage", "installer", "installerHandover", "actualMeasuresInstalled", +]; +const INSTALL_LABELS: Partial> = { + dealname: "Address", + landlordPropertyId: "Ref", + displayStage: "Current Stage", + installer: "Installer", + installerHandover: "Handover Date", + actualMeasuresInstalled: "Measures Installed", +}; + +const LODGE_COLUMNS: (keyof CD)[] = [ + "dealname", "landlordPropertyId", "displayStage", "lodgementStatus", "postSapScore", "measuresLodgementDate", "fullLodgementDate", +]; +const LODGE_LABELS: Partial> = { + dealname: "Address", + landlordPropertyId: "Ref", + displayStage: "Current Stage", + lodgementStatus: "Lodgement Status", + postSapScore: "Post-SAP Score", + measuresLodgementDate: "Measures Lodged", + fullLodgementDate: "Full Lodgement Date", +}; + +const EARLY_COLUMNS: (keyof CD)[] = [ + "dealname", "landlordPropertyId", "displayStage", "preSapScore", "outcome", "raStatus", +]; +const EARLY_LABELS: Partial> = { + dealname: "Address", + landlordPropertyId: "Ref", + displayStage: "Current Stage", + preSapScore: "Pre-SAP Score", + outcome: "Survey Outcome", + raStatus: "Assessment Status", +}; + +// ----------------------------------------------------------------------- +// 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 ( + + + + + ); +} + +// ----------------------------------------------------------------------- +// Clickable phase summary card (coordination, design, install, lodgement) +// ----------------------------------------------------------------------- +function PhaseCard({ + icon: Icon, + title, + completedCount, + inProgressCount, + total, + completedPct, + inProgressPct, + ringColor, + onClick, +}: { + icon: React.ElementType; + title: string; + completedCount: number; + inProgressCount: number; + total: number; + completedPct: number; + inProgressPct: number; + ringColor: string; + onClick: () => void; +}) { + return ( + +
+
+
+
+ +
+ + {title} + +
+ +
+
+ Complete + {completedCount} +
+
+
+
+ {inProgressCount > 0 && ( + <> +
+ In progress + {inProgressCount} +
+
+
+
+ + )} +
+
+ +
+ +
+ {completedPct.toFixed(0)}% +
+
+
+ +
+ + View breakdown +
+ + ); +} + +// ----------------------------------------------------------------------- +// Main component +// ----------------------------------------------------------------------- interface ProgressOverviewProps { data: ProjectProgressData; onOpenTable?: ( stage: string, deals: ClassifiedDeal[], - columns?: (keyof HubspotDeal)[], - columnLabels?: Partial>, + columns?: (keyof CD)[], + columnLabels?: Partial>, breakdown?: Record ) => void; } -export default function ProgressOverview({ - data, - onOpenTable, -}: ProgressOverviewProps) { - // Pre-computed values from props +export default function ProgressOverview({ data, onOpenTable }: ProgressOverviewProps) { const { completedPercentage, completedCount, - totalDeals, + nonQueryTotal, queriesDeals, + stageProgress, coordination, design, + install, + lodgement, } = 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 */} -
-
-

- Work Completed -

-

- End-to-end project overview -

-
+ + - {/* Circular Progress */} -
- - {/* Background circle */} - - {/* Progress circle */} - - - - - - - - - {/* Center text */} -
- - {completedPercentage.toFixed(0)}% - - - {completedCount}/{totalDeals} - -
-
-
- - {/* 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( - item.stage, - item.deals, - ["dealname", "landlordPropertyId"], - { - dealname: "Address Ref.", - landlordPropertyId: "Property Ref.", - } - ); - } - }} - whileHover={{ scale: 1.02 }} - className="group relative text-left" - > - -
-
-

- {item.stage} -

-

- {item.count} -

-
- -
-

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

-
- -
- View - -
-
-
-
- ))} -
- ) : null; - })()} - - {/* Project Summary Cards - Coordination & Design */} -
- - - -
- - {/* Queries / Attention Required Section */} - {queriesDeals.length > 0 && ( + {/* ── Completion header ──────────────────────────────────────────── */} + onOpenTable?.( + "Completed Properties", + data.completedDeals, + ["dealname", "landlordPropertyId", "displayStage", "actualMeasuresInstalled", "postSapScore", "fullLodgementDate"], + { + dealname: "Address", + landlordPropertyId: "Ref", + displayStage: "Stage", + actualMeasuresInstalled: "Measures Installed", + postSapScore: "Post-SAP", + 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-sm transition-all duration-200" > - -
- {/* 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 - +
+
+ +
+ + {completedPercentage.toFixed(0)}% +
- +
+
+ + Work Completed +
+

+ {completedCount} + / {nonQueryTotal} +

+

Properties fully lodged and funded

+
+ +
- )} -
+ + {/* ── Early stage chips ─────────────────────────────────────────── */} + {earlyItems.length > 0 && ( +
+ {earlyItems.map((item) => { + const c = STAGE_COLORS[item.stage]; + return ( + + onOpenTable?.(item.stage, item.deals, EARLY_COLUMNS, EARLY_LABELS) + } + className={`group text-left rounded-xl border p-3 transition-all duration-200 hover:shadow-sm ${c.bg} ${c.border} hover:opacity-90`} + > +
+ + + {item.stage} + +
+

{item.count}

+

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

+
+ ); + })} +
+ )} + + {/* ── Phase summary cards ──────────────────────────────────────── */} +
+ + onOpenTable?.( + "Coordination Status", + [...coordination.completedDeals, ...coordination.inProgressDeals], + COORD_COLUMNS, + COORD_LABELS, + { + "Coordination Complete": coordination.completedDeals, + "In Progress": coordination.inProgressDeals, + } + ) + } + /> + + onOpenTable?.( + "Design Status", + [...design.completedDeals, ...design.inProgressDeals], + DESIGN_COLUMNS, + DESIGN_LABELS, + { + "Design Complete": design.completedDeals, + "In Progress": design.inProgressDeals, + } + ) + } + /> + + onOpenTable?.( + "Installation Status", + [...install.completedDeals, ...install.inProgressDeals], + INSTALL_COLUMNS, + INSTALL_LABELS, + { + "Install Complete": install.completedDeals, + "In Progress": install.inProgressDeals, + } + ) + } + /> + + onOpenTable?.( + "Lodgement Status", + [...lodgement.completedDeals, ...lodgement.inProgressDeals], + LODGE_COLUMNS, + LODGE_LABELS, + { + "Fully Lodged": lodgement.completedDeals, + "In Progress": lodgement.inProgressDeals, + } + ) + } + /> +
+ + {/* ── Requires input ────────────────────────────────────────────── */} + {queriesDeals.length > 0 && ( + + onOpenTable?.( + "Properties Requiring Attention", + queriesDeals, + QUERIES_COLUMNS, + QUERIES_LABELS + ) + } + className="group w-full text-left rounded-xl border border-amber-300 bg-gradient-to-r from-amber-50 to-white p-4 hover:border-amber-400 hover:shadow-sm transition-all duration-200" + > +
+
+ +
+
+
+ Requires Your Input + + {queriesDeals.length} + +
+

+ Click to see the issue, survey outcome, and coordination status for each property +

+
+ +
+
+ )} + + ); } 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..6a957e4 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx @@ -1,18 +1,16 @@ 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 LiveTracker from "./LiveTracker"; import { computeLiveTrackerData } from "./transforms"; -import type { HubspotDeal } from "./types"; +// MOCK PHASE: using mock data while DB migration is in progress +// When migration lands, restore the DB query below and remove this import +import { MOCK_DEALS } from "./mockData"; export default async function LiveReportingPage(props: { params: Promise<{ slug: string }>; }) { - const { slug: portfolioId } = await props.params; + const { slug: _portfolioId } = await props.params; const user = await getServerSession(AuthOptions); if (!user?.user) { @@ -20,41 +18,13 @@ export default async function LiveReportingPage(props: { redirect("/"); } - // 🏢 Fetch the company - - const [company] = await surveyDB - .select() - .from(hubspotCompanyData) - .where(eq(hubspotCompanyData.groupId, portfolioId)); - - if (!company) { - return ( -
-
- No information to show. -
-
- ); - } - - // 💼 Fetch deals for that company - const deals = await surveyDB - .select() - .from(hubspotDealData) - .where(eq(hubspotDealData.companyId, company.companyId)); - - if (!deals || deals.length === 0) { - return ( -
-
- No information to show. -
-
- ); - } - - // 🔄 Transform raw deals to typed and computed data - const trackerData = computeLiveTrackerData(deals as HubspotDeal[]); + // MOCK PHASE ↓↓↓ + // TODO: replace with real DB query once migration is applied: + // const [company] = await surveyDB.select().from(hubspotCompanyData).where(eq(hubspotCompanyData.groupId, portfolioId)); + // const deals = await surveyDB.select().from(hubspotDealData).where(eq(hubspotDealData.companyId, company.companyId)); + // const trackerData = computeLiveTrackerData(deals as HubspotDeal[]); + const trackerData = computeLiveTrackerData(MOCK_DEALS); + // ↑↑↑ END MOCK PHASE return (
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..4119cf6 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,42 @@ 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 "Completed"; + if (deal.lodgementStatus) return "Lodgement"; + if (deal.actualMeasuresInstalled || deal.installerHandover) return "Installation Complete"; + return "Awaiting Install"; +} + // ----------------------------------------------------------------------- // 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 +141,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 // ----------------------------------------------------------------------- @@ -162,12 +224,16 @@ export function computeProjectProgress( const totalDeals = deals.length; // Coordination phase: - // completed = Design in Progress + Completed (i.e. coordination is done) + // completed = Design in Progress + Awaiting Install + Installation Complete + Lodgement + Completed // 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", + "Awaiting Install", + "Installation Complete", + "Lodgement", + "Completed", + ].includes(d.displayStage) ); const coordInProgressDeals = deals.filter( (d) => d.displayStage === "Coordination in Progress" @@ -179,33 +245,78 @@ 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 = Awaiting Install + Installation Complete + Lodgement + Completed // in progress = Design in Progress + const designCompletedDeals = deals.filter((d) => + [ + "Awaiting Install", + "Installation Complete", + "Lodgement", + "Completed", + ].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 = Lodgement + Completed + // in progress = Installation Complete + const installCompletedDeals = deals.filter((d) => + ["Lodgement", "Completed"].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 = Completed + // in progress = Lodgement + const lodgementInProgressDeals = deals.filter( + (d) => d.displayStage === "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 +331,10 @@ export function computeProjectProgress( totalDeals, coordination, design, + install, + lodgement, + dampMouldRisk: computeDampMouldRisk(deals), + funnelStages: computeFunnelStages(deals), }; } 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..8b0ae9a 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,46 @@ 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; + postSapScore: string | null; + lodgementStatus: string | null; + measuresLodgementDate: Date | null; + fullLodgementDate: Date | null; + + // ── Internally tracked stubs (no HubSpot source yet) ───────────────── + raDateBooking: Date | null; + raDateActual: Date | null; + raStatus: string | null; + postEpcDateBooking: Date | null; + postEpcDateActual: Date | null; + postEpcStatus: string | null; + postEprDate: Date | null; + postEprStatus: 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 +70,15 @@ export type DisplayStage = | "Assessment in Progress" | "Coordination in Progress" | "Design in Progress" + | "Awaiting Install" + | "Installation Complete" + | "Lodgement" | "Completed" | "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 +95,7 @@ export type StageProgressItem = { }; // ----------------------------------------------------------------------- -// Coordination/Design summary card data +// Coordination/Design/Install/Lodgement summary card data // ----------------------------------------------------------------------- export type WorkPhaseStats = { completedDeals: ClassifiedDeal[]; @@ -69,6 +107,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 +143,10 @@ export type ProjectProgressData = { totalDeals: number; coordination: WorkPhaseStats; design: WorkPhaseStats; + install: WorkPhaseStats; + lodgement: WorkPhaseStats; + dampMouldRisk: DampMouldRiskData; + funnelStages: FunnelStage[]; }; // ----------------------------------------------------------------------- @@ -114,15 +179,34 @@ export type LiveTrackerProps = { // ----------------------------------------------------------------------- // 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; + s3FileUri: string; + s3JsonUri: string | null; + docType: string; + s3FileUploadTimestamp: string; // ISO string + uprn: string; +}; + +export type DocumentDrawerState = { + open: boolean; + uprn: string | null; + dealname: string | null; +}; + // ----------------------------------------------------------------------- // Surveyor outcome constants (single source of truth) // ----------------------------------------------------------------------- @@ -149,5 +233,83 @@ export const STAGE_ORDER: DisplayStage[] = [ "Assessment in Progress", "Coordination in Progress", "Design in Progress", + "Awaiting Install", + "Installation Complete", + "Lodgement", "Completed", ]; + +// ----------------------------------------------------------------------- +// 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-violet-50", + text: "text-violet-700", + border: "border-violet-200", + dot: "bg-violet-400", + }, + "Coordination in Progress": { + bg: "bg-amber-50", + text: "text-amber-700", + border: "border-amber-200", + dot: "bg-amber-400", + }, + "Design in Progress": { + bg: "bg-orange-50", + text: "text-orange-700", + border: "border-orange-200", + dot: "bg-orange-400", + }, + "Awaiting Install": { + bg: "bg-purple-50", + text: "text-purple-700", + border: "border-purple-200", + dot: "bg-purple-400", + }, + "Installation Complete": { + bg: "bg-teal-50", + text: "text-teal-700", + border: "border-teal-200", + dot: "bg-teal-400", + }, + Lodgement: { + bg: "bg-cyan-50", + text: "text-cyan-700", + border: "border-cyan-200", + dot: "bg-cyan-400", + }, + Completed: { + 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", + }, +}; From cc5e21727f9cc8ad15421686aff1d54570fd003e Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 1 Apr 2026 11:13:14 +0000 Subject: [PATCH 06/25] consolidated ui in live project tracking and adding graphs --- src/app/db/db.ts | 2 + .../your-projects/live/AnalyticsView.tsx | 381 ++++++++++++++++ .../live/CompletionTrendsChart.tsx | 269 ++++++++++++ .../your-projects/live/DampMouldRiskPanel.tsx | 240 +++++++++++ .../your-projects/live/LiveTracker.tsx | 114 ++--- .../your-projects/live/ProgressOverview.tsx | 408 +++++------------- .../your-projects/live/PropertyDrawer.tsx | 313 ++++++++++++++ .../your-projects/live/PropertyTable.tsx | 378 ++++++++++++++++ .../live/PropertyTableColumns.tsx | 310 +++++++++++++ .../your-projects/live/TableViewer.tsx | 57 ++- .../(portfolio)/your-projects/live/page.tsx | 76 +++- .../your-projects/live/transforms.ts | 10 + .../(portfolio)/your-projects/live/types.ts | 12 +- 13 files changed, 2176 insertions(+), 394 deletions(-) create mode 100644 src/app/portfolio/[slug]/(portfolio)/your-projects/live/AnalyticsView.tsx create mode 100644 src/app/portfolio/[slug]/(portfolio)/your-projects/live/CompletionTrendsChart.tsx create mode 100644 src/app/portfolio/[slug]/(portfolio)/your-projects/live/DampMouldRiskPanel.tsx create mode 100644 src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyDrawer.tsx create mode 100644 src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTable.tsx create mode 100644 src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTableColumns.tsx diff --git a/src/app/db/db.ts b/src/app/db/db.ts index fc9d118..1d2d778 100644 --- a/src/app/db/db.ts +++ b/src/app/db/db.ts @@ -10,6 +10,7 @@ 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 * as CrmSchema from "@/app/db/schema/crm/hubspot_deal_table"; export const pool = new Pool({ host: process.env.DB_HOST, @@ -31,6 +32,7 @@ const schema = { ...EnergyAssessmentsSchema, ...FundingSchema, ...Users, + ...CrmSchema, }; export const db = drizzle(pool, { 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..8aaf9b9 --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/AnalyticsView.tsx @@ -0,0 +1,381 @@ +"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 ProgressOverview from "./ProgressOverview"; +import SurveyedResultsPieChart from "./SurveyedResultsPieChart"; +import DampMouldRiskPanel from "./DampMouldRiskPanel"; +import CompletionTrendsChart from "./CompletionTrendsChart"; +import { STAGE_COLORS, STAGE_ORDER } from "./types"; +import type { + ProjectData, + ClassifiedDeal, + TableModal, + FunnelStage, +} 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} + + )} +

+
+ +
+
+ ); +} + +// ----------------------------------------------------------------------- +// 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">("cumulative"); + + const visibleStages = funnelStages.filter( + (s) => s.currentCount > 0 || s.cumulativeCount > 0, + ); + + 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 ( + + onOpenTable( + `Pipeline — ${s.stage}`, + deals, + [ + "dealname", + "landlordPropertyId", + "displayStage", + "coordinator", + "designer", + "installer", + ], + { + dealname: "Address", + landlordPropertyId: "Ref", + displayStage: "Stage", + coordinator: "Coordinator", + designer: "Designer", + installer: "Installer", + }, + undefined, + `Pipeline — ${s.stage}`, + mode === "cumulative" + ? `Properties that have reached the "${s.stage}" stage or beyond.` + : `Properties currently at the "${s.stage}" stage.`, + undefined, + ) + } + 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 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 3: Progress overview only (no donut chart) */} +
+ + + +
+ + {/* Row 4: Pipeline Funnel */} + + + {/* Row 5: Damp & Mould Risk (moved up) */} + +
+ ); +} 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..f510ea7 --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/CompletionTrendsChart.tsx @@ -0,0 +1,269 @@ +"use client"; + +import { useState } from "react"; +import { Card, Title, LineChart, Legend } from "@tremor/react"; +import { Button } from "@/app/shadcn_components/ui/button"; +import { Input } from "@/app/shadcn_components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, +} from "@/app/shadcn_components/ui/select"; +import type { ClassifiedDeal } from "./types"; + +interface CompletionTrendsChartProps { + deals: ClassifiedDeal[]; + isDomnaUser?: boolean; + projectCode?: string; +} + +const METRICS = [ + { key: "bookings", label: "Bookings", dateField: "confirmedSurveyDate" }, + { + key: "assessments", + label: "Completed Assessments", + dateField: "raDateActual", + }, + { + key: "coordination", + label: "Completed Coordination", + dateField: "ioeV1Date", + }, + { key: "design", label: "Completed Designs", dateField: "designDate" }, + { + key: "lodgement", + label: "Completed Lodgements", + dateField: "fullLodgementDate", + }, +]; + +// Returns the Monday of the week containing `date`, as a locale string key +function getMondayOfWeek(date: Date): string { + const d = new Date(date); + const day = d.getDay(); // 0=Sun … 6=Sat + d.setDate(d.getDate() - (day === 0 ? 6 : day - 1)); + d.setHours(0, 0, 0, 0); + // Use ISO date string as sort key; displayed as en-GB short date + return d.toISOString().split("T")[0]; // "2026-03-30" +} + +function formatMonday(isoDate: string): string { + return new Date(isoDate).toLocaleDateString("en-GB", { + day: "numeric", + month: "short", + year: "numeric", + }); +} + +function aggregateByWeek(deals: ClassifiedDeal[], dateField: string) { + const weekCounts: Record = {}; + for (const deal of deals) { + 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; + } + return Object.entries(weekCounts) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([isoKey, value]) => ({ week: formatMonday(isoKey), value })); +} + +// Fills all missing Monday ISO-date keys between min and max with 0s +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; +} + +// Coordination-specific aggregator: buckets V1 and V2 completions separately +function aggregateCoordinationByWeek( + deals: ClassifiedDeal[], +): Array<{ week: string; "V1 (MTP)": number; "V2 (Re-model)": 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) => ({ + week: formatMonday(isoKey), + "V1 (MTP)": v1Counts[isoKey] ?? 0, + "V2 (Re-model)": v2Counts[isoKey] ?? 0, + })); +} + +export default function CompletionTrendsChart({ + deals, + isDomnaUser, + projectCode, +}: CompletionTrendsChartProps) { + const [metric, setMetric] = useState(METRICS[0].key); + // Targets: { [week]: number } + const [targets, setTargets] = useState<{ [week: string]: number }>({}); + const [targetInput, setTargetInput] = useState<{ + week: string; + value: string; + }>({ week: "", value: "" }); + + const selectedMetric = METRICS.find((m) => m.key === metric)!; + const isCoordination = metric === "coordination"; + + const coordData = isCoordination ? aggregateCoordinationByWeek(deals) : null; + const singleData = isCoordination + ? null + : aggregateByWeek(deals, selectedMetric.dateField); + + // Merge targets into non-coordination chart data + const chartData = isCoordination + ? coordData! + : singleData!.map((d) => ({ + week: d.week, + [selectedMetric.label]: d.value, + ...(targets[d.week] !== undefined ? { Target: targets[d.week] } : {}), + })); + + // Add target handler (Domna only) + const handleAddTarget = () => { + if (!targetInput.week || !targetInput.value) return; + setTargets((prev) => ({ + ...prev, + [targetInput.week]: Number(targetInput.value), + })); + setTargetInput({ week: "", value: "" }); + }; + + return ( + +
+
+ + Trends Over Time + +

+ Switch between metrics to see weekly trends. +

+
+
+ +
+
+ + {isDomnaUser && !isCoordination && ( +
+ + Add/Edit Targets (visible to Domna users only): + + + setTargetInput((ti) => ({ ...ti, week: e.target.value })) + } + className="w-32 text-xs" + /> + + setTargetInput((ti) => ({ ...ti, value: e.target.value })) + } + className="w-24 text-xs" + /> + +
+ )} + + {isCoordination ? ( + <> + + + + ) : ( + <> + + + + )} +
+ ); +} 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..d8e1e83 --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DampMouldRiskPanel.tsx @@ -0,0 +1,240 @@ +"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 bothFlaggedDeals = risk.surveyFlagDeals.filter((d) => !!d.dampMouldFlag); + + 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 + ) + } + /> + + onOpenTable( + "Damp & Mould — Flagged at Both Stages", + bothFlaggedDeals, + 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/LiveTracker.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx index ccdb2d5..1f13111 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx @@ -18,7 +18,6 @@ import type { LiveTrackerProps, TableModal, ClassifiedDeal, - HubspotDeal, DocumentDrawerState, } from "./types"; @@ -28,13 +27,15 @@ export default function LiveTracker({ majorConditionDeals, }: LiveTrackerProps) { // ── Tab state ──────────────────────────────────────────────────────── - const [activeTab, setActiveTab] = useState<"analytics" | "properties">("analytics"); + const [activeTab, setActiveTab] = useState<"analytics" | "properties">( + "analytics", + ); // ── 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, ); // ── Drill-down table modal (used by AnalyticsView) ─────────────────── @@ -50,18 +51,18 @@ export default function LiveTracker({ 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, }); }; @@ -131,11 +132,21 @@ export default function LiveTracker({ onChange={(e) => setCurrentProjectCode(e.target.value)} className="px-3 py-1.5 border border-brandblue/20 rounded-lg bg-white text-sm text-gray-800 font-medium focus:ring-2 focus:ring-brandblue focus:border-brandblue focus:outline-none transition-all appearance-none pr-8" > - {projectCodes.map((code) => ( - - ))} + {projectCodes.map((code) => + code === "__ALL__" ? ( + + ) : ( + + ), + )}
)} @@ -143,6 +154,7 @@ export default function LiveTracker({
@@ -175,47 +187,49 @@ export default function LiveTracker({ {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("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"; - return ( -
-

- {category} -

-

- {items.length} -

-

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

-
- ); - })} + return ( +
+

+ {category} +

+

+ {items.length} +

+

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

+
+ ); + }, + )}
)}
-
- +
+ {}} />
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 3644571..7eeaaa5 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ProgressOverview.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ProgressOverview.tsx @@ -1,102 +1,63 @@ "use client"; import { motion } from "framer-motion"; -import { AlertCircle, CheckCircle2, ArrowRight, Users, PenLine, Hammer, FileCheck } from "lucide-react"; +import { CheckCircle2, ArrowRight } from "lucide-react"; import { Card, CardContent } from "@/app/shadcn_components/ui/card"; import { STAGE_COLORS } from "./types"; -import type { ProjectProgressData, ClassifiedDeal, ClassifiedDeal as CD } from "./types"; +import type { + ProjectProgressData, + ClassifiedDeal, +} from "./types"; -// ----------------------------------------------------------------------- -// Columns shown per card — enough context to understand WHY -// ----------------------------------------------------------------------- -const QUERIES_COLUMNS: (keyof CD)[] = [ - "dealname", "landlordPropertyId", "displayStage", "outcome", "coordinationStatus", "outcomeNotes", +const EARLY_COLUMNS: (keyof ClassifiedDeal)[] = [ + "dealname", + "landlordPropertyId", + "displayStage", + "preSapScore", + "outcome", ]; -const QUERIES_LABELS: Partial> = { - dealname: "Address", - landlordPropertyId: "Ref", - displayStage: "Reason / Stage", - outcome: "Survey Outcome", - coordinationStatus: "Coordination Status", - outcomeNotes: "Notes", -}; - -const COORD_COLUMNS: (keyof CD)[] = [ - "dealname", "landlordPropertyId", "displayStage", "coordinator", "coordinationStatus", "ioeV1Date", -]; -const COORD_LABELS: Partial> = { - dealname: "Address", - landlordPropertyId: "Ref", - displayStage: "Current Stage", - coordinator: "Coordinator", - coordinationStatus: "Coordination Status", - ioeV1Date: "IOE/MTP Date", -}; - -const DESIGN_COLUMNS: (keyof CD)[] = [ - "dealname", "landlordPropertyId", "displayStage", "designer", "designStatus", "approvedPackage", "designDate", -]; -const DESIGN_LABELS: Partial> = { - dealname: "Address", - landlordPropertyId: "Ref", - displayStage: "Current Stage", - designer: "Designer", - designStatus: "Design Status", - approvedPackage: "Package", - designDate: "Design Date", -}; - -const INSTALL_COLUMNS: (keyof CD)[] = [ - "dealname", "landlordPropertyId", "displayStage", "installer", "installerHandover", "actualMeasuresInstalled", -]; -const INSTALL_LABELS: Partial> = { - dealname: "Address", - landlordPropertyId: "Ref", - displayStage: "Current Stage", - installer: "Installer", - installerHandover: "Handover Date", - actualMeasuresInstalled: "Measures Installed", -}; - -const LODGE_COLUMNS: (keyof CD)[] = [ - "dealname", "landlordPropertyId", "displayStage", "lodgementStatus", "postSapScore", "measuresLodgementDate", "fullLodgementDate", -]; -const LODGE_LABELS: Partial> = { - dealname: "Address", - landlordPropertyId: "Ref", - displayStage: "Current Stage", - lodgementStatus: "Lodgement Status", - postSapScore: "Post-SAP Score", - measuresLodgementDate: "Measures Lodged", - fullLodgementDate: "Full Lodgement Date", -}; - -const EARLY_COLUMNS: (keyof CD)[] = [ - "dealname", "landlordPropertyId", "displayStage", "preSapScore", "outcome", "raStatus", -]; -const EARLY_LABELS: Partial> = { +const EARLY_LABELS: Partial> = { dealname: "Address", landlordPropertyId: "Ref", displayStage: "Current Stage", preSapScore: "Pre-SAP Score", outcome: "Survey Outcome", - raStatus: "Assessment Status", }; // ----------------------------------------------------------------------- // Circular progress ring (SVG) // ----------------------------------------------------------------------- -function RingProgress({ pct, color = "#14163d", size = 80 }: { pct: number; color?: string; size?: number }) { +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 ( - + @@ -104,91 +65,6 @@ function RingProgress({ pct, color = "#14163d", size = 80 }: { pct: number; colo ); } -// ----------------------------------------------------------------------- -// Clickable phase summary card (coordination, design, install, lodgement) -// ----------------------------------------------------------------------- -function PhaseCard({ - icon: Icon, - title, - completedCount, - inProgressCount, - total, - completedPct, - inProgressPct, - ringColor, - onClick, -}: { - icon: React.ElementType; - title: string; - completedCount: number; - inProgressCount: number; - total: number; - completedPct: number; - inProgressPct: number; - ringColor: string; - onClick: () => void; -}) { - return ( - -
-
-
-
- -
- - {title} - -
- -
-
- Complete - {completedCount} -
-
-
-
- {inProgressCount > 0 && ( - <> -
- In progress - {inProgressCount} -
-
-
-
- - )} -
-
- -
- -
- {completedPct.toFixed(0)}% -
-
-
- -
- - View breakdown -
- - ); -} - // ----------------------------------------------------------------------- // Main component // ----------------------------------------------------------------------- @@ -197,33 +73,36 @@ interface ProgressOverviewProps { onOpenTable?: ( stage: string, deals: ClassifiedDeal[], - columns?: (keyof CD)[], - columnLabels?: Partial>, - breakdown?: Record + columns?: (keyof ClassifiedDeal)[], + columnLabels?: Partial>, + breakdown?: Record, ) => void; } -export default function ProgressOverview({ data, onOpenTable }: ProgressOverviewProps) { +export default function ProgressOverview({ + data, + onOpenTable, +}: ProgressOverviewProps) { const { completedPercentage, completedCount, nonQueryTotal, - queriesDeals, stageProgress, - coordination, - design, - install, - lodgement, } = data; // 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); + const earlyStages = [ + "Scope & Planning", + "Booking in Progress", + "Assessment in Progress", + ]; + const earlyItems = stageProgress.filter( + (s) => earlyStages.includes(s.stage) && s.count > 0, + ); return ( - + - {/* ── Completion header ──────────────────────────────────────────── */}
- +
{completedPercentage.toFixed(0)}% @@ -256,13 +148,19 @@ export default function ProgressOverview({ data, onOpenTable }: ProgressOverview
- Work Completed + + Work Completed +

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

+

+ Properties fully lodged and funded

-

Properties fully lodged and funded

@@ -278,13 +176,26 @@ export default function ProgressOverview({ data, onOpenTable }: ProgressOverview key={item.stage} whileHover={{ scale: 1.03 }} onClick={() => - onOpenTable?.(item.stage, item.deals, EARLY_COLUMNS, EARLY_LABELS) + onOpenTable?.( + item.stage, + item.deals, + EARLY_COLUMNS, + EARLY_LABELS, + undefined, + item.stage, + `Properties currently in the "${item.stage}" stage.`, + undefined, + ) } - className={`group text-left rounded-xl border p-3 transition-all duration-200 hover:shadow-sm ${c.bg} ${c.border} hover:opacity-90`} + 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}
@@ -298,131 +209,6 @@ export default function ProgressOverview({ data, onOpenTable }: ProgressOverview
)} - {/* ── Phase summary cards ──────────────────────────────────────── */} -
- - onOpenTable?.( - "Coordination Status", - [...coordination.completedDeals, ...coordination.inProgressDeals], - COORD_COLUMNS, - COORD_LABELS, - { - "Coordination Complete": coordination.completedDeals, - "In Progress": coordination.inProgressDeals, - } - ) - } - /> - - onOpenTable?.( - "Design Status", - [...design.completedDeals, ...design.inProgressDeals], - DESIGN_COLUMNS, - DESIGN_LABELS, - { - "Design Complete": design.completedDeals, - "In Progress": design.inProgressDeals, - } - ) - } - /> - - onOpenTable?.( - "Installation Status", - [...install.completedDeals, ...install.inProgressDeals], - INSTALL_COLUMNS, - INSTALL_LABELS, - { - "Install Complete": install.completedDeals, - "In Progress": install.inProgressDeals, - } - ) - } - /> - - onOpenTable?.( - "Lodgement Status", - [...lodgement.completedDeals, ...lodgement.inProgressDeals], - LODGE_COLUMNS, - LODGE_LABELS, - { - "Fully Lodged": lodgement.completedDeals, - "In Progress": lodgement.inProgressDeals, - } - ) - } - /> -
- - {/* ── Requires input ────────────────────────────────────────────── */} - {queriesDeals.length > 0 && ( - - onOpenTable?.( - "Properties Requiring Attention", - queriesDeals, - QUERIES_COLUMNS, - QUERIES_LABELS - ) - } - className="group w-full text-left rounded-xl border border-amber-300 bg-gradient-to-r from-amber-50 to-white p-4 hover:border-amber-400 hover:shadow-sm transition-all duration-200" - > -
-
- -
-
-
- Requires Your Input - - {queriesDeals.length} - -
-

- Click to see the issue, survey outcome, and coordination status for each property -

-
- -
-
- )} ); 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..c4464bc --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyDrawer.tsx @@ -0,0 +1,313 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { + FileDown, + FileText, + Code2, + BarChart3, + 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"; + +// Human-readable labels for DB_REPORT_TYPES enum values +const DOC_TYPE_LABELS: Record = { + ECO_CONDITION_REPORT: "Condition Report (PAS 2035)", + ENERGY_PERFORMANCE_REPORT_SUMMARY_INFORMATION: "EPC Summary Report", + LIG_XML: "LIG XML", + RDSAP_XML: "RdSAP XML", + FULLSAP_XML: "Full SAP XML", + DECENT_HOMES_RAW_DATA: "Decent Homes Raw Data", + DECENT_HOMES_PROPERTY_META: "Decent Homes Property Meta", + DECENT_HOMES_SUMMARY: "Decent Homes Summary", +}; + +// Icon + colour per doc category +function docTypeStyle(docType: string): { + icon: React.ReactNode; + bg: string; + text: string; + border: string; +} { + if (docType.includes("XML")) { + return { + icon: , + bg: "bg-amber-50", + text: "text-amber-700", + border: "border-amber-200", + }; + } + if (docType.includes("DECENT_HOMES")) { + return { + icon: , + bg: "bg-violet-50", + text: "text-violet-700", + border: "border-violet-200", + }; + } + return { + icon: , + bg: "bg-sky-50", + text: "text-sky-700", + border: "border-sky-200", + }; +} + +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 [signing, setSigning] = useState(false); + const style = docTypeStyle(doc.docType); + const label = DOC_TYPE_LABELS[doc.docType] ?? doc.docType; + + async function handleDownload() { + setSigning(true); + try { + // Extract S3 key from the full URI — same pattern as TableViewer.tsx + const key = doc.s3FileUri.split(".amazonaws.com/")[1]; + if (!key) { + window.open(doc.s3FileUri, "_blank"); + return; + } + 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(); + window.open(data.url, "_blank"); + } catch { + // Fallback: open raw URI + window.open(doc.s3FileUri, "_blank"); + } finally { + setSigning(false); + } + } + + return ( + +
+ {/* Doc type badge */} + + {style.icon} + {label} + +
+ +
+ + {formatDate(doc.s3FileUploadTimestamp)} + + +
+
+ ); +} + +// ----------------------------------------------------------------------- +// PropertyDrawer — main component +// ----------------------------------------------------------------------- +interface PropertyDrawerProps { + open: boolean; + uprn: string | null; + dealname: string | null; + onClose: () => void; +} + +export default function PropertyDrawer({ + open, + uprn, + dealname, + onClose, +}: PropertyDrawerProps) { + const { + data: documents = [], + isLoading, + isError, + } = useQuery({ + queryKey: ["property-documents", uprn], + // TODO: Replace with real implementation when available + queryFn: async () => [], + enabled: open && !!uprn, + staleTime: 30_000, + }); + + // Group docs by category for display + const grouped = (documents as PropertyDocument[]).reduce< + Record + >((acc: Record, doc: PropertyDocument) => { + const category = doc.docType.includes("XML") + ? "XML Files" + : doc.docType.includes("DECENT_HOMES") + ? "Decent Homes" + : "Survey Reports"; + (acc[category] ??= []).push(doc); + return acc; + }, {}); + + const hasDocuments = documents.length > 0; + + return ( + !v && onClose()} direction="right"> + + {/* Remove the default drag handle */} +
+ + +
+
+ + {dealname ?? "Property Documents"} + + {uprn && ( + + UPRN: {uprn} + + )} +
+ + + +
+ + {hasDocuments && !isLoading && ( +
+ + + {documents.length} document{documents.length !== 1 ? "s" : ""} + +
+ )} +
+ + {/* Body */} +
+ {/* Loading state */} + {isLoading && ( +
+ {[1, 2, 3].map((i) => ( +
+ ))} +
+ )} + + {/* Error state */} + {isError && !isLoading && ( +
+
+ +
+

+ Could not load documents +

+

+ Please try again later. +

+
+ )} + + {/* Empty state */} + {!isLoading && !isError && !hasDocuments && ( +
+
+ +
+

+ No documents uploaded +

+

+ Survey documents will appear here once uploaded for this + property. +

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

+ {category} +

+
+ {docs.map((doc) => ( + + ))} +
+
+ ))} +
+
+ + {/* 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..3d9c447 --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTable.tsx @@ -0,0 +1,378 @@ +"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, DisplayStage } 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", + postSapScore: "Post-SAP", + lodgementStatus: "Lodgement Status", + designDate: "Design Date", + fullLodgementDate: "Lodgement Date", +}; + +interface PropertyTableProps { + data: ClassifiedDeal[]; + onOpenDrawer: (uprn: string | null, dealname: string | null) => void; + showDocuments?: boolean; +} + +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, showDocuments = false }: PropertyTableProps) { + const [globalFilter, setGlobalFilter] = useState(""); + const [stageFilter, setStageFilter] = 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, + postSapScore: false, + lodgementStatus: false, + designDate: false, + fullLodgementDate: false, + }); + + // Pre-filter by stage before TanStack gets it + const filteredData = useMemo(() => { + if (stageFilter === "all") return data; + return data.filter((d) => d.displayStage === stageFilter); + }, [data, stageFilter]); + + const columns = useMemo( + () => createPropertyTableColumns(onOpenDrawer, showDocuments), + [onOpenDrawer, showDocuments] + ); + + 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 */} + + + {/* 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}" ` : ""} + 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..7f9b0ce --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTableColumns.tsx @@ -0,0 +1,310 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; +import { ArrowUpDown, FileDown } from "lucide-react"; +import { STAGE_COLORS } from "./types"; +import type { ClassifiedDeal, DisplayStage } 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 +// ----------------------------------------------------------------------- +export function createPropertyTableColumns( + onOpenDrawer: (uprn: string | null, dealname: string | null) => void, + showDocuments: boolean = false, +): ColumnDef[] { + const columns: ColumnDef[] = [ + // ── Address ────────────────────────────────────────────────────────── + { + accessorKey: "dealname", + id: "dealname", + header: ({ column }) => , + cell: ({ row }) => ( +
+

+ {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} + + ); + }, + }, + + // ── Post-SAP score ─────────────────────────────────────────────────── + { + accessorKey: "postSapScore", + id: "postSapScore", + header: ({ column }) => , + cell: ({ row }) => { + const score = row.original.postSapScore; + if (!score) return ; + const n = Number(score); + const colour = + n >= 70 + ? "text-emerald-700 bg-emerald-50" + : n >= 50 + ? "text-sky-700 bg-sky-50" + : "text-amber-700 bg-amber-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: () => null, + cell: ({ row }) => ( + + ), + enableSorting: false, + enableHiding: false, + }); + } + + return columns; +} diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/TableViewer.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/TableViewer.tsx index 680c0be..3770b26 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/TableViewer.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/TableViewer.tsx @@ -9,6 +9,9 @@ interface TableViewerProps { columns?: (keyof HubspotDeal)[]; columnLabels?: Partial>; breakdown?: Record; + title?: string; + description?: string; + reason?: string; } export default function TableViewer({ @@ -16,16 +19,32 @@ export default function TableViewer({ columns, columnLabels, breakdown, + title, + description, + reason, }: TableViewerProps) { const [searchTerms, setSearchTerms] = useState>({}); - const visibleColumns = columns?.length - ? columns + + // Always include key context columns if not present + const contextCols: (keyof HubspotDeal)[] = [ + "outcomeNotes", + "majorConditionIssueDescription", + "coordinationStatus", + "designStatus", + ]; + let visibleColumns: (keyof HubspotDeal)[] = columns?.length + ? [...columns] : (Object.keys(data?.[0] || {}) as (keyof HubspotDeal)[]); + for (const col of contextCols) { + if (!visibleColumns.includes(col)) { + visibleColumns.push(col); + } + } // Helper: Get category for a row based on breakdown const getCategoryForRow = ( row: ClassifiedDeal, - brk: Record | undefined + brk: Record | undefined, ): string | undefined => { if (!brk) return undefined; for (const [category, items] of Object.entries(brk)) { @@ -77,9 +96,11 @@ export default function TableViewer({ visibleColumns.every((col) => { const term = searchTerms[col]?.toLowerCase() || ""; if (!term) return true; - const value = String(row[col as keyof ClassifiedDeal] ?? "").toLowerCase(); + const value = String( + row[col as keyof ClassifiedDeal] ?? "", + ).toLowerCase(); return value.includes(term); - }) + }), ); // Inline sort derivation (no useMemo) @@ -152,11 +173,30 @@ export default function TableViewer({ return (
+ {/* Context header */} + {(title || description || reason) && ( +
+ {title && ( +

{title}

+ )} + {description && ( +

{description}

+ )} + {reason && ( +
+ {reason} +
+ )} +
+ )} {visibleColumns.map((col) => ( - ))} 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 6a957e4..f7eee9a 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx @@ -1,11 +1,61 @@ import { getServerSession } from "next-auth"; import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions"; import { redirect } from "next/navigation"; +import { eq } from "drizzle-orm"; import LiveTracker from "./LiveTracker"; import { computeLiveTrackerData } from "./transforms"; -// MOCK PHASE: using mock data while DB migration is in progress -// When migration lands, restore the DB query below and remove this import -import { MOCK_DEALS } from "./mockData"; +import { db } from "@/app/db/db"; +import { hubspotDealData } from "@/app/db/schema/crm/hubspot_deal_table"; +import type { HubspotDeal } from "./types"; +import type { InferSelectModel } from "drizzle-orm"; + +// ⚠️ ⚠️ ⚠️ HARDCODED COMPANY ID — temporary for testing only. +// TODO: derive this from the portfolio slug once the portfolio↔company mapping exists. +// Do NOT ship this to production without replacing it with the real lookup. +const HARDCODED_COMPANY_ID = "86970043613"; + +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, // DB: dampmouldGrowth + preSapScore: row.preSap, // DB: preSap + coordinator: row.coordinator, + ioeV1Date: row.mtpCompletionDate, // DB: mtpCompletionDate + ioeV2Date: row.mtpReModelCompletionDate, // DB: mtpReModelCompletionDate + ioeV3Date: row.ioeV3CompletionDate, // DB: ioeV3CompletionDate + proposedMeasures: row.proposedMeasures, + approvedPackage: row.approvedPackage, + designer: row.designer, + designDate: row.designCompletionDate, // DB: designCompletionDate + actualMeasuresInstalled: row.actualMeasuresInstalled, + installer: row.installer, + installerHandover: row.installerHandover, + lodgementStatus: row.lodgementStatus, + measuresLodgementDate: row.measuresLodgementDate, + fullLodgementDate: row.lodgementDate, // DB: lodgementDate + confirmedSurveyDate: row.confirmedSurveyDate, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + }; +} export default async function LiveReportingPage(props: { params: Promise<{ slug: string }>; @@ -18,13 +68,17 @@ export default async function LiveReportingPage(props: { redirect("/"); } - // MOCK PHASE ↓↓↓ - // TODO: replace with real DB query once migration is applied: - // const [company] = await surveyDB.select().from(hubspotCompanyData).where(eq(hubspotCompanyData.groupId, portfolioId)); - // const deals = await surveyDB.select().from(hubspotDealData).where(eq(hubspotDealData.companyId, company.companyId)); - // const trackerData = computeLiveTrackerData(deals as HubspotDeal[]); - const trackerData = computeLiveTrackerData(MOCK_DEALS); - // ↑↑↑ END MOCK PHASE + // ⚠️ Using HARDCODED_COMPANY_ID — see constant above before deploying + const rawDeals = await db + .select() + .from(hubspotDealData) + .where(eq(hubspotDealData.companyId, HARDCODED_COMPANY_ID)); + + console.log("Fetched deals from DB:", rawDeals.length); + + const trackerData = computeLiveTrackerData( + rawDeals.map(mapDbRowToHubspotDeal), + ); return (
@@ -42,5 +96,3 @@ export default async function LiveReportingPage(props: {
); } - - 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 4119cf6..ab5916c 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.ts +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.ts @@ -398,6 +398,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 8b0ae9a..6eb365d 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts @@ -40,20 +40,10 @@ export type HubspotDeal = { actualMeasuresInstalled: string | null; installer: string | null; installerHandover: string | null; - postSapScore: string | null; lodgementStatus: string | null; measuresLodgementDate: Date | null; fullLodgementDate: Date | null; - - // ── Internally tracked stubs (no HubSpot source yet) ───────────────── - raDateBooking: Date | null; - raDateActual: Date | null; - raStatus: string | null; - postEpcDateBooking: Date | null; - postEpcDateActual: Date | null; - postEpcStatus: string | null; - postEprDate: Date | null; - postEprStatus: string | null; + confirmedSurveyDate: Date | null; createdAt: Date; updatedAt: Date; From 4106dc3e2d376a30567470c8919b339f433a9db9 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 1 Apr 2026 17:29:20 +0000 Subject: [PATCH 07/25] adding booking, assessment graphs, removed repeated section, added survey issues --- .../your-projects/live/AnalyticsView.tsx | 21 ++++-------- .../live/CompletionTrendsChart.tsx | 33 +++++++++++++------ 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/AnalyticsView.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/AnalyticsView.tsx index 8aaf9b9..bec0cd2 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/AnalyticsView.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/AnalyticsView.tsx @@ -4,10 +4,10 @@ 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 ProgressOverview from "./ProgressOverview"; 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, @@ -351,19 +351,6 @@ export default function AnalyticsView({ - {/* Row 3: Progress overview only (no donut chart) */} -
- - - -
- {/* Row 4: Pipeline Funnel */} + + {/* 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 index f510ea7..cdcc269 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/CompletionTrendsChart.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/CompletionTrendsChart.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import { Card, Title, LineChart, Legend } from "@tremor/react"; +import { Card, Title, BarChart, Legend } from "@tremor/react"; import { Button } from "@/app/shadcn_components/ui/button"; import { Input } from "@/app/shadcn_components/ui/input"; import { @@ -56,9 +56,14 @@ function formatMonday(isoDate: string): string { }); } -function aggregateByWeek(deals: ClassifiedDeal[], dateField: string) { +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 @@ -69,9 +74,11 @@ function aggregateByWeek(deals: ClassifiedDeal[], dateField: string) { const key = getMondayOfWeek(d); weekCounts[key] = (weekCounts[key] || 0) + 1; } - return Object.entries(weekCounts) - .sort(([a], [b]) => a.localeCompare(b)) - .map(([isoKey, value]) => ({ week: formatMonday(isoKey), value })); + const allKeys = fillWeekGaps(Object.keys(weekCounts)); + return allKeys.map((isoKey) => ({ + week: formatMonday(isoKey), + value: weekCounts[isoKey] ?? 0, + })); } // Fills all missing Monday ISO-date keys between min and max with 0s @@ -141,11 +148,18 @@ export default function CompletionTrendsChart({ const selectedMetric = METRICS.find((m) => m.key === metric)!; const isCoordination = metric === "coordination"; + const isDesign = metric === "design"; const coordData = isCoordination ? aggregateCoordinationByWeek(deals) : null; const singleData = isCoordination ? null - : aggregateByWeek(deals, selectedMetric.dateField); + : aggregateByWeek( + deals, + selectedMetric.dateField, + isDesign + ? (d) => d.designStatus?.toUpperCase() === "UPLOADED" + : undefined, + ); // Merge targets into non-coordination chart data const chartData = isCoordination @@ -227,7 +241,7 @@ export default function CompletionTrendsChart({ {isCoordination ? ( <> - ) : ( <> - Date: Wed, 1 Apr 2026 17:29:32 +0000 Subject: [PATCH 08/25] added missing survey issues --- .../your-projects/live/SurveyIssuesPanel.tsx | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 src/app/portfolio/[slug]/(portfolio)/your-projects/live/SurveyIssuesPanel.tsx 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..37ba210 --- /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 = [...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 +

+
+ ))} +
+
+
+ ); +} From 66c9b94f41f4733bd64301713d319c74fd7f1c1f Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 1 Apr 2026 18:29:50 +0000 Subject: [PATCH 09/25] Updated UI and removed s3 url from csv download --- .../your-projects/live/AnalyticsView.tsx | 2 +- .../live/CompletionTrendsChart.tsx | 301 +++++++++++++----- .../your-projects/live/LiveTracker.tsx | 11 +- .../your-projects/live/TableViewer.tsx | 251 --------------- .../(portfolio)/your-projects/live/page.tsx | 2 + .../(portfolio)/your-projects/live/types.ts | 18 +- tailwind.config.js | 32 ++ 7 files changed, 277 insertions(+), 340 deletions(-) delete mode 100644 src/app/portfolio/[slug]/(portfolio)/your-projects/live/TableViewer.tsx diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/AnalyticsView.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/AnalyticsView.tsx index bec0cd2..8235469 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/AnalyticsView.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/AnalyticsView.tsx @@ -324,7 +324,7 @@ export default function AnalyticsView({ value={currentProject.allDeals.length} onClick={() => onOpenTable( - `${currentProjectCode} — All Properties`, + currentProjectCode === "__ALL__" ? "All Properties" : `${currentProjectCode} — All Properties`, currentProject.allDeals, ["dealname", "landlordPropertyId"], { dealname: "Address Ref.", landlordPropertyId: "Property Ref." }, diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/CompletionTrendsChart.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/CompletionTrendsChart.tsx index cdcc269..a3eb771 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/CompletionTrendsChart.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/CompletionTrendsChart.tsx @@ -23,7 +23,7 @@ const METRICS = [ { key: "assessments", label: "Completed Assessments", - dateField: "raDateActual", + dateField: "surveyedDate", }, { key: "coordination", @@ -38,13 +38,64 @@ const METRICS = [ }, ]; -// Returns the Monday of the week containing `date`, as a locale string key +// Brand colour palette (hex values work directly with Tremor v3 colors prop) +const C = { + blue: "#5d6be0", // lighter periwinkle blue + midblue: "#3943b7", // brandmidblue + lightblue: "#8b96e9", // soft pale blue for 3rd series + paleblue: "#b8bef4", // 4th series — replaces brown in bar charts + brown: "#c4a47c", // accent only — trend/target lines +}; + +function ChartTooltip({ + payload, + active, + label, +}: { + payload?: { name: string; value: number; color: string }[]; + active?: boolean; + label?: string; +}) { + if (!active || !payload?.length) return null; + return ( +
+

{label}

+ {payload.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)", +]; + +// Returns the Monday of the week containing `date`, as an ISO date string key function getMondayOfWeek(date: Date): string { const d = new Date(date); const day = d.getDay(); // 0=Sun … 6=Sat d.setDate(d.getDate() - (day === 0 ? 6 : day - 1)); d.setHours(0, 0, 0, 0); - // Use ISO date string as sort key; displayed as en-GB short date return d.toISOString().split("T")[0]; // "2026-03-30" } @@ -56,6 +107,20 @@ function formatMonday(isoDate: string): string { }); } +// Fills all missing Monday ISO-date keys between min and max +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, @@ -81,21 +146,7 @@ function aggregateByWeek( })); } -// Fills all missing Monday ISO-date keys between min and max with 0s -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; -} - -// Coordination-specific aggregator: buckets V1 and V2 completions separately +// Coordination: buckets V1 and V2 completions separately function aggregateCoordinationByWeek( deals: ClassifiedDeal[], ): Array<{ week: string; "V1 (MTP)": number; "V2 (Re-model)": number }> { @@ -133,13 +184,114 @@ function aggregateCoordinationByWeek( })); } +// Assessments: Retrofit Assessment vs EPC, keyed by surveyedDate +function aggregateAssessmentsByWeek( + deals: ClassifiedDeal[], +): Array<{ week: string; "Retrofit Assessment": number; EPC: 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) => ({ + week: formatMonday(isoKey), + "Retrofit Assessment": retrofitCounts[isoKey] ?? 0, + EPC: epcCounts[isoKey] ?? 0, + })); +} + +// Lodgements: Stage 1 vs Lodged Measures +function aggregateLodgementsByWeek( + deals: ClassifiedDeal[], +): Array<{ week: string; "Stage 1 Lodgement": number; "Lodged Measures": 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) => ({ + week: formatMonday(isoKey), + "Stage 1 Lodgement": stageCounts[isoKey] ?? 0, + "Lodged Measures": measuresCounts[isoKey] ?? 0, + })); +} + +// Designs: stacked by design type (Uploaded only) +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) }; + for (const label of DESIGN_TYPE_ORDER) { + entry[label] = counts[isoKey]?.[label] ?? 0; + } + return entry; + }); +} + export default function CompletionTrendsChart({ deals, isDomnaUser, projectCode, }: CompletionTrendsChartProps) { const [metric, setMetric] = useState(METRICS[0].key); - // Targets: { [week]: number } const [targets, setTargets] = useState<{ [week: string]: number }>({}); const [targetInput, setTargetInput] = useState<{ week: string; @@ -148,29 +300,43 @@ export default function CompletionTrendsChart({ const selectedMetric = METRICS.find((m) => m.key === metric)!; const isCoordination = metric === "coordination"; - const isDesign = metric === "design"; + const isAssessments = metric === "assessments"; + const isLodgement = metric === "lodgement"; + const isDesign = metric === "design"; + const isStacked = isCoordination || isAssessments || isLodgement || isDesign; - const coordData = isCoordination ? aggregateCoordinationByWeek(deals) : null; - const singleData = isCoordination - ? null - : aggregateByWeek( - deals, - selectedMetric.dateField, - isDesign - ? (d) => d.designStatus?.toUpperCase() === "UPLOADED" - : undefined, - ); + // Compute chart data, categories, and colours in one place + let chartData: Record[]; + let categories: string[]; + let colors: string[]; - // Merge targets into non-coordination chart data - const chartData = isCoordination - ? coordData! - : singleData!.map((d) => ({ - week: d.week, - [selectedMetric.label]: d.value, - ...(targets[d.week] !== undefined ? { Target: targets[d.week] } : {}), - })); + 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, + ...(targets[d.week] !== undefined ? { Target: targets[d.week] } : {}), + })); + categories = [selectedMetric.label, ...(isDomnaUser ? ["Target"] : [])]; + colors = [C.blue, C.brown]; + } - // Add target handler (Domna only) const handleAddTarget = () => { if (!targetInput.week || !targetInput.value) return; setTargets((prev) => ({ @@ -207,7 +373,7 @@ export default function CompletionTrendsChart({ - {isDomnaUser && !isCoordination && ( + {isDomnaUser && !isStacked && (
Add/Edit Targets (visible to Domna users only): @@ -239,43 +405,24 @@ export default function CompletionTrendsChart({
)} - {isCoordination ? ( - <> - - - - ) : ( - <> - - - + + {isStacked && ( + )} ); 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 1f13111..4ecd754 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx @@ -10,8 +10,9 @@ import { } from "@/app/shadcn_components/ui/tabs"; import { Card, CardContent } from "@/app/shadcn_components/ui/card"; import { BarChart2, Table2 } from "lucide-react"; -import TableViewer from "./TableViewer"; +import DrillDownTable from "./DrillDownTable"; import PropertyTable from "./PropertyTable"; +import type { HubspotDeal } from "./types"; import PropertyDrawer from "./PropertyDrawer"; import AnalyticsView from "./AnalyticsView"; import type { @@ -229,13 +230,17 @@ export default function LiveTracker({
- {}} /> +
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 3770b26..0000000 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/TableViewer.tsx +++ /dev/null @@ -1,251 +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; - title?: string; - description?: string; - reason?: string; -} - -export default function TableViewer({ - data, - columns, - columnLabels, - breakdown, - title, - description, - reason, -}: TableViewerProps) { - const [searchTerms, setSearchTerms] = useState>({}); - - // Always include key context columns if not present - const contextCols: (keyof HubspotDeal)[] = [ - "outcomeNotes", - "majorConditionIssueDescription", - "coordinationStatus", - "designStatus", - ]; - let visibleColumns: (keyof HubspotDeal)[] = columns?.length - ? [...columns] - : (Object.keys(data?.[0] || {}) as (keyof HubspotDeal)[]); - for (const col of contextCols) { - if (!visibleColumns.includes(col)) { - visibleColumns.push(col); - } - } - - // 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 ( -
- {/* Context header */} - {(title || description || reason) && ( -
- {title && ( -

{title}

- )} - {description && ( -

{description}

- )} - {reason && ( -
- {reason} -
- )} -
- )} -
+
{columnLabels?.[col] || (col as string)} @@ -197,10 +237,7 @@ export default function TableViewer({ > {visibleColumns.map((col) => (
- {renderCellContent( - col, - row[col as keyof ClassifiedDeal] - )} + {renderCellContent(col, row[col as keyof ClassifiedDeal])}
- - - {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 f7eee9a..eca568f 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx @@ -52,6 +52,8 @@ function mapDbRowToHubspotDeal(row: DbDeal): HubspotDeal { measuresLodgementDate: row.measuresLodgementDate, fullLodgementDate: row.lodgementDate, // DB: lodgementDate confirmedSurveyDate: row.confirmedSurveyDate, + surveyedDate: row.SurveyedDate, // DB: surveyed_date + designType: row.dealType, // DB: design_type createdAt: row.createdAt, updatedAt: row.updatedAt, }; 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 6eb365d..f00495b 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts @@ -44,6 +44,8 @@ export type HubspotDeal = { measuresLodgementDate: Date | null; fullLodgementDate: Date | null; confirmedSurveyDate: Date | null; + surveyedDate: Date | null; + designType: string | null; createdAt: Date; updatedAt: Date; @@ -255,16 +257,16 @@ export const STAGE_COLORS: Record< dot: "bg-violet-400", }, "Coordination in Progress": { - bg: "bg-amber-50", - text: "text-amber-700", - border: "border-amber-200", - dot: "bg-amber-400", + bg: "bg-indigo-50", + text: "text-indigo-700", + border: "border-indigo-200", + dot: "bg-indigo-400", }, "Design in Progress": { - bg: "bg-orange-50", - text: "text-orange-700", - border: "border-orange-200", - dot: "bg-orange-400", + bg: "bg-blue-50", + text: "text-blue-700", + border: "border-blue-200", + dot: "bg-blue-400", }, "Awaiting Install": { bg: "bg-purple-50", 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]", From 8ca1bf27995d8459f212ae593f0ec2e637713c40 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 1 Apr 2026 22:32:25 +0000 Subject: [PATCH 10/25] modified table components and hooked up documents --- .../live-tracking/property-documents/route.ts | 55 ++++ src/app/db/db.ts | 2 + .../your-projects/live/DrillDownTable.tsx | 302 ++++++++++++++++++ .../your-projects/live/LiveTracker.tsx | 11 +- .../your-projects/live/PropertyDrawer.tsx | 110 +++---- .../your-projects/live/PropertyTable.tsx | 62 +++- .../live/PropertyTableColumns.tsx | 54 +++- .../(portfolio)/your-projects/live/page.tsx | 48 ++- .../(portfolio)/your-projects/live/types.ts | 34 +- 9 files changed, 574 insertions(+), 104 deletions(-) create mode 100644 src/app/api/live-tracking/property-documents/route.ts create mode 100644 src/app/portfolio/[slug]/(portfolio)/your-projects/live/DrillDownTable.tsx 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..9ebf596 --- /dev/null +++ b/src/app/api/live-tracking/property-documents/route.ts @@ -0,0 +1,55 @@ +import { NextResponse } from "next/server"; +import { eq, or } 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 { + const conditions = []; + + if (uprnParam) { + const uprnBigInt = BigInt(uprnParam); + conditions.push(eq(uploadedFiles.uprn, uprnBigInt)); + } + + if (landlordPropertyIdParam) { + conditions.push( + eq(uploadedFiles.landlordPropertyId, landlordPropertyIdParam), + ); + } + + const rows = await db + .select() + .from(uploadedFiles) + .where(conditions.length === 1 ? conditions[0] : or(...conditions)); + + 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/db/db.ts b/src/app/db/db.ts index 1d2d778..5219612 100644 --- a/src/app/db/db.ts +++ b/src/app/db/db.ts @@ -11,6 +11,7 @@ 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 * as CrmSchema from "@/app/db/schema/crm/hubspot_deal_table"; +import * as UploadedFilesSchema from "@/app/db/schema/uploaded_files"; export const pool = new Pool({ host: process.env.DB_HOST, @@ -33,6 +34,7 @@ const schema = { ...FundingSchema, ...Users, ...CrmSchema, + ...UploadedFilesSchema, }; export const db = drizzle(pool, { 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..eaf3444 --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DrillDownTable.tsx @@ -0,0 +1,302 @@ +"use client"; + +import { useMemo, useState } from "react"; +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; +} + +async function handlePhotoDownload(rawUrl: string) { + try { + 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"); + } +} + +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 4ecd754..5418d15 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx @@ -20,12 +20,14 @@ import type { TableModal, ClassifiedDeal, DocumentDrawerState, + DocStatusMap, } from "./types"; export default function LiveTracker({ projects, totalDeals, majorConditionDeals, + docStatusMap, }: LiveTrackerProps) { // ── Tab state ──────────────────────────────────────────────────────── const [activeTab, setActiveTab] = useState<"analytics" | "properties">( @@ -46,6 +48,7 @@ export default function LiveTracker({ const [drawerState, setDrawerState] = useState({ open: false, uprn: null, + landlordPropertyId: null, dealname: null, }); @@ -68,8 +71,8 @@ export default function LiveTracker({ }); }; - const handleOpenDrawer = (uprn: string | null, dealname: string | null) => { - setDrawerState({ open: true, uprn, dealname }); + const handleOpenDrawer = (uprn: string | null, landlordPropertyId: string | null, dealname: string | null) => { + setDrawerState({ open: true, uprn, landlordPropertyId, dealname }); }; if (!totalDeals) { @@ -156,6 +159,7 @@ export default function LiveTracker({ data={currentProject?.allDeals ?? []} onOpenDrawer={handleOpenDrawer} showDocuments={true} + docStatusMap={docStatusMap} />
@@ -253,9 +257,10 @@ export default function LiveTracker({ - setDrawerState({ open: false, uprn: null, dealname: null }) + setDrawerState({ open: false, uprn: null, landlordPropertyId: null, dealname: null }) } />
diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyDrawer.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyDrawer.tsx index c4464bc..0199ada 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyDrawer.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyDrawer.tsx @@ -6,8 +6,6 @@ import { motion, AnimatePresence } from "framer-motion"; import { FileDown, FileText, - Code2, - BarChart3, Loader2, FolderOpen, X, @@ -23,47 +21,22 @@ import { } from "@/app/shadcn_components/ui/drawer"; import type { PropertyDocument } from "./types"; -// Human-readable labels for DB_REPORT_TYPES enum values +// Human-readable labels for the main DB fileType enum values const DOC_TYPE_LABELS: Record = { - ECO_CONDITION_REPORT: "Condition Report (PAS 2035)", - ENERGY_PERFORMANCE_REPORT_SUMMARY_INFORMATION: "EPC Summary Report", - LIG_XML: "LIG XML", - RDSAP_XML: "RdSAP XML", - FULLSAP_XML: "Full SAP XML", - DECENT_HOMES_RAW_DATA: "Decent Homes Raw Data", - DECENT_HOMES_PROPERTY_META: "Decent Homes Property Meta", - DECENT_HOMES_SUMMARY: "Decent Homes Summary", + 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", }; -// Icon + colour per doc category -function docTypeStyle(docType: string): { - icon: React.ReactNode; - bg: string; - text: string; - border: string; -} { - if (docType.includes("XML")) { - return { - icon: , - bg: "bg-amber-50", - text: "text-amber-700", - border: "border-amber-200", - }; - } - if (docType.includes("DECENT_HOMES")) { - return { - icon: , - bg: "bg-violet-50", - text: "text-violet-700", - border: "border-violet-200", - }; - } - return { - icon: , - bg: "bg-sky-50", - text: "text-sky-700", - border: "border-sky-200", - }; +// All survey docs go under this group for now (extensible later) +function getDocCategory(_docType: string): string { + return "Survey Documents"; } function formatDate(iso: string): string { @@ -83,29 +56,25 @@ function formatDate(iso: string): string { // ----------------------------------------------------------------------- function DocumentRow({ doc }: { doc: PropertyDocument }) { const [signing, setSigning] = useState(false); - const style = docTypeStyle(doc.docType); const label = DOC_TYPE_LABELS[doc.docType] ?? doc.docType; async function handleDownload() { setSigning(true); try { - // Extract S3 key from the full URI — same pattern as TableViewer.tsx - const key = doc.s3FileUri.split(".amazonaws.com/")[1]; - if (!key) { - window.open(doc.s3FileUri, "_blank"); - return; - } const res = await fetch("/api/sign-s3-url", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ key }), + body: JSON.stringify({ key: doc.s3FileKey }), }); if (!res.ok) throw new Error("Failed to get signed URL"); const data = await res.json(); window.open(data.url, "_blank"); } catch { - // Fallback: open raw URI - window.open(doc.s3FileUri, "_blank"); + // Fallback: construct raw S3 URL + window.open( + `https://${doc.s3FileBucket}.s3.amazonaws.com/${doc.s3FileKey}`, + "_blank", + ); } finally { setSigning(false); } @@ -120,17 +89,15 @@ function DocumentRow({ doc }: { doc: PropertyDocument }) { >
{/* Doc type badge */} - - {style.icon} + + {label}
- {formatDate(doc.s3FileUploadTimestamp)} + {formatDate(doc.s3UploadTimestamp)}
+ 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, }); 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 eca568f..87c9391 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx @@ -1,12 +1,14 @@ import { getServerSession } from "next-auth"; import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions"; import { redirect } from "next/navigation"; -import { eq } from "drizzle-orm"; +import { eq, inArray } from "drizzle-orm"; import LiveTracker from "./LiveTracker"; import { computeLiveTrackerData } from "./transforms"; import { db } from "@/app/db/db"; import { hubspotDealData } from "@/app/db/schema/crm/hubspot_deal_table"; -import type { HubspotDeal } from "./types"; +import { uploadedFiles } from "@/app/db/schema/uploaded_files"; +import type { HubspotDeal, DocStatusMap, DocStatus } from "./types"; +import { EXPECTED_SURVEY_DOC_TYPES } from "./types"; import type { InferSelectModel } from "drizzle-orm"; // ⚠️ ⚠️ ⚠️ HARDCODED COMPANY ID — temporary for testing only. @@ -78,9 +80,43 @@ export default async function LiveReportingPage(props: { console.log("Fetched deals from DB:", rawDeals.length); - const trackerData = computeLiveTrackerData( - rawDeals.map(mapDbRowToHubspotDeal), - ); + 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; + } + } return (
@@ -94,7 +130,7 @@ export default async function LiveReportingPage(props: {
- +
); } 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 f00495b..c51e7d8 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts @@ -167,6 +167,7 @@ export type LiveTrackerProps = { projects: ProjectData[]; totalDeals: number; majorConditionDeals: ClassifiedDeal[]; // for Awaab's Law card + docStatusMap: DocStatusMap; }; // ----------------------------------------------------------------------- @@ -186,16 +187,39 @@ export type TableModal = { // ----------------------------------------------------------------------- export type PropertyDocument = { id: string; - s3FileUri: string; - s3JsonUri: string | null; - docType: string; - s3FileUploadTimestamp: string; // ISO string - uprn: 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; }; From 988bd2ce8f40a0a10cf1fffbbe7cb74ce01ba01e Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 3 Apr 2026 10:45:51 +0000 Subject: [PATCH 11/25] polishing project reporting ui --- .../live-tracking/property-documents/route.ts | 31 ++-- .../your-projects/live/AnalyticsView.tsx | 7 +- .../live/CompletionTrendsChart.tsx | 49 +++++ .../your-projects/live/DampMouldRiskPanel.tsx | 24 +-- .../your-projects/live/LiveTracker.tsx | 49 ++++- .../your-projects/live/PropertyDrawer.tsx | 175 ++++++++++++------ .../your-projects/live/transforms.ts | 37 ++-- .../(portfolio)/your-projects/live/types.ts | 42 +++-- 8 files changed, 283 insertions(+), 131 deletions(-) diff --git a/src/app/api/live-tracking/property-documents/route.ts b/src/app/api/live-tracking/property-documents/route.ts index 9ebf596..912dba8 100644 --- a/src/app/api/live-tracking/property-documents/route.ts +++ b/src/app/api/live-tracking/property-documents/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from "next/server"; -import { eq, or } from "drizzle-orm"; +import { eq } from "drizzle-orm"; import { db } from "@/app/db/db"; import { uploadedFiles } from "@/app/db/schema/uploaded_files"; @@ -16,23 +16,24 @@ export async function GET(req: Request) { } try { - const conditions = []; - - if (uprnParam) { - const uprnBigInt = BigInt(uprnParam); - conditions.push(eq(uploadedFiles.uprn, uprnBigInt)); - } - - if (landlordPropertyIdParam) { - conditions.push( - eq(uploadedFiles.landlordPropertyId, landlordPropertyIdParam), - ); - } + // 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() + .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(conditions.length === 1 ? conditions[0] : or(...conditions)); + .where(condition); const documents = rows.map((row) => ({ id: String(row.id), diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/AnalyticsView.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/AnalyticsView.tsx index 8235469..5aea535 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/AnalyticsView.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/AnalyticsView.tsx @@ -14,6 +14,7 @@ import type { ClassifiedDeal, TableModal, FunnelStage, + DisplayStage, } from "./types"; // ----------------------------------------------------------------------- @@ -117,8 +118,9 @@ function PipelineFunnel({ }) { const [mode, setMode] = useState<"current" | "cumulative">("cumulative"); + const ALWAYS_VISIBLE: DisplayStage[] = ["At Lodgement", "Project Complete"]; const visibleStages = funnelStages.filter( - (s) => s.currentCount > 0 || s.cumulativeCount > 0, + (s) => s.currentCount > 0 || s.cumulativeCount > 0 || ALWAYS_VISIBLE.includes(s.stage), ); const maxCount = Math.max( @@ -337,8 +339,9 @@ export default function AnalyticsView({ {/* Row 1.5: Completion trends chart */} {/* Row 2: section header */} diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/CompletionTrendsChart.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/CompletionTrendsChart.tsx index a3eb771..9f92ec7 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/CompletionTrendsChart.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/CompletionTrendsChart.tsx @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; +import { AlertCircle } from "lucide-react"; import { Card, Title, BarChart, Legend } from "@tremor/react"; import { Button } from "@/app/shadcn_components/ui/button"; import { Input } from "@/app/shadcn_components/ui/input"; @@ -16,6 +17,12 @@ interface CompletionTrendsChartProps { deals: ClassifiedDeal[]; isDomnaUser?: boolean; projectCode?: string; + onOpenTable?: ( + stage: string, + deals: ClassifiedDeal[], + columns?: (keyof ClassifiedDeal)[], + columnLabels?: Partial>, + ) => void; } const METRICS = [ @@ -290,6 +297,7 @@ export default function CompletionTrendsChart({ deals, isDomnaUser, projectCode, + onOpenTable, }: CompletionTrendsChartProps) { const [metric, setMetric] = useState(METRICS[0].key); const [targets, setTargets] = useState<{ [week: string]: number }>({}); @@ -305,6 +313,17 @@ export default function CompletionTrendsChart({ const isDesign = metric === "design"; const isStacked = isCoordination || isAssessments || isLodgement || isDesign; + // Assessments from external surveyors (no surveyedDate recorded) + const undatedAssessments = isAssessments + ? deals.filter((d) => { + const o = d.outcome ?? ""; + return ( + (o === "Surveyed" || o === "Surveyed - Pending Upload") && + !d.surveyedDate + ); + }) + : []; + // Compute chart data, categories, and colours in one place let chartData: Record[]; let categories: string[]; @@ -417,6 +436,36 @@ export default function CompletionTrendsChart({ stack={isStacked} customTooltip={ChartTooltip} /> + {isAssessments && undatedAssessments.length > 0 && ( +
+
+ + + {undatedAssessments.length}{" "} + assessment{undatedAssessments.length !== 1 ? "s" : ""} from external surveyors have no date recorded + +
+ {onOpenTable && ( + + )} +
+ )} {isStacked && ( !!d.dampMouldFlag); - const noRisk = risk.surveyFlagCount === 0 && risk.coordinatorFlagCount === 0; @@ -166,14 +164,14 @@ export default function DampMouldRiskPanel({
) : ( <> -
+
onOpenTable( "Damp & Mould — Survey Stage Flags", @@ -189,7 +187,7 @@ export default function DampMouldRiskPanel({ count={risk.coordinatorFlagCount} total={totalDeals} icon={Droplets} - color="orange" + color="red" onClick={() => onOpenTable( "Damp & Mould — Coordination Stage Flags", @@ -199,22 +197,6 @@ export default function DampMouldRiskPanel({ ) } /> - - onOpenTable( - "Damp & Mould — Flagged at Both Stages", - bothFlaggedDeals, - coordColumns, - coordLabels - ) - } - />
{/* Missed risk callout */} 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 5418d15..04086ea 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx @@ -9,9 +9,10 @@ import { TabsTrigger, } from "@/app/shadcn_components/ui/tabs"; import { Card, CardContent } from "@/app/shadcn_components/ui/card"; -import { BarChart2, Table2 } from "lucide-react"; +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 AnalyticsView from "./AnalyticsView"; @@ -30,7 +31,7 @@ export default function LiveTracker({ docStatusMap, }: LiveTrackerProps) { // ── Tab state ──────────────────────────────────────────────────────── - const [activeTab, setActiveTab] = useState<"analytics" | "properties">( + const [activeTab, setActiveTab] = useState<"analytics" | "properties" | "documents">( "analytics", ); @@ -89,7 +90,7 @@ export default function LiveTracker({
setActiveTab(v as "analytics" | "properties")} + onValueChange={(v) => setActiveTab(v as "analytics" | "properties" | "documents")} > {/* Tab bar */} @@ -107,6 +108,13 @@ export default function LiveTracker({ Properties + + + Document Management + {/* Analytics tab */} @@ -158,7 +166,38 @@ export default function LiveTracker({ +
+ + {/* Document Management tab */} + +
+ {projects.length > 1 && ( +
+ Project: + +
+ )} +
@@ -194,7 +233,7 @@ export default function LiveTracker({
{Object.entries(openTable.breakdown).map( ([category, items]) => { - const isCompleted = category.includes("Completed"); + 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"; diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyDrawer.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyDrawer.tsx index 0199ada..dec0cba 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyDrawer.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyDrawer.tsx @@ -1,11 +1,12 @@ "use client"; import { useQuery } from "@tanstack/react-query"; -import { useState } from "react"; +import { useRef, useState } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { FileDown, FileText, + FileX, Loader2, FolderOpen, X, @@ -20,6 +21,7 @@ import { 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 = { @@ -61,10 +63,10 @@ function DocumentRow({ doc }: { doc: PropertyDocument }) { async function handleDownload() { setSigning(true); try { - const res = await fetch("/api/sign-s3-url", { + const res = await fetch("/api/sign-document-url", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ key: doc.s3FileKey }), + 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(); @@ -85,33 +87,34 @@ function DocumentRow({ doc }: { doc: PropertyDocument }) { layout initial={{ opacity: 0, y: 4 }} animate={{ opacity: 1, y: 0 }} - className="flex items-center justify-between gap-3 p-3 rounded-lg border border-gray-100 bg-white hover:border-brandblue/20 hover:shadow-sm transition-all duration-150" + className="flex items-center justify-between gap-4 px-4 py-3 rounded-lg border border-gray-100 bg-white hover:border-brandblue/20 hover:shadow-sm transition-all duration-150" > + {/* Left: icon + label + date stacked */}
- {/* Doc type badge */} - - - {label} - +
+ +
+
+

{label}

+

+ {formatDate(doc.s3UploadTimestamp)} +

+
-
- - {formatDate(doc.s3UploadTimestamp)} - - -
+ {/* Right: download button */} + ); } @@ -136,16 +139,19 @@ export default function PropertyDrawer({ }: PropertyDrawerProps) { const canQuery = !!(uprn || landlordPropertyId); const { - data: documents = [], - isLoading, + 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}`); + 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; }, @@ -153,8 +159,17 @@ export default function PropertyDrawer({ 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 as PropertyDocument[]).reduce< + const grouped = documents.reduce< Record >((acc, doc) => { const category = getDocCategory(doc.docType); @@ -164,24 +179,29 @@ export default function PropertyDrawer({ 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} @@ -196,7 +216,7 @@ export default function PropertyDrawer({
- {hasDocuments && !isLoading && ( + {hasDocuments && !isFetching && (
@@ -209,7 +229,7 @@ export default function PropertyDrawer({ {/* Body */}
{/* Loading state */} - {isLoading && ( + {isFetching && (
{[1, 2, 3].map((i) => (
@@ -235,25 +255,43 @@ export default function PropertyDrawer({
)} - {/* Empty state */} - {!isLoading && !isError && !hasDocuments && ( -
-
- + {/* 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} + +
+ ))}
-

- No documents uploaded -

-

- Survey documents will appear here once uploaded for this - property. -

)} {/* Document groups */} - {!isLoading && + {!isFetching && !isError && hasDocuments && Object.entries(grouped).map(([category, docs]) => ( @@ -274,6 +312,35 @@ export default function PropertyDrawer({ ))} + + {/* 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 */} 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 ab5916c..d8d6bf3 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.ts +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.ts @@ -94,10 +94,11 @@ function resolveAfterAssessmentStage( // Called when design is UPLOADED — resolves install / lodgement / completed // ----------------------------------------------------------------------- function resolvePostDesignStage(deal: HubspotDeal): DisplayStage { - if (deal.fullLodgementDate) return "Completed"; - if (deal.lodgementStatus) return "Lodgement"; + 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 "Awaiting Install"; + return "Installation in Progress"; } // ----------------------------------------------------------------------- @@ -216,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; @@ -224,15 +225,16 @@ export function computeProjectProgress( const totalDeals = deals.length; // Coordination phase: - // completed = Design in Progress + Awaiting Install + Installation Complete + Lodgement + Completed + // 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) => [ "Design in Progress", - "Awaiting Install", + "Installation in Progress", "Installation Complete", - "Lodgement", - "Completed", + "At Lodgement", + "At Post Survey", + "Project Complete", ].includes(d.displayStage) ); const coordInProgressDeals = deals.filter( @@ -252,14 +254,15 @@ export function computeProjectProgress( }; // Design phase: - // completed = Awaiting Install + Installation Complete + Lodgement + Completed + // completed = Installation in Progress + Installation Complete + At Lodgement + At Post Survey + Project Complete // in progress = Design in Progress const designCompletedDeals = deals.filter((d) => [ - "Awaiting Install", + "Installation in Progress", "Installation Complete", - "Lodgement", - "Completed", + "At Lodgement", + "At Post Survey", + "Project Complete", ].includes(d.displayStage) ); const designInProgressDeals = deals.filter( @@ -279,10 +282,10 @@ export function computeProjectProgress( }; // Install phase: - // completed = Lodgement + Completed + // completed = At Lodgement + At Post Survey + Project Complete // in progress = Installation Complete const installCompletedDeals = deals.filter((d) => - ["Lodgement", "Completed"].includes(d.displayStage) + ["At Lodgement", "At Post Survey", "Project Complete"].includes(d.displayStage) ); const installInProgressDeals = deals.filter( (d) => d.displayStage === "Installation Complete" @@ -301,10 +304,10 @@ export function computeProjectProgress( }; // Lodgement phase: - // completed = Completed - // in progress = Lodgement + // completed = At Post Survey + Project Complete + // in progress = At Lodgement const lodgementInProgressDeals = deals.filter( - (d) => d.displayStage === "Lodgement" + (d) => d.displayStage === "At Lodgement" ); const lodgement: WorkPhaseStats = { 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 c51e7d8..931413c 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts @@ -62,10 +62,11 @@ export type DisplayStage = | "Assessment in Progress" | "Coordination in Progress" | "Design in Progress" - | "Awaiting Install" + | "Installation in Progress" | "Installation Complete" - | "Lodgement" - | "Completed" + | "At Lodgement" + | "At Post Survey" + | "Project Complete" | "Queries" | "Unknown Stage"; @@ -249,10 +250,11 @@ export const STAGE_ORDER: DisplayStage[] = [ "Assessment in Progress", "Coordination in Progress", "Design in Progress", - "Awaiting Install", + "Installation in Progress", "Installation Complete", - "Lodgement", - "Completed", + "At Lodgement", + "At Post Survey", + "Project Complete", ]; // ----------------------------------------------------------------------- @@ -275,10 +277,10 @@ export const STAGE_COLORS: Record< dot: "bg-sky-400", }, "Assessment in Progress": { - bg: "bg-violet-50", - text: "text-violet-700", - border: "border-violet-200", - dot: "bg-violet-400", + bg: "bg-blue-100", + text: "text-blue-900", + border: "border-blue-400", + dot: "bg-blue-700", }, "Coordination in Progress": { bg: "bg-indigo-50", @@ -292,11 +294,11 @@ export const STAGE_COLORS: Record< border: "border-blue-200", dot: "bg-blue-400", }, - "Awaiting Install": { - bg: "bg-purple-50", - text: "text-purple-700", - border: "border-purple-200", - dot: "bg-purple-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", @@ -304,13 +306,19 @@ export const STAGE_COLORS: Record< border: "border-teal-200", dot: "bg-teal-400", }, - Lodgement: { + "At Lodgement": { bg: "bg-cyan-50", text: "text-cyan-700", border: "border-cyan-200", dot: "bg-cyan-400", }, - Completed: { + "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", From ac30e3c13a01ae1590ac306aed5ff28906bbc566 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sat, 4 Apr 2026 13:35:50 +0000 Subject: [PATCH 12/25] adding portfolio organisation (wip_ --- src/app/db/db.ts | 2 + src/app/db/migrations/meta/_journal.json | 7 + .../settings/PortfolioSettings.tsx | 4 + .../[slug]/(portfolio)/settings/page.tsx | 11 +- .../your-projects/live/AnalyticsView.tsx | 96 +++++-- .../live/CompletionTrendsChart.tsx | 272 +++++++++--------- .../your-projects/live/LiveTracker.tsx | 11 + .../your-projects/live/PropertyTable.tsx | 11 +- .../live/PropertyTableColumns.tsx | 40 +-- .../(portfolio)/your-projects/live/page.tsx | 90 ++++-- 10 files changed, 314 insertions(+), 230 deletions(-) diff --git a/src/app/db/db.ts b/src/app/db/db.ts index 5219612..992e73d 100644 --- a/src/app/db/db.ts +++ b/src/app/db/db.ts @@ -12,6 +12,7 @@ import * as Relations from "@/app/db/schema/relations"; import * as Users from "@/app/db/schema/users"; 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, @@ -35,6 +36,7 @@ const schema = { ...Users, ...CrmSchema, ...UploadedFilesSchema, + ...PortfolioOrgSchema, }; export const db = drizzle(pool, { diff --git a/src/app/db/migrations/meta/_journal.json b/src/app/db/migrations/meta/_journal.json index 9fee291..4b628e3 100644 --- a/src/app/db/migrations/meta/_journal.json +++ b/src/app/db/migrations/meta/_journal.json @@ -1142,6 +1142,13 @@ "when": 1775041844023, "tag": "0162_powerful_paladin", "breakpoints": true + }, + { + "idx": 163, + "version": "7", + "when": 1775309608582, + "tag": "0163_fat_mentallo", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/app/portfolio/[slug]/(portfolio)/settings/PortfolioSettings.tsx b/src/app/portfolio/[slug]/(portfolio)/settings/PortfolioSettings.tsx index b493531..169b965 100644 --- a/src/app/portfolio/[slug]/(portfolio)/settings/PortfolioSettings.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/settings/PortfolioSettings.tsx @@ -35,6 +35,7 @@ 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"; +import OrganisationLinkCard from "./OrganisationLinkCard"; // dropdown selection component for both goal and status @@ -215,9 +216,11 @@ async function deletePortfolio({ export default function PortfolioSettings({ portfolioId, portfolioSettingsData, + isDomnaUser = false, }: { portfolioId: string; portfolioSettingsData: PortfolioSettingsType; + isDomnaUser?: boolean; }) { // This is a client component so we can access the session directly const session = useSession(); @@ -475,6 +478,7 @@ export default function PortfolioSettings({
+ {isDomnaUser && }
diff --git a/src/app/portfolio/[slug]/(portfolio)/settings/page.tsx b/src/app/portfolio/[slug]/(portfolio)/settings/page.tsx index 67ae43f..4732c67 100644 --- a/src/app/portfolio/[slug]/(portfolio)/settings/page.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/settings/page.tsx @@ -1,3 +1,5 @@ +import { getServerSession } from "next-auth"; +import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions"; import { getPortfolioSettings } from "../../utils"; import PortfolioSettings from "./PortfolioSettings"; @@ -8,7 +10,13 @@ export default async function PortfolioSettingsPage( ) { const params = await props.params; const portfolioId = params.slug; - const portfolioSettingsData = await getPortfolioSettings(portfolioId); + + const [portfolioSettingsData, session] = await Promise.all([ + getPortfolioSettings(portfolioId), + getServerSession(AuthOptions), + ]); + + const isDomnaUser = !!session?.user?.email?.endsWith("@domna.homes"); return ( <> @@ -16,6 +24,7 @@ export default async function PortfolioSettingsPage( diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/AnalyticsView.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/AnalyticsView.tsx index 5aea535..73b3dec 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/AnalyticsView.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/AnalyticsView.tsx @@ -95,6 +95,66 @@ function StatCard({ ); } +// ----------------------------------------------------------------------- +// 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 // ----------------------------------------------------------------------- @@ -116,7 +176,7 @@ function PipelineFunnel({ reason?: string, ) => void; }) { - const [mode, setMode] = useState<"current" | "cumulative">("cumulative"); + const [mode, setMode] = useState<"current" | "cumulative">("current"); const ALWAYS_VISIBLE: DisplayStage[] = ["At Lodgement", "Project Complete"]; const visibleStages = funnelStages.filter( @@ -180,34 +240,10 @@ function PipelineFunnel({ key={s.stage} whileHover={{ scale: 1.01, y: -1 }} transition={{ duration: 0.15 }} - onClick={() => - onOpenTable( - `Pipeline — ${s.stage}`, - deals, - [ - "dealname", - "landlordPropertyId", - "displayStage", - "coordinator", - "designer", - "installer", - ], - { - dealname: "Address", - landlordPropertyId: "Ref", - displayStage: "Stage", - coordinator: "Coordinator", - designer: "Designer", - installer: "Installer", - }, - undefined, - `Pipeline — ${s.stage}`, - mode === "cumulative" - ? `Properties that have reached the "${s.stage}" stage or beyond.` - : `Properties currently at the "${s.stage}" stage.`, - undefined, - ) - } + onClick={() => { + 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" > @@ -339,8 +375,6 @@ export default function AnalyticsView({ {/* Row 1.5: Completion trends chart */} diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/CompletionTrendsChart.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/CompletionTrendsChart.tsx index 9f92ec7..d869e97 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/CompletionTrendsChart.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/CompletionTrendsChart.tsx @@ -2,9 +2,18 @@ import { useState } from "react"; import { AlertCircle } from "lucide-react"; -import { Card, Title, BarChart, Legend } from "@tremor/react"; -import { Button } from "@/app/shadcn_components/ui/button"; -import { Input } from "@/app/shadcn_components/ui/input"; +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, @@ -15,7 +24,6 @@ import type { ClassifiedDeal } from "./types"; interface CompletionTrendsChartProps { deals: ClassifiedDeal[]; - isDomnaUser?: boolean; projectCode?: string; onOpenTable?: ( stage: string, @@ -45,13 +53,13 @@ const METRICS = [ }, ]; -// Brand colour palette (hex values work directly with Tremor v3 colors prop) +// Brand colour palette const C = { - blue: "#5d6be0", // lighter periwinkle blue - midblue: "#3943b7", // brandmidblue - lightblue: "#8b96e9", // soft pale blue for 3rd series - paleblue: "#b8bef4", // 4th series — replaces brown in bar charts - brown: "#c4a47c", // accent only — trend/target lines + blue: "#5d6be0", + midblue: "#3943b7", + lightblue: "#8b96e9", + paleblue: "#b8bef4", + brown: "#c4a47c", }; function ChartTooltip({ @@ -64,10 +72,13 @@ function ChartTooltip({ 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}

- {payload.map((item, i) => ( + {visible.map((item, i) => (
= {}; for (const deal of deals) { if (filter && !filter(deal)) continue; - const date = deal[dateField as keyof ClassifiedDeal] as - | string - | Date - | null; + const date = deal[dateField as keyof ClassifiedDeal] as string | Date | null; if (!date) continue; const d = new Date(date); if (isNaN(d.getTime())) continue; @@ -153,16 +159,14 @@ function aggregateByWeek( })); } -// Coordination: buckets V1 and V2 completions separately function aggregateCoordinationByWeek( deals: ClassifiedDeal[], -): Array<{ week: string; "V1 (MTP)": number; "V2 (Re-model)": number }> { +): 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())) { @@ -170,7 +174,6 @@ function aggregateCoordinationByWeek( 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())) { @@ -184,17 +187,16 @@ function aggregateCoordinationByWeek( Array.from(new Set([...Object.keys(v1Counts), ...Object.keys(v2Counts)])), ); - return allKeys.map((isoKey) => ({ - week: formatMonday(isoKey), - "V1 (MTP)": v1Counts[isoKey] ?? 0, - "V2 (Re-model)": v2Counts[isoKey] ?? 0, - })); + 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 }; + }); } -// Assessments: Retrofit Assessment vs EPC, keyed by surveyedDate function aggregateAssessmentsByWeek( deals: ClassifiedDeal[], -): Array<{ week: string; "Retrofit Assessment": number; EPC: number }> { +): Array<{ week: string; "Retrofit Assessment": number; EPC: number; _total: number }> { const retrofitCounts: Record = {}; const epcCounts: Record = {}; @@ -204,32 +206,27 @@ function aggregateAssessmentsByWeek( 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)]), - ), + Array.from(new Set([...Object.keys(retrofitCounts), ...Object.keys(epcCounts)])), ); - return allKeys.map((isoKey) => ({ - week: formatMonday(isoKey), - "Retrofit Assessment": retrofitCounts[isoKey] ?? 0, - EPC: epcCounts[isoKey] ?? 0, - })); + 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 }; + }); } -// Lodgements: Stage 1 vs Lodged Measures function aggregateLodgementsByWeek( deals: ClassifiedDeal[], -): Array<{ week: string; "Stage 1 Lodgement": number; "Lodged Measures": number }> { +): Array<{ week: string; "Stage 1 Lodgement": number; "Lodged Measures": number; _total: number }> { const stageCounts: Record = {}; const measuresCounts: Record = {}; @@ -251,19 +248,16 @@ function aggregateLodgementsByWeek( } const allKeys = fillWeekGaps( - Array.from( - new Set([...Object.keys(stageCounts), ...Object.keys(measuresCounts)]), - ), + Array.from(new Set([...Object.keys(stageCounts), ...Object.keys(measuresCounts)])), ); - return allKeys.map((isoKey) => ({ - week: formatMonday(isoKey), - "Stage 1 Lodgement": stageCounts[isoKey] ?? 0, - "Lodged Measures": measuresCounts[isoKey] ?? 0, - })); + 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 }; + }); } -// Designs: stacked by design type (Uploaded only) function aggregateDesignsByWeek( deals: ClassifiedDeal[], ): Array> { @@ -275,10 +269,8 @@ function aggregateDesignsByWeek( 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; } @@ -286,25 +278,34 @@ function aggregateDesignsByWeek( 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) { - entry[label] = counts[isoKey]?.[label] ?? 0; + 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, - isDomnaUser, - projectCode, onOpenTable, }: CompletionTrendsChartProps) { const [metric, setMetric] = useState(METRICS[0].key); - const [targets, setTargets] = useState<{ [week: string]: number }>({}); - const [targetInput, setTargetInput] = useState<{ - week: string; - value: string; - }>({ week: "", value: "" }); const selectedMetric = METRICS.find((m) => m.key === metric)!; const isCoordination = metric === "coordination"; @@ -313,18 +314,15 @@ export default function CompletionTrendsChart({ const isDesign = metric === "design"; const isStacked = isCoordination || isAssessments || isLodgement || isDesign; - // Assessments from external surveyors (no surveyedDate recorded) + // External assessments with no date const undatedAssessments = isAssessments ? deals.filter((d) => { const o = d.outcome ?? ""; - return ( - (o === "Surveyed" || o === "Surveyed - Pending Upload") && - !d.surveyedDate - ); + return (o === "Surveyed" || o === "Surveyed - Pending Upload") && !d.surveyedDate; }) : []; - // Compute chart data, categories, and colours in one place + // Build chart data let chartData: Record[]; let categories: string[]; let colors: string[]; @@ -350,33 +348,32 @@ export default function CompletionTrendsChart({ chartData = singleData.map((d) => ({ week: d.week, [selectedMetric.label]: d.value, - ...(targets[d.week] !== undefined ? { Target: targets[d.week] } : {}), })); - categories = [selectedMetric.label, ...(isDomnaUser ? ["Target"] : [])]; - colors = [C.blue, C.brown]; + categories = [selectedMetric.label]; + colors = [C.blue]; } - const handleAddTarget = () => { - if (!targetInput.week || !targetInput.value) return; - setTargets((prev) => ({ - ...prev, - [targetInput.week]: Number(targetInput.value), - })); - setTargetInput({ week: "", value: "" }); - }; + const totalCompleted = computeTotalCompleted(metric, chartData, categories); return ( -
-
- + {/* Header row */} + <div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4 mb-4"> + <div className="flex flex-col gap-2"> + <Title className="text-brandblue text-lg font-bold"> Trends Over Time -

- Switch between metrics to see weekly trends. -

+ {totalCompleted !== null && ( +
+ {totalCompleted} + + {metric === "bookings" ? "booked to date" : "completed to date"} + + +
+ )}
-
+
- setTargetInput((ti) => ({ ...ti, week: e.target.value })) - } - className="w-32 text-xs" - /> - - setTargetInput((ti) => ({ ...ti, value: e.target.value })) - } - className="w-24 text-xs" - /> - -
- )} - - + {/* Undated external assessments — shown above the chart */} {isAssessments && undatedAssessments.length > 0 && ( -
+
{undatedAssessments.length}{" "} - assessment{undatedAssessments.length !== 1 ? "s" : ""} from external surveyors have no date recorded + external assessment{undatedAssessments.length !== 1 ? "s" : ""} have no date recorded
{onOpenTable && ( @@ -466,11 +420,59 @@ export default function CompletionTrendsChart({ )}
)} + + {/* 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/LiveTracker.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx index 04086ea..502bd71 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx @@ -15,6 +15,7 @@ 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, @@ -53,6 +54,9 @@ export default function LiveTracker({ dealname: null, }); + // ── Property detail drawer ─────────────────────────────────────────── + const [detailDeal, setDetailDeal] = useState(null); + const handleOpenTable = ( stage: string, filteredDeals: ClassifiedDeal[], @@ -166,6 +170,7 @@ export default function LiveTracker({
@@ -302,6 +307,12 @@ export default function LiveTracker({ setDrawerState({ open: false, uprn: null, landlordPropertyId: null, dealname: null }) } /> + + {/* ── Property detail drawer ─────────────────────────────────────── */} + setDetailDeal(null)} + />
); } diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTable.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTable.tsx index 4a61021..ac29be0 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTable.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTable.tsx @@ -38,7 +38,7 @@ import { import { Search, SlidersHorizontal, ChevronLeft, ChevronRight, Download } from "lucide-react"; import { createPropertyTableColumns } from "./PropertyTableColumns"; import { STAGE_ORDER } from "./types"; -import type { ClassifiedDeal, DisplayStage, DocStatusMap } from "./types"; +import type { ClassifiedDeal, DocStatusMap } from "./types"; // Human-readable labels for toggle dropdown const COLUMN_LABELS: Record = { @@ -52,7 +52,6 @@ const COLUMN_LABELS: Record = { approvedPackage: "Approved Package", actualMeasuresInstalled: "Installed Measures", preSapScore: "Pre-SAP", - postSapScore: "Post-SAP", lodgementStatus: "Lodgement Status", designDate: "Design Date", fullLodgementDate: "Lodgement Date", @@ -63,6 +62,7 @@ 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; } @@ -96,7 +96,7 @@ function escapeCell(value: unknown): string { : str; } -export default function PropertyTable({ data, onOpenDrawer, showDocuments = false, docStatusMap = {} }: PropertyTableProps) { +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"); @@ -112,7 +112,6 @@ export default function PropertyTable({ data, onOpenDrawer, showDocuments = fals approvedPackage: false, actualMeasuresInstalled: false, preSapScore: false, - postSapScore: false, lodgementStatus: false, designDate: false, fullLodgementDate: false, @@ -137,8 +136,8 @@ export default function PropertyTable({ data, onOpenDrawer, showDocuments = fals }, [data, stageFilter, docFilter, docStatusMap]); const columns = useMemo( - () => createPropertyTableColumns(onOpenDrawer, showDocuments, docStatusMap), - [onOpenDrawer, showDocuments, docStatusMap] + () => createPropertyTableColumns(onOpenDrawer, showDocuments, docStatusMap, onOpenDetail), + [onOpenDrawer, showDocuments, docStatusMap, onOpenDetail] ); const table = useReactTable({ diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTableColumns.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTableColumns.tsx index 07d4e53..9562418 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTableColumns.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTableColumns.tsx @@ -1,7 +1,7 @@ "use client"; import { ColumnDef } from "@tanstack/react-table"; -import { ArrowUpDown, FileDown, CheckCircle2, AlertCircle, FileX } from "lucide-react"; +import { ArrowUpDown, CheckCircle2, AlertCircle, FileX } from "lucide-react"; import { STAGE_COLORS } from "./types"; import type { ClassifiedDeal, DisplayStage, DocStatusMap } from "./types"; @@ -48,6 +48,7 @@ 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 ────────────────────────────────────────────────────────── @@ -57,9 +58,18 @@ export function createPropertyTableColumns( header: ({ column }) => , cell: ({ row }) => (
-

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

+ {onOpenDetail ? ( + + ) : ( +

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

+ )}
), enableHiding: false, @@ -221,28 +231,6 @@ export function createPropertyTableColumns( }, }, - // ── Post-SAP score ─────────────────────────────────────────────────── - { - accessorKey: "postSapScore", - id: "postSapScore", - header: ({ column }) => , - cell: ({ row }) => { - const score = row.original.postSapScore; - if (!score) return ; - const n = Number(score); - const colour = - n >= 70 - ? "text-emerald-700 bg-emerald-50" - : n >= 50 - ? "text-sky-700 bg-sky-50" - : "text-amber-700 bg-amber-50"; - return ( - - {score} - - ); - }, - }, // ── Lodgement status ───────────────────────────────────────────────── { 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 87c9391..55da823 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx @@ -7,14 +7,13 @@ import { computeLiveTrackerData } from "./transforms"; 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"; - -// ⚠️ ⚠️ ⚠️ HARDCODED COMPANY ID — temporary for testing only. -// TODO: derive this from the portfolio slug once the portfolio↔company mapping exists. -// Do NOT ship this to production without replacing it with the real lookup. -const HARDCODED_COMPANY_ID = "86970043613"; +import { Card, CardContent } from "@/app/shadcn_components/ui/card"; +import { Building2 } from "lucide-react"; type DbDeal = InferSelectModel; @@ -37,25 +36,25 @@ function mapDbRowToHubspotDeal(row: DbDeal): HubspotDeal { designStatus: row.designStatus, pashubLink: row.pashubLink, sharepointLink: row.sharepointLink, - dampMouldFlag: row.dampmouldGrowth, // DB: dampmouldGrowth - preSapScore: row.preSap, // DB: preSap + dampMouldFlag: row.dampmouldGrowth, + preSapScore: row.preSap, coordinator: row.coordinator, - ioeV1Date: row.mtpCompletionDate, // DB: mtpCompletionDate - ioeV2Date: row.mtpReModelCompletionDate, // DB: mtpReModelCompletionDate - ioeV3Date: row.ioeV3CompletionDate, // DB: ioeV3CompletionDate + ioeV1Date: row.mtpCompletionDate, + ioeV2Date: row.mtpReModelCompletionDate, + ioeV3Date: row.ioeV3CompletionDate, proposedMeasures: row.proposedMeasures, approvedPackage: row.approvedPackage, designer: row.designer, - designDate: row.designCompletionDate, // DB: designCompletionDate + designDate: row.designCompletionDate, actualMeasuresInstalled: row.actualMeasuresInstalled, installer: row.installer, installerHandover: row.installerHandover, lodgementStatus: row.lodgementStatus, measuresLodgementDate: row.measuresLodgementDate, - fullLodgementDate: row.lodgementDate, // DB: lodgementDate + fullLodgementDate: row.lodgementDate, confirmedSurveyDate: row.confirmedSurveyDate, - surveyedDate: row.SurveyedDate, // DB: surveyed_date - designType: row.dealType, // DB: design_type + surveyedDate: row.SurveyedDate, + designType: row.dealType, createdAt: row.createdAt, updatedAt: row.updatedAt, }; @@ -64,26 +63,64 @@ function mapDbRowToHubspotDeal(row: DbDeal): HubspotDeal { export default async function LiveReportingPage(props: { params: Promise<{ slug: string }>; }) { - const { slug: _portfolioId } = await props.params; + const { slug: portfolioId } = await props.params; const user = await getServerSession(AuthOptions); if (!user?.user) { - console.error("User not found"); redirect("/"); } - // ⚠️ Using HARDCODED_COMPANY_ID — see constant above before deploying + // 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 pageHeader = ( +
+
Live Projects
+

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

+
+
+ ); + + if (!link.length || !link[0].hubspotCompanyId) { + return ( +
+ {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. +

+
+
+
+
+ ); + } + + const companyId = link[0].hubspotCompanyId; + const rawDeals = await db .select() .from(hubspotDealData) - .where(eq(hubspotDealData.companyId, HARDCODED_COMPANY_ID)); - - console.log("Fetched deals from DB:", rawDeals.length); + .where(eq(hubspotDealData.companyId, companyId)); const deals = rawDeals.map(mapDbRowToHubspotDeal); const trackerData = computeLiveTrackerData(deals); - // ── Fetch survey document status for all properties ───────────────── + // Fetch survey document status for all properties const uprnList = deals .map((d) => d.uprn) .filter((u): u is string => !!u) @@ -120,16 +157,7 @@ export default async function LiveReportingPage(props: { return (
-
-
- Live Projects -
-

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

-
-
- + {pageHeader}
); From f8e6d7afa2976f5ebced8fa1b3f4c8259cdc89da Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sat, 4 Apr 2026 13:37:42 +0000 Subject: [PATCH 13/25] added missing files --- src/app/api/organisations/route.ts | 24 + .../[portfolioId]/organisation/route.ts | 94 + src/app/api/sign-document-url/route.ts | 38 + src/app/db/migrations/0163_fat_mentallo.sql | 11 + src/app/db/migrations/meta/0163_snapshot.json | 6311 +++++++++++++++++ src/app/db/schema/portfolio_organisation.ts | 24 + .../settings/OrganisationLinkCard.tsx | 280 + .../your-projects/live/DocumentTable.tsx | 289 + .../live/DocumentTableColumns.tsx | 147 + .../live/PropertyDetailDrawer.tsx | 271 + 10 files changed, 7489 insertions(+) create mode 100644 src/app/api/organisations/route.ts create mode 100644 src/app/api/portfolio/[portfolioId]/organisation/route.ts create mode 100644 src/app/api/sign-document-url/route.ts create mode 100644 src/app/db/migrations/0163_fat_mentallo.sql create mode 100644 src/app/db/migrations/meta/0163_snapshot.json create mode 100644 src/app/db/schema/portfolio_organisation.ts create mode 100644 src/app/portfolio/[slug]/(portfolio)/settings/OrganisationLinkCard.tsx create mode 100644 src/app/portfolio/[slug]/(portfolio)/your-projects/live/DocumentTable.tsx create mode 100644 src/app/portfolio/[slug]/(portfolio)/your-projects/live/DocumentTableColumns.tsx create mode 100644 src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyDetailDrawer.tsx 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/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/db/migrations/0163_fat_mentallo.sql b/src/app/db/migrations/0163_fat_mentallo.sql new file mode 100644 index 0000000..3031759 --- /dev/null +++ b/src/app/db/migrations/0163_fat_mentallo.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/meta/0163_snapshot.json b/src/app/db/migrations/meta/0163_snapshot.json new file mode 100644 index 0000000..33c2c68 --- /dev/null +++ b/src/app/db/migrations/meta/0163_snapshot.json @@ -0,0 +1,6311 @@ +{ + "id": "57226b46-c2f5-478d-83ea-53b233df9925", + "prevId": "848b8a62-58f6-4c42-841a-3f9d2aff5b58", + "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 + }, + "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/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/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)/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/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}

+ )} +
+ + )} + + + ); +} From 06cfdf6689f0d46898b8218d8b8b32e8cb9ab3b3 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sat, 4 Apr 2026 13:40:33 +0000 Subject: [PATCH 14/25] added migration files --- src/app/db/migrations/0164_high_sumo.sql | 11 + src/app/db/migrations/meta/0164_snapshot.json | 6317 +++++++++++++++++ src/app/db/migrations/meta/_journal.json | 9 +- 3 files changed, 6336 insertions(+), 1 deletion(-) create mode 100644 src/app/db/migrations/0164_high_sumo.sql create mode 100644 src/app/db/migrations/meta/0164_snapshot.json 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/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/_journal.json b/src/app/db/migrations/meta/_journal.json index 03f3aad..bb63278 100644 --- a/src/app/db/migrations/meta/_journal.json +++ b/src/app/db/migrations/meta/_journal.json @@ -1149,6 +1149,13 @@ "when": 1775123235194, "tag": "0163_cultured_madripoor", "breakpoints": true + }, + { + "idx": 164, + "version": "7", + "when": 1775310006908, + "tag": "0164_high_sumo", + "breakpoints": true } ] -} +} \ No newline at end of file From 5fce761b938dff32797142fb30322e6207636cf2 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sat, 4 Apr 2026 19:02:22 +0000 Subject: [PATCH 15/25] removed raw fetch from property drawer --- .../your-projects/live/PropertyDrawer.tsx | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyDrawer.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyDrawer.tsx index dec0cba..6dc83ef 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyDrawer.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyDrawer.tsx @@ -1,7 +1,7 @@ "use client"; -import { useQuery } from "@tanstack/react-query"; -import { useRef, useState } from "react"; +import { useQuery, useMutation } from "@tanstack/react-query"; +import { useRef } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { FileDown, @@ -57,12 +57,10 @@ function formatDate(iso: string): string { // Individual document row // ----------------------------------------------------------------------- function DocumentRow({ doc }: { doc: PropertyDocument }) { - const [signing, setSigning] = useState(false); const label = DOC_TYPE_LABELS[doc.docType] ?? doc.docType; - async function handleDownload() { - setSigning(true); - try { + const { mutate: download, isPending: signing } = useMutation({ + mutationFn: async () => { const res = await fetch("/api/sign-document-url", { method: "POST", headers: { "Content-Type": "application/json" }, @@ -70,17 +68,12 @@ function DocumentRow({ doc }: { doc: PropertyDocument }) { }); if (!res.ok) throw new Error("Failed to get signed URL"); const data = await res.json(); - window.open(data.url, "_blank"); - } catch { - // Fallback: construct raw S3 URL - window.open( - `https://${doc.s3FileBucket}.s3.amazonaws.com/${doc.s3FileKey}`, - "_blank", - ); - } finally { - setSigning(false); - } - } + return data.url as string; + }, + onSuccess: (url) => { + window.open(url, "_blank"); + }, + }); return ( download()} disabled={signing} className="shrink-0 inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-brandblue text-white text-xs font-medium hover:bg-brandblue/90 transition-colors disabled:opacity-60 disabled:cursor-not-allowed" > From 716b0d5d791980812b7a87b983910b9f06c03295 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sat, 4 Apr 2026 19:11:53 +0000 Subject: [PATCH 16/25] removed raw fetch --- .../your-projects/live/DrillDownTable.tsx | 58 ++++++++++--------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DrillDownTable.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DrillDownTable.tsx index eaf3444..7adf28e 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DrillDownTable.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DrillDownTable.tsx @@ -1,6 +1,7 @@ "use client"; import { useMemo, useState } from "react"; +import { useMutation } from "@tanstack/react-query"; import { useReactTable, getCoreRowModel, @@ -39,27 +40,35 @@ function escapeCell(value: unknown): string { : str; } -async function handlePhotoDownload(rawUrl: string) { - try { - const key = rawUrl.split(".amazonaws.com/")[1]; - if (!key) return alert("Invalid S3 key"); +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"); + }, + }); - 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 ( + + ); } function PhotoDownloadCell({ value }: { value: unknown }) { @@ -81,14 +90,7 @@ function PhotoDownloadCell({ value }: { value: unknown }) { return (
{urls.map((url, idx) => ( - + ))}
); From 4e6728bf84c04924c3ddf40ed8b5a8e39a9c7217 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sat, 4 Apr 2026 19:19:58 +0000 Subject: [PATCH 17/25] removed redundant args --- .../(portfolio)/your-projects/live/ProgressOverview.tsx | 8 -------- 1 file changed, 8 deletions(-) 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 7eeaaa5..92113c7 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ProgressOverview.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ProgressOverview.tsx @@ -124,10 +124,6 @@ export default function ProgressOverview({ actualMeasuresInstalled: "Measures Installed", fullLodgementDate: "Lodgement Date", }, - undefined, - "Completed Properties", - "These properties have completed all stages and are fully lodged/funded.", - "Work completed and funding claimed.", ) } 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" @@ -181,10 +177,6 @@ export default function ProgressOverview({ item.deals, EARLY_COLUMNS, EARLY_LABELS, - undefined, - item.stage, - `Properties currently in the "${item.stage}" stage.`, - undefined, ) } className={`group text-left rounded-xl border p-3 transition-all duration-200 hover:shadow-md ${c.bg} ${c.border} hover:opacity-95`} From ace1e5ff7ccda2114b4141db1622d8dcff0ef980 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sat, 4 Apr 2026 19:22:59 +0000 Subject: [PATCH 18/25] fixing build error --- .../[slug]/(portfolio)/your-projects/live/SurveyIssuesPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/SurveyIssuesPanel.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/SurveyIssuesPanel.tsx index 37ba210..d8c576b 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/SurveyIssuesPanel.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/SurveyIssuesPanel.tsx @@ -49,7 +49,7 @@ export default function SurveyIssuesPanel({ if (!groups.has(key)) groups.set(key, []); groups.get(key)!.push(deal); } - const sortedGroups = [...groups.entries()].sort( + const sortedGroups = Array.from(groups.entries()).sort( (a, b) => b[1].length - a[1].length, ); From 5d94fb3b0051380a42cae32f4f09fb6e18e1eaaa Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sat, 4 Apr 2026 19:26:45 +0000 Subject: [PATCH 19/25] fixing build erroor --- .../[slug]/(portfolio)/your-projects/live/transforms.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d8d6bf3..fe1da87 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.ts +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.ts @@ -375,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); From a849731848b1481203dfa14074ba35a851e30467 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sat, 4 Apr 2026 20:44:44 +0000 Subject: [PATCH 20/25] rebuilding the settings page and including logs --- src/app/iq/IQDashboard.tsx | 146 ----- src/app/iq/SubtaskDetails.tsx | 289 --------- src/app/iq/TaskList.tsx | 293 ---------- src/app/iq/page.tsx | 5 - .../settings/PortfolioSettings.tsx | 548 ------------------ .../[slug]/(portfolio)/settings/page.tsx | 36 +- 6 files changed, 6 insertions(+), 1311 deletions(-) delete mode 100644 src/app/iq/IQDashboard.tsx delete mode 100644 src/app/iq/SubtaskDetails.tsx delete mode 100644 src/app/iq/TaskList.tsx delete mode 100644 src/app/iq/page.tsx delete mode 100644 src/app/portfolio/[slug]/(portfolio)/settings/PortfolioSettings.tsx diff --git a/src/app/iq/IQDashboard.tsx b/src/app/iq/IQDashboard.tsx deleted file mode 100644 index 262a35f..0000000 --- a/src/app/iq/IQDashboard.tsx +++ /dev/null @@ -1,146 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import TaskList from "./TaskList"; -import SubtaskDetails from "./SubtaskDetails"; - -export interface Task { - id: string; - taskSource: string; - jobStarted: string | null; - jobCompleted: string | null; - status: string; - service: string | null; - updatedAt: string; -} - -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: Task[]; - total: number; - limit: number; - offset: number; -} - -export default function IQDashboard() { - const [tasks, setTasks] = useState([]); - const [selectedTaskId, setSelectedTaskId] = useState(null); - const [subtasks, setSubtasks] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [total, setTotal] = useState(0); - const [limit, setLimit] = useState(20); - const [offset, setOffset] = useState(0); - const [loadingMore, setLoadingMore] = useState(false); - - // Fetch tasks with pagination - const fetchTasks = async (newLimit: number, newOffset: number) => { - try { - if (newOffset === 0) setLoading(true); - else setLoadingMore(true); - - const response = await fetch( - `/api/tasks?limit=${newLimit}&offset=${newOffset}` - ); - if (!response.ok) throw new Error("Failed to fetch tasks"); - const data: TasksResponse = await response.json(); - - if (newOffset === 0) { - setTasks(data.tasks); - } else { - setTasks((prev) => [...prev, ...data.tasks]); - } - - setTotal(data.total); - setLimit(data.limit); - setOffset(newOffset + data.tasks.length); - setError(null); - } catch (err) { - setError(err instanceof Error ? err.message : "An error occurred"); - if (newOffset === 0) setTasks([]); - } finally { - setLoading(false); - setLoadingMore(false); - } - }; - - // Initial load (first 20) - useEffect(() => { - fetchTasks(20, 0); - }, []); - - const handleLoadMore = () => { - fetchTasks(20, offset); - }; - - const handleRefresh = () => { - setOffset(0); - fetchTasks(20, 0); - }; - - // Fetch subtasks when a task is selected - useEffect(() => { - if (!selectedTaskId) { - setSubtasks([]); - return; - } - - const fetchSubtasks = async () => { - try { - const response = await fetch(`/api/tasks/${selectedTaskId}`); - if (!response.ok) throw new Error("Failed to fetch subtasks"); - const data = await response.json(); - setSubtasks(data); - } catch (err) { - setSubtasks([]); - } - }; - - fetchSubtasks(); - }, [selectedTaskId]); - - return ( -
- {/* Left sidebar - Task list */} -
- -
- - {/* Right side - Subtask details */} -
- {selectedTaskId ? ( - t.id === selectedTaskId)} - /> - ) : ( -
- Select a task to view its subtasks -
- )} -
-
- ); -} diff --git a/src/app/iq/SubtaskDetails.tsx b/src/app/iq/SubtaskDetails.tsx deleted file mode 100644 index 7139a1a..0000000 --- a/src/app/iq/SubtaskDetails.tsx +++ /dev/null @@ -1,289 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { SubTask, Task } from "./IQDashboard"; -import { Badge } from "@/app/shadcn_components/ui/badge"; -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 } from "lucide-react"; - -interface SubtaskDetailsProps { - selectedTaskId: string; - subtasks: SubTask[]; - task?: Task; -} - -function getStatusColor( - status: string -): "default" | "secondary" | "destructive" | "outline" { - switch (status.toLowerCase()) { - case "completed": - return "default"; - case "in progress": - return "secondary"; - case "failed": - return "destructive"; - default: - return "outline"; - } -} - -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}
-      
-
- ); -} - -interface ExpandedSubtask { - [key: string]: boolean; -} - -function ExpandableSubtaskTile({ - subtask, - index, - isExpanded, - onToggle, -}: { - subtask: SubTask; - index: number; - isExpanded: boolean; - onToggle: () => void; -}) { - return ( - - {/* Tile Header */} - - - {/* Expanded Content */} - {isExpanded && ( -
- {/* 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 */} - {subtask.cloudLogsURL && ( -
-

- Cloud Logs -

- - {subtask.cloudLogsURL} - -
- )} - - {/* Updated */} -

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

-
- )} -
- ); -} - -export default function SubtaskDetails({ - selectedTaskId, - subtasks, - task, -}: SubtaskDetailsProps) { - const [expandedSubtasks, setExpandedSubtasks] = useState({}); - - const toggleSubtask = (subtaskId: string) => { - setExpandedSubtasks((prev) => ({ - ...prev, - [subtaskId]: !prev[subtaskId], - })); - }; - - return ( -
- {/* Task Header */} - {task && ( -
-
-
-

- {task.taskSource} -

- - {task.status} - -
-
-
-

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()} -

-
- )} -
-

Updated

-

- {new Date(task.updatedAt).toLocaleString()} -

-
-
-
-
- )} - - {/* Subtasks List */} - -
-
-

- Subtasks ({subtasks.length}) -

- - {subtasks.length === 0 && ( -
- No subtasks found -
- )} - -
- {subtasks.map((subtask, index) => ( - toggleSubtask(subtask.id)} - /> - ))} -
-
-
-
-
- ); -} diff --git a/src/app/iq/TaskList.tsx b/src/app/iq/TaskList.tsx deleted file mode 100644 index 8be56cc..0000000 --- a/src/app/iq/TaskList.tsx +++ /dev/null @@ -1,293 +0,0 @@ -"use client"; - -import { useState, useMemo } from "react"; -import { Task } from "./IQDashboard"; -import { Badge } from "@/app/shadcn_components/ui/badge"; -import { ScrollArea } from "@/app/shadcn_components/ui/scroll-area"; -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 TaskListProps { - tasks: Task[]; - 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 getStatusColor( - status: string -): "default" | "secondary" | "destructive" | "outline" { - switch (status.toLowerCase()) { - case "completed": - case "in progress": - return "default"; - case "failed": - return "destructive"; - default: - return "secondary"; - } -} - -export default function TaskList({ - tasks, - selectedTaskId, - onSelectTask, - loading, - loadingMore, - error, - total, - onLoadMore, - onRefresh, -}: TaskListProps) { - const [searchQuery, setSearchQuery] = useState(""); - const [statusFilter, setStatusFilter] = useState("all"); - const [serviceFilter, setServiceFilter] = useState("all"); - const [sortBy, setSortBy] = useState("recent"); - - // Get unique statuses and services for filter options - 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] - ); - - // Filter and sort tasks - const filteredTasks = useMemo(() => { - let result = tasks; - - // Status filter - if (statusFilter !== "all") { - result = result.filter((t) => t.status === statusFilter); - } - - // Service filter - if (serviceFilter !== "all") { - result = result.filter((t) => t.service === serviceFilter); - } - - // Search query - 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) - ); - } - - // Sort - 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 */} -
- {/* Search */} - setSearchQuery(e.target.value)} - className="text-sm" - /> - - {/* Sort */} - - - {/* Status Filter */} - {uniqueStatuses.length > 0 && ( - - )} - - {/* Service Filter */} - {uniqueServices.length > 0 && ( - - )} - - {/* Reset Filters */} - {(searchQuery || statusFilter !== "all" || serviceFilter !== "all") && ( - - )} -
- - {/* Content */} - - {error && ( -
- {error} -
- )} - - {loading && ( -
- Loading tasks... -
- )} - - {!loading && !error && tasks.length === 0 && ( -
- No tasks found -
- )} - -
- {filteredTasks.map((task) => ( - - ))} - - {/* Load More Button */} - {tasks.length < total && ( -
- -
- )} -
-
-
- ); -} diff --git a/src/app/iq/page.tsx b/src/app/iq/page.tsx deleted file mode 100644 index 4410678..0000000 --- a/src/app/iq/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import IQDashboard from "./IQDashboard"; - -export default function IQPage() { - return ; -} 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 169b965..0000000 --- a/src/app/portfolio/[slug]/(portfolio)/settings/PortfolioSettings.tsx +++ /dev/null @@ -1,548 +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"; -import OrganisationLinkCard from "./OrganisationLinkCard"; - -// 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, - isDomnaUser = false, -}: { - portfolioId: string; - portfolioSettingsData: PortfolioSettingsType; - isDomnaUser?: boolean; -}) { - // 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 -

-
- - - - - - -
-
-
-
- - {isDomnaUser && } -
- - - - - 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/page.tsx b/src/app/portfolio/[slug]/(portfolio)/settings/page.tsx index 4732c67..8d30a54 100644 --- a/src/app/portfolio/[slug]/(portfolio)/settings/page.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/settings/page.tsx @@ -1,32 +1,8 @@ -import { getServerSession } from "next-auth"; -import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions"; -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, session] = await Promise.all([ - getPortfolioSettings(portfolioId), - getServerSession(AuthOptions), - ]); - - const isDomnaUser = !!session?.user?.email?.endsWith("@domna.homes"); - - return ( - <> -
- -
- - ); +export default async function SettingsRootPage(props: { + params: Promise<{ slug: string }>; +}) { + const { slug } = await props.params; + redirect(`/portfolio/${slug}/settings/general`); } From 69123a944972bcd6fe41282f312aa8b8b26871f8 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sat, 4 Apr 2026 20:53:49 +0000 Subject: [PATCH 21/25] tweaking design --- .../[slug]/(portfolio)/settings/UsersPermissionsCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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"; From c8d1bfefa084d70beb12c42b2be4ffcaa18eca08 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sat, 4 Apr 2026 21:13:57 +0000 Subject: [PATCH 22/25] fxing scoll area margin problems --- .../portfolio/[portfolioId]/tasks/route.ts | 53 +++ .../settings/SettingsSidebarLink.tsx | 53 +++ .../settings/connected-organisation/page.tsx | 22 ++ .../settings/general/DangerZone.tsx | 151 ++++++++ .../settings/general/GeneralSettingsForm.tsx | 238 ++++++++++++ .../(portfolio)/settings/general/page.tsx | 20 ++ .../[slug]/(portfolio)/settings/layout.tsx | 58 +++ .../settings/logs/PortfolioLogs.tsx | 120 +++++++ .../settings/logs/PortfolioSubtaskDetails.tsx | 301 ++++++++++++++++ .../settings/logs/PortfolioTaskList.tsx | 339 ++++++++++++++++++ .../[slug]/(portfolio)/settings/logs/page.tsx | 22 ++ .../(portfolio)/settings/user-access/page.tsx | 13 + 12 files changed, 1390 insertions(+) create mode 100644 src/app/api/portfolio/[portfolioId]/tasks/route.ts create mode 100644 src/app/portfolio/[slug]/(portfolio)/settings/SettingsSidebarLink.tsx create mode 100644 src/app/portfolio/[slug]/(portfolio)/settings/connected-organisation/page.tsx create mode 100644 src/app/portfolio/[slug]/(portfolio)/settings/general/DangerZone.tsx create mode 100644 src/app/portfolio/[slug]/(portfolio)/settings/general/GeneralSettingsForm.tsx create mode 100644 src/app/portfolio/[slug]/(portfolio)/settings/general/page.tsx create mode 100644 src/app/portfolio/[slug]/(portfolio)/settings/layout.tsx create mode 100644 src/app/portfolio/[slug]/(portfolio)/settings/logs/PortfolioLogs.tsx create mode 100644 src/app/portfolio/[slug]/(portfolio)/settings/logs/PortfolioSubtaskDetails.tsx create mode 100644 src/app/portfolio/[slug]/(portfolio)/settings/logs/PortfolioTaskList.tsx create mode 100644 src/app/portfolio/[slug]/(portfolio)/settings/logs/page.tsx create mode 100644 src/app/portfolio/[slug]/(portfolio)/settings/user-access/page.tsx 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/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/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..1f6cdae --- /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..4596d5e --- /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..247713b --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/settings/logs/PortfolioSubtaskDetails.tsx @@ -0,0 +1,301 @@ +"use client"; + +import { useState } 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>({}); + + 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/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 ( +
+ +
+ ); +} From 295196fee376cf6be0a16bb47837fa2efaf08d73 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sat, 4 Apr 2026 23:18:51 +0000 Subject: [PATCH 23/25] polishing ui for logging and settings --- src/app/portfolio/[slug]/(portfolio)/settings/layout.tsx | 2 +- .../[slug]/(portfolio)/settings/logs/PortfolioLogs.tsx | 2 +- .../(portfolio)/settings/logs/PortfolioSubtaskDetails.tsx | 8 ++++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/app/portfolio/[slug]/(portfolio)/settings/layout.tsx b/src/app/portfolio/[slug]/(portfolio)/settings/layout.tsx index 1f6cdae..db1a38b 100644 --- a/src/app/portfolio/[slug]/(portfolio)/settings/layout.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/settings/layout.tsx @@ -15,7 +15,7 @@ export default async function SettingsLayout({ const isDomnaUser = !!session?.user?.email?.endsWith("@domna.homes"); return ( -
+