mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-30 12:55:02 +00:00
Add button and scenario selection / prioritisation
This commit is contained in:
parent
2b4d9b0771
commit
cf821c9150
4 changed files with 355 additions and 0 deletions
56
package-lock.json
generated
56
package-lock.json
generated
|
|
@ -11,6 +11,9 @@
|
|||
"@aws-sdk/client-s3": "^3.971.0",
|
||||
"@aws-sdk/client-sqs": "^3.864.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.927.0",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@headlessui/react": "^2.2.7",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
|
|
@ -1242,6 +1245,59 @@
|
|||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/accessibility": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
||||
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/core": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dnd-kit/accessibility": "^3.1.1",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/sortable": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
|
||||
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dnd-kit/core": "^6.3.0",
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/utilities": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
||||
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@drizzle-team/brocli": {
|
||||
"version": "0.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@
|
|||
"@aws-sdk/client-s3": "^3.971.0",
|
||||
"@aws-sdk/client-sqs": "^3.864.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.927.0",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@headlessui/react": "^2.2.7",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,289 @@
|
|||
// "use client"
|
||||
|
||||
// import { useState } from "react";
|
||||
// import {
|
||||
// DropdownMenu,
|
||||
// DropdownMenuContent,
|
||||
// DropdownMenuTrigger,
|
||||
// } from "@/app/shadcn_components/ui/dropdown-menu";
|
||||
// import { Button } from "@/app/shadcn_components/ui/button";
|
||||
// import { Checkbox } from "@/app/shadcn_components/ui/checkbox";
|
||||
|
||||
// export interface RecommendationsOptionsProps {
|
||||
// onApply: (value: boolean) => Promise<void> | void;
|
||||
// disabled?: boolean;
|
||||
// }
|
||||
|
||||
// export function RecommendationsOptions({
|
||||
// onApply, disabled = false
|
||||
// }: RecommendationsOptionsProps) {
|
||||
// console.log("Generating Recommendations button");
|
||||
|
||||
// const [isApplying, setIsApplying] = useState(false);
|
||||
|
||||
// return (
|
||||
// <DropdownMenu
|
||||
// onOpenChange={(open) => {
|
||||
// if (open) {
|
||||
// console.log("dropdown menu is open");
|
||||
// }
|
||||
// }}
|
||||
// >
|
||||
// <DropdownMenuTrigger asChild>
|
||||
// <Button
|
||||
// variant="outline"
|
||||
// size="sm"
|
||||
// disabled={ disabled || isApplying }
|
||||
// className={`
|
||||
// rounded-md px-3 py-2 text-sm font-medium transition
|
||||
// ${
|
||||
// disabled
|
||||
// ? "bg-gray-200 text-gray-400 cursor-not-allowed"
|
||||
// : "bg-brandblue text-white hover:bg-hoverblue"
|
||||
// }`}
|
||||
// >
|
||||
// Calculate Recommendations
|
||||
// </Button>
|
||||
// </DropdownMenuTrigger>
|
||||
// </DropdownMenu>
|
||||
// )
|
||||
// }
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/app/shadcn_components/ui/dropdown-menu";
|
||||
import { Button } from "@/app/shadcn_components/ui/button";
|
||||
import { Checkbox } from "@/app/shadcn_components/ui/checkbox";
|
||||
import { Label } from "@/app/shadcn_components/ui/label";
|
||||
import { GripVertical } from "lucide-react";
|
||||
import { HelpCircle } from "lucide-react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/app/shadcn_components/ui/tooltip";
|
||||
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
SortableContext,
|
||||
useSortable,
|
||||
verticalListSortingStrategy,
|
||||
arrayMove,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
|
||||
export interface RecommendationsOptionsProps {
|
||||
onApply: (value: {
|
||||
selectedScenarios: number[] | null;
|
||||
prioritisedScenarios: number[] | null;
|
||||
}) => Promise<void> | void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const fakeScenarios = [
|
||||
{ id: 1, name: "EPC C" },
|
||||
{ id: 2, name: "EPC C - Minor Works" },
|
||||
];
|
||||
|
||||
function SortableScenarioItem({
|
||||
id,
|
||||
name,
|
||||
index,
|
||||
}: {
|
||||
id: number;
|
||||
name: string;
|
||||
index: number;
|
||||
}) {
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } =
|
||||
useSortable({ id });
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={`flex items-center gap-2 p-2 border rounded bg-muted cursor-grab
|
||||
${isDragging ? "opacity-70 scale-105 shadow-lg" : ""}
|
||||
`}
|
||||
{...attributes}
|
||||
>
|
||||
<GripVertical
|
||||
className="h-4 w-4 text-muted-foreground"
|
||||
{...listeners}
|
||||
/>
|
||||
|
||||
<span className="flex-1">{name}</span>
|
||||
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Priority {index + 1}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function RecommendationsOptions({
|
||||
onApply,
|
||||
disabled = false,
|
||||
}: RecommendationsOptionsProps) {
|
||||
const [isApplying, setIsApplying] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedScenarios, setSelectedScenarios] = useState<number[]>([]);
|
||||
|
||||
const toggleScenario = (id: number) => {
|
||||
setSelectedScenarios((prev) =>
|
||||
prev.includes(id)
|
||||
? prev.filter((s) => s !== id)
|
||||
: [...prev, id]
|
||||
);
|
||||
};
|
||||
|
||||
const handleSelectAll = () => {
|
||||
setSelectedScenarios(fakeScenarios.map((s) => s.id));
|
||||
};
|
||||
|
||||
const handleDeselectAll = () => {
|
||||
setSelectedScenarios([]);
|
||||
};
|
||||
|
||||
const handleDragEnd = (event: any) => {
|
||||
const { active, over } = event;
|
||||
if (!over || active.id === over.id) return;
|
||||
|
||||
setSelectedScenarios((items) => {
|
||||
const oldIndex = items.indexOf(active.id);
|
||||
const newIndex = items.indexOf(over.id);
|
||||
return arrayMove(items, oldIndex, newIndex);
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setIsApplying(true);
|
||||
|
||||
await onApply({
|
||||
selectedScenarios:
|
||||
selectedScenarios.length > 0 ? selectedScenarios : null,
|
||||
prioritisedScenarios:
|
||||
selectedScenarios.length > 0 ? selectedScenarios : null,
|
||||
});
|
||||
|
||||
setIsApplying(false);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setSelectedScenarios([]);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const selectedScenarioObjects = selectedScenarios.map(
|
||||
(id) => fakeScenarios.find((s) => s.id === id)!
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={disabled || isApplying}
|
||||
className={`
|
||||
rounded-md px-3 py-2 text-sm font-medium transition
|
||||
${
|
||||
disabled
|
||||
? "bg-gray-200 text-gray-400 cursor-not-allowed"
|
||||
: "bg-brandblue text-white hover:bg-hoverblue"
|
||||
}
|
||||
`}
|
||||
>
|
||||
Calculate Recommendations
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent className="w-80 p-4 space-y-4">
|
||||
<div className="flex justify-between">
|
||||
<Button size="sm" variant="ghost" onClick={handleSelectAll}>
|
||||
Select all
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onClick={handleDeselectAll}>
|
||||
Deselect all
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-semibold">Select scenarios</h4>
|
||||
|
||||
{fakeScenarios.map((scenario) => (
|
||||
<div key={scenario.id} className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={selectedScenarios.includes(scenario.id)}
|
||||
onCheckedChange={() => toggleScenario(scenario.id)}
|
||||
/>
|
||||
<Label>{scenario.name}</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{selectedScenarioObjects.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<h4 className="font-semibold">Drag to prioritise</h4>
|
||||
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
This decides the order of selection if multiple scenarios have equal
|
||||
outputs.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
<DndContext
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={selectedScenarios}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{selectedScenarioObjects.map((scenario, index) => (
|
||||
<SortableScenarioItem
|
||||
key={scenario.id}
|
||||
id={scenario.id}
|
||||
name={scenario.name}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button variant="ghost" size="sm" onClick={handleCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleSubmit} disabled={isApplying}>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ import type {
|
|||
ScenarioSummary,
|
||||
} from "./types";
|
||||
import { ReportingFunctionalityButtons } from "./ReportingFunctionalityButtons";
|
||||
import { RecommendationsOptions } from "./RecommendationsOptions";
|
||||
|
||||
interface ReportingClientAreaProps {
|
||||
baseline: BaselineMetrics;
|
||||
|
|
@ -266,6 +267,12 @@ export function ReportingClientArea({
|
|||
</button>
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
<RecommendationsOptions
|
||||
onApply={() => {console.log('Generat Recommendations')}}
|
||||
disabled={scenarioBusy}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* LOADING + ERROR STATES */}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue