Merge pull request #27 from Hestia-Homes/main

Dev deployment with new pieces of ui for valuation improvement
This commit is contained in:
KhalimCK 2024-10-24 17:16:21 +01:00 committed by GitHub
commit d781671e97
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 477 additions and 19 deletions

View file

@ -29,6 +29,7 @@ export async function GET(
currentEpcRating: true,
currentSapPoints: true,
updatedAt: true,
currentValuation: true,
},
where: eq(property.id, BigInt(propertyId)),
with: {

View file

@ -26,19 +26,25 @@ export function BrandButton({
}: {
label: string;
onClick: Dispatch<SetStateAction<any>>;
backgroundColor: "brandblue" | "brandgold"; // Restrict backgroundColor to these two options
backgroundColor:
| "brandblue"
| "brandgold"
| "brandmidblue"
| "brandlightblue"; // Restrict backgroundColor to these two options
}) {
// Dictionary to map background colors to hover colors
const hoverColors = {
brandblue: "hover:bg-hoverblue",
brandgold: "hover:bg-hovergold",
brandmidblue: "hover:bg-hoverblue",
brandlightblue: "hover:bg-brandmidblue",
};
return (
<button
type="button"
className={`inline-flex justify-center rounded-md border border-transparent px-4 py-2 text-sm font-medium text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2
${backgroundColor === "brandblue" ? "bg-brandblue" : "bg-brandgold"}
bg-${backgroundColor}
${hoverColors[backgroundColor]}`}
onClick={onClick}
>

View file

@ -24,6 +24,7 @@ const TitleMap = {
internal_wall_insulation: "Internal Wall Insulation",
external_wall_insulation: "External Wall Insulation",
cavity_wall_insulation: "Cavity Wall Insulation",
extension_cavity_wall_insulation: "Extension Cavity Wall Insulation",
// Roof
loft_insulation: "Loft Insulation",
room_roof_insulation: "Room Roof Insulation",

View file

@ -3,9 +3,11 @@
import {
Recommendation,
RecommendationType,
Plan,
} from "@/app/db/schema/recommendations";
import RecommendationCard from "./RecommendationCard";
import WorksPackageCard from "./WorksPackageCard";
import ValuationImpactComponent from "./ValuationImpactComponent";
import { Separator } from "@/app/shadcn_components/ui/separator";
import { PropertyMeta } from "@/app/db/schema/property";
import { sapToEpc } from "@/app/utils";
@ -20,6 +22,7 @@ import {
interface RecommendationContainerProps {
recommendations: Recommendation[];
propertyMeta: PropertyMeta;
planMeta: Plan;
}
const typeToCategoryMap: { [key in RecommendationType]?: RecommendationType } =
@ -27,6 +30,7 @@ const typeToCategoryMap: { [key in RecommendationType]?: RecommendationType } =
internal_wall_insulation: "wall_insulation",
external_wall_insulation: "wall_insulation",
cavity_wall_insulation: "wall_insulation",
extension_cavity_wall_insulation: "extension_cavity_wall_insulation",
loft_insulation: "roof_insulation",
room_roof_insulation: "roof_insulation",
flat_roof_insulation: "roof_insulation",
@ -49,6 +53,7 @@ const emptyImpactState = {
export default function RecommendationContainer({
recommendations,
propertyMeta,
planMeta,
}: RecommendationContainerProps) {
const categorizedRecommendations = recommendations.reduce((acc, curr) => {
const typeKey = curr.type as RecommendationType;
@ -336,6 +341,12 @@ export default function RecommendationContainer({
totalEnergyCostSavings={totalEnergyCostSavings}
totalKwhSavings={totalKwhSavings}
/>
<ValuationImpactComponent
currentValuation={propertyMeta.currentValuation}
valuationIncreaseLowerBound={planMeta.valuationIncreaseLowerBound}
valuationIncreaseUpperBound={planMeta.valuationIncreaseUpperBound}
/>
</div>
<Separator className="mb-4 bg-brandblue" />

View file

@ -0,0 +1,62 @@
"use client";
import { BrandButton } from "../Buttons";
import { useState } from "react";
export default function ValuationImpactComponent({
currentValuation,
valuationIncreaseLowerBound,
valuationIncreaseUpperBound,
}: {
currentValuation: number | null;
valuationIncreaseLowerBound: number | null;
valuationIncreaseUpperBound: number | null;
}) {
const [fundingModalIsOpen, setFundingModalIsOpen] = useState(false);
// If we have no current valuation, we return no component
if (!currentValuation) {
return <></>;
}
const lowerBoundValuation =
currentValuation + (valuationIncreaseLowerBound ?? 0);
const upperBoundValuation =
currentValuation + (valuationIncreaseUpperBound ?? 0);
function openFundingModal() {
setFundingModalIsOpen(true);
}
return (
<div className="col-span-1 md:col-span-2 lg:col-span-3 w-full p-4 bg-brandblue rounded-lg shadow-md grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="flex flex-col items-center text-center">
<span className="text-gray-100 text-lg">Current Value</span>
<span className="text-3xl font-bold text-brandgold mt-1">
£{currentValuation.toLocaleString()}
</span>
</div>
<div className="flex flex-col items-center text-center">
<span className="text-gray-100 text-lg">After Retrofit Valuation</span>
<div className="text-2xl text-brandgold mt-1">
£{lowerBoundValuation.toLocaleString()} - £
{upperBoundValuation.toLocaleString()}
</div>
<span className="text-sm text-gray-100 mt-1">
Estimated improvement: £
{valuationIncreaseLowerBound?.toLocaleString()} - £
{valuationIncreaseUpperBound?.toLocaleString()}
</span>
</div>
<div className="flex flex-col items-center text-center">
<span className="text-gray-100 text-lg mb-2">Funding Options</span>
<BrandButton
label="See More"
onClick={openFundingModal}
backgroundColor="brandlightblue"
/>
</div>
</div>
);
}

View file

@ -2,7 +2,6 @@
import {
Cog6ToothIcon,
CalculatorIcon,
BuildingOfficeIcon,
ChartBarIcon,
} from "@heroicons/react/24/outline";
@ -29,11 +28,7 @@ export function Toolbar({ portfolioId }: ToolbarProps) {
const router = useRouter();
function handleClickSettings() {
console.log("Settings were clicked, implement me");
}
function handleClickPortfolioPlan() {
router.push(`/portfolio/${portfolioId}/plan`);
router.push(`/portfolio/${portfolioId}/settings`);
}
function handleClickPortfolio() {
@ -65,14 +60,6 @@ export function Toolbar({ portfolioId }: ToolbarProps) {
Summary
</NavigationMenuItem>
{/* <NavigationMenuItem
className={navigationMenuTriggerStyle() + " ml-3 mr-2"}
onClick={handleClickPortfolioPlan}
>
<CalculatorIcon className="h-4 w-4 mr-2" />
Portfolio Plan
</NavigationMenuItem> */}
<NavigationMenuItem
className={navigationMenuTriggerStyle() + " ml-3 mr-2"}
onClick={handleClickSettings}

View file

@ -32,6 +32,7 @@ export interface PropertyMeta {
currentEpcRating: string;
currentSapPoints: number;
updatedAt: string;
currentValuation: number | null;
detailsEpc: {
currentEnergyDemand: number | null;
co2Emissions: number | null;

View file

@ -189,7 +189,8 @@ export type RecommendationType =
| "cylinder_thermostat"
| "trickle_vents"
| "mixed_glazing"
| "draught_proofing";
| "draught_proofing"
| "extension_cavity_wall_insulation";
export type UnnestedRecommendation = {
quantity: number;

View file

@ -0,0 +1,283 @@
"use client";
import { useState } from "react";
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 { PortfolioStatus as PortfolioStatusOptions } from "@/app/db/schema/portfolio";
import { PortfolioGoal as PortfolioGoalOptions } from "@/app/db/schema/portfolio";
// dropdown selection component for both goal and status
export function SettingsDropdown({
startingValue,
options,
setOption,
}: {
startingValue: string;
options: string[];
setOption: (option: string) => void;
}) {
function handleValueChange(newValue: string) {
setOption(newValue);
}
return (
<Select onValueChange={(newValue) => handleValueChange(newValue)}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder={startingValue} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{options.map((option, idx) => (
<SelectItem value={option} key={idx}>
{option}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
);
}
export default function PortfolioSettings({
portfolioId,
portfolioSettingsData,
}: {
portfolioId: string;
portfolioSettingsData: PortfolioSettingsType;
}) {
// Running in the client
const router = useRouter();
// Set up state for portfolioName, portfolioBudget, portfolioGoal and portfolioStatus
// Syntax const [variable, function whos only job is to update the value of variable] = useState(initial value)
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("");
function handleOpenDeleteModal() {
setDeleteConfirmationByName("");
setIsDeleteModalOpen(true);
}
function handleDeleteConfirmation() {
console.log("we be deletin stuff");
// if (deleteConfirmationByName === portfolioName) {
// //apiDeletePortfolio(portfolioId)
// router.refresh();
// setIsDeleteModalOpen(false);
// } else {
// // Error if the names don't match
// console.log("Portfolio name does not match");
// }
router.push("/home");
}
// RENAMING FUNCTIONS
// Change NAME functionality - changing state
function handlePortfolioNameChange(e: React.ChangeEvent<HTMLInputElement>) {
setPortfolioName(e.target.value);
}
// The onClick function called to update the NAME in the DB
function handleRenameDb() {
// apiRanameFunction(portfolioSettingsData.name)
// Update portfolioName
router.refresh();
}
// BUDGET CHANGING FUNCTIONS
// Change BUDGET functionality - changing state
function handlePortfolioBudgetUpdate(e: React.ChangeEvent<HTMLInputElement>) {
setPortfolioBudget(Number(e.target.value));
}
// The onClick function called to update the BUDGET in the DB
function handleBudgetUpdateDb() {
// apiBudgetChangeFunction(portfolioSettingsData.budget)
// Update portfolioBudget
router.refresh();
}
// CHANGING GOAL AND STATUS FUNCTIONALITY
// The onClick function called to update the GOAL in the DB
function handleGoalUpdateDb() {
// apiGoalChangeFunction(portfolioSettingsData.goal)
// Update portfolioGoal
router.refresh();
}
// The onClick function called to update the BUDGET in the DB
function handleStatusUpdateDb() {
// apiStatusChangeFunction(portfolioSettingsData.status)
// Update portfolioStatus
router.refresh();
}
// HTML to render the page
return (
<>
<div className="p-4 mt-5 flex justify-center max-w-8xl w-8xl pt-5 bg-gray-50 rounded-lg text-brandblue">
<div className="grid grid-cols-[max-content_1fr_min-content] gap-x-[5px] gap-y-4">
{/* Row 1: Name */}
<div className="flex items-center">
<span>Name:</span>
</div>
<div className="flex items-center">
<Input value={portfolioName} onChange={handlePortfolioNameChange} />
</div>
<div className="flex items-center">
<Button className="w-full" onClick={handleRenameDb}>
Rename
</Button>
</div>
{/* Row 2: Budget */}
<div className="flex items-center">
<span>Budget:</span>
</div>
<div className="flex items-center max-w-8xl">
<Input
type="number"
value={portfolioBudget ?? undefined}
onChange={handlePortfolioBudgetUpdate}
onKeyDown={(e) => handleNumericKeyDown(e)}
/>
</div>
<div className="flex items-center">
<Button className="w-full" onClick={handleBudgetUpdateDb}>
Update
</Button>
</div>
{/* Row 3: Goal */}
<div className="flex items-center">
<span>Goal:</span>
</div>
<div className="flex items-center">
<SettingsDropdown
startingValue={portfolioGoal}
options={PortfolioGoalOptions}
setOption={setPortfolioGoal}
/>
</div>
<Button className="w-full" onClick={handleGoalUpdateDb}>
Update
</Button>
{/* Row 4: Status */}
<div className="flex items-center">
<span>Status:</span>
</div>
<div className="flex items-center">
<SettingsDropdown
startingValue={portfolioStatus}
options={PortfolioStatusOptions}
setOption={setPortfolioStatus}
/>
</div>
<Button className="w-full" onClick={handleStatusUpdateDb}>
Update
</Button>
<div className="col-span-3"> Portfolio Name: {portfolioName}</div>
<div className="col-span-3"> Portfolio Budget: {portfolioBudget}</div>
<div className="col-span-3"> Goal value: {portfolioGoal}</div>
<div> Status value: {portfolioStatus}</div>
{/* Row 5: Delete */}
<div className="col-span-2"></div>
<div className="flex items-center">
<Button
className="max-width: 100% bg-red-700"
onClick={handleOpenDeleteModal}
>
Delete Portfolio
</Button>
</div>
{/* Delete portfolio modal */}
<Dialog open={isDeleteModalOpen} onOpenChange={setIsDeleteModalOpen}>
<DialogContent>
<DialogTitle>Are you sure?</DialogTitle>
<p>
To confirm, please type the name of the portfolio (
<strong>{portfolioSettingsData.name}</strong>)
</p>
<input
type="text"
value={deleteConfirmationByName}
onChange={(e) => setDeleteConfirmationByName(e.target.value)}
placeholder="Type portfolio name"
/>
<DialogFooter>
<Button
className="bg-green-600"
onClick={() => setIsDeleteModalOpen(false)}
>
Cancel
</Button>
<Button
className="bg-red-700"
onClick={handleDeleteConfirmation}
disabled={
deleteConfirmationByName !== portfolioSettingsData.name
}
>
Delete
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</div>
</>
);
}

View file

@ -0,0 +1,38 @@
import { getPortfolioSettings } from "../../utils";
import PortfolioSettings from "./PortfolioSettings";
export default async function PortfolioSettingsPage({
params,
}: {
params: { slug: string };
}) {
const portfolioId = params.slug;
// fetch data securely on the server
// Stef's page!!!!
// 1) Rename
// 2) Update budget, status, goal
// 3) Delete - much harder
// fetch data in the server - name, budget, goal,
// pass it to a client component to render and take user input
const portfolioSettingsData = await getPortfolioSettings(portfolioId);
// Get goal options and status options by importing something like this:
// import {
// PortfolioStatus,
// PortfolioGoal,
// } from "./../../db/schema/portfolio";
// and then pass them to the portfoliosettings component
return (
<>
<div className="flex justify-center max-w-8xl w-8xl">
<PortfolioSettings
portfolioId={portfolioId}
portfolioSettingsData={portfolioSettingsData}
/>
</div>
</>
);
}

View file

@ -1,5 +1,5 @@
import RecommendationContainer from "@/app/components/building-passport/RecommendationContainer";
import { getPropertyMeta, getRecommendations } from "../../utils";
import { getPropertyMeta, getRecommendations, getPlanMeta } from "../../utils";
export default async function Recommendations({
params,
@ -8,6 +8,7 @@ export default async function Recommendations({
}) {
const propertyMeta = await getPropertyMeta(params.propertyId);
const recommendations = await getRecommendations(params.planId);
const planMeta = await getPlanMeta(params.planId);
return (
<div className="leading-loose tracking-wider">
@ -15,6 +16,7 @@ export default async function Recommendations({
<RecommendationContainer
recommendations={recommendations}
propertyMeta={propertyMeta}
planMeta={planMeta}
/>
</div>
);

View file

@ -1,6 +1,8 @@
import {
Recommendation,
planRecommendations,
plan,
Plan,
} from "@/app/db/schema/recommendations";
import { db } from "@/app/db/db";
import {
@ -14,7 +16,6 @@ import {
NonIntrusiveSurveyData,
nonInstrusiveSurvey,
} from "@/app/db/schema/property";
import { plan, Plan } from "@/app/db/schema/recommendations";
import { getRating } from "@/app/utils";
import { eq, desc } from "drizzle-orm";
import {
@ -85,6 +86,18 @@ export async function getRecommendations(
return recommendations;
}
export async function getPlanMeta(planId: string): Promise<Plan> {
const data = await db.query.plan.findFirst({
where: eq(plan.id, BigInt(planId)),
});
if (!data) {
throw new Error("Network response was not ok");
}
return data;
}
type PlanRelation = Plan & {
planRecommendations: {
recommendation: {

View file

@ -1,3 +1,7 @@
import type {
PortfolioStatus,
PortfolioGoal,
} from "./../../db/schema/portfolio";
import { formatNumber } from "@/app/utils";
import { and, eq, inArray } from "drizzle-orm";
import { db } from "@/app/db/db";
@ -15,6 +19,38 @@ import {
ScenarioSelect,
} from "@/app/db/schema/recommendations";
export interface PortfolioSettingsType {
name: string;
budget: number | null;
goal: (typeof PortfolioGoal)[number];
status: (typeof PortfolioStatus)[number];
}
export async function getPortfolioSettings(
portfolioId: string
): Promise<PortfolioSettingsType> {
//name, budget, goal, status
const data = await db
.select({
name: portfolio.name,
budget: portfolio.budget,
goal: portfolio.goal,
status: portfolio.status,
})
.from(portfolio)
.where(eq(portfolio.id, BigInt(portfolioId)));
if (data.length === 0) {
throw new Error("Portfolio not found");
}
if (data.length > 1) {
throw new Error("More than one portfolio found");
}
return data[0];
}
export async function getPortfolio(portfolioId: string): Promise<Portfolio> {
const data = await db
.select()

View file

@ -1,4 +1,16 @@
import { Rating } from "./db/schema/property";
import { KeyboardEvent} from "react";
export function handleNumericKeyDown(event: KeyboardEvent<HTMLInputElement>) {
/**
* Allowing: Integers | Backspace | Tab | Delete | Left & Right arrow keys
**/
const regex = new RegExp(/(^\d*$)|(Backspace|Tab|Delete|ArrowLeft|ArrowRight|ArrowUp|ArrowDown)/);
return !event.key.match(regex) && event.preventDefault();
}
export function convertDaysToWorkingWeeks(days: number | null) {
if (days === null) {
@ -149,3 +161,5 @@ export function roundToDecimalPlaces(
const factor = 10 ** decimalPlaces;
return Math.round(number * factor) / factor;
}

View file

@ -104,6 +104,7 @@ module.exports = {
hovergold: "#c79d12",
brandbrown: "#3d1e05",
brandmidblue: "#3943b7",
brandlightblue: "#00a9f4",
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
@ -145,6 +146,7 @@ module.exports = {
hovertan: "#947750",
brandbrown: "#3d1e05",
brandmidblue: "#3943b7",
brandlightblue: "#00a9f4",
},
borderRadius: {
lg: `var(--radius)`,