mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-30 12:55:02 +00:00
Working on remote assessment modal ui
This commit is contained in:
parent
d4bf6fad9a
commit
b99069a7d4
9 changed files with 3384 additions and 230 deletions
|
|
@ -17,16 +17,18 @@ import UploadCsvModal from "@/app/portfolio/[slug]/components/UploadCsvModal";
|
|||
import RemoteAssessmentModal from "@/app/portfolio/[slug]/components/RemoteAssessmentModal";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ScenarioSelect } from "@/app/db/schema/recommendations";
|
||||
|
||||
interface ToolbarProps {
|
||||
portfolioId: string;
|
||||
scenarios: ScenarioSelect[];
|
||||
}
|
||||
|
||||
const navigationMenuTriggerStyle = cva(
|
||||
"bg-gray-50 cursor-pointer group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-gray-200 hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-gray-200 "
|
||||
);
|
||||
|
||||
export function Toolbar({ portfolioId }: ToolbarProps) {
|
||||
export function Toolbar({ portfolioId, scenarios }: ToolbarProps) {
|
||||
const router = useRouter();
|
||||
|
||||
function handleClickSettings() {
|
||||
|
|
@ -94,6 +96,7 @@ export function Toolbar({ portfolioId }: ToolbarProps) {
|
|||
isOpen={isRemoteAssessmentOpen}
|
||||
setIsOpen={setIsRemoteAssessmentOpen}
|
||||
portfolioId={portfolioId}
|
||||
scenarios={scenarios}
|
||||
/>
|
||||
<UploadCsvModal
|
||||
isOpen={modalIsOpen}
|
||||
|
|
|
|||
1
src/app/db/migrations/0100_wakeful_doctor_doom.sql
Normal file
1
src/app/db/migrations/0100_wakeful_doctor_doom.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "scenario" ADD COLUMN "goal_value" text;
|
||||
2988
src/app/db/migrations/meta/0100_snapshot.json
Normal file
2988
src/app/db/migrations/meta/0100_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -701,6 +701,13 @@
|
|||
"when": 1742138715788,
|
||||
"tag": "0099_faulty_nicolaos",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 100,
|
||||
"version": "5",
|
||||
"when": 1749810028140,
|
||||
"tag": "0100_wakeful_doctor_doom",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -13,7 +13,6 @@ import {
|
|||
} from "drizzle-orm/pg-core";
|
||||
import { Material, material } from "./materials";
|
||||
import { InferModel } from "drizzle-orm";
|
||||
import { ar } from "drizzle-orm/column.d-b7dc3bdb";
|
||||
|
||||
export const recommendation = pgTable("recommendation", {
|
||||
id: bigserial("id", { mode: "bigint" }).primaryKey(),
|
||||
|
|
@ -108,6 +107,7 @@ export const scenario = pgTable("scenario", {
|
|||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
housingType: housingTypeEnum("housing_type").notNull(),
|
||||
goal: goalEnum("goal").notNull(),
|
||||
goalValue: text("goal_value"),
|
||||
triggerFilePath: text("trigger_file_path"),
|
||||
alreadyInstalledFilePath: text("already_installed_file_path"),
|
||||
patchesFilePath: text("patches_file_path"),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Toolbar } from "@/app/components/portfolio/Toolbar";
|
||||
import { getPortfolio } from "../utils";
|
||||
import { getPortfolio, getPortfolioScenarios } from "../utils";
|
||||
|
||||
export default async function PortfolioLayout({
|
||||
children, // will be a page or nested layout
|
||||
|
|
@ -10,6 +10,8 @@ export default async function PortfolioLayout({
|
|||
}) {
|
||||
const portfolioId = params.slug;
|
||||
const { name: portfolioName } = await getPortfolio(portfolioId);
|
||||
// We retrieve the scenarios associated with the portfolio
|
||||
const scenarios = await getPortfolioScenarios(portfolioId);
|
||||
|
||||
return (
|
||||
<section>
|
||||
|
|
@ -21,7 +23,7 @@ export default async function PortfolioLayout({
|
|||
<div className="flex justify-center">
|
||||
<div className="grid grid-cols-8 w-full max-w-8xl">
|
||||
<div className="col-span-12 justify-center bg-gray-50 py-2">
|
||||
<Toolbar portfolioId={portfolioId} />
|
||||
<Toolbar portfolioId={portfolioId} scenarios={scenarios} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ import {
|
|||
FormDescription,
|
||||
} from "@/app/shadcn_components/ui/form";
|
||||
import { useToast } from "@/app/hooks/use-toast";
|
||||
import { ScenarioSelect } from "@/app/db/schema/recommendations";
|
||||
import { useState } from "react";
|
||||
import { SelectScenarioDropdown } from "./SelectScenarioDropdown";
|
||||
|
||||
type Option = {
|
||||
label: string;
|
||||
|
|
@ -175,7 +178,7 @@ export function SelectDropdown({
|
|||
<Menu as="div" className="relative inline-block text-left w-full">
|
||||
<Float>
|
||||
<Menu.Button className="inline-flex justify-center w-1/2 px-4 py-2 text-sm font-medium text-white bg-brandblue rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
|
||||
{selectedOption || "Select an option"}
|
||||
{selectedOption || "Select option"}
|
||||
<ChevronDownIcon
|
||||
className="ml-2 -mr-1 h-5 w-5 text-violet-200 hover:text-violet-100"
|
||||
aria-hidden="true"
|
||||
|
|
@ -227,7 +230,7 @@ export function SelectUpDropdown({
|
|||
<Menu as="div" className="relative inline-block text-left w-full">
|
||||
<Float placement="right-end" offset={4} shift>
|
||||
<Menu.Button className={menuButtonStyle(width)}>
|
||||
{selectedOption || "Select an option"}
|
||||
{selectedOption || "Select option"}
|
||||
<ChevronRightIcon
|
||||
className="ml-2 -mr-1 h-5 w-5 text-violet-200 hover:text-violet-100"
|
||||
aria-hidden="true"
|
||||
|
|
@ -520,14 +523,32 @@ function useCreateRemoteAssessment({
|
|||
}
|
||||
|
||||
export default function RemoteAssessmentModal({
|
||||
portfolioId,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
portfolioId,
|
||||
scenarios,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
setIsOpen: (open: boolean) => void;
|
||||
portfolioId: string;
|
||||
scenarios: ScenarioSelect[];
|
||||
}) {
|
||||
const NEW_SENTINEL = "__new__";
|
||||
const [selectedScenario, setSelectedScenario] = useState<string | null>(null);
|
||||
const { toast } = useToast();
|
||||
|
||||
const scenarioOptions: Option[] = useMemo(
|
||||
() => [
|
||||
{ label: "Create new scenario…", value: NEW_SENTINEL, disabled: false },
|
||||
...scenarios.map((s) => ({
|
||||
label: s.name,
|
||||
value: s.name,
|
||||
disabled: false,
|
||||
})),
|
||||
],
|
||||
[scenarios]
|
||||
);
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
|
|
@ -539,45 +560,62 @@ export default function RemoteAssessmentModal({
|
|||
postcode: "",
|
||||
uprn: 0,
|
||||
valuation: 0,
|
||||
propertyType: null,
|
||||
builtForm: null,
|
||||
},
|
||||
});
|
||||
const { reset, setValue } = form;
|
||||
|
||||
// const [toastState, setToastState] = useState<any>([]);
|
||||
const { toast } = useToast();
|
||||
const {
|
||||
handleSubmit: triggerAssessment,
|
||||
presignedUrlIsLoading,
|
||||
presignedUrlIsError,
|
||||
} = useCreateRemoteAssessment({
|
||||
portfolioId,
|
||||
uprn: form.watch("uprn"),
|
||||
addressLineOne: form.watch("addressLineOne"),
|
||||
postcode: form.watch("postcode"),
|
||||
valuation: form.watch("valuation"),
|
||||
propertyType: form.watch("propertyType"),
|
||||
builtForm: form.watch("builtForm"),
|
||||
});
|
||||
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
try {
|
||||
await handleSubmit(data);
|
||||
form.reset();
|
||||
setIsOpen(false);
|
||||
toast({
|
||||
title: "The Remote Assesment has been sent",
|
||||
description: "",
|
||||
const onSelectScenario = (opt: Option) => {
|
||||
setSelectedScenario(opt.value);
|
||||
if (opt.value === NEW_SENTINEL) {
|
||||
reset({
|
||||
...form.getValues(),
|
||||
scenario: "",
|
||||
housingType: "",
|
||||
goal: "",
|
||||
goalValue: "",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error submitting form:", error);
|
||||
} else {
|
||||
const picked = scenarios.find((s) => s.name === opt.value);
|
||||
if (!picked) return;
|
||||
setValue("scenario", picked.name);
|
||||
setValue("housingType", picked.housingType);
|
||||
setValue("goal", picked.goal);
|
||||
setValue("goalValue", picked.goalValue);
|
||||
}
|
||||
};
|
||||
|
||||
const { handleSubmit, presignedUrlIsLoading, presignedUrlIsError } =
|
||||
useCreateRemoteAssessment({
|
||||
portfolioId,
|
||||
uprn: form.watch("uprn"),
|
||||
addressLineOne: form.watch("addressLineOne"),
|
||||
postcode: form.watch("postcode"),
|
||||
valuation: form.watch("valuation"),
|
||||
propertyType: form.watch("propertyType"),
|
||||
builtForm: form.watch("builtForm"),
|
||||
});
|
||||
const onSubmit = form.handleSubmit(async (data) => {
|
||||
console.log("Triggered");
|
||||
await triggerAssessment(data);
|
||||
form.reset();
|
||||
setIsOpen(false);
|
||||
toast({ title: "Remote assessment sent" });
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
onClose={() => setIsOpen(false)}
|
||||
>
|
||||
<Transition show={isOpen} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="fixed inset-0 z-10 overflow-y-auto"
|
||||
onClose={() => setIsOpen(false)}
|
||||
>
|
||||
<div className="min-h-screen px-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
|
|
@ -587,32 +625,51 @@ export default function RemoteAssessmentModal({
|
|||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
||||
<Dialog.Overlay className="fixed inset-0 bg-black bg-opacity-25" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="w-1/2 max-w-screen-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-900"
|
||||
>
|
||||
Remote Assessment Details
|
||||
</Dialog.Title>
|
||||
<FormProvider {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-6"
|
||||
>
|
||||
{/* Spacer for centering */}
|
||||
<span
|
||||
className="inline-block h-screen align-middle"
|
||||
aria-hidden="true"
|
||||
>
|
||||
​
|
||||
</span>
|
||||
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<div className="inline-block w-full max-w-2xl p-6 my-8 overflow-hidden text-left align-middle transition-all transform bg-white shadow-xl rounded-2xl">
|
||||
<Dialog.Title className="text-lg font-medium">
|
||||
Remote Assessment Details
|
||||
</Dialog.Title>
|
||||
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={onSubmit} className="space-y-6 mt-4">
|
||||
{/* Scenario selector */}
|
||||
<FormItem>
|
||||
<FormLabel>Select scenario</FormLabel>
|
||||
<FormControl>
|
||||
<SelectScenarioDropdown
|
||||
scenarios={scenarios.map((s) => ({
|
||||
label: s.name,
|
||||
value: s.name,
|
||||
}))}
|
||||
selectedValue={selectedScenario}
|
||||
onSelect={onSelectScenario}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
|
||||
{selectedScenario !== null && (
|
||||
<>
|
||||
{/* 2) Scenario Name */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="scenario"
|
||||
|
|
@ -622,6 +679,7 @@ export default function RemoteAssessmentModal({
|
|||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter scenario name"
|
||||
disabled={selectedScenario !== NEW_SENTINEL}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
|
@ -630,6 +688,7 @@ export default function RemoteAssessmentModal({
|
|||
)}
|
||||
/>
|
||||
|
||||
{/* 3) Housing Type */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="housingType"
|
||||
|
|
@ -637,19 +696,24 @@ export default function RemoteAssessmentModal({
|
|||
<FormItem>
|
||||
<FormLabel>Housing Type</FormLabel>
|
||||
<FormControl>
|
||||
<SelectDropdown
|
||||
options={selecthousingTypeOptions}
|
||||
selectedOption={field.value}
|
||||
onSelectOption={(option) =>
|
||||
field.onChange(option.value)
|
||||
}
|
||||
/>
|
||||
{selectedScenario === NEW_SENTINEL ? (
|
||||
<SelectDropdown
|
||||
options={selecthousingTypeOptions}
|
||||
selectedOption={field.value}
|
||||
onSelectOption={(o) =>
|
||||
field.onChange(o.value)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Input value={field.value} disabled />
|
||||
)}
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* 4) Goal */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="goal"
|
||||
|
|
@ -657,19 +721,24 @@ export default function RemoteAssessmentModal({
|
|||
<FormItem>
|
||||
<FormLabel>Goal</FormLabel>
|
||||
<FormControl>
|
||||
<SelectDropdown
|
||||
options={selectGoalOptions}
|
||||
selectedOption={field.value}
|
||||
onSelectOption={(option) =>
|
||||
field.onChange(option.value)
|
||||
}
|
||||
/>
|
||||
{selectedScenario === NEW_SENTINEL ? (
|
||||
<SelectDropdown
|
||||
options={selectGoalOptions}
|
||||
selectedOption={field.value}
|
||||
onSelectOption={(o) =>
|
||||
field.onChange(o.value)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Input value={field.value} disabled />
|
||||
)}
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* 5) Goal Value */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="goalValue"
|
||||
|
|
@ -677,181 +746,173 @@ export default function RemoteAssessmentModal({
|
|||
<FormItem>
|
||||
<FormLabel>Goal Value</FormLabel>
|
||||
<FormControl>
|
||||
<SelectDropdown
|
||||
options={goalValueOptions}
|
||||
{selectedScenario === NEW_SENTINEL ? (
|
||||
<SelectDropdown
|
||||
options={goalValueOptions}
|
||||
selectedOption={field.value}
|
||||
onSelectOption={(o) =>
|
||||
field.onChange(o.value)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Input value={field.value} disabled />
|
||||
)}
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="addressLineOne"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Address</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Enter address" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="postcode"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Postcode</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Enter postcode" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="uprn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>UPRN</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Enter UPRN"
|
||||
{...field}
|
||||
onChange={(e) =>
|
||||
field.onChange(Number(e.target.value))
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="valuation"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Valuation</FormLabel>
|
||||
<FormDescription>
|
||||
The valuation can be found at{" "}
|
||||
<a
|
||||
href={`https://www.zoopla.co.uk/property/uprn/${form.watch(
|
||||
"uprn"
|
||||
)}/`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
zoopla property page
|
||||
</a>
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Enter valuation"
|
||||
{...field}
|
||||
onChange={(e) =>
|
||||
field.onChange(Number(e.target.value))
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm text-gray-600">
|
||||
<strong>Optional:</strong> Property Type and Built Form
|
||||
are only required if no EPC is available.
|
||||
</p>
|
||||
<div className="flex gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="propertyType"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormLabel>Property Type</FormLabel>
|
||||
<FormControl>
|
||||
<SelectUpDropdown
|
||||
options={propertyTypeOptions}
|
||||
selectedOption={field.value}
|
||||
onSelectOption={(option) =>
|
||||
field.onChange(option.value)
|
||||
}
|
||||
onSelectOption={(o) => field.onChange(o.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="addressLineOne"
|
||||
name="builtForm"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Address</FormLabel>
|
||||
<FormItem className="w-full">
|
||||
<FormLabel>Built Form</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Enter address" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="postcode"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Postcode</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Enter postcode" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="uprn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>UPRN</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Enter UPRN"
|
||||
{...field}
|
||||
onChange={(e) =>
|
||||
field.onChange(Number(e.target.value))
|
||||
}
|
||||
<SelectUpDropdown
|
||||
options={builtFormOptions}
|
||||
selectedOption={field.value}
|
||||
onSelectOption={(o) => field.onChange(o.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="valuation"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Valuation </FormLabel>
|
||||
<FormDescription>
|
||||
The valuation can be found at{" "}
|
||||
<a
|
||||
href={`https://www.zoopla.co.uk/property/uprn/${form.watch(
|
||||
"uprn"
|
||||
)}/`}
|
||||
target="_blank"
|
||||
>
|
||||
`https://www.zoopla.co.uk/property/uprn/
|
||||
{form.watch("uprn")}/`
|
||||
</a>
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Enter valuation"
|
||||
{...field}
|
||||
onChange={(e) =>
|
||||
field.onChange(Number(e.target.value))
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={presignedUrlIsLoading}>
|
||||
{presignedUrlIsLoading ? "Submitting…" : "Submit"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm text-gray-600">
|
||||
<strong>Optional:</strong> Property Type and Built
|
||||
Form are only required if no EPC is available.
|
||||
</p>
|
||||
<div className="flex flex-row gap-4">
|
||||
<div className="w-full">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="propertyType"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Property Type</FormLabel>
|
||||
<FormControl>
|
||||
<SelectUpDropdown
|
||||
options={propertyTypeOptions}
|
||||
selectedOption={field.value}
|
||||
onSelectOption={(option) =>
|
||||
field.onChange(option.value)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="builtForm"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Built Form</FormLabel>
|
||||
<FormControl>
|
||||
<SelectUpDropdown
|
||||
options={builtFormOptions}
|
||||
selectedOption={field.value}
|
||||
onSelectOption={(option) =>
|
||||
field.onChange(option.value)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-4 mt-6">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={presignedUrlIsLoading}>
|
||||
{presignedUrlIsLoading ? "Submitting..." : "Submit"}
|
||||
</Button>
|
||||
</div>
|
||||
{presignedUrlIsError && (
|
||||
<p className="text-red-500 mt-2">
|
||||
Error uploading files
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
</FormProvider>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
{presignedUrlIsError && (
|
||||
<p className="text-red-500">Error uploading files</p>
|
||||
)}
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
function setIsOpen(arg0: boolean) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
import { Menu, Transition } from "@headlessui/react";
|
||||
import { Fragment } from "react";
|
||||
import { Button } from "@/app/shadcn_components/ui/button";
|
||||
import { PlusIcon, ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||
|
||||
export type ScenarioOption = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
interface ScenarioSelectProps {
|
||||
selectedValue: string | null;
|
||||
onSelect: (option: ScenarioOption) => void;
|
||||
scenarios: ScenarioOption[];
|
||||
}
|
||||
|
||||
export function SelectScenarioDropdown({
|
||||
selectedValue,
|
||||
onSelect,
|
||||
scenarios,
|
||||
}: ScenarioSelectProps) {
|
||||
const createOption: ScenarioOption = {
|
||||
label: "Create new scenario…",
|
||||
value: "__new__",
|
||||
};
|
||||
const options = [createOption, ...scenarios];
|
||||
|
||||
const selectedLabel =
|
||||
options.find((o) => o.value === selectedValue)?.label ||
|
||||
"Select or create...";
|
||||
|
||||
return (
|
||||
<Menu as="div" className="relative w-full text-left">
|
||||
<Menu.Button
|
||||
as={Button}
|
||||
variant="default"
|
||||
className="w-full justify-between bg-brandmidblue text-white"
|
||||
>
|
||||
<span>{selectedLabel}</span>
|
||||
<ChevronDownIcon className="ml-2 h-5 w-5" aria-hidden="true" />
|
||||
</Menu.Button>
|
||||
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-150"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute mt-2 w-full bg-white border border-gray-200 rounded-lg shadow-lg z-10">
|
||||
{options.map((opt) => (
|
||||
<Menu.Item key={opt.value}>
|
||||
{({ active }) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={`w-full flex items-center text-left px-4 py-2 ${
|
||||
active ? "bg-brandmidblue text-white" : "text-gray-700"
|
||||
}`}
|
||||
onClick={() => onSelect(opt)}
|
||||
>
|
||||
{opt.value === createOption.value && (
|
||||
<PlusIcon
|
||||
className={`mr-2 h-5 w-5 ${
|
||||
active ? "text-white" : "text-brandmidblue"
|
||||
}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
{opt.label}
|
||||
</Button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
|
@ -68,6 +68,18 @@ export async function getPortfolio(portfolioId: string): Promise<Portfolio> {
|
|||
return data[0];
|
||||
}
|
||||
|
||||
export async function getPortfolioScenarios(
|
||||
portfolioId: string
|
||||
): Promise<ScenarioSelect[]> {
|
||||
// This function will grab all scenarios from the database for a given portfolio
|
||||
const scenarios = await db
|
||||
.select()
|
||||
.from(scenario)
|
||||
.where(eq(scenario.portfolioId, BigInt(portfolioId)));
|
||||
|
||||
return scenarios;
|
||||
}
|
||||
|
||||
export async function getPortfolioPerformance(
|
||||
portfolioId: string
|
||||
): Promise<ScenarioSelect[]> {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue