From 715fa05c2ccfe723a46a12a265346cad0a348f0a Mon Sep 17 00:00:00 2001 From: Adam Trincas Date: Mon, 23 Dec 2024 10:52:20 +0100 Subject: [PATCH] Project details implementation --- client/src/app/(overview)/store.ts | 27 +- client/src/app/(overview)/url-store.ts | 22 +- client/src/components/ui/bar-chart/index.tsx | 71 --- client/src/components/ui/graph.tsx | 62 ++- .../src/containers/overview/filters/index.tsx | 4 +- .../src/containers/overview/header/index.tsx | 4 +- .../overview/header/parameters/index.tsx | 16 +- .../overview/map/layers/projects/index.tsx | 4 +- .../abatement-potential/index.tsx | 44 ++ .../project-details/compare-button/index.tsx | 19 + .../project-details/cost-estimates/index.tsx | 59 ++ .../overview/project-details/cost/index.tsx | 66 +++ .../overview/project-details/footer/index.tsx | 60 ++ .../overview/project-details/index.tsx | 520 +++--------------- .../project-details/left-over/index.tsx | 60 ++ .../project-details/navigation/index.tsx | 54 ++ .../project-details/parameters/index.tsx | 25 +- .../score-card-rating/index.tsx | 38 ++ .../score-card-ratings/index.tsx | 64 +++ .../overview/project-details/utils.ts | 44 ++ .../overview/table/toolbar/search/index.tsx | 4 +- .../overview/table/view/key-costs/index.tsx | 7 +- .../overview/table/view/overview/index.tsx | 16 +- .../view/scorecard-prioritization/index.tsx | 12 +- .../cost-details/table/utils.ts | 42 +- .../projects/custom-project/cost/index.tsx | 57 +- .../custom-project/left-over/index.tsx | 49 +- client/src/hooks/use-feature-flags.ts | 1 + client/src/lib/format.tsx | 17 - client/src/lib/query-keys.ts | 7 +- client/src/lib/utils.ts | 18 + shared/contracts/projects.contract.ts | 3 + shared/dtos/projects/project-scorecard.dto.ts | 24 +- 33 files changed, 806 insertions(+), 714 deletions(-) delete mode 100644 client/src/components/ui/bar-chart/index.tsx create mode 100644 client/src/containers/overview/project-details/abatement-potential/index.tsx create mode 100644 client/src/containers/overview/project-details/compare-button/index.tsx create mode 100644 client/src/containers/overview/project-details/cost-estimates/index.tsx create mode 100644 client/src/containers/overview/project-details/cost/index.tsx create mode 100644 client/src/containers/overview/project-details/footer/index.tsx create mode 100644 client/src/containers/overview/project-details/left-over/index.tsx create mode 100644 client/src/containers/overview/project-details/navigation/index.tsx create mode 100644 client/src/containers/overview/project-details/score-card-rating/index.tsx create mode 100644 client/src/containers/overview/project-details/score-card-ratings/index.tsx create mode 100644 client/src/containers/overview/project-details/utils.ts diff --git a/client/src/app/(overview)/store.ts b/client/src/app/(overview)/store.ts index 0d7fe945..1d0108e5 100644 --- a/client/src/app/(overview)/store.ts +++ b/client/src/app/(overview)/store.ts @@ -1,6 +1,14 @@ import { MapMouseEvent } from "react-map-gl"; +import { + COST_TYPE_SELECTOR, + PROJECT_PRICE_TYPE, + PROJECT_SIZE_FILTER, +} from "@shared/entities/projects.entity"; import { atom } from "jotai"; +import { z } from "zod"; + +import { filtersSchema } from "@/app/(overview)/url-store"; export const projectsUIState = atom<{ filtersOpen: boolean; @@ -15,8 +23,23 @@ export const popupAtom = atom<{ export const projectDetailsAtom = atom<{ isOpen: boolean; - projectName: string; + id: string; + visibleProjectIds: string[]; }>({ isOpen: false, - projectName: "", + id: "", + visibleProjectIds: [], }); + +export type ProjectDetailsFilters = Pick< + z.infer, + "projectSizeFilter" | "priceType" | "costRangeSelector" +>; + +const INITIAL_FILTERS_STATE: ProjectDetailsFilters = { + projectSizeFilter: PROJECT_SIZE_FILTER.MEDIUM, + priceType: PROJECT_PRICE_TYPE.MARKET_PRICE, + costRangeSelector: COST_TYPE_SELECTOR.NPV, +}; + +export const projectDetailsFiltersAtom = atom(INITIAL_FILTERS_STATE); diff --git a/client/src/app/(overview)/url-store.ts b/client/src/app/(overview)/url-store.ts index 93570745..ee06d7f7 100644 --- a/client/src/app/(overview)/url-store.ts +++ b/client/src/app/(overview)/url-store.ts @@ -1,5 +1,3 @@ -import { ReactNode } from "react"; - import { ACTIVITY, RESTORATION_ACTIVITY_SUBTYPE, @@ -25,18 +23,16 @@ import { TABLE_VIEWS } from "@/containers/overview/table/toolbar/table-selector" const SUB_ACTIVITIES = RESTORATION_ACTIVITY_SUBTYPE; -interface ParameterOption { - label: string; - value: string; - disabled?: boolean; -} - -export interface Parameter { - key: keyof Omit, "keyword">; +export interface Parameter> { + key: T; label: string; className: string; - options: ParameterOption[]; - tooltipContent?: ReactNode; + tooltipContent?: React.ReactNode; + options: { + label: string; + value: string; + disabled?: boolean; + }[]; } export const filtersSchema = z.object({ @@ -65,7 +61,7 @@ export const INITIAL_FILTERS_STATE: z.infer = { abatementPotentialRange: INITIAL_ABATEMENT_POTENTIAL_RANGE, }; -export function useGlobalFilters() { +export function useProjectOverviewFilters() { const [popup, setPopup] = useAtom(popupAtom); const [filters, setFilters] = useQueryState( "filters", diff --git a/client/src/components/ui/bar-chart/index.tsx b/client/src/components/ui/bar-chart/index.tsx deleted file mode 100644 index 972983e6..00000000 --- a/client/src/components/ui/bar-chart/index.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { toCompactAmount } from "@/lib/format"; - -interface BarChartProps { - total: number; - segments: { - value: number; - label: string; - colorClass: string; - }[]; - orientation?: "horizontal" | "vertical"; -} - -const BarChart = ({ - total, - segments, - orientation = "horizontal", -}: BarChartProps) => { - const getSize = (value: number) => { - const percentage = (value / total) * 100; - return `${Math.max(percentage, 0)}%`; - }; - - if (orientation === "horizontal") { - return ( -
-
- {segments.map((segment, index) => ( -
-
-
- ${toCompactAmount(segment.value)} -
-
-
- ))} -
-
- ); - } - - return ( -
-
- {segments.map((segment, index) => ( -
-
-
- ${toCompactAmount(segment.value)} -
-
-
- ))} -
-
- ); -}; - -export default BarChart; diff --git a/client/src/components/ui/graph.tsx b/client/src/components/ui/graph.tsx index 4a910ad4..7f5a38a6 100644 --- a/client/src/components/ui/graph.tsx +++ b/client/src/components/ui/graph.tsx @@ -30,7 +30,7 @@ const getSize = (value: number, total: number) => { * 2. Split mode (when leftover is provided): Shows total on left and segments with leftover on right */ const Graph: FC = ({ total, leftover, segments }) => { - if (leftover) { + if (typeof leftover === "number") { return (
@@ -149,4 +149,62 @@ const GraphLegend: FC = ({ items }) => { ); }; -export { Graph, GraphLegend }; +interface GraphWithLegendProps { + /** The total value to be displayed */ + total: number; + /** Optional value that, when provided, shows a split view with total on left and segments with leftover on right */ + leftover?: number; + /** Array of segments with their corresponding legend items */ + items: Array<{ + value: number; + label: string; + textColor: string; + bgColor: string; + }>; +} + +const GraphWithLegend: FC = ({ + total, + leftover, + items, +}) => { + return ( +
+
+
+ + {renderCurrency(leftover || total)} + +
+ ({ + label, + textColor, + bgColor, + })), + ]} + /> +
+ ({ + value, + colorClass: bgColor, + }))} + leftover={leftover} + /> +
+ ); +}; + +export { Graph, GraphLegend, GraphWithLegend }; diff --git a/client/src/containers/overview/filters/index.tsx b/client/src/containers/overview/filters/index.tsx index afcce826..bd6b173d 100644 --- a/client/src/containers/overview/filters/index.tsx +++ b/client/src/containers/overview/filters/index.tsx @@ -15,7 +15,7 @@ import { cn } from "@/lib/utils"; import { projectsUIState } from "@/app/(overview)/store"; import { INITIAL_FILTERS_STATE, - useGlobalFilters, + useProjectOverviewFilters, } from "@/app/(overview)/url-store"; import { Button } from "@/components/ui/button"; @@ -39,7 +39,7 @@ import { export const FILTERS_SIDEBAR_WIDTH = 320; export default function ProjectsFilters() { - const [filters, setFilters] = useGlobalFilters(); + const [filters, setFilters] = useProjectOverviewFilters(); const setFiltersOpen = useSetAtom(projectsUIState); const [costValuesState, setCostValuesState] = useState([ filters.costRange[0] || INITIAL_COST_RANGE[filters.costRangeSelector][0], diff --git a/client/src/containers/overview/header/index.tsx b/client/src/containers/overview/header/index.tsx index 42c3b542..edb6a62f 100644 --- a/client/src/containers/overview/header/index.tsx +++ b/client/src/containers/overview/header/index.tsx @@ -7,7 +7,7 @@ import { Settings2Icon } from "lucide-react"; import { cn } from "@/lib/utils"; import { projectsUIState } from "@/app/(overview)/store"; -import { useGlobalFilters } from "@/app/(overview)/url-store"; +import { useProjectOverviewFilters } from "@/app/(overview)/url-store"; import { INITIAL_FILTERS_STATE } from "@/app/(overview)/url-store"; import ParametersProjects from "@/containers/overview/header/parameters"; @@ -17,7 +17,7 @@ import { SidebarTrigger } from "@/components/ui/sidebar"; export default function ProjectsHeader() { const setFiltersOpen = useSetAtom(projectsUIState); - const [filters] = useGlobalFilters(); + const [filters] = useProjectOverviewFilters(); /* eslint-disable @typescript-eslint/no-unused-vars */ const { diff --git a/client/src/containers/overview/header/parameters/index.tsx b/client/src/containers/overview/header/parameters/index.tsx index 525dc726..0c1fccf1 100644 --- a/client/src/containers/overview/header/parameters/index.tsx +++ b/client/src/containers/overview/header/parameters/index.tsx @@ -3,11 +3,12 @@ import { COST_TYPE_SELECTOR, PROJECT_PRICE_TYPE, } from "@shared/entities/projects.entity"; -import { z } from "zod"; import { FILTER_KEYS } from "@/app/(overview)/constants"; -import { Parameter, useGlobalFilters } from "@/app/(overview)/url-store"; -import { filtersSchema } from "@/app/(overview)/url-store"; +import { + Parameter, + useProjectOverviewFilters, +} from "@/app/(overview)/url-store"; import { FILTERS } from "@/constants/tooltip"; @@ -80,10 +81,11 @@ export const PROJECT_PARAMETERS: Parameter[] = [ ] as const; export default function ParametersProjects() { - const [filters, setFilters] = useGlobalFilters(); + const [filters, setFilters] = useProjectOverviewFilters(); + const handleParameters = async ( v: string, - parameter: keyof Omit, "keyword">, + parameter: keyof typeof filters, ) => { await setFilters((prev) => ({ ...prev, @@ -104,7 +106,9 @@ export default function ParametersProjects() {