diff --git a/README.md b/README.md
index b58ac46f..e720d029 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,14 @@ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next
## Getting Started
+When first getting set up you'll firstly want to install the existing dependencies. To do this, simply run
+
+```bash
+npm install
+# or
+yarn install
+```
+
First, run the development server:
```bash
diff --git a/public/.well-known/microsoft-identity-association.json b/public/.well-known/microsoft-identity-association.json
new file mode 100644
index 00000000..975f09e1
--- /dev/null
+++ b/public/.well-known/microsoft-identity-association.json
@@ -0,0 +1,7 @@
+{
+ "associatedApplications": [
+ {
+ "applicationId": "069e75ee-ba54-45ff-ba77-a06f29c0e21c"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts
index bd4ae388..8cc60197 100644
--- a/src/app/api/auth/[...nextauth]/route.ts
+++ b/src/app/api/auth/[...nextauth]/route.ts
@@ -1,15 +1,18 @@
import NextAuth, { NextAuthOptions } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
-import AzureADProvider from "next-auth/providers/azure-ad";
+import AzureADB2CProvider from "next-auth/providers/azure-ad-b2c";
+import CredentialsProvider from "next-auth/providers/credentials";
+
import { db } from "@/app/db/db";
import { user as userTable, User } from "@/app/db/schema/users";
import { eq } from "drizzle-orm";
const { GOOGLE_CLIENT_ID = "", GOOGLE_CLIENT_SECRET = "" } = process.env;
const {
- AZURE_AD_CLIENT_ID = "",
- AZURE_AD_CLIENT_SECRET = "",
- AZURE_AD_TENANT_ID = "",
+ AZURE_AD_B2C_TENANT_NAME = "",
+ AZURE_AD_B2C_CLIENT_ID = "",
+ AZURE_AD_B2C_CLIENT_SECRET = "",
+ AZURE_AD_B2C_PRIMARY_USER_FLOW = "",
} = process.env;
type OauthProvider = "google";
@@ -32,10 +35,50 @@ export const AuthOptions: NextAuthOptions = {
},
},
}),
- AzureADProvider({
- clientId: AZURE_AD_CLIENT_ID,
- clientSecret: AZURE_AD_CLIENT_SECRET,
- tenantId: AZURE_AD_TENANT_ID,
+ AzureADB2CProvider({
+ tenantId: AZURE_AD_B2C_TENANT_NAME,
+ clientId: AZURE_AD_B2C_CLIENT_ID,
+ clientSecret: AZURE_AD_B2C_CLIENT_SECRET,
+ primaryUserFlow: AZURE_AD_B2C_PRIMARY_USER_FLOW,
+ authorization: {
+ params: {
+ scope: "openid profile offline_access",
+ prompt: "login",
+ },
+ },
+ }),
+ CredentialsProvider({
+ name: "Email Login",
+ credentials: {
+ email: {
+ label: "Email",
+ type: "email",
+ },
+ },
+ async authorize(credentials, req) {
+ if (!credentials || !credentials.email) {
+ throw new Error("Email is required");
+ }
+
+ const { email } = credentials;
+
+ // Query the database to find the user by email
+ const dbUser = await db
+ .select()
+ .from(userTable)
+ .where(eq(userTable.email, email));
+
+ // If the email exists, return the user object (no password check)
+ if (dbUser.length === 1) {
+ return {
+ id: dbUser[0].id.toString(), // Convert bigint to string to avoid serialization issues
+ email: dbUser[0].email,
+ dbId: dbUser[0].id.toString(), // Ensure dbId is added and is a string
+ };
+ }
+
+ return null;
+ },
}),
],
pages: {
diff --git a/src/app/api/energy-assessment-documents/route.ts b/src/app/api/energy-assessment-documents/route.ts
new file mode 100644
index 00000000..86512b72
--- /dev/null
+++ b/src/app/api/energy-assessment-documents/route.ts
@@ -0,0 +1,81 @@
+// pages/api/get-presigned-url.ts
+import S3 from "aws-sdk/clients/s3";
+import STS from "aws-sdk/clients/sts"; // Import STS for temporary credentials
+import { NextRequest, NextResponse } from "next/server";
+import { z } from "zod";
+
+// Validate the input
+const PresignedUrlBodySchema = z.object({
+ fileKey: z.string(),
+});
+
+// Function to get temporary credentials using GetSessionToken
+async function getTemporaryCredentials() {
+ const sts = new STS({
+ accessKeyId: process.env.RETROFIT_ENERGY_ASSESSMENTS_AWS_ACCESS_KEY, // Your permanent access key
+ secretAccessKey: process.env.ENERGY_ASSESSMENTS_AWS_SECRET, // Your permanent secret access key
+ region: process.env.PRESIGN_AWS_REGION,
+ });
+
+ try {
+ // Request temporary credentials with GetSessionToken
+ const data = await sts.getSessionToken({ DurationSeconds: 900 }).promise(); // Token valid for 15 minutes
+
+ // Check if credentials are present
+ if (!data.Credentials) {
+ throw new Error("Failed to retrieve temporary credentials");
+ }
+
+ return data.Credentials;
+ } catch (error) {
+ console.error("Error fetching temporary credentials:", error);
+ throw error;
+ }
+}
+
+// API handler
+export async function POST(request: NextRequest) {
+ const body = await request.json();
+ let validatedBody;
+
+ try {
+ validatedBody = PresignedUrlBodySchema.parse(body);
+ } catch (error) {
+ console.error("Invalid input: ", error);
+ return new NextResponse(JSON.stringify({ msg: "Invalid input" }), {
+ status: 400,
+ });
+ }
+
+ try {
+ // Get temporary credentials using GetSessionToken
+ const credentials = await getTemporaryCredentials();
+
+ // Initialize S3 with temporary credentials
+ const s3 = new S3({
+ signatureVersion: "v4",
+ region: process.env.PRESIGN_AWS_REGION,
+ accessKeyId: credentials.AccessKeyId,
+ secretAccessKey: credentials.SecretAccessKey,
+ sessionToken: credentials.SessionToken, // Include session token
+ });
+
+ const { fileKey } = validatedBody;
+
+ // Generate presigned URL valid for 5 minutes
+ const preSignedUrl = await s3.getSignedUrl("getObject", {
+ Bucket: process.env.RETROFIT_ENERGY_ASSESSMENTS_BUCKET,
+ Key: fileKey,
+ Expires: 5 * 60, // URL expiration in seconds
+ });
+
+ return new NextResponse(JSON.stringify({ url: preSignedUrl }), {
+ status: 200,
+ });
+ } catch (error) {
+ console.error("Error generating presigned URL:", error);
+ return new NextResponse(JSON.stringify({ msg: "Internal server error" }), {
+ status: 500,
+ });
+ }
+}
diff --git a/src/app/beta/page.tsx b/src/app/beta/page.tsx
index ed4ef26e..14afc451 100644
--- a/src/app/beta/page.tsx
+++ b/src/app/beta/page.tsx
@@ -1,3 +1,3 @@
export default function Beta() {
- return
This application is not ready for general usage
;
+ return You do not have access to this application currently
;
}
diff --git a/src/app/components/Buttons.tsx b/src/app/components/Buttons.tsx
index 7da4ab2e..9d2d87d7 100644
--- a/src/app/components/Buttons.tsx
+++ b/src/app/components/Buttons.tsx
@@ -18,3 +18,31 @@ export function TanButton({
);
}
+
+export function BrandButton({
+ label,
+ onClick,
+ backgroundColor,
+}: {
+ label: string;
+ onClick: Dispatch>;
+ backgroundColor: "brandblue" | "brandgold"; // Restrict backgroundColor to these two options
+}) {
+ // Dictionary to map background colors to hover colors
+ const hoverColors = {
+ brandblue: "hover:bg-hoverblue",
+ brandgold: "hover:bg-hovergold",
+ };
+
+ return (
+
+ );
+}
diff --git a/src/app/components/StatusBadge.tsx b/src/app/components/StatusBadge.tsx
index b9aae3f9..de90e3a6 100644
--- a/src/app/components/StatusBadge.tsx
+++ b/src/app/components/StatusBadge.tsx
@@ -20,7 +20,11 @@ export default function StatusBadge({
return (
- {statusConfig.text}
+
+ {statusConfig.text}
+
@@ -60,6 +64,12 @@ const statusColor: {
hoverText: "This portfolio is currently in the assessment stage",
propertyHoverText: "This property is currently in the assessment stage",
},
+ survey: {
+ class: "bg-brandblue hover:bg-hoverblue",
+ text: "survey",
+ hoverText: "This portfolio is currently in the survey stage",
+ propertyHoverText: "This property is currently in the survey stage",
+ },
tendering: {
class: "bg-emerald-500 hover:bg-emerald-500",
text: "Tendering",
diff --git a/src/app/components/building-passport/EpcCard.tsx b/src/app/components/building-passport/EpcCard.tsx
index 8b9ca1fe..7f4df5c9 100644
--- a/src/app/components/building-passport/EpcCard.tsx
+++ b/src/app/components/building-passport/EpcCard.tsx
@@ -6,12 +6,14 @@ export default function EpcCard({
expected = false,
kwh = null,
carbon = null,
+ sap = null,
}: {
epcRating: string;
fullMargin: boolean;
expected?: boolean;
kwh?: number | null;
carbon?: number | null;
+ sap?: string | null;
}) {
let marginClass = "";
if (fullMargin) {
@@ -30,28 +32,35 @@ export default function EpcCard({
return (
{title}
-
{epcRating}
+
+ {/* EPC Rating and SAP Rating */}
+
+ {/* EPC Rating */}
+
{epcRating}
+
+ {/* SAP Rating (only if sap is provided) */}
+ {sap !== null && (
+
{sap}
+ )}
+
{(kwh || carbon) && (
{kwh && (
- {" "}
- {/* Added vertical padding to each row */}
| {kwh.toFixed(0)} kWh |
)}
{carbon && (
- {" "}
|
- {carbon}t CO 2
+ {carbon}t CO2
|
)}
diff --git a/src/app/components/building-passport/RecommendationCard.tsx b/src/app/components/building-passport/RecommendationCard.tsx
index cace8863..f53826c2 100644
--- a/src/app/components/building-passport/RecommendationCard.tsx
+++ b/src/app/components/building-passport/RecommendationCard.tsx
@@ -17,6 +17,7 @@ const alreadyInstalledStyling =
const TitleMap = {
mechanical_ventilation: "Mechanical Ventilation",
+ trickle_vents: "Trickle Vents",
sealing_open_fireplace: "Sealing Open Fireplace",
low_energy_lighting: "Low Energy Lighting",
// Walls
@@ -33,6 +34,7 @@ const TitleMap = {
exposed_floor_insulation: "Exposed Floor Insulation",
// Windows
windows_glazing: "Window Glazing",
+ mixed_glazing: "Mixed - Secondary and Double Glazing",
// Solar pv
solar_pv: "Solar Photovoltaic Panels System",
// Heating
@@ -47,6 +49,8 @@ const TitleMap = {
roof_insulation: "Roof Insulation",
// Cylinder thermostat
cylinder_thermostat: "Cylinder Thermostat",
+ // Draught proofing
+ draught_proofing: "Draught Proofing",
};
type RecommendationCardProps = {
diff --git a/src/app/components/building-passport/RecommendationContainer.tsx b/src/app/components/building-passport/RecommendationContainer.tsx
index 951e6af4..50cf27b5 100644
--- a/src/app/components/building-passport/RecommendationContainer.tsx
+++ b/src/app/components/building-passport/RecommendationContainer.tsx
@@ -127,6 +127,21 @@ export default function RecommendationContainer({
(rec: Recommendation) => rec.default
) || emptyImpactState;
+ const defaultTrickleVentsRecommendations =
+ categorizedRecommendations.trickle_vents?.find(
+ (rec: Recommendation) => rec.default
+ ) || emptyImpactState;
+
+ const defaultMixedGlazingRecommendations =
+ categorizedRecommendations.mixed_glazing?.find(
+ (rec: Recommendation) => rec.default
+ ) || emptyImpactState;
+
+ const defaultDraughtProofingRecommendations =
+ categorizedRecommendations.draught_proofing?.find(
+ (rec: Recommendation) => rec.default
+ ) || emptyImpactState;
+
const [costMap, setCostMap] = useState({
wall_insulation: defaultWallsRecommendations.estimatedCost || 0,
floor_insulation: defaultFloorRecommendations.estimatedCost || 0,
@@ -145,6 +160,9 @@ export default function RecommendationContainer({
defaultSecondaryHeatingRecommendations.estimatedCost || 0,
cylinder_thermostat:
defaultCylinderThermostatRecommendations.estimatedCost || 0,
+ trickle_vents: defaultTrickleVentsRecommendations.estimatedCost || 0,
+ mixed_glazing: defaultMixedGlazingRecommendations.estimatedCost || 0,
+ draught_proofing: defaultDraughtProofingRecommendations.estimatedCost || 0,
});
const [sapMap, setSapMap] = useState({
@@ -163,6 +181,9 @@ export default function RecommendationContainer({
secondary_heating: defaultSecondaryHeatingRecommendations.sapPoints || 0,
cylinder_thermostat:
defaultCylinderThermostatRecommendations.sapPoints || 0,
+ trickle_vents: defaultTrickleVentsRecommendations.sapPoints || 0,
+ mixed_glazing: defaultMixedGlazingRecommendations.sapPoints || 0,
+ draught_proofing: defaultDraughtProofingRecommendations.sapPoints || 0,
});
const [labourDaysMap, setLabourDaysMap] = useState({
@@ -181,6 +202,9 @@ export default function RecommendationContainer({
secondary_heating: defaultSecondaryHeatingRecommendations.labourDays || 0,
cylinder_thermostat:
defaultCylinderThermostatRecommendations.labourDays || 0,
+ trickle_vents: defaultTrickleVentsRecommendations.labourDays || 0,
+ mixed_glazing: defaultMixedGlazingRecommendations.labourDays || 0,
+ draught_proofing: defaultDraughtProofingRecommendations.labourDays || 0,
});
const [co2SavingsMap, setCo2SavingsMap] = useState({
@@ -204,6 +228,10 @@ export default function RecommendationContainer({
defaultSecondaryHeatingRecommendations.co2EquivalentSavings || 0,
cylinder_thermostat:
defaultCylinderThermostatRecommendations.co2EquivalentSavings || 0,
+ trickle_vents: defaultTrickleVentsRecommendations.co2EquivalentSavings || 0,
+ mixed_glazing: defaultMixedGlazingRecommendations.co2EquivalentSavings || 0,
+ draught_proofing:
+ defaultDraughtProofingRecommendations.co2EquivalentSavings || 0,
});
const [energyCostSavingsMap, setEnergyCostSavingsMap] =
@@ -228,6 +256,10 @@ export default function RecommendationContainer({
defaultSecondaryHeatingRecommendations.energyCostSavings || 0,
cylinder_thermostat:
defaultCylinderThermostatRecommendations.energyCostSavings || 0,
+ trickle_vents: defaultTrickleVentsRecommendations.energyCostSavings || 0,
+ mixed_glazing: defaultMixedGlazingRecommendations.energyCostSavings || 0,
+ draught_proofing:
+ defaultDraughtProofingRecommendations.energyCostSavings || 0,
});
const [kwhSavingsMap, setKwhSavingsMap] = useState({
@@ -246,6 +278,9 @@ export default function RecommendationContainer({
secondary_heating: defaultSecondaryHeatingRecommendations.kwhSavings || 0,
cylinder_thermostat:
defaultCylinderThermostatRecommendations.kwhSavings || 0,
+ trickle_vents: defaultTrickleVentsRecommendations.kwhSavings || 0,
+ mixed_glazing: defaultMixedGlazingRecommendations.kwhSavings || 0,
+ draught_proofing: defaultDraughtProofingRecommendations.kwhSavings || 0,
});
const [totalEstimatedCost, setTotalEstimatedCost] = useState(
diff --git a/src/app/components/building-passport/Toolbar.tsx b/src/app/components/building-passport/Toolbar.tsx
index 8f57d216..58866edf 100644
--- a/src/app/components/building-passport/Toolbar.tsx
+++ b/src/app/components/building-passport/Toolbar.tsx
@@ -39,6 +39,16 @@ export function Toolbar({ propertyId, portfolioId }: ToolbarProps) {
);
+ const energyAssessmentsReportButton = (
+
+
+ Energy Assessment
+
+ );
+
const solarAnalysisButton = (
-
- Plan optimiser
- */}
+ {energyAssessmentsReportButton}
void }) => {
+ e.preventDefault();
+ const res = await signIn("credentials", {
+ email,
+ });
+
+ if (res?.error) {
+ setError("You are not a valid user.");
+ } else {
+ console.log("Login successful");
+ }
+ };
+
+ const handleEmailChange = (e: {
+ target: { value: SetStateAction };
+ }) => {
+ setEmail(e.target.value);
+ if (error) {
+ setError(undefined); // Clear the error when the user starts typing
+ }
+ };
+
+ // Sync initial error state with server-side error prop
+ useEffect(() => {
+ setError(initialError);
+ }, [initialError]);
+
+ return (
+
+ );
+}
diff --git a/src/app/components/signin/MicrosoftSignInButton.jsx b/src/app/components/signin/MicrosoftSignInButton.jsx
index bb881fd3..f53399f4 100644
--- a/src/app/components/signin/MicrosoftSignInButton.jsx
+++ b/src/app/components/signin/MicrosoftSignInButton.jsx
@@ -13,7 +13,7 @@ const MicrosoftSignInButton = () => {