Add button and scenario selection / prioritisation

This commit is contained in:
Daniel Roth 2026-02-27 17:04:56 +00:00
parent 2b4d9b0771
commit cf821c9150
4 changed files with 355 additions and 0 deletions

56
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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>
);
}

View file

@ -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 */}