+
+
+ {/* Handles */}
+
setIsDragging('min')}
+ />
+
setIsDragging('max')}
+ />
+
+ {/* Values */}
+
+ {minValue.toFixed(2)}
+ {maxValue.toFixed(2)}
+
+
+ );
+};
diff --git a/src/components/ExpandableMenu/ExpandableMenu.module.css b/src/components/ExpandableMenu/ExpandableMenu.module.css
new file mode 100644
index 00000000..c8ce7022
--- /dev/null
+++ b/src/components/ExpandableMenu/ExpandableMenu.module.css
@@ -0,0 +1,91 @@
+.menuContainer {
+ position: absolute;
+ display: inline-flex;
+ align-items: center;
+ /* background-color: #ffffff;
+ border: 1px solid #e2e8f0; */
+ border-radius: 7px;
+ height: 22px;
+ width: 22px;
+ padding: 4px;
+ right: 28px;
+ gap: 2px;
+ overflow: hidden;
+ transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.menuContainer.left {
+ flex-direction: row-reverse;
+ /* right: auto; */
+ /* left: 28px; */
+}
+
+.menuContainer:hover {
+ width: 103px;
+ background-color: #ffffff;
+ /* border: 1px solid #e2e8f0; */
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.kebabIcon {
+ min-width: 12px;
+ height: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ color: #64748b;
+}
+
+.menuContainer:hover .kebabIcon {
+ transform: rotate(90deg);
+}
+
+.menuItems {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ opacity: 0;
+ transform: translateX(-10px);
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.menuContainer:hover .menuItems {
+ opacity: 1;
+ transform: translateX(0);
+}
+
+.menuContainer.left .menuItems {
+ transform: translateX(10px);
+}
+
+.menuContainer.left:hover .menuItems {
+ transform: translateX(0);
+ padding-right: 3px;
+ padding-left: 3px;
+}
+
+.menuItem {
+ min-width: 20px;
+ height: 20px;
+ border-radius: 7px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+ flex-shrink: 0;
+ border: none;
+ background: transparent;
+ padding: 0;
+ opacity: 0.6;
+}
+
+.menuItem:hover {
+ transition: all 0.3s;
+ opacity: 1;
+}
+
+.menuItem.active {
+ color: #3b82f6;
+}
diff --git a/src/components/ExpandableMenu/ExpandableMenu.tsx b/src/components/ExpandableMenu/ExpandableMenu.tsx
new file mode 100644
index 00000000..4206f908
--- /dev/null
+++ b/src/components/ExpandableMenu/ExpandableMenu.tsx
@@ -0,0 +1,30 @@
+import { faEllipsisV } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { ReactNode } from 'react';
+import styles from './ExpandableMenu.module.css';
+
+interface ExpandableMenuProps {
+ direction?: 'left' | 'right';
+ children?: ReactNode;
+ className?: string;
+}
+
+export function ExpandableMenu({ direction = 'left', children, className }: ExpandableMenuProps) {
+ const containerClassName = [styles.menuContainer, styles[direction], className].filter(Boolean).join(' ');
+
+ return (
+
{
+ e.stopPropagation();
+ }}
+ >
+
+
+
+
{children}
+
+ );
+}
+
+export default ExpandableMenu;
diff --git a/src/components/ExpandableMenu/MenuButton.tsx b/src/components/ExpandableMenu/MenuButton.tsx
new file mode 100644
index 00000000..f25b4226
--- /dev/null
+++ b/src/components/ExpandableMenu/MenuButton.tsx
@@ -0,0 +1,17 @@
+import React, { ButtonHTMLAttributes } from 'react';
+import styles from './ExpandableMenu.module.css';
+
+interface MenuButtonProps extends ButtonHTMLAttributes
{
+ active?: boolean;
+}
+
+function MenuButton({ children, className, active, ...props }: MenuButtonProps) {
+ const buttonClassName = [styles.menuItem, active && styles.active, className].filter(Boolean).join(' ');
+ return (
+
+ {children}
+
+ );
+}
+
+export default MenuButton;
diff --git a/src/components/FileDrop/FileDrop.module.css b/src/components/FileDrop/FileDrop.module.css
new file mode 100644
index 00000000..45977413
--- /dev/null
+++ b/src/components/FileDrop/FileDrop.module.css
@@ -0,0 +1,114 @@
+.fileDrop {
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 1rem;
+ width: 100%;
+
+ border: 2px dashed #cccccc;
+ border-radius: 10px;
+ padding: 2rem;
+ text-align: center;
+ transition: border-color 0.3s, background-color 0.3s;
+
+ background-color: #f9f9f9;
+ display: flex;
+}
+
+.fileDropImage {
+ width: 120px;
+ height: 120px;
+ margin: 0 auto;
+}
+
+.fileNameWrapper {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ border: 1px solid #cccccc;
+ border-radius: 7px;
+ padding: 0.5rem;
+}
+
+.removeFileButton {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ cursor: pointer;
+ border-radius: 4px;
+
+ width: 15px;
+ height: 15px;
+ margin-left: 1rem;
+}
+
+.removeFileIcon:hover {
+ background-color: #e8e8e8;
+}
+
+.removeFileIcon {
+ border-radius: 4px;
+ width: 30px;
+ height: 30px;
+ transition: background-color 0.3s ease;
+}
+
+.dragging {
+ border-color: #6ca6fd;
+ background-color: #e8f0ff;
+}
+
+/* Spinner animation */
+.spinner {
+ border: 4px solid #ccc;
+ border-top: 4px solid #6ca6fd;
+ border-radius: 50%;
+ width: 40px;
+ height: 40px;
+ animation: spin 1s linear infinite;
+}
+
+.label {
+ font-weight: 500;
+ color: #333;
+ margin: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ width: 100%;
+}
+
+.fileListLabel {
+ text-align: left;
+}
+
+.hiddenInput {
+ display: none;
+}
+
+.chooseFileButton {
+ background-color: #6ca6fd;
+ color: white;
+ border: none;
+ padding: 10px 20px;
+ cursor: pointer;
+ border-radius: 4px;
+ font-size: 14px;
+ margin-top: 10px;
+ transition: background-color 0.3s ease;
+}
+
+.chooseFileButton:hover {
+ background-color: #568ade;
+}
+
+/* Keyframes for the spinner */
+@keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
diff --git a/src/components/FileDrop/FileDrop.tsx b/src/components/FileDrop/FileDrop.tsx
new file mode 100644
index 00000000..7613bc7d
--- /dev/null
+++ b/src/components/FileDrop/FileDrop.tsx
@@ -0,0 +1,107 @@
+import React, { useEffect, useState } from 'react';
+import styles from './FileDrop.module.css';
+import FileDropIcon from '../../assets/svgs/upload-svgrepo-com.svg';
+import RemoveIcon from '../../assets/svgs/times-svgrepo-com.svg';
+
+interface FileDropProps {
+ multiFile?: boolean;
+}
+
+function FileDrop({ multiFile = true }: FileDropProps) {
+ const [files, setFiles] = useState([]);
+ const [isDragging, setIsDragging] = useState(false);
+
+ const fileInputRef = React.useRef(null);
+
+ useEffect(() => {
+ console.log(files);
+ }, [files]);
+
+ const handleDragOver = (event: React.DragEvent) => {
+ event.preventDefault();
+ setIsDragging(true);
+ };
+
+ const handleDragLeave = () => {
+ setIsDragging(false);
+ };
+
+ const handleDrop = (event: React.DragEvent) => {
+ event.preventDefault();
+ setIsDragging(false);
+ const droppedFiles = Array.from(event.dataTransfer.files);
+ if (!multiFile) {
+ setFiles(droppedFiles.slice(0, 1));
+ } else {
+ const uniqueFiles = filterDuplicateFiles([...files, ...droppedFiles]);
+ setFiles(uniqueFiles);
+ }
+ };
+
+ const handleFileSelection = (event: React.ChangeEvent) => {
+ if (event.target.files) {
+ const selectedFiles = Array.from(event.target.files);
+ if (!multiFile) {
+ setFiles(selectedFiles.slice(0, 1));
+ } else {
+ const uniqueFiles = filterDuplicateFiles([...files, ...selectedFiles]);
+ setFiles(uniqueFiles);
+ }
+
+ event.target.value = '';
+ }
+ };
+
+ const handleFileRemoval = (fileToRemove: File) => {
+ setFiles(prevFiles => prevFiles.filter(file => file !== fileToRemove));
+ };
+
+ const filterDuplicateFiles = (fileArray: File[]) => {
+ const fileMap = new Map();
+ fileArray.forEach(file => {
+ fileMap.set(file.name, file);
+ });
+ return Array.from(fileMap.values());
+ };
+
+ return (
+
+
+
Drag & Drop
+
fileInputRef.current?.click()}>
+ Choose File
+
+
+ {files.length > 0 && (
+ <>
+ {files.map((file, index) => (
+
+
{file.name}
+
handleFileRemoval(file)}
+ aria-label={`Remove ${file.name}`}
+ >
+
+
+
+ ))}
+ >
+ )}
+
+ );
+}
+
+export default FileDrop;
diff --git a/src/components/Layout/Navbar/menuItems.ts b/src/components/Layout/Navbar/menuItems.ts
index 90fdb363..f6cf4e77 100644
--- a/src/components/Layout/Navbar/menuItems.ts
+++ b/src/components/Layout/Navbar/menuItems.ts
@@ -15,7 +15,8 @@ import {
REVEAL_MANAGE,
RESOURCE_PLANNING_PAGE,
DATA_PROCESSING_PROGRESS,
- REVEAL_SIMULATION
+ REVEAL_SIMULATION,
+ CampaignManage
} from '../../../constants';
export const MAIN_MENU = [
@@ -48,6 +49,11 @@ export const MAIN_MENU = [
pageTitle: 'Resource Planning',
route: RESOURCE_PLANNING_PAGE,
roles: [PLAN_VIEW]
+ },
+ {
+ pageTitle: 'CampaignManage',
+ route: CampaignManage,
+ roles: [REVEAL_SIMULATION]
}
]
},
@@ -102,7 +108,7 @@ export const MAIN_MENU = [
pageTitle: 'dataProcessingProgress',
route: DATA_PROCESSING_PROGRESS,
roles: ['data_processing_progress']
- }
+ },
]
}
];
diff --git a/src/components/MapBox/index.css b/src/components/MapBox/index.css
index 032a4c0b..3db72c40 100644
--- a/src/components/MapBox/index.css
+++ b/src/components/MapBox/index.css
@@ -8,16 +8,16 @@
.sidebar-adjust {
z-index: 1;
- margin: 1em;
+ /* margin: 1em; */
}
.sidebar-adjust-list {
z-index: 1;
margin: 1em;
- width:18%
+ width: 18%;
}
-.span-header{
+.span-header {
font-size: medium;
text-decoration-line: underline;
font-weight: bold;
@@ -75,11 +75,19 @@ ul {
}
.mapboxgl-popup-content {
- font: 'BRFirma', 'Helvetica Neue', sans-serif;
+ /* font: 'BRFirma', 'Helvetica Neue', sans-serif; */
padding: 0 !important;
+ border-radius: 8px !important;
width: 100%;
- font-weight: 300;
+ background: rgba(255, 255, 255, 0.9) !important;
+ font-weight: 500;
font-size: medium;
+ box-shadow: rgba(50, 50, 93, 0.25) 0px 13px 27px -5px, rgba(0, 0, 0, 0.3) 0px 8px 16px -8px !important;
+}
+
+.mapboxgl-popup-tip {
+ border-top-color: rgba(255, 255, 255, 0.8) !important;
+ display: none !important;
}
.mapboxgl-popup-content h4 {
@@ -92,20 +100,20 @@ ul {
font-size: medium;
}
-.mapboxgl-popup-content p {
+/* .mapboxgl-popup-content p {
margin: 0;
padding: 10px;
-}
+} */
-.mapboxgl-popup-content div {
+/* .mapboxgl-popup-content div {
padding: 10px;
color: black !important;
-}
+} */
-.mapboxgl-popup-anchor-top>.mapboxgl-popup-content {
+.mapboxgl-popup-anchor-top > .mapboxgl-popup-content {
margin-top: 15px;
}
-.mapboxgl-popup-anchor-top>.mapboxgl-popup-tip {
+.mapboxgl-popup-anchor-top > .mapboxgl-popup-tip {
border-bottom-color: #91c949;
-}
\ No newline at end of file
+}
diff --git a/src/components/PageWrapper/PageWrapper.tsx b/src/components/PageWrapper/PageWrapper.tsx
index 08d77708..22c9e86d 100644
--- a/src/components/PageWrapper/PageWrapper.tsx
+++ b/src/components/PageWrapper/PageWrapper.tsx
@@ -8,7 +8,7 @@ interface Props {
const PageWrapper = ({ title, children }: Props) => {
return (
-
+
{title ? (
<>
{title}
diff --git a/src/components/RangeInput/RangeInput.module.css b/src/components/RangeInput/RangeInput.module.css
new file mode 100644
index 00000000..360e5c27
--- /dev/null
+++ b/src/components/RangeInput/RangeInput.module.css
@@ -0,0 +1,115 @@
+.rangeContainer {
+ width: 100%;
+ margin-bottom: 1rem;
+}
+
+.rangeWrapper {
+ position: relative;
+ width: 100%;
+ height: 4px;
+ margin: 10px 0;
+}
+
+.range {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 100%;
+ height: 4px;
+ background: transparent;
+ position: absolute;
+ top: 0;
+ left: 0;
+ margin: 0;
+ z-index: 2;
+ cursor: pointer;
+}
+
+.range::-webkit-slider-runnable-track {
+ -webkit-appearance: none;
+ width: 100%;
+ height: 4px;
+ background: transparent;
+ border-radius: 2px;
+}
+
+.range::-moz-range-track {
+ width: 100%;
+ height: 4px;
+ background: transparent;
+ border-radius: 2px;
+}
+
+.range::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 13px;
+ height: 13px;
+ border-radius: 50%;
+ background-color: var(--thumb-color, #3b82f6);
+ /* border: 2px solid #fff; */
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1), 0 4px 6px -1px rgba(0, 0, 0, 0.1);
+ cursor: pointer;
+ margin-top: -5px;
+ transition: transform 0.15s ease-in-out;
+}
+
+.range::-moz-range-thumb {
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ background-color: var(--thumb-color, #3b82f6);
+ border: 2px solid #fff;
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1), 0 4px 6px -1px rgba(0, 0, 0, 0.1);
+ cursor: pointer;
+ transition: transform 0.15s ease-in-out;
+}
+
+.range::-webkit-slider-thumb:hover {
+ transform: scale(1.1);
+}
+
+.range::-moz-range-thumb:hover {
+ transform: scale(1.1);
+}
+
+.trackBackground {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: #e2e8f0;
+ border-radius: 2px;
+ z-index: 1;
+}
+
+.trackProgress {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ border-radius: 2px;
+ z-index: 1;
+ pointer-events: none;
+}
+
+.valueContainer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-top: 0.5rem;
+}
+
+.value {
+ font-size: 0.875rem;
+ color: #64748b;
+ font-weight: 500;
+}
+
+.label {
+ display: block;
+ font-size: 1rem;
+ font-weight: 500;
+ color: #1f2937;
+ margin-bottom: 0.5rem;
+}
diff --git a/src/components/RangeInput/RangeInput.tsx b/src/components/RangeInput/RangeInput.tsx
new file mode 100644
index 00000000..538b8da0
--- /dev/null
+++ b/src/components/RangeInput/RangeInput.tsx
@@ -0,0 +1,67 @@
+import { ChangeEvent } from 'react';
+import styles from './RangeInput.module.css';
+
+interface RangeInputProps {
+ min: number;
+ max: number;
+ step?: number;
+ value: number;
+ label?: string;
+ trackColor?: string;
+ thumbColor?: string;
+ onChange: (value: number) => void;
+}
+
+function RangeInput({
+ min,
+ max,
+ step = 1,
+ value,
+ label,
+ trackColor = '#3b82f6',
+ thumbColor = '#3b82f6',
+ onChange
+}: RangeInputProps) {
+ const handleChange = (e: ChangeEvent) => {
+ onChange(Number(e.target.value));
+ };
+
+ const percentage = ((value - min) / (max - min)) * 100;
+
+ return (
+
+ {label &&
{label} }
+
+
+ {min}
+ {value}
+ {max}
+
+
+ );
+}
+
+export default RangeInput;
diff --git a/src/components/Router.tsx b/src/components/Router.tsx
index 08165431..8289acc0 100644
--- a/src/components/Router.tsx
+++ b/src/components/Router.tsx
@@ -11,7 +11,8 @@ import {
REPORTING_PAGE,
RESOURCE_PLANNING_PAGE,
SIMULATION_PAGE,
- TAG_MANAGEMENT
+ TAG_MANAGEMENT,
+ CampaignManage
} from '../constants/';
import Home from '../pages/HomePage';
import Plan from '../pages/Plan';
@@ -27,6 +28,7 @@ import MetaDataImport from '../pages/MetaDataImport';
import ResourcePlanning from '../pages/ResourcePlanning';
import DataProcessingProgress from '../features/technical/components/DataProcessingProgress';
import TagManagement2 from '../pages/TagManagement/TagManagement2';
+import Campaign from '../pages/Campaign';
const Router = () => {
const { keycloak, initialized } = useKeycloak();
@@ -47,6 +49,7 @@ const Router = () => {
} />
} />
} />
+ } />
} />
} />
}>
diff --git a/src/components/Switch/Switch.module.css b/src/components/Switch/Switch.module.css
new file mode 100644
index 00000000..9d53571d
--- /dev/null
+++ b/src/components/Switch/Switch.module.css
@@ -0,0 +1,74 @@
+.container {
+ /* width: 100%; */
+ max-width: 17rem;
+ margin: 0 auto;
+}
+
+.switchContainer {
+ position: relative;
+ background-color: #f3f4f6;
+ padding: 4px;
+ border-radius: 0.5rem;
+ display: flex;
+ align-items: center;
+}
+
+.switchBackground {
+ position: absolute;
+ height: calc(100% - 8px);
+ width: 48.5%;
+ background-color: white;
+ border-radius: 0.375rem;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+ transition: transform 300ms ease-in-out;
+ top: 4px;
+ bottom: 4px;
+}
+
+.switchBackground.right {
+ transform: translateX(100%);
+}
+
+.option {
+ position: relative;
+ flex: 1;
+ width: 50%;
+ padding: 0.5rem 0;
+ font-size: 0.875rem;
+ font-weight: 500;
+ border-radius: 0.375rem;
+ border: none;
+ background: none;
+ cursor: pointer;
+ transition: color 300ms;
+}
+
+.option.active {
+ color: #111827;
+}
+
+.option.inactive {
+ color: #6b7280;
+}
+
+.contentArea {
+ position: relative;
+ margin-top: 1rem;
+ overflow: hidden;
+ border-radius: 0.5rem;
+}
+
+.contentWrapper {
+ display: flex;
+ width: 200%;
+ transition: transform 300ms ease-in-out;
+}
+
+.contentWrapper.right {
+ transform: translateX(-50%);
+}
+
+.content {
+ width: 50%;
+ flex-shrink: 0;
+}
diff --git a/src/components/Switch/Switch.tsx b/src/components/Switch/Switch.tsx
new file mode 100644
index 00000000..0dbc6ef7
--- /dev/null
+++ b/src/components/Switch/Switch.tsx
@@ -0,0 +1,41 @@
+import React, { useState } from 'react';
+import styles from './Switch.module.css';
+
+interface SwitchProps {
+ leftOption: string;
+ rightOption: string;
+ leftContent: React.ReactNode;
+ rightContent: React.ReactNode;
+}
+
+export function Switch({ leftOption, rightOption, leftContent, rightContent }: SwitchProps) {
+ const [isRight, setIsRight] = useState(false);
+
+ return (
+
+
+
+
+
setIsRight(false)}
+ className={`${styles.option} ${!isRight ? styles.active : styles.inactive}`}
+ >
+ {leftOption}
+
+
setIsRight(true)}
+ className={`${styles.option} ${isRight ? styles.active : styles.inactive}`}
+ >
+ {rightOption}
+
+
+
+
+
+
{leftContent}
+
{rightContent}
+
+
+
+ );
+}
diff --git a/src/components/SwitchButton/SwitchButton.module.css b/src/components/SwitchButton/SwitchButton.module.css
new file mode 100644
index 00000000..2a440283
--- /dev/null
+++ b/src/components/SwitchButton/SwitchButton.module.css
@@ -0,0 +1,50 @@
+.switchContainer {
+ display: flex;
+ align-items: center;
+}
+
+.switchTitle {
+ margin-right: 1.5rem;
+}
+
+.switchCheckbox {
+ height: 0;
+ width: 0;
+ visibility: hidden;
+}
+
+.switchLabel {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ cursor: pointer;
+ width: 35px;
+ height: 17px;
+ background: grey;
+ border-radius: 100px;
+ position: relative;
+ /* transition: background-color 0.1s; */
+}
+
+.switchLabel .switchButton {
+ content: '';
+ position: absolute;
+ top: 2px;
+ left: 2px;
+ width: 13px;
+ height: 13px;
+ border-radius: 45px;
+ transition: 0.2s;
+ background: #fff;
+ overflow: hidden;
+ box-shadow: 0 0 4px 0 rgba(10, 10, 10, 0.562);
+}
+
+.switchCheckbox:checked + .switchLabel .switchButton {
+ left: calc(100% - 2px);
+ transform: translateX(-100%);
+}
+
+.switchLabel:active .switchButton {
+ width: 13px;
+}
diff --git a/src/components/SwitchButton/SwitchButton.tsx b/src/components/SwitchButton/SwitchButton.tsx
new file mode 100644
index 00000000..bf7a4ee8
--- /dev/null
+++ b/src/components/SwitchButton/SwitchButton.tsx
@@ -0,0 +1,24 @@
+import style from './SwitchButton.module.css';
+
+interface SwitchButtonProps {
+ title: string;
+ isOn: boolean;
+ id: string;
+ handleToggle: (e: any) => void;
+ colorOne?: string;
+ colorTwo?: string;
+}
+
+export default function SwitchButton({ title, id, isOn, handleToggle, colorOne, colorTwo }: SwitchButtonProps) {
+ return (
+
+
+ {title}
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/Table/SimulationResultExpandingTable.module.css b/src/components/Table/SimulationResultExpandingTable.module.css
new file mode 100644
index 00000000..06e374eb
--- /dev/null
+++ b/src/components/Table/SimulationResultExpandingTable.module.css
@@ -0,0 +1,39 @@
+.methodItemWrapper {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ border-radius: 5px;
+ width: 100%;
+}
+
+.methodItem {
+ width: 17px;
+ height: 17px;
+ border-radius: 5px;
+}
+
+.name {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ width: 100%;
+ padding: 0.5rem;
+}
+
+.row {
+ display: flex;
+ align-items: center;
+ padding: 0.5rem 0;
+ cursor: pointer;
+ padding-right: 1rem;
+}
+
+.expander {
+ cursor: pointer;
+ padding-right: 1rem;
+}
+
+.details {
+ display: flex;
+ align-items: center;
+}
diff --git a/src/components/Table/SimulationResultExpandingTable.tsx b/src/components/Table/SimulationResultExpandingTable.tsx
index e7f5f85c..f11186e5 100644
--- a/src/components/Table/SimulationResultExpandingTable.tsx
+++ b/src/components/Table/SimulationResultExpandingTable.tsx
@@ -1,18 +1,8 @@
-import React from 'react';
-import { Button, Table } from 'react-bootstrap';
-import { useExpanded, useTable } from 'react-table';
+import { useState } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import {
- ROW_DEPTH_COLOR_1,
- ROW_DEPTH_COLOR_2,
- ROW_DEPTH_COLOR_3,
- SIMULATION_LOCATION_TABLE_COLUMNS
-} from '../../constants';
import { useAppSelector } from '../../store/hooks';
-import { useTranslation } from 'react-i18next';
import { MarkedLocation } from '../../features/planSimulation/components/Simulation';
-import { AnalysisLayer } from '../../features/planSimulation/components/Simulation';
-import { getBackgroundStyle } from '../../features/planSimulation/components/SimulationMapView/SimulationMapView';
+import styles from './SimulationResultExpandingTable.module.css';
interface Props {
data: any;
@@ -27,253 +17,68 @@ interface Props {
const SimulationResultExpandingTable = ({
data,
clickHandler,
- detailsClickHandler,
- summaryClickHandler,
markedLocations,
showOnlyMarkedLocations,
markedParents
}: Props) => {
const isDarkMode = useAppSelector(state => state.darkMode.value);
+ const [expandedRows, setExpandedRows] = useState>(new Set());
- const getColorLevel = (depth: number) => {
- if (depth === 0) {
- return '';
- } else if (depth === 1) {
- return ROW_DEPTH_COLOR_1;
- } else if (depth === 2) {
- return ROW_DEPTH_COLOR_2;
- } else {
- return ROW_DEPTH_COLOR_3;
- }
+ const toggleRow = (identifier: string) => {
+ setExpandedRows(prev => {
+ const newExpandedRows = new Set(prev);
+ if (newExpandedRows.has(identifier)) {
+ newExpandedRows.delete(identifier);
+ } else {
+ newExpandedRows.add(identifier);
+ }
+ return newExpandedRows;
+ });
};
- const mapRows = (row: any): object[] => {
- if (row.headOf !== undefined) {
- return row.headOf.map((el: any) => {
- return {
- name: el.name,
- identifier: el.identifier,
- active: el.active.toString(),
- headOf: el.headOf,
- type: el.type.valueCodableConcept
- };
- });
- } else if (row.children !== undefined && row.children.length > 0) {
- return row.children.map((el: any) => {
- return {
- identifier: el.identifier,
- children: el.children,
- method: el.method,
- properties: {
- name: el.properties.name,
- status: el.properties.status,
- externalId: el.properties.externalId,
- geographicLevel: el.properties.geographicLevel,
- result: el.properties.result,
- hasResultChild: el.properties.hasResultChild
- },
- aggregates: el.aggregates
- };
- });
- } else {
- return [];
- }
+ const renderRow = (row: any, depth: number) => {
+ const { identifier, properties } = row;
+
+ const isMarked = markedLocations.some(location => location.identifier === identifier);
+ const isMarkedParent = markedParents.has(identifier);
+ const isExpanded = expandedRows.has(identifier);
+
+ return (
+
+
{
+ clickHandler(identifier);
+ toggleRow(identifier);
+ }}
+ >
+
+ {/* {properties.hasResultChild && } */}
+ {row.children.length > 0 && }
+
+
{properties.name}
+ {/*
+ summaryClickHandler(row)}>Summary
+
*/}
+
+ {isExpanded && row.children && row.children.map((childRow: any) => renderRow(childRow, depth + 1))}
+
+ );
};
- const columns = React.useMemo(
- () => [
- {
- // Build our expander column
- id: 'expander', // Make sure it has an ID
- Cell: ({ row }: { row: any }) =>
- // Use the row.canExpand and row.getToggleRowExpandedProps prop getter
- // to build the toggle for expanding a row
- row.canExpand ? (
-
- {row.isExpanded ? (
-
- ) : (
-
- )}
-
- ) : null
- },
- ...SIMULATION_LOCATION_TABLE_COLUMNS
- ],
- []
- );
+ // Filter data if showOnlyMarkedLocations is true
+ const filteredData = showOnlyMarkedLocations
+ ? data.filter((row: any) => markedLocations.some(location => location.identifier === row.identifier))
+ : data;
- const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable(
- {
- columns,
- data,
- getSubRows: (row: any) => mapRows(row),
- autoResetExpanded: false
- },
- useExpanded // Use the useExpanded plugin hook
- );
- const { t } = useTranslation();
return (
-
-
- {headerGroups.map(headerGroup => (
-
- {headerGroup.headers.map(column => {
- return (
-
- {column.Header !== undefined && column.Header !== null && column.id !== 'expander'
- ? t('simulationPage.' + column.Header.toString())
- : ''}
-
- );
- })}
-
- ))}
-
-
- {rows.map(row => {
- prepareRow(row);
- return (
- //row.depth is not existing in react table types for some reason, casting to any type solves the issue
-
- {row.cells
- .filter(cell => {
- const cellData = cell.row.original as any;
- return (
- (showOnlyMarkedLocations &&
- (markedParents.has(cellData.identifier) ||
- markedLocations
- .map(markedLocation => markedLocation.identifier)
- .includes(cellData.identifier))) ||
- !showOnlyMarkedLocations
- );
- })
- .map(cell => {
- const cellData = cell.row.original as any;
- if (cellData.properties?.hasResultChild || cellData.properties?.result) {
- if (cell.column.id === 'resultName') {
- return (
- {
- if (cell.column.id !== 'expander') {
- clickHandler(cellData.identifier);
- }
- }}
- >
- {
- markedLocation.identifier)
- .includes(cellData.identifier)
- ? cellData.properties?.result
- ? 'black'
- : 'grey'
- : 'red',
- fontWeight: cellData.properties?.result ? 'bold' : 'normal'
- }}
- >
- {cell.render('Cell')}
- {cellData.method?.map((methodItem: AnalysisLayer) => (
-
- {' '}
-
- ))}
-
- }{' '}
- {cellData.properties?.hasResultChild ? '*' : ''}
- {markedParents.has(cellData.identifier) ? * : ''}
-
- );
- } else if (cell.column.id === 'details') {
- return (
- {
- if (cell.column.id !== 'expander') {
- clickHandler(cellData.identifier);
- }
- }}
- >
- {/*{*/}
- {/* cellData.properties?.result?(*/}
- {
- detailsClickHandler(cellData.identifier);
- }}
- >
- {t('simulationPage.details')}
-
- {!showOnlyMarkedLocations && (
- {
- summaryClickHandler(cellData);
- }}
- >
- {t('simulationPage.summary')}
-
- )}
-
- );
- } else {
- return (
- {
- if (cell.column.id !== 'expander') {
- let col = row.original as any;
- clickHandler(col.identifier);
- }
- }}
- >
- {cell.render('Cell')}
-
- );
- }
- }
-
- return null;
- })}
-
- );
- })}
-
-
+
+ {filteredData.map((row: any) => renderRow(row, 0))}
+
);
};
diff --git a/src/constants/routes.tsx b/src/constants/routes.tsx
index 521ed5a9..bb0e0f59 100644
--- a/src/constants/routes.tsx
+++ b/src/constants/routes.tsx
@@ -23,6 +23,8 @@ export const SIMULATION_PAGE = '/plans/simulation';
export const RESOURCE_PLANNING_PAGE = '/plans/resource-planning';
+export const CampaignManage = '/plans/campaign-management';
+
//REPORTING PAGES
export const REPORTING_PAGE = '/reports';
diff --git a/src/contexts/PolygonContext.tsx b/src/contexts/PolygonContext.tsx
new file mode 100644
index 00000000..f68a192a
--- /dev/null
+++ b/src/contexts/PolygonContext.tsx
@@ -0,0 +1,219 @@
+import { createContext, useContext, useReducer } from 'react';
+
+type PolygonActions =
+ | { type: 'SET REPORT'; payload: any }
+ | { type: 'UPDATE_DATASET_OPACITY'; payload: { [id: string]: number } }
+ | { type: 'SET_ASSIGNED'; payload: { [identifier: string]: boolean } }
+ | { type: 'SET_PLANID'; payload: string }
+ | { type: 'SET_SIMULATION_ID'; payload: any }
+ | { type: 'SET_TARGET_AREAS'; payload: any[] }
+ | { type: 'SET_HIERARCHY'; payload: any[] }
+ | { type: 'SET_NEW_DATASETS'; payload: any[] }
+ | { type: 'SET_DATASET'; payload: any[] }
+ | { type: 'TOGGLE_DATASET_VISIBILITY'; payload: any }
+ | { type: 'ADD_DATASET'; payload: any }
+ | { type: 'UPDATE_DATASET'; payload: any }
+ | { type: 'DELETE_DATASET'; payload: any }
+ | { type: 'UPDATE_DATASET_FILTER'; payload: any }
+ | { type: 'SELECT_SINGLE'; payload: any }
+ | { type: 'TOGGLE_MULTISELECT'; payload: any }
+ | { type: 'SET_ADMIN0_LOCATION_ID'; payload: any }
+ | { type: 'SET_DEFAULT_HIERARCHY_DATA'; payload: any }
+ | { type: 'CLEAR_SELECTION' };
+
+interface InitialStateInterface {
+ opacitySliderValue: { [id: string]: number };
+ // using this as a map with assigned flags for all loaded children,
+ // as assigned flag changes and updated location data are not re-fetched from backend
+ locationReport: any;
+ assingedLocations: { [identifier: string]: boolean };
+ simulationId: string;
+ planid: string;
+ polygons: any[];
+ datasets: any[];
+ selected: any | null;
+ multiselect: any[];
+ admin0LocationId: string;
+ targetAreas: any[];
+ defaultHierarchyData: any;
+}
+
+const initialState: InitialStateInterface = {
+ locationReport: {},
+ opacitySliderValue: {},
+ assingedLocations: {},
+ simulationId: '',
+ planid: '',
+ polygons: [],
+ datasets: [],
+ selected: null,
+ multiselect: [],
+ admin0LocationId: '',
+ targetAreas: [],
+ defaultHierarchyData: null
+};
+
+export interface SelectedPolygon {
+ assigned: boolean;
+ childrenNumber: number;
+ externalId: string;
+ id: string;
+ geographicLevel: string;
+ name: string;
+ parentIdentifier: string;
+ simulationSearchResult: boolean;
+ status: string;
+}
+
+export interface PolygonContextInterface {
+ state: InitialStateInterface;
+ dispatch: React.Dispatch;
+}
+
+// Reducer
+function polygonReducer(state: InitialStateInterface, action: PolygonActions): InitialStateInterface {
+ switch (action.type) {
+ case 'SET REPORT':
+ return { ...state, locationReport: action.payload };
+ case 'UPDATE_DATASET_OPACITY':
+ return { ...state, opacitySliderValue: { ...state?.opacitySliderValue, ...action.payload } };
+ case 'SET_ASSIGNED':
+ return { ...state, assingedLocations: action.payload };
+ case 'SET_PLANID':
+ return { ...state, planid: action.payload };
+ case 'SET_SIMULATION_ID':
+ return { ...state, simulationId: action.payload };
+ case 'SET_TARGET_AREAS':
+ return { ...state, targetAreas: action.payload };
+ case 'SET_ADMIN0_LOCATION_ID':
+ return { ...state, admin0LocationId: action.payload };
+ case 'SET_HIERARCHY':
+ return { ...state, polygons: action.payload };
+ case 'SET_DEFAULT_HIERARCHY_DATA':
+ return { ...state, defaultHierarchyData: action.payload };
+ case 'SET_NEW_DATASETS':
+ const datasets =
+ action.payload.length === 0
+ ? []
+ : action.payload.map((dataset: any) => ({
+ ...dataset,
+ hidden: false,
+ selectedRange: {
+ minValue: dataset.selectedRange?.minValue || 0,
+ maxValue: dataset.selectedRange?.maxValue || 0
+ },
+ filter: {
+ minValue: dataset.filter?.minValue || 0,
+ maxValue: dataset.filter?.maxValue || 0
+ }
+ }));
+ return {
+ ...state,
+ datasets
+ };
+ case 'SET_DATASET':
+ return {
+ ...state,
+ datasets: state.datasets.map((d: any) => {
+ const corresponding = action.payload.find(ud => ud.identifier === d.identifier);
+ return {
+ ...corresponding,
+ filter: d.filter,
+ selectedRange: d.selectedRange,
+ hidden: d.hidden
+ };
+ })
+ };
+ case 'DELETE_DATASET':
+ return {
+ ...state,
+ datasets: state.datasets.filter((dataset: any) => dataset.identifier !== action.payload)
+ };
+ case 'UPDATE_DATASET':
+ return {
+ ...state,
+ datasets: state.datasets.map(dataset =>
+ dataset.identifier === action.payload.datasetId
+ ? { ...dataset, filter: action.payload.filter, selectedRange: action.payload.filter }
+ : dataset
+ )
+ };
+ case 'UPDATE_DATASET_FILTER':
+ return {
+ ...state,
+ datasets: state.datasets.map(dataset =>
+ dataset.identifier === action.payload.datasetId
+ ? { ...dataset, selectedRange: action.payload.filter || dataset.filter }
+ : dataset
+ )
+ };
+ case 'TOGGLE_DATASET_VISIBILITY':
+ return {
+ ...state,
+ datasets: state.datasets.map((dataset: any) =>
+ dataset.identifier === action.payload.identifier ? action.payload : dataset
+ )
+ };
+ case 'ADD_DATASET':
+ return {
+ ...state,
+ datasets: [
+ ...state.datasets,
+ {
+ ...action.payload,
+ hidden: false,
+ selectedRange: {
+ minValue: 0,
+ maxValue: 0
+ },
+ filter: {
+ minValue: 0,
+ maxValue: 0
+ }
+ }
+ ]
+ };
+ case 'SELECT_SINGLE':
+ if (JSON.stringify(state.selected) === JSON.stringify(action.payload?.properties)) {
+ return { ...state, selected: null };
+ } else {
+ return { ...state, selected: action.payload?.properties, multiselect: [] };
+ }
+ case 'TOGGLE_MULTISELECT':
+ const multiselect = state.multiselect.some((item: any) => item.properties.id === action.payload.properties.id)
+ ? state.multiselect.filter((item: any) => item.properties.id !== action.payload.properties.id)
+ : [...state.multiselect, action.payload];
+ return { ...state, multiselect, selected: null };
+ case 'CLEAR_SELECTION':
+ return { ...state, selected: null, multiselect: [] };
+ default:
+ return state;
+ }
+}
+
+// Context
+const PolygonStateContext = createContext(initialState);
+const PolygonDispatchContext = createContext(null);
+
+// Provider
+export function PolygonProvider({ children }: { children: React.ReactNode }) {
+ const [state, dispatch] = useReducer(polygonReducer, initialState);
+
+ return (
+
+ {children}
+
+ );
+}
+
+// Custom Hook
+export function usePolygonContext() {
+ const state = useContext(PolygonStateContext);
+ const dispatch = useContext(PolygonDispatchContext);
+
+ if (state === undefined || dispatch === undefined) {
+ throw new Error('usePolygonContext must be used within a PolygonProvider');
+ }
+
+ return { state, dispatch };
+}
diff --git a/src/features/location/components/DatasetsAccordion/DatasetsAccordion.module.css b/src/features/location/components/DatasetsAccordion/DatasetsAccordion.module.css
new file mode 100644
index 00000000..6f6bf9f5
--- /dev/null
+++ b/src/features/location/components/DatasetsAccordion/DatasetsAccordion.module.css
@@ -0,0 +1,43 @@
+.popupWrapper {
+ padding-top: 1rem;
+ padding-right: 0.5rem;
+ padding-left: 0.5rem;
+ width: fit-content;
+}
+
+.datasetInput {
+ width: 13.5rem;
+}
+
+.datasetInput:focus,
+input:focus {
+ outline: none;
+}
+
+.switchPanel {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: center;
+ padding-left: 1rem;
+ padding-right: 1rem;
+}
+
+.rangeSwitchPicker {
+ margin-top: 1rem;
+ margin-left: 1rem;
+}
+
+.icon {
+ width: 20px;
+ height: 20px;
+}
+
+.iconPen {
+ width: 14px;
+ height: 14px;
+}
+
+.datasetNameLabel {
+ margin-right: 1rem;
+}
diff --git a/src/features/location/components/DatasetsAccordion/DatasetsAccordion.tsx b/src/features/location/components/DatasetsAccordion/DatasetsAccordion.tsx
new file mode 100644
index 00000000..61889fa2
--- /dev/null
+++ b/src/features/location/components/DatasetsAccordion/DatasetsAccordion.tsx
@@ -0,0 +1,286 @@
+import { useEffect, useRef, useState } from 'react';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Color, ColorPicker, useColor } from 'react-color-palette';
+import { CustomPopup } from '../../../../components/CustomPopup/CustomPopup';
+import { DualRangeSlider } from '../../../../components/DualRangeSlider/DualRangeSlider';
+import { Switch } from '../../../../components/Switch/Switch';
+import SwitchButton from '../../../../components/SwitchButton/SwitchButton';
+import RangeInput from '../../../../components/RangeInput/RangeInput';
+import ItemMenu from './ItemMenu';
+
+import styles from '../accordion/Accordion.module.css';
+import DatasetStyles from './DatasetsAccordion.module.css';
+import { DataSetList, updateDataset } from '../../../planSimulation/components/SimulationMapView/api/datasetsAPI';
+import { usePolygonContext } from '../../../../contexts/PolygonContext';
+interface DatasetsAccordionProps {
+ open?: boolean;
+ dataset: DataSetList;
+ updateDatasetHandler: (datasetId: string) => void;
+ removeDatasetHandler: (datasetId: string) => void;
+}
+
+function DatasetsAccordion({
+ open = false,
+ dataset,
+ updateDatasetHandler,
+ removeDatasetHandler
+}: DatasetsAccordionProps) {
+ const [range, setRange] = useState({ min: dataset.filter?.minValue, max: dataset.filter?.maxValue });
+ const [currentRange, setCurrentRange] = useState({
+ min: dataset.selectedRange?.minValue,
+ max: dataset.selectedRange?.minValue
+ });
+
+ const [isOpen, setOpen] = useState(open);
+ const [showModal, setShowModal] = useState(false);
+
+ const [customColor, setCustomColor] = useColor('hex', dataset.hexColor);
+ // SLIDER
+ const [opacitySliderValue, setOpacitySliderValue] = useState(100);
+
+ const [borderColor, setBorderColor] = useColor('hex', dataset?.borderColor);
+ const [borderValue, setBorderValue] = useState(dataset.lineWidth);
+
+ const [checked, setChecked] = useState(false);
+
+ const [isVisible, setIsVisible] = useState(false);
+ const [edit, setEdit] = useState(false);
+
+ const [datasetName, setDatasetName] = useState(dataset.name);
+ const [tempName, setTempName] = useState(dataset.name);
+
+ const colorPickerRef = useRef(null);
+
+ const { dispatch } = usePolygonContext();
+ const { state } = usePolygonContext();
+
+ const handleColorPopup = (event: any) => {
+ event.stopPropagation();
+ setShowModal(!showModal);
+ };
+
+ const handleRangeChange = (minValue: number, maxValue: number) => {
+ setCurrentRange({ min: minValue, max: maxValue });
+ dispatch({
+ type: 'UPDATE_DATASET_FILTER',
+ payload: {
+ datasetId: dataset.identifier,
+ filter: {
+ minValue,
+ maxValue
+ }
+ }
+ });
+ };
+
+ const handleDatasetUpdate = async () => {
+ try {
+ const UpdatedSimulation = await updateDataset({
+ simulationId: state.simulationId,
+ datasetId: dataset.identifier,
+ name: tempName,
+ hexColor: customColor.hex,
+ lineWidth: borderValue,
+ borderColor: borderColor.hex
+ });
+ updateDatasetHandler(UpdatedSimulation.datasets);
+ } catch (error) {
+ console.error('Failed to update dataset:', error);
+ }
+ };
+
+ const handleNameChange = (event: React.ChangeEvent) => {
+ setTempName(event.target.value);
+ };
+
+ const handleKeyDown = (event: React.KeyboardEvent) => {
+ if (event.key === 'Escape') {
+ setTempName(datasetName); // Revert to the committed value
+ setEdit(false); // Exit edit mode
+ } else if (event.key === 'Enter') {
+ setDatasetName(tempName); // Commit the new value
+ handleDatasetUpdate(); // Call the API to update the dataset
+ setEdit(false); // Exit edit mode
+ }
+ };
+
+ const removeDataset = async () => {
+ try {
+ removeDatasetHandler(dataset.identifier);
+ } catch (error) {
+ console.error('Failed to delete dataset:', error);
+ }
+ };
+
+ const handleChecked = () => {
+ setChecked(!checked);
+ setCurrentRange(range);
+ dispatch({
+ type: 'UPDATE_DATASET_FILTER',
+ payload: {
+ datasetId: dataset.identifier,
+ filter: {
+ minValue: range.min,
+ maxValue: range.max
+ }
+ }
+ });
+ };
+
+ const handleDatasetSliderValue = (newOpacity: number) => {
+ setOpacitySliderValue(newOpacity);
+
+ dispatch({
+ type: 'UPDATE_DATASET_OPACITY',
+ payload: {
+ [dataset.identifier]: newOpacity
+ }
+ });
+ };
+
+ useEffect(() => {
+ setIsVisible(dataset.hidden);
+ setRange({ min: dataset.filter?.minValue, max: dataset.filter?.maxValue });
+ setCurrentRange({ min: dataset.selectedRange?.minValue, max: dataset.selectedRange?.maxValue });
+ }, [dataset]);
+
+ return (
+
+
setOpen(!isOpen)}
+ style={{ position: 'relative' }}
+ >
+
+
+ dispatch({
+ type: 'TOGGLE_DATASET_VISIBILITY',
+ payload: {
+ ...dataset,
+ hidden: !isVisible
+ }
+ })
+ }
+ onEdit={() => {
+ setEdit(!edit);
+ }}
+ onDelete={() => {
+ removeDataset();
+ }}
+ />
+ {
+ setShowModal(false);
+ handleDatasetUpdate();
+ }}
+ referenceElement={colorPickerRef.current}
+ hasBackdrop={false}
+ >
+
+
+
+
+
+ }
+ rightContent={
+
+
+
+
+ }
+ />
+
+
+
+ {edit ? (
+
e.stopPropagation()}
+ />
+ ) : (
+
{datasetName}
+ )}
+
+
+
+
+ );
+}
+
+export default DatasetsAccordion;
diff --git a/src/features/location/components/DatasetsAccordion/ItemMenu.tsx b/src/features/location/components/DatasetsAccordion/ItemMenu.tsx
new file mode 100644
index 00000000..f7dc192f
--- /dev/null
+++ b/src/features/location/components/DatasetsAccordion/ItemMenu.tsx
@@ -0,0 +1,39 @@
+import ExpandableMenu from '../../../../components/ExpandableMenu/ExpandableMenu';
+import MenuButton from '../../../../components/ExpandableMenu/MenuButton';
+import Trash from '../../../../assets/svgs/trash-bin.svg';
+import Pen from '../../../../assets/svgs/pen-solid.svg';
+import Eye from '../../../../assets/svgs/eye-regular.svg';
+import EyeSlash from '../../../../assets/svgs/eye-slash-regular.svg';
+import styles from './DatasetsAccordion.module.css';
+
+interface ItemMenuProps {
+ isVisible?: boolean;
+ direction?: 'left' | 'right';
+ onToggleVisibility?: any;
+ onEdit?: () => void;
+ onDelete?: () => void;
+}
+
+export function ItemMenu({
+ isVisible = true,
+ direction = 'right',
+ onToggleVisibility,
+ onEdit,
+ onDelete
+}: ItemMenuProps) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default ItemMenu;
diff --git a/src/features/location/components/GaugeChart/GaugeChart.module.css b/src/features/location/components/GaugeChart/GaugeChart.module.css
new file mode 100644
index 00000000..0be3d3cc
--- /dev/null
+++ b/src/features/location/components/GaugeChart/GaugeChart.module.css
@@ -0,0 +1,49 @@
+.gaugeContainer {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ padding: 1rem;
+ border: 1px solid #0000001a;
+ border-radius: 5px;
+ background-color: white;
+}
+
+.chartTitle {
+ font-size: 16px;
+ font-weight: bold;
+ margin-bottom: 8px;
+ display: flex;
+ align-items: center;
+}
+
+.chartWrapper {
+ position: relative;
+ width: 180px;
+ height: 90px;
+}
+
+.label {
+ position: absolute;
+ left: 50%;
+ transform: translate(-50%, -80%);
+ font-size: 1.2rem;
+ font-weight: bold;
+ cursor: pointer;
+}
+
+.rangeLabels {
+ display: flex;
+ justify-content: space-between;
+ width: 100%;
+ max-width: 180px;
+ margin-top: 4px;
+ font-size: 14px;
+ color: #555;
+}
+
+.gaugeIcon {
+ height: 20px;
+ width: 20px;
+ margin-right: 0.5rem;
+}
diff --git a/src/features/location/components/GaugeChart/GaugeChart.tsx b/src/features/location/components/GaugeChart/GaugeChart.tsx
new file mode 100644
index 00000000..3a0e2cd9
--- /dev/null
+++ b/src/features/location/components/GaugeChart/GaugeChart.tsx
@@ -0,0 +1,74 @@
+import { useState } from 'react';
+import { Chart, ArcElement, Tooltip } from 'chart.js';
+import { Doughnut } from 'react-chartjs-2';
+import styles from './GaugeChart.module.css';
+import GaugeIcon from '../../../../assets/svgs/gaugeIcon.svg';
+
+Chart.register(ArcElement, Tooltip);
+
+interface GaugeChartProps {
+ valueInPercentage: number; // Current value in percentage
+ absolutValue: number; // Current value
+ minValue: number; // Minimum value of the range
+ maxValue: number; // Maximum value of the range
+ label: string; // Chart label
+ color: string; // Gauge color
+}
+
+export default function GaugeChart({
+ absolutValue,
+ valueInPercentage,
+ minValue,
+ maxValue,
+ label,
+ color
+}: GaugeChartProps) {
+ const [isHovered, setIsHovered] = useState(false);
+ const clampedValue = absolutValue;
+ const percentage = valueInPercentage;
+
+ const formatNumber = (num: number): string => {
+ if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(0)}m`;
+ if (num >= 1_000) return `${(num / 1_000).toFixed(1)}k`;
+ return num.toString();
+ };
+
+ const data = {
+ datasets: [
+ {
+ data: [percentage, 100 - percentage],
+ backgroundColor: [color, '#E0E0E0'],
+ borderWidth: 0,
+ cutout: '60%',
+ circumference: 180, // Half-circle gauge
+ rotation: 270
+ }
+ ]
+ };
+
+ const options = {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: { legend: { display: false } },
+ tooltips: { enabled: false }
+ };
+
+ return (
+
+
+
+ {label}
+
+
+
+
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}>
+ {isHovered ? formatNumber(clampedValue) : `${percentage.toFixed(0)}%`}
+
+
+
+ {minValue}
+ {maxValue}
+
+
+ );
+}
diff --git a/src/features/location/components/StackedBarChart/StackedBarChart.module.css b/src/features/location/components/StackedBarChart/StackedBarChart.module.css
new file mode 100644
index 00000000..1d274d29
--- /dev/null
+++ b/src/features/location/components/StackedBarChart/StackedBarChart.module.css
@@ -0,0 +1,26 @@
+.chartContainer {
+ padding: 1rem;
+ border: 1px solid #0000001a;
+ border-radius: 5px;
+ background-color: white;
+}
+
+.title {
+ font-size: 1.2rem;
+ margin-bottom: 1rem;
+ text-align: center;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.chartWrapper {
+ height: 150px;
+ position: relative;
+}
+
+.barChartIcon {
+ height: 20px;
+ width: 20px;
+ margin-right: 0.5rem;
+}
diff --git a/src/features/location/components/StackedBarChart/StackedBarChart.tsx b/src/features/location/components/StackedBarChart/StackedBarChart.tsx
new file mode 100644
index 00000000..3deceaf1
--- /dev/null
+++ b/src/features/location/components/StackedBarChart/StackedBarChart.tsx
@@ -0,0 +1,75 @@
+import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js';
+import { Bar } from 'react-chartjs-2';
+import BarChartIcon from '../../../../assets/svgs/barChart.svg';
+import styles from './StackedBarChart.module.css';
+
+ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
+
+interface ChartData {
+ label: string;
+ data: number[];
+ backgroundColor: string;
+}
+
+interface StackedBarChartProps {
+ chartData?: ChartData[];
+ title?: string;
+ max?: number;
+}
+
+const StackedBarChart = ({ chartData, title = 'Structure Visit Status', max }: StackedBarChartProps) => {
+ const defaultData = {
+ labels: [''],
+ datasets: chartData || [
+ { label: 'Complete', data: [0], backgroundColor: '#4CAF50' },
+ { label: 'Incomplete', data: [0], backgroundColor: '#FFC107' },
+ { label: 'Not Visited', data: [0], backgroundColor: '#F44336' }
+ ]
+ };
+
+ const options = {
+ indexAxis: 'y' as const,
+ responsive: true,
+ scales: {
+ x: {
+ stacked: true,
+ beginAtZero: true,
+ max: max || 100
+ },
+ y: {
+ stacked: true,
+ ticks: { display: false },
+ grid: { display: false }
+ }
+ },
+ plugins: {
+ tooltip: {
+ callbacks: {
+ afterBody: (context: any) => {
+ const value = context[0]?.raw || 0;
+ const total = 100;
+ return `${value} out of ${total} (${((value / total) * 100).toFixed(1)}%)`;
+ }
+ }
+ },
+ legend: {
+ position: 'bottom' as const
+ }
+ },
+ maintainAspectRatio: false
+ };
+
+ return (
+
+
+
+ {title}
+
+
+
+
+
+ );
+};
+
+export default StackedBarChart;
diff --git a/src/features/location/components/accordion/Accordion.module.css b/src/features/location/components/accordion/Accordion.module.css
new file mode 100644
index 00000000..9eab0d3e
--- /dev/null
+++ b/src/features/location/components/accordion/Accordion.module.css
@@ -0,0 +1,65 @@
+.wrapper {
+ width: 600px;
+ margin: 0 auto;
+}
+
+.accordion_Wrapper + * {
+ /* margin-top: 0.5em; */
+}
+
+.accordion_item {
+ overflow: hidden;
+ transition: max-height 0.3s cubic-bezier(1, 0, 1, 0);
+ height: auto;
+ max-height: 9999px;
+}
+
+.parrentAccordion {
+ border-bottom: 1px solid #ddd;
+}
+.noBorder {
+ border-bottom: none !important;
+}
+.accordion_item.collapsed {
+ max-height: 0;
+ transition: max-height 0.35s cubic-bezier(0, 1, 0, 1);
+ /* border-bottom: 1px solid #ddd; */
+}
+.accordion_title {
+ font-weight: 600;
+ cursor: pointer;
+ color: #666;
+ padding: 1rem 1rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.accordion_dataset {
+ font-weight: 600;
+ cursor: pointer;
+ color: #666;
+ padding: 0.7rem 0;
+ display: flex;
+ flex-direction: row-reverse;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 1.5rem;
+}
+
+.colorBox {
+ min-width: 20px;
+ min-height: 20px;
+ margin-left: auto;
+ border-radius: 5px;
+}
+
+.accordion_title:hover,
+.accordion_title.open {
+ color: black;
+}
+
+.accordion_content {
+ padding: 0em 1.5em;
+ padding-bottom: 1rem;
+}
diff --git a/src/features/location/components/accordion/Accordion.tsx b/src/features/location/components/accordion/Accordion.tsx
new file mode 100644
index 00000000..e226e6fe
--- /dev/null
+++ b/src/features/location/components/accordion/Accordion.tsx
@@ -0,0 +1,57 @@
+import React, { useEffect, useState } from 'react';
+import styles from './Accordion.module.css';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+
+interface AccordionProps {
+ title: string;
+ children: React.ReactNode;
+ open?: boolean;
+ parent?: boolean;
+ customTitle?: React.ReactNode;
+ removeBorderBottom?: boolean;
+ border?: boolean;
+}
+
+function Accordion({
+ title,
+ open = false,
+ children,
+ parent = true,
+ customTitle,
+ removeBorderBottom = false,
+ border = true
+}: AccordionProps) {
+ const [isOpen, setOpen] = useState(open);
+
+ useEffect(() => {
+ setOpen(open);
+ }, [open]);
+
+ return (
+
+
{
+ setOpen(!isOpen);
+ }}
+ style={{ position: 'relative' }}
+ >
+ {customTitle ? customTitle : {title} }
+
+
+
+
+ );
+}
+
+export default Accordion;
diff --git a/src/features/location/components/doughnutChart/DoghnutChart.tsx b/src/features/location/components/doughnutChart/DoghnutChart.tsx
new file mode 100644
index 00000000..adac232b
--- /dev/null
+++ b/src/features/location/components/doughnutChart/DoghnutChart.tsx
@@ -0,0 +1,97 @@
+import { Chart as ChartJS, ArcElement, Tooltip, ChartData, ChartOptions } from 'chart.js';
+import { Doughnut } from 'react-chartjs-2';
+import { useRef, useEffect, useState } from 'react';
+
+ChartJS.register(ArcElement, Tooltip);
+
+interface DoghnutChartProps {
+ data: ChartData<'doughnut'>;
+ cutoutPercentage?: number;
+ fontSize?: number;
+}
+
+const createCustomLegendPlugin = (fontSize: number, threshold: number = 5) => ({
+ id: 'customLegend',
+ afterDraw: (chart: any) => {
+ const { ctx, width, height } = chart;
+ const data = chart.data;
+ const centerX = width / 2;
+ const centerY = height / 2;
+
+ // Calculate radius based on chart size
+ const chartArea = Math.min(width, height);
+ const radius = (chartArea * 0.85) / 2; // Outer radius of the doughnut
+ const labelDistance = radius + 10; // Labels positioned just outside the doughnut
+
+ const total = data.datasets[0].data.reduce((sum: number, value: number) => sum + value, 0);
+
+ let currentAngle = -0.5 * Math.PI; // Starting angle at top
+
+ data.labels.forEach((label: string, index: number) => {
+ const value = data.datasets[0].data[index];
+ const slicePercentage = (value / total) * 100;
+
+ if (slicePercentage < threshold) {
+ currentAngle += (2 * Math.PI * value) / total;
+ return;
+ }
+
+ const sliceAngle = (2 * Math.PI * value) / total;
+ const midAngle = currentAngle + sliceAngle / 2;
+
+ const labelX = centerX + Math.cos(midAngle) * labelDistance;
+ const labelY = centerY + Math.sin(midAngle) * labelDistance;
+
+ // Save the current context state
+ ctx.save();
+
+ // Set font for labels
+ ctx.font = `${fontSize}px Arial`;
+ ctx.fillStyle = '#333';
+
+ // Align text to the right or left based on angle
+ ctx.textAlign = midAngle < -Math.PI / 2 || midAngle > Math.PI / 2 ? 'right' : 'left';
+ ctx.textBaseline = 'middle';
+
+ // Draw the label outside, centered on the slice
+ ctx.fillText(label, labelX, labelY);
+
+ // Restore the context state
+ ctx.restore();
+
+ // Update the angle for the next slice
+ currentAngle += sliceAngle;
+ });
+ },
+});
+
+export function DoghnutChart({ data, cutoutPercentage = 40, fontSize = 14 }: DoghnutChartProps) {
+ const chartRef = useRef
(null);
+ const [chartData, setChartData] = useState(data);
+ // Options for the Doughnut chart
+ const options: ChartOptions<'doughnut'> = {
+ cutout: `${cutoutPercentage}%`,
+ radius: '85%', // Full radius to make the chart bigger
+ responsive: true, // Ensures the chart resizes based on the container size
+ maintainAspectRatio: false, // Allows the chart to stretch based on its parent container
+ plugins: {
+ legend: {
+ display: false
+ },
+ tooltip: {
+ enabled: true
+ }
+ }
+ };
+
+ // Register the custom plugin for dynamic label positioning
+ const customLegendPlugin = createCustomLegendPlugin(fontSize);
+ ChartJS.register(customLegendPlugin);
+
+ // Update chart data when props change
+ useEffect(() => {
+ setChartData(data);
+ }, [data]);
+
+ return ;
+}
diff --git a/src/features/location/components/drawer/Drawer.tsx b/src/features/location/components/drawer/Drawer.tsx
new file mode 100644
index 00000000..983b77a0
--- /dev/null
+++ b/src/features/location/components/drawer/Drawer.tsx
@@ -0,0 +1,44 @@
+import React, { useEffect, useRef, useState } from 'react';
+import styleClasses from './drawer.module.css';
+
+interface DrawerProps {
+ heading?: string;
+ anchor: string;
+ open: boolean;
+ children: React.ReactNode;
+}
+
+export const Drawer = ({ children, open, anchor, heading }: DrawerProps) => {
+ const drawerRef = useRef(null);
+ const [isOverFlowing, setIsOverFlowing] = useState(false);
+
+ useEffect(() => {
+ const checkOverflow = () => {
+ if (drawerRef.current) {
+ setIsOverFlowing(drawerRef.current.scrollHeight > drawerRef.current.clientHeight);
+ }
+ };
+
+ checkOverflow();
+
+ window.addEventListener('resize', checkOverflow);
+ return () => {
+ window.removeEventListener('resize', checkOverflow);
+ };
+ }, [children]);
+
+ return (
+ <>
+
+ {heading &&
{heading}
}
+
{children}
+
+ >
+ );
+};
diff --git a/src/features/location/components/drawer/drawer.module.css b/src/features/location/components/drawer/drawer.module.css
new file mode 100644
index 00000000..66a89253
--- /dev/null
+++ b/src/features/location/components/drawer/drawer.module.css
@@ -0,0 +1,75 @@
+.drawer {
+ color: rgb(7, 7, 7);
+ background-color: #f9f9f9;
+ border: 1px solid #0000001a;
+ transition: width 0.3s ease;
+ position: relative;
+ display: flex;
+ overflow-y: scroll;
+ overflow-x: hidden;
+ flex-direction: column;
+ flex-shrink: 0;
+ height: 90vh;
+}
+
+.scroll {
+ overflow-y: scroll;
+}
+
+.left {
+ width: 0;
+}
+
+.right {
+ width: 0;
+}
+
+.open {
+ width: 309px;
+ height: 90vh;
+}
+
+.closed {
+ width: 0;
+}
+
+.heading {
+ flex-shrink: 0;
+ padding: 25px 20px;
+ font-size: 1.6rem;
+ white-space: nowrap;
+ overflow: hidden;
+ border-bottom: 1px solid #0000001a;
+}
+
+.children {
+ flex-shrink: 0;
+ width: 309px;
+}
+
+.customScroll::-webkit-scrollbar {
+ width: 5px;
+ display: none;
+}
+
+.customScroll:hover::-webkit-scrollbar {
+ display: block;
+}
+
+.customScroll::-webkit-scrollbar-track {
+ background: #0000001a;
+ margin: 4px;
+}
+
+.customScroll::-webkit-scrollbar-thumb {
+ background-color: #949494;
+ border-radius: 10px;
+}
+
+.customScroll::-webkit-scrollbar-button {
+ display: none !important;
+}
+
+.customScroll::-webkit-scrollbar-thumb:hover {
+ background-color: #949494a1;
+}
diff --git a/src/features/location/components/locations/Locations.tsx b/src/features/location/components/locations/Locations.tsx
index eda35167..0a421be8 100644
--- a/src/features/location/components/locations/Locations.tsx
+++ b/src/features/location/components/locations/Locations.tsx
@@ -152,9 +152,11 @@ const Locations = () => {
const clearHandler = () => {
setCurrentLocation(undefined);
if (locationList && locationList.content.length) {
- getLocationById(locationList.content[0].identifier).then(res => {
- setCurrentLocation(res);
- }).catch(err => toast.error(err));
+ getLocationById(locationList.content[0].identifier)
+ .then(res => {
+ setCurrentLocation(res);
+ })
+ .catch(err => toast.error(err));
}
};
diff --git a/src/features/plan/api/index.ts b/src/features/plan/api/index.ts
index ebd5bb50..caca44e4 100644
--- a/src/features/plan/api/index.ts
+++ b/src/features/plan/api/index.ts
@@ -55,7 +55,7 @@ export const updatePlanStatus = async (id: string): Promise => {
export const updatePlanDetails = async (planDetails: PlanCreateModel, planId: string): Promise => {
const data = await api.put(`${PLAN}/${planId}`, planDetails).then(response => response.data);
return data;
-}
+};
export const createGoal = async (goal: Goal, planId: string): Promise => {
const data = await api.post(`${PLAN}/${planId}/goal`, goal).then(response => response.data);
@@ -85,9 +85,7 @@ export const updateAction = async (action: Action, planId: string, goalId: strin
};
export const deleteAction = async (actionId: string, planId: string, goalId: string): Promise => {
- const data = await api
- .delete(`${PLAN}/${planId}/goal/${goalId}/action/${actionId}`)
- .then(response => response.data);
+ const data = await api.delete(`${PLAN}/${planId}/goal/${goalId}/action/${actionId}`).then(response => response.data);
return data;
};
@@ -103,7 +101,12 @@ export const createCondition = async (
return data;
};
-export const deleteCondition = async (condition: ConditionModel, planId: string, goalId: string, actionId: string): Promise => {
+export const deleteCondition = async (
+ condition: ConditionModel,
+ planId: string,
+ goalId: string,
+ actionId: string
+): Promise => {
const data = await api
.delete(`${PLAN}/${planId}/goal/${goalId}/action/${actionId}/condition/${condition.identifier}`)
.then(response => response.data);
diff --git a/src/features/planSimulation/components/AssignToTeamsDialog/AssignToTeamsDialog.module.css b/src/features/planSimulation/components/AssignToTeamsDialog/AssignToTeamsDialog.module.css
new file mode 100644
index 00000000..575d30d2
--- /dev/null
+++ b/src/features/planSimulation/components/AssignToTeamsDialog/AssignToTeamsDialog.module.css
@@ -0,0 +1,213 @@
+.overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 5000;
+}
+
+.dialog {
+ background: white;
+ border-radius: 8px;
+ width: 100%;
+ max-width: 425px;
+ padding: 24px;
+ position: relative;
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
+}
+
+.header {
+ margin-bottom: 20px;
+}
+
+.title {
+ font-size: 18px;
+ font-weight: 600;
+ color: #111827;
+}
+
+.searchContainer {
+ position: relative;
+ margin-bottom: 16px;
+}
+
+.searchIcon {
+ position: absolute;
+ left: 12px;
+ top: 50%;
+ transform: translateY(-50%);
+ color: #6b7280;
+}
+
+.searchInput {
+ width: 100%;
+ padding: 8px 12px 8px 36px;
+ border: 1px solid #e5e7eb;
+ border-radius: 6px;
+ font-size: 14px;
+}
+
+.searchInput:focus {
+ outline: none;
+ border-color: #2563eb;
+ box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1);
+}
+
+.teamsList {
+ height: 300px;
+ overflow-y: auto;
+ margin: 16px 0;
+}
+
+.teamButton {
+ width: 100%;
+ padding: 12px;
+ background: none;
+ border: 1px solid transparent;
+ border-radius: 8px;
+ text-align: left;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 8px;
+ transition: all 0.2s;
+}
+
+.teamButton:not(.inactive):hover {
+ background-color: #f3f4f6;
+}
+
+.teamButton.selected {
+ background-color: #f3f4f6;
+ border-color: #10b981;
+}
+
+.teamButton.inactive {
+ opacity: 0.6;
+ cursor: not-allowed;
+ background-color: #f3f4f6;
+}
+
+.teamInfo {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.teamHeader {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.teamName {
+ font-weight: 500;
+ color: #111827;
+}
+
+.statusDot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ margin-left: 4px;
+}
+
+.statusDot.active {
+ background-color: #10b981;
+}
+
+.statusDot.inactive {
+ background-color: #e5e7eb;
+}
+
+.teamMembers {
+ font-size: 12px;
+ color: #6b7280;
+}
+
+.actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 8px;
+ margin-top: 24px;
+}
+
+.cancelButton {
+ padding: 8px 16px;
+ border: 1px solid #e5e7eb;
+ background: white;
+ color: #374151;
+ border-radius: 6px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: background-color 0.2s;
+}
+
+.cancelButton:hover {
+ background-color: #f9fafb;
+}
+
+.assignButton {
+ padding: 8px 16px;
+ border: none;
+ background-color: #059669;
+ color: white;
+ border-radius: 6px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: background-color 0.2s;
+}
+
+.unassignButton {
+ padding: 8px 16px;
+ border: none;
+ background-color: #f87171;
+ color: white;
+ border-radius: 6px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: background-color 0.2s;
+}
+
+.assignedTeam {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 4px 8px;
+ border: 1px solid #e5e7eb;
+ background: white;
+ color: #374151;
+ font-size: 0.8rem;
+ border-radius: 6px;
+}
+
+.unassignButton:hover {
+ background-color: #f87171;
+}
+
+.assignButton:hover {
+ background-color: #047857;
+}
+
+.assignButton:disabled {
+ background-color: #9ca3af;
+ cursor: not-allowed;
+}
+
+.checkIcon {
+ width: 25px;
+}
+
+.unnasignedSection {
+ padding: 8px 16px;
+ border: 1px solid #e5e7eb;
+ background: white;
+ color: #374151;
+ border-radius: 6px;
+}
diff --git a/src/features/planSimulation/components/AssignToTeamsDialog/AssignToTeamsDialog.tsx b/src/features/planSimulation/components/AssignToTeamsDialog/AssignToTeamsDialog.tsx
new file mode 100644
index 00000000..cd4e207e
--- /dev/null
+++ b/src/features/planSimulation/components/AssignToTeamsDialog/AssignToTeamsDialog.tsx
@@ -0,0 +1,160 @@
+import { useState, useEffect } from 'react';
+import styles from './AssignToTeamsDialog.module.css';
+import CheckIcon from '../../../../assets/svgs/check-circle.svg';
+import { usePolygonContext } from '../../../../contexts/PolygonContext';
+import { assignLocationToTeam, getLocationsAssignedToATeam } from './api/teamAssignmentAPI';
+import { getSimulationData } from '../SimulationMapView/api/datasetsAPI';
+
+interface AssignToTeamsDialogProps {
+ isOpen: boolean;
+ onOpenChange: (open: boolean) => void;
+ onAssign: (teamId: number) => void;
+ teams: any[];
+ selectedLocation: any;
+}
+
+export function AssignToTeamsDialog({
+ isOpen,
+ onOpenChange,
+ onAssign,
+ teams,
+ selectedLocation
+}: AssignToTeamsDialogProps) {
+ const [selectedTeam, setSelectedTeam] = useState(null);
+ const [searchQuery, setSearchQuery] = useState('');
+ const { state, dispatch } = usePolygonContext();
+ const planId = state.planid;
+
+ useEffect(() => {
+ if (isOpen) {
+ document.body.style.overflow = 'hidden';
+ } else {
+ document.body.style.overflow = 'unset';
+ }
+
+ return () => {
+ document.body.style.overflow = 'unset';
+ };
+ }, [isOpen]);
+
+ if (!selectedLocation) return null;
+
+ const assignedTeamsToLocation = JSON.parse(selectedLocation?.properties?.teamsAssigned || '[]');
+
+ // Extract identifiers from assigned teams
+ const assignedTeamIdentifiers = assignedTeamsToLocation.map((team: any) => team.identifier);
+
+ const unassignedTeams = teams.filter(team => !assignedTeamIdentifiers.includes(team.identifier));
+ const assignedTeams = teams.filter(team => assignedTeamIdentifiers.includes(team.identifier));
+ // const selectedLocationsForTeam
+
+ // console.log('Unassigned Teams', unassignedTeams);
+ // console.log('Assigned Teams', assignedTeamsToLocation);
+
+ const handleAssign = async () => {
+ if (selectedTeam && selectedLocation) {
+ if (assignedTeams.some(team => team.identifier === selectedTeam)) {
+ assignLocationToTeam(selectedTeam, [''], planId);
+ console.log('Unassigning Location from Team');
+ } else {
+ getLocationsAssignedToATeam(selectedTeam.toString(), planId).then(response => {
+ const selectedLocationsId = [...response, selectedLocation.properties.id];
+ assignLocationToTeam(selectedTeam, selectedLocationsId, planId);
+ });
+ console.log('Assigning Location to Team', selectedTeam);
+ }
+ const simulationData = await getSimulationData(state.planid);
+ dispatch({ type: 'SET_TARGET_AREAS', payload: simulationData.targetAreas });
+ dispatch({ type: 'CLEAR_SELECTION' });
+
+ onAssign(selectedTeam);
+ setSelectedTeam(null);
+ setSearchQuery('');
+ onOpenChange(false);
+ }
+ };
+
+ const handleClose = () => {
+ setSelectedTeam(null);
+ setSearchQuery('');
+ onOpenChange(false);
+ };
+
+ // console.log('Selected Team', selectedTeam);
+ // console.log('AssignedTeams', assignedTeams);
+
+ if (!isOpen) return null;
+
+ return (
+
+
e.stopPropagation()}>
+
+
+
+ {assignedTeams.length > 0 && (
+ <>
+ {assignedTeams.map(team => (
+
setSelectedTeam(team.identifier)}
+ className={`${styles.teamButton} ${selectedTeam === team.identifier ? styles.selected : ''}`}
+ >
+
+
+
+ {team.members.length} members • {team.active ? 'Active' : 'Inactive'}
+
+
+
+ assigned
+
+
+ ))}
+ >
+ )}
+ {unassignedTeams.map(team => (
+
setSelectedTeam(team.identifier)}
+ className={`${styles.teamButton} ${selectedTeam === team.identifier ? styles.selected : ''}`}
+ >
+
+
+
+ {team.members.length} members • {team.active ? 'Active' : 'Inactive'}
+
+
+ {selectedTeam === team.identifier && team.active && (
+
+ )}
+
+ ))}
+
+
+
+
+ Cancel
+
+
+ {selectedTeam && assignedTeams.some(team => team.identifier === selectedTeam) ? (
+
+ Unassign
+
+ ) : (
+
+ Assign
+
+ )}
+
+
+
+ );
+}
diff --git a/src/features/planSimulation/components/AssignToTeamsDialog/api/teamAssignmentAPI.ts b/src/features/planSimulation/components/AssignToTeamsDialog/api/teamAssignmentAPI.ts
new file mode 100644
index 00000000..167e4942
--- /dev/null
+++ b/src/features/planSimulation/components/AssignToTeamsDialog/api/teamAssignmentAPI.ts
@@ -0,0 +1,16 @@
+import api from '../../../../../api/axios';
+
+export const assignLocationToTeam = async (teamId: number, locationId: string[], planId: string) => {
+ const response = await api.post(`/plan/assignLocationsToTeam/${planId}`, {
+ organizationIdentifier: teamId,
+ locationIdentifiers: locationId
+ });
+
+ return response.data;
+};
+
+export const getLocationsAssignedToATeam = async (teamId: string, planId: string) => {
+ const response = await api.get(`/plan/${planId}/assigned-locations/${teamId}`);
+
+ return response.data;
+};
diff --git a/src/features/planSimulation/components/Campaign/Target.module.css b/src/features/planSimulation/components/Campaign/Target.module.css
new file mode 100644
index 00000000..305fb6c8
--- /dev/null
+++ b/src/features/planSimulation/components/Campaign/Target.module.css
@@ -0,0 +1,65 @@
+.container {
+ display: flex;
+ flex-direction: column;
+}
+
+.targetItem {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.3rem 0.8rem;
+ transition: background-color 0.3s;
+}
+
+.targetItem:hover {
+ background-color: #f3f3f3;
+}
+
+.dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background-color: #0ac600;
+ margin-right: 0;
+ margin-top: 10;
+}
+
+.value {
+ font-size: 12px;
+ font-weight: bold;
+ color: #333;
+ display: flex;
+}
+
+.text {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.hoverButton {
+ border: none;
+ cursor: pointer;
+ padding: 0;
+ background-color: transparent;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.hoverButton img {
+ transition: width 0.2s;
+ width: 0rem;
+ opacity: 0.7;
+ height: 17px;
+}
+
+.targetItem:hover .hoverButton img {
+ visibility: visible;
+ width: 17px;
+ height: 17px;
+}
+
+.targetItem:hover .hoverButton {
+ margin-left: 0.5rem;
+}
diff --git a/src/features/planSimulation/components/Campaign/Target.tsx b/src/features/planSimulation/components/Campaign/Target.tsx
new file mode 100644
index 00000000..caa16d04
--- /dev/null
+++ b/src/features/planSimulation/components/Campaign/Target.tsx
@@ -0,0 +1,32 @@
+import style from './Target.module.css';
+import Delete from '../../../../assets/svgs/trash-bin.svg';
+
+const Target = ({ targetAreas }: any) => {
+ console.log();
+
+ return (
+ <>
+
+ {targetAreas.targetAreasList.map((area: any, index: number) => (
+
+
+
+
{area?.properties?.name || ''}
+
+
+ {/*
{Math.round(area?.properties?.population?.sum) || ''}
*/}
+
{Math.round(area?.properties?.numberOfTeams)}
+
+ {targetAreas.remove && (
+ targetAreas.remove(area.identifier)} />
+ )}
+
+
+
+ ))}
+
+ >
+ );
+};
+
+export default Target;
diff --git a/src/features/planSimulation/components/CampaignManagement.module.css b/src/features/planSimulation/components/CampaignManagement.module.css
new file mode 100644
index 00000000..e5730b39
--- /dev/null
+++ b/src/features/planSimulation/components/CampaignManagement.module.css
@@ -0,0 +1,13 @@
+.modalButton{
+ margin-top: 30px;
+ width: 200px;
+ color: white;
+
+}
+
+.modalContainer{
+ margin-top: 30px;
+ width: 400px;
+ height: 100px;
+
+}
\ No newline at end of file
diff --git a/src/features/planSimulation/components/CampaignManagement.tsx b/src/features/planSimulation/components/CampaignManagement.tsx
new file mode 100644
index 00000000..383a12a4
--- /dev/null
+++ b/src/features/planSimulation/components/CampaignManagement.tsx
@@ -0,0 +1,730 @@
+import { re } from 'mathjs';
+import { useRef, useState, useEffect, useCallback, useMemo } from 'react';
+import { Button, Col, Container, Form, Modal, Row } from 'react-bootstrap';
+import { useWindowResize } from '../../../hooks/useWindowResize';
+import { getGeneratedLocationHierarchyList, getLocationHierarchyList } from '../../location/api';
+import { LocationHierarchyModel } from '../../location/providers/types';
+import { Drawer } from '../../location/components/drawer/Drawer';
+import Accordion from '../../location/components/accordion/Accordion';
+import DrawerButton from '../../../components/DrawerButton/DrawerButton';
+import { CustomPopup } from '../../../components/CustomPopup/CustomPopup';
+import Target from './Campaign/Target';
+import Teams from './Teams/Teams';
+import UserModal from './UsersModal';
+import style from './CampaignManagement.module.css';
+import { bbox, Geometry, polygon } from '@turf/turf';
+import { LngLatBounds, Map as MapBoxMap } from 'mapbox-gl';
+
+import {
+ getDefaultHierarchyData,
+ getHierarchy,
+ getHierarchyPolygon,
+ getPlanInfo
+} from './SimulationMapView/api/hierarchyAPI';
+import Hierarchy from './Hierarchy/Hierarchy';
+import SimulationMapView from './SimulationMapView/SimulationMapView';
+import { Color } from 'react-color-palette';
+import { hex } from 'color-convert';
+import { usePolygonContext } from '../../../contexts/PolygonContext';
+import AddTargetAreaForm from './SimulationMapView/components/AddTargetAreaForm/AddTargetAreaForm';
+import AddDatasetForm from './SimulationMapView/components/AddDatasetForm/AddDatasetForm';
+import CampaignTotalsAccordion from './SimulationMapView/components/CampaignTotalsAccordion/CampaignTotalsAccordion';
+import DatasetsAccordion from '../../location/components/DatasetsAccordion/DatasetsAccordion';
+import Dashboard from './Dashboard/Dashboard';
+
+import {
+ getDataAssociatedEntityTags,
+ getEntityList,
+ getEventBasedEntityTags,
+ getFullLocationsSSE,
+ getLocationsSSE,
+ submitSimulationRequest,
+ updateSimulationRequest
+} from '../api';
+import {
+ ComplexTagResponse,
+ EntityTag,
+ HierarchyType,
+ LocationMetadataObj,
+ Metadata,
+ MetadataDefinition,
+ OperatorSignEnum,
+ PlanningLocationResponse,
+ PlanningLocationResponseTagged,
+ PlanningParentLocationResponse,
+ RevealFeature,
+ SearchLocationProperties
+} from '../providers/types';
+
+import {
+ AddDatasetResponse,
+ DataSetList,
+ deleteDataset,
+ getLocationPolygonsWithDatasets,
+ getSimulationData,
+ LocationData
+} from './SimulationMapView/api/datasetsAPI';
+import { toast } from 'react-toastify';
+import { assignLocationsToPlan } from '../../assignment/api';
+import { getReportForLocation } from '../reportsAPI/reportsApi';
+import { getOrganizationListSummary, getOrganizatonsWithMembers } from './Teams/api/teamAPI';
+
+export interface Stats {
+ [key: string]: Metadata;
+}
+export interface Children {
+ level: string;
+ childrenList: string[];
+}
+export interface StatsLayer {
+ [layer: string]: Stats;
+}
+export interface MarkedLocation {
+ identifier: string;
+ ancestry: string[] | undefined;
+}
+export interface AnalysisLayer {
+ labelName: string;
+ color: Color;
+ colorHex: string;
+}
+
+const CampaignManagement = () => {
+ const divRef = useRef(null);
+ const divHeight = useWindowResize(divRef.current);
+ const [mapFullScreen, setMapFullScreen] = useState(true);
+ const [showResult, setShowResult] = useState(false);
+ const [combinedHierarchyList, setCombinedHierarchyList] = useState();
+ const [leftOpen, setLeftOpen] = useState(false);
+ const [rightOpen, setRightOpen] = useState(false);
+ const [showModal, setShowModal] = useState(false);
+ const [highestLocations, setHighestLocations] = useState();
+ const [resultsLoadingState, setResultsLoadingState] = useState<'notstarted' | 'error' | 'started' | 'complete'>(
+ 'notstarted'
+ );
+ const [parentsLoadingState, setParentsLoadingState] = useState<'notstarted' | 'error' | 'started' | 'complete'>(
+ 'notstarted'
+ );
+ const [currentLocationId, setCurrentLocationId] = useState();
+ const [polygonsWithData, setPolygonsWithData] = useState();
+ const [includeGeometry, setIncludeGeometry] = useState(true);
+ const [selectedLocationChildren, setSelectedLocationChildren] = useState([]);
+ const [geometry, setGeometry] = useState();
+ const [toLocation, setToLocation] = useState();
+ const [resetMap, setResetMap] = useState(false);
+ const [mapData, setMapData] = useState();
+ const [statsLayerMetadata, setStatsLayerMetadata] = useState({});
+ const map = useRef();
+ const [markedLocations, setMarkedLocations] = useState([]);
+ const [datasetList, setDatasetList] = useState([]);
+ const [openCustomModal, setOpenCustomModal] = useState();
+ const [entityTags, setEntityTags] = useState([]);
+ const [entityTagsOriginal, setEntityTagsOriginal] = useState([]);
+ const [parentMapData, setParentMapData] = useState();
+ const [parentChild, setParentChild] = useState<{ [parent: string]: Children }>({});
+ const [analysisResultEntityTags, setAnalysisResultEntityTags] = useState();
+ const [analysisLayerDetails, setAnalysisLayerDetails] = useState([]);
+ const [chartData, setChartData] = useState>({});
+ const [totals, setTotals] = useState>({});
+ const [selectedMapData, setSelectedMapData] = useState();
+ const [summary, setSummary] = useState({});
+ const levelsLoaded = useRef([]);
+ const [aggregationSummary, setAggregationSummary] = useState({});
+ const [aggregationSummaryDefinition, setAggregationSummaryDefinition] = useState({});
+ const [mapDataLoad, setMapDataLoad] = useState({
+ features: [],
+ parents: [],
+ type: 'FeatureCollection',
+ identifier: undefined,
+ method: undefined,
+ source: undefined
+ });
+
+ const [locationReport, setLocationReport] = useState({});
+ const [teamsList, setTeamsList] = useState([]);
+
+ const { dispatch } = usePolygonContext();
+ const { state } = usePolygonContext();
+ const [labels, setLabels] = useState([]);
+
+ const fetchSimulationAndData = async () => {
+ const simulationIdentifier = await fetchPlanInfo();
+
+ try {
+ const simulationData = await getSimulationData(simulationIdentifier);
+ dispatch({ type: 'SET_NEW_DATASETS', payload: simulationData.datasets });
+ dispatch({ type: 'SET_SIMULATION_ID', payload: simulationData.identifier });
+ dispatch({ type: 'SET_TARGET_AREAS', payload: simulationData.targetAreas });
+ } catch (error) {
+ console.error('Failed to fetch simulation:', error);
+ }
+ };
+
+ const fetchDefaultHierarchyData = async () => {
+ const hierarchyData = await getDefaultHierarchyData();
+ try {
+ dispatch({ type: 'SET_DEFAULT_HIERARCHY_DATA', payload: hierarchyData });
+ } catch (error) {
+ console.error('Failed to fetch hierarchy data:', error);
+ }
+ };
+
+ useMemo(() => {
+ setDatasetList(state.datasets);
+ }, [state.datasets]);
+
+ // we are updating selectedLocationChildren whenever an assignment happens,
+ // because assigned flag on these locations is not updated (it is still the one we got on location fetch)
+ useEffect(() => {
+ setSelectedLocationChildren(prev =>
+ prev.map(obj => ({
+ ...obj,
+ properties: {
+ ...obj.properties,
+ assigned: state.assingedLocations[obj.identifier]
+ }
+ }))
+ );
+ }, [state.assingedLocations]);
+
+ useEffect(() => {
+ if (currentLocationId && polygonsWithData && polygonsWithData[currentLocationId]) {
+ const children = Object.values(polygonsWithData)
+ .map((polygon: any) => polygon.polygonData)
+ .filter((polygon: any) => polygon.properties.parentIdentifier === currentLocationId);
+
+ setSelectedLocationChildren(children);
+
+ // when locations loaded, we are setting their assigned flag values as default values in assignment map
+ // this way, state.assignedLocations is our single source of truth
+ const assignedMap = children.reduce(
+ (map, obj) => {
+ return {
+ ...map,
+ [obj.identifier]: map[obj.identifier] ?? obj.properties.assigned
+ };
+ },
+ { ...state.assingedLocations }
+ );
+ dispatch({ type: 'SET_ASSIGNED', payload: assignedMap });
+
+ const selectedLocation = polygonsWithData[currentLocationId].polygonData;
+
+ if (selectedLocation) {
+ setGeometry(selectedLocation);
+ setToLocation(JSON.parse(JSON.stringify(bbox(selectedLocation.geometry))));
+
+ // report on location
+ getReportForLocation(state.planid, selectedLocation.identifier).then(report => {
+ setLocationReport(report);
+ });
+
+ const populationData = transformPopulationData(selectedLocation?.properties?.population);
+ if (populationData !== null) {
+ setChartData(populationData.chartData);
+ setLabels(populationData.labels);
+ setTotals(populationData.totals);
+ }
+ }
+ }
+ }, [currentLocationId, polygonsWithData]);
+
+ useEffect(() => {
+ let populationData: any;
+ if (state.selected) {
+ populationData = transformPopulationData(JSON.parse(state.selected.population));
+
+ getReportForLocation(state.planid, state.selected.id).then(report => {
+ setLocationReport(report);
+ });
+ if (populationData !== null) {
+ setChartData(populationData.chartData);
+ setLabels(populationData.labels);
+ setTotals(populationData.totals);
+ }
+ } else if (!state.selected && currentLocationId) {
+ const selectedLocation = polygonsWithData[currentLocationId].polygonData;
+ const populationData = transformPopulationData(selectedLocation?.properties?.population);
+ if (populationData !== null) {
+ setChartData(populationData.chartData);
+ setLabels(populationData.labels);
+ setTotals(populationData.totals);
+ }
+ }
+ }, [state.selected]);
+
+ useEffect(() => {
+ fetchHierarchy();
+ fetchSimulationAndData();
+ fetchDefaultHierarchyData();
+ }, []);
+
+ // AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+
+ useEffect(() => {
+ Promise.all([
+ getLocationHierarchyList(50, 0, true),
+ getEntityList(),
+ getGeneratedLocationHierarchyList()
+ // getComplexTagReponses()
+ ])
+ .then(([locationHierarchyList, entityList, generatedHierarchyList]) => {
+ let generatedHierarchyItems = generatedHierarchyList?.map(generatedHierarchy => {
+ return {
+ identifier: generatedHierarchy.identifier,
+ name: generatedHierarchy.name,
+ nodeOrder: generatedHierarchy.nodeOrder,
+ type: HierarchyType.GENERATED
+ };
+ });
+
+ let list = locationHierarchyList?.content.map(savedHierarchy => {
+ return {
+ identifier: savedHierarchy.identifier,
+ name: savedHierarchy.name,
+ nodeOrder: savedHierarchy.nodeOrder,
+ type: HierarchyType.SAVED
+ };
+ });
+
+ let combinedList = list.concat(generatedHierarchyItems);
+ setCombinedHierarchyList(combinedList);
+
+ // setComplexTags(complexTagResponses);
+ })
+ .catch(err => toast.error(err));
+ }, []);
+
+ const updateMarkedLocations = (identifier: string, ancestry: string[] | undefined, marked: boolean) => {
+ setMarkedLocations(markedLocations => {
+ let newMarkedLocations: MarkedLocation[] = [];
+
+ if (markedLocations) {
+ markedLocations.forEach(markedLocation => {
+ newMarkedLocations.push({
+ identifier: markedLocation.identifier,
+ ancestry: markedLocation.ancestry
+ });
+ });
+ if (!marked) {
+ newMarkedLocations = newMarkedLocations?.filter(markedLocation => markedLocation.identifier !== identifier);
+ } else {
+ if (!newMarkedLocations.map(markedLocation => markedLocation.identifier).includes(identifier)) {
+ newMarkedLocations.push({
+ identifier: identifier,
+ ancestry: ancestry
+ });
+ }
+ }
+ }
+ return newMarkedLocations;
+ });
+ };
+
+ useEffect(() => {
+ if (markedLocations.length > 0) {
+ let newMarkedParents = new Set();
+ markedLocations.forEach(markedLocation => {
+ markedLocation.ancestry?.forEach(ancestor => {
+ if (ancestor !== markedLocation.identifier) {
+ newMarkedParents.add(ancestor);
+ }
+ });
+ });
+ }
+ }, [markedLocations]);
+
+ const updateParentAsHasResultOrIsResult = (
+ parent: RevealFeature,
+ lowestLocation: RevealFeature,
+ mapDataClone: PlanningLocationResponseTagged
+ ) => {
+ if (parent.children) {
+ if (!parent.children.map((locationChild: any) => locationChild.identifier).includes(lowestLocation.identifier)) {
+ parent.children.push(lowestLocation);
+ }
+ } else {
+ parent.children = [];
+ parent.children.push(lowestLocation);
+ if (parent.properties != null) {
+ if (parent.identifier) {
+ parent.properties.result = mapDataClone?.features[parent.identifier] != null;
+ }
+ }
+ }
+ if (parent.properties != null) {
+ if (
+ !parent.properties.hasOwnProperty('hasResultChild') ||
+ (parent.properties.hasOwnProperty('hasResultChild') && !parent.properties.hasResultChild)
+ ) {
+ if (lowestLocation.properties != null) {
+ parent.properties.hasResultChild = !!(
+ mapDataClone?.features[lowestLocation.properties.identifier] != null ||
+ lowestLocation.properties?.hasResultChild ||
+ lowestLocation.properties?.result
+ );
+ }
+ }
+
+ if (
+ !parent.properties.hasOwnProperty('hasMarkedChild') ||
+ (parent.properties.hasOwnProperty('hasMarkedChild') && !parent.properties.hasMarkedChild)
+ ) {
+ if (lowestLocation.properties != null) {
+ parent.properties.hasMarkedChild = !!(
+ lowestLocation.properties?.mark || lowestLocation.properties?.hasMarkedChild
+ );
+ }
+ }
+ }
+ };
+
+ const getLocationHierarchyFromLowestLocation = useCallback(
+ (lowestLocation: RevealFeature, mapDataClone: PlanningLocationResponseTagged) => {
+ let parent: RevealFeature = mapDataClone?.parents[lowestLocation.properties?.parent];
+
+ if (parent) {
+ updateParentAsHasResultOrIsResult(parent, lowestLocation, mapDataClone);
+ setParentChild(newParentChild => {
+ if (lowestLocation?.identifier) {
+ if (parent.identifier) {
+ if (newParentChild[parent.identifier] && newParentChild[parent.identifier].childrenList.length > 0) {
+ if (!newParentChild[parent.identifier].childrenList.includes(lowestLocation?.identifier)) {
+ newParentChild[parent.identifier].childrenList.push(lowestLocation?.identifier);
+ }
+ } else {
+ newParentChild[parent.identifier] = {
+ level: lowestLocation.properties?.geographicLevel,
+ childrenList: [lowestLocation.identifier]
+ };
+ }
+ newParentChild[parent.identifier].level = lowestLocation.properties?.geographicLevel;
+ }
+ }
+ return newParentChild;
+ });
+ getLocationHierarchyFromLowestLocation(parent, mapDataClone);
+ }
+ },
+ []
+ );
+
+ useEffect(() => {
+ if (mapData && mapData?.features && Object.keys(mapData?.features).length > 0) {
+ let max = Number.MIN_VALUE;
+
+ if (max != null) {
+ Object.keys(mapData?.features).forEach(key => {
+ if (mapData?.features[key]?.properties?.geographicLevelNodeNumber > max) {
+ max = mapData?.features[key]?.properties?.geographicLevelNodeNumber;
+ }
+ });
+
+ let lowestLocations: RevealFeature[] = Object.keys(mapData.features)
+ .filter(key => mapData.features[key].properties?.geographicLevelNodeNumber === max)
+ .map(key => {
+ return mapData.features[key];
+ })
+ .map((val: RevealFeature) => {
+ if (val.properties != null) {
+ val.properties.result = true;
+ }
+ return val;
+ });
+
+ if (!mapData.source || mapData.source !== 'uploadHandler') {
+ lowestLocations.forEach(lowestLocation => {
+ getLocationHierarchyFromLowestLocation(lowestLocation, mapData);
+ });
+ }
+ }
+
+ let min = Number.MAX_VALUE;
+
+ if (min !== null) {
+ Object.keys(mapData?.parents).forEach(key => {
+ if (mapData?.parents[key]?.properties?.geographicLevelNodeNumber < min) {
+ min = mapData?.parents[key]?.properties?.geographicLevelNodeNumber;
+ }
+ });
+
+ // if (mapData.parents) {
+ // let highestLocations: any[] = Object.keys(mapData.parents)
+ // .filter(
+ // key =>
+ // mapData.parents[key].properties !== null &&
+ // mapData.parents[key].properties?.geographicLevelNodeNumber === min
+ // )
+ // .map(key => mapData.parents[key]);
+ // // setHighestLocations(highestLocations);
+ // }
+ }
+ }
+ }, [mapData, getLocationHierarchyFromLowestLocation, markedLocations]);
+
+ const fetchHierarchy = async () => {
+ const hierarchyData = await getHierarchy();
+ try {
+ setHighestLocations(hierarchyData);
+ dispatch({ type: 'SET_HIERARCHY', payload: hierarchyData });
+ } catch (error) {
+ console.error('Failed to fetch hierarchy:', error);
+ }
+ };
+ const fetchPlanInfo = async () => {
+ try {
+ const planInfo = await getPlanInfo();
+ dispatch({ type: 'SET_PLANID', payload: planInfo.identifier });
+ return planInfo.identifier;
+ } catch (error) {
+ console.error('Failed to fetch plan info:', error);
+ }
+ };
+
+ const checkifChildrenLoaded = (polygonsWithData: any, selectedLocationId: any) => {
+ if (polygonsWithData?.[selectedLocationId]?.childrenLoaded === undefined) {
+ return true;
+ } else if (polygonsWithData?.[selectedLocationId]?.childrenLoaded === true) {
+ return false;
+ } else {
+ return true;
+ }
+ };
+
+ useEffect(() => {
+ fetchHierarchy();
+ fetchSimulationAndData();
+ }, []);
+
+ const loadLocationHandler = async (locationId: string) => {
+ setCurrentLocationId(locationId);
+ if (state.datasets.length === 0 && polygonsWithData?.[locationId]?.childrenLoaded) {
+ setIncludeGeometry(false);
+
+ const k = Object.values(polygonsWithData)
+ .map((polygon: any) => polygon.polygonData)
+ .filter(p => p.properties.parentIdentifier === locationId);
+
+ setSelectedLocationChildren(k);
+
+ const selectedLocation = polygonsWithData?.[locationId]?.polygonData;
+ if (selectedLocation) {
+ setGeometry(selectedLocation);
+ setToLocation(JSON.parse(JSON.stringify(bbox(selectedLocation.geometry))));
+ }
+ } else {
+ const includeGeometry: boolean = checkifChildrenLoaded(polygonsWithData, locationId);
+
+ let configObj: any = {
+ datasetsIds: [],
+ includeGeometry: includeGeometry,
+ parentLocationId: locationId, //current location identifier
+ simulationId: state.simulationId
+ };
+
+ const polygonsWithDatasets = await getLocationPolygonsWithDatasets(configObj);
+ if (!polygonsWithDatasets || polygonsWithDatasets.length === 0) {
+ toast.error('Cannot get results. Please try again.');
+ return;
+ }
+
+ if (includeGeometry) {
+ setPolygonsWithData((prev: any) => {
+ const updatedPolygons = { ...prev };
+ polygonsWithDatasets.forEach((location: any) => {
+ updatedPolygons[location.identifier] = {
+ polygonData: location,
+ childrenLoaded: location.identifier === locationId
+ };
+ });
+
+ return updatedPolygons;
+ });
+ } else {
+ setPolygonsWithData((prev: any) => {
+ const updatedPolygons = { ...prev };
+ // polygonsWithDatasets.forEach((location: any) => {
+ // updatedPolygons[location.identifier].polygonData.properties.metadata = location.properties.metadata;
+ // });
+
+ return updatedPolygons;
+ });
+ }
+ }
+ };
+
+ const processChildren = useCallback(
+ (mapDataClone: any) => {
+ let geoLevel: string = mapDataClone.properties.geographicLevel;
+
+ if (!summary[geoLevel]) {
+ summary[geoLevel] = {};
+ }
+ summary[geoLevel][mapDataClone.identifier] = mapDataClone.properties;
+ summary[geoLevel][mapDataClone.identifier]['aggregates'] = mapDataClone.aggregates;
+ setSummary(summary);
+
+ if (mapDataClone.children) {
+ mapDataClone.children.forEach((child: any) => processChildren(child));
+ }
+ },
+ [summary]
+ );
+
+ useEffect(() => {
+ if (Object.keys(summary).length === 0) {
+ if (selectedMapData) {
+ processChildren(selectedMapData);
+ }
+ }
+ }, [selectedMapData, summary, processChildren]);
+
+ const handleRemoveTargetArea = (id: string) => {
+ const assignedAreas = state.targetAreas?.flatMap((ta: any) => [...ta.ancestry, ta.identifier]) || [];
+ const targetArea = state.targetAreas?.find(ta => ta.identifier === id);
+ const toExcludeSet = new Set([...targetArea.ancestry, id]);
+ const filtered = assignedAreas.filter(item => !toExcludeSet.has(item));
+ assignLocationsToPlan(state.planid, filtered).then(async () => {
+ // update assignment map
+ dispatch({ type: 'SET_ASSIGNED', payload: { ...state.assingedLocations, [id]: false } });
+ // refetch target areas, so the map updates
+ const simulationData = await getSimulationData(state.planid);
+ dispatch({ type: 'SET_TARGET_AREAS', payload: simulationData.targetAreas });
+ dispatch({ type: 'CLEAR_SELECTION' });
+ });
+ };
+
+ const fetchTeamsData = async () => {
+ getOrganizatonsWithMembers().then((data: any) => {
+ setTeamsList(data);
+ });
+ };
+
+ const campaignTotals = {
+ label: 'Target Areas',
+ total: state.targetAreas.length,
+ targetAreasList: state.targetAreas,
+ remove: handleRemoveTargetArea
+ };
+
+ return (
+ <>
+
+
+
+ {highestLocations && (
+
+
+
+ )}
+
+
+ setOpenCustomModal(1)}>Manage Teams
+ setOpenCustomModal(undefined)} hasBackdrop>
+
+
+
+
+
setLeftOpen(!leftOpen)}
+ leftOpenState={leftOpen}
+ rightOpenState={rightOpen}
+ rightOpenHandler={() => setRightOpen(!rightOpen)}
+ fullScreenHandler={() => {
+ setMapFullScreen(!mapFullScreen);
+ }}
+ fullScreen={mapFullScreen}
+ toLocation={toLocation} // bbox
+ entityTags={entityTags}
+ parentMapData={parentMapData}
+ setMapDataLoad={setMapDataLoad}
+ chunkedData={mapDataLoad}
+ resetMap={resetMap}
+ setResetMap={setResetMap}
+ stats={statsLayerMetadata}
+ resultsLoadingState={resultsLoadingState}
+ parentsLoadingState={parentsLoadingState}
+ map={map}
+ updateMarkedLocations={updateMarkedLocations}
+ parentChild={parentChild}
+ analysisLayerDetails={analysisLayerDetails}
+ />
+
+ {Object.keys(locationReport).length > 0 && (
+
+
+
+ )}
+ {campaignTotals.targetAreasList.length !== 0 && (
+
+
+
+ )}
+
+
+
+ >
+ );
+};
+
+export default CampaignManagement;
+
+const transformPopulationData = (population: any) => {
+ const mergedPyramids = mergeAgeGroups(population?.Pyramids) || [];
+ if (!mergedPyramids || mergedPyramids.length === 0) {
+ return null;
+ }
+ const ageGroups = mergedPyramids.map((group: any) => group.AgeGroup.replace('_', '-'));
+ const summaryData = mergedPyramids.map((group: any) => Math.round(group.TotalPop));
+ const maleData = mergedPyramids.map((group: any) => Math.round(group.MalePop));
+ const femaleData = mergedPyramids.map((group: any) => Math.round(group.FemalePop));
+
+ return {
+ labels: ageGroups,
+ chartData: {
+ summary: summaryData,
+ male: maleData,
+ female: femaleData
+ },
+ totals: {
+ summary: Math.round(population.sum),
+ male: Math.round(population.male),
+ female: Math.round(population.female)
+ }
+ };
+};
+
+const mergeAgeGroups = (pyramids: any[]) => {
+ if (!pyramids || pyramids.length === 0) return [];
+ const mergedGroups: any[] = [];
+
+ for (let i = 0; i < pyramids.length; i += 2) {
+ const first = pyramids[i];
+ const second = pyramids[i + 1] || null;
+
+ const mergedGroup = {
+ AgeGroup: second
+ ? `${first.AgeGroup.split('_')[0]}-${second.AgeGroup.split('_')[1]}`
+ : first.AgeGroup.replace('_', '-'),
+ MalePop: Math.round(first.MalePop + (second?.MalePop || 0)),
+ FemalePop: Math.round(first.FemalePop + (second?.FemalePop || 0)),
+ TotalPop: Math.round(first.TotalPop + (second?.TotalPop || 0))
+ };
+
+ mergedGroups.push(mergedGroup);
+ }
+
+ return mergedGroups;
+};
diff --git a/src/features/planSimulation/components/Dashboard/ChartSwitch/ChartSwitch.module.css b/src/features/planSimulation/components/Dashboard/ChartSwitch/ChartSwitch.module.css
new file mode 100644
index 00000000..a0ccf186
--- /dev/null
+++ b/src/features/planSimulation/components/Dashboard/ChartSwitch/ChartSwitch.module.css
@@ -0,0 +1,55 @@
+.container {
+ width: 100%;
+ max-width: 28rem;
+ margin: 0 auto;
+}
+
+.switchContainer {
+ padding: 4px;
+ position: relative;
+ background-color: #f3f4f6;
+ border-radius: 0.5rem;
+ display: flex;
+ align-items: center;
+}
+
+.switchBackground {
+ position: absolute;
+ height: calc(100% - 8px);
+ width: 32.2%; /* Adjust width for each button */
+ background-color: white;
+ border-radius: 0.375rem;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+ transition: transform 300ms ease-in-out;
+}
+
+/* Adjust position for each selected option */
+.switchBackground.middle {
+ transform: translateX(100%); /* Center on the middle button */
+}
+
+.switchBackground.right {
+ transform: translateX(200%); /* Move to the right button */
+}
+
+.option {
+ position: relative;
+ flex: 1;
+ width: 33.33%;
+ padding: 0.5rem 0;
+ font-size: 0.875rem;
+ font-weight: 500;
+ border: none;
+ background: none;
+ cursor: pointer;
+ transition: color 300ms;
+ text-align: center; /* Center text within each button */
+}
+
+.option.active {
+ color: #111827; /* Active text color */
+}
+
+.option.inactive {
+ color: #6b7280; /* Inactive text color */
+}
diff --git a/src/features/planSimulation/components/Dashboard/ChartSwitch/ChartSwitch.tsx b/src/features/planSimulation/components/Dashboard/ChartSwitch/ChartSwitch.tsx
new file mode 100644
index 00000000..d8acefc3
--- /dev/null
+++ b/src/features/planSimulation/components/Dashboard/ChartSwitch/ChartSwitch.tsx
@@ -0,0 +1,51 @@
+import { useState } from 'react';
+import styles from './ChartSwitch.module.css';
+
+interface SwitchProps {
+ leftOption: string;
+ middleOption: string;
+ rightOption: string;
+ onOptionChange: (option: string) => void;
+}
+
+export function ChartSwitch({ leftOption, middleOption, rightOption, onOptionChange }: SwitchProps) {
+ const [selectedOption, setSelectedOption] = useState(leftOption);
+
+ const handleOptionClick = (option: string) => {
+ setSelectedOption(option);
+ onOptionChange(option); // Notify the parent component
+ };
+
+ return (
+
+
+
+
+
handleOptionClick(leftOption)}
+ className={`${styles.option} ${selectedOption === leftOption ? styles.active : styles.inactive}`}
+ >
+ {leftOption}
+
+
+
handleOptionClick(middleOption)}
+ className={`${styles.option} ${selectedOption === middleOption ? styles.active : styles.inactive}`}
+ >
+ {middleOption}
+
+
+
handleOptionClick(rightOption)}
+ className={`${styles.option} ${selectedOption === rightOption ? styles.active : styles.inactive}`}
+ >
+ {rightOption}
+
+
+
+ );
+}
diff --git a/src/features/planSimulation/components/Dashboard/Dashboard.module.css b/src/features/planSimulation/components/Dashboard/Dashboard.module.css
new file mode 100644
index 00000000..9e60ac33
--- /dev/null
+++ b/src/features/planSimulation/components/Dashboard/Dashboard.module.css
@@ -0,0 +1,127 @@
+.statisticsWrapper {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ gap: 1rem;
+}
+
+.dashBoardSectionWrapper {
+ padding: 1rem;
+ border: 1px solid #0000001a;
+ border-radius: 5px;
+ background-color: white;
+}
+
+.populationHeading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 1.5rem;
+}
+
+.populationHeadingIcon {
+ margin-right: 0.5rem;
+ font-size: 1.5rem;
+ color: #000000c4;
+}
+
+.populationHeadingH3 {
+ margin: 0;
+ font-size: 1.2rem;
+}
+
+.populationChartWrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.populationChartSum {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ padding-top: 15px;
+}
+
+.populationChartSumH3 {
+ font-size: 1rem;
+ margin: 0;
+}
+
+.populationChartSumP {
+ font-size: 2rem;
+ font-weight: bold;
+ margin: 0;
+}
+
+.doughnut {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.structureWrapper {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.structureText {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 1rem;
+}
+
+.facilitiesStatisticWrapper {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 1rem;
+}
+
+.facilitiesWrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.facilitiesText {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.houseIcon {
+ height: 17px;
+ width: 17px;
+ margin-right: 0.5rem;
+}
+
+.chartControlsWrapper {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background-color: #f3f4f6;
+ padding: 0.5rem;
+ border-radius: 5px;
+}
+
+.chartControlsButton {
+ padding: 0.2rem 1.2rem;
+ border: none;
+ border-radius: 5px;
+}
+
+.active {
+ color: #111827;
+ background-color: white;
+}
+
+.inactive {
+ color: #6b7280;
+}
diff --git a/src/features/planSimulation/components/Dashboard/Dashboard.tsx b/src/features/planSimulation/components/Dashboard/Dashboard.tsx
new file mode 100644
index 00000000..ac97bb6e
--- /dev/null
+++ b/src/features/planSimulation/components/Dashboard/Dashboard.tsx
@@ -0,0 +1,137 @@
+import style from './Dashboard.module.css';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { DoghnutChart } from '../../../location/components/doughnutChart/DoghnutChart';
+import { faUsers, faSitemap, faHouseUser, faDiceD20 } from '@fortawesome/free-solid-svg-icons';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { useState } from 'react';
+import { ChartSwitch } from './ChartSwitch/ChartSwitch';
+import StackedBarChart from '../../../location/components/StackedBarChart/StackedBarChart';
+import GaugeChart from '../../../location/components/GaugeChart/GaugeChart';
+
+library.add(faUsers, faSitemap, faHouseUser, faDiceD20);
+
+interface DashboardProps {
+ chartData: Record;
+ chartLabels: string[];
+ totals: Record;
+ polulationChart?: boolean;
+ buildingsChart?: boolean;
+ targetAreaChart?: boolean;
+ structures?: number;
+ locationReport?: any;
+}
+
+function Dashboard({
+ locationReport,
+ chartData,
+ chartLabels,
+ totals,
+ polulationChart = true,
+ buildingsChart = true,
+ targetAreaChart = false,
+ structures = 0
+}: DashboardProps) {
+ const [chartDataType, setChartDataType] = useState<'summary' | 'male' | 'female'>('summary');
+
+ const handleOptionChange = (option: string) => {
+ // Update chart data based on the selected option
+ switch (option) {
+ case 'Summary':
+ setChartDataType('summary');
+ break;
+ case 'Male':
+ setChartDataType('male');
+ break;
+ case 'Female':
+ setChartDataType('female');
+ break;
+ default:
+ setChartDataType('summary');
+ }
+ };
+
+ const generateChartData = [
+ { label: 'Complete', data: [locationReport?.totalComplete], backgroundColor: '#008000' },
+ { label: 'Incomplete', data: [locationReport?.totalIncomplete], backgroundColor: '#cd1c18' },
+ { label: 'Not Visited', data: [locationReport?.totalNotVisited], backgroundColor: '#FFE066' }
+ ];
+
+ return (
+
+ {polulationChart && (
+
+
+
+
Population
+
+
+
+
+
+
+
Total:
+
{totals[chartDataType]?.toLocaleString()}
+
+
+
+
+ )}
+ {targetAreaChart && Object.keys(locationReport).length > 0 && (
+ <>
+
+
+
+ >
+ )}
+
+ {!!(buildingsChart && structures && structures > 0) && (
+
+
+
+
Structures
+
+
{structures}
+
+ )}
+
+ );
+}
+
+export default Dashboard;
diff --git a/src/features/planSimulation/components/Dataset/Dataset.module.css b/src/features/planSimulation/components/Dataset/Dataset.module.css
new file mode 100644
index 00000000..b2f35854
--- /dev/null
+++ b/src/features/planSimulation/components/Dataset/Dataset.module.css
@@ -0,0 +1,3 @@
+.filterWrapper {
+ padding-left: 1rem;
+}
diff --git a/src/features/planSimulation/components/Dataset/Dataset.tsx b/src/features/planSimulation/components/Dataset/Dataset.tsx
new file mode 100644
index 00000000..5f4e3d48
--- /dev/null
+++ b/src/features/planSimulation/components/Dataset/Dataset.tsx
@@ -0,0 +1,39 @@
+import { Color } from 'react-color-palette';
+import { DualRangeSlider } from '../../../../components/DualRangeSlider/DualRangeSlider';
+import SwitchButton from '../../../../components/SwitchButton/SwitchButton';
+import { useState } from 'react';
+
+interface DatasetProps {
+ dataset: {
+ name: string;
+ color: Color;
+ };
+ color?: Color;
+}
+
+function Dataset({ dataset, color }: DatasetProps) {
+ const [checked, setChecked] = useState(false);
+
+ return (
+ <>
+ setChecked(!checked)}
+ colorOne={color && color.hex}
+ />
+
+ >
+ );
+}
+
+export default Dataset;
diff --git a/src/features/planSimulation/components/Dataset/Filter/Filter.module.css b/src/features/planSimulation/components/Dataset/Filter/Filter.module.css
new file mode 100644
index 00000000..e69de29b
diff --git a/src/features/planSimulation/components/Dataset/Filter/Filter.tsx b/src/features/planSimulation/components/Dataset/Filter/Filter.tsx
new file mode 100644
index 00000000..070b94f6
--- /dev/null
+++ b/src/features/planSimulation/components/Dataset/Filter/Filter.tsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import SwitchButton from '../../../../../components/SwitchButton/SwitchButton';
+import { DualRangeSlider } from '../../../../../components/DualRangeSlider/DualRangeSlider';
+
+function Filter({ dataset, color }: any) {
+ const [checked, setChecked] = React.useState(false);
+
+ return (
+ <>
+ setChecked(!checked)}
+ colorOne={color.hex}
+ />
+
+ >
+ );
+}
+
+export default Filter;
diff --git a/src/features/planSimulation/components/Hierarchy/Hierarchy.module.css b/src/features/planSimulation/components/Hierarchy/Hierarchy.module.css
new file mode 100644
index 00000000..ec8465dd
--- /dev/null
+++ b/src/features/planSimulation/components/Hierarchy/Hierarchy.module.css
@@ -0,0 +1,78 @@
+.hierarchy {
+ margin-bottom: 10px;
+}
+
+.itemHeader {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin: 5px 0;
+ padding: 3px;
+ cursor: pointer;
+ border-radius: 0.375rem;
+ transition: all 0.2s;
+}
+
+.itemHeader:hover {
+ background-color: #f3f3f3;
+}
+
+.expandButton {
+ margin-left: 10px;
+ cursor: pointer;
+ background: none;
+ border: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 20px;
+ height: 20px;
+ border-radius: 0.375rem;
+ transition: all 0.2s;
+}
+
+.icon {
+ width: 0.9rem;
+ height: 0.9rem;
+}
+
+.removeButton {
+ width: 22px;
+ height: 22px;
+ cursor: pointer;
+ opacity: 0.5;
+}
+
+.expandButton:hover {
+ background-color: #ffffff;
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
+}
+
+.children {
+ margin-left: 15px;
+ border-left: 2px solid #cccccc63;
+ padding-left: 10px;
+ overflow: hidden;
+ transition: all 0.2s;
+}
+.search {
+ margin-bottom: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: white;
+ padding: 5px 10px;
+ border: 1px solid #0000001a;
+ border-radius: 0.375rem;
+}
+
+.searchBarWrapper {
+ width: 22px;
+ height: 22px;
+}
+
+.searchInput {
+ width: 100%;
+ padding: 5px;
+ border: none;
+}
diff --git a/src/features/planSimulation/components/Hierarchy/Hierarchy.tsx b/src/features/planSimulation/components/Hierarchy/Hierarchy.tsx
new file mode 100644
index 00000000..30b7bf90
--- /dev/null
+++ b/src/features/planSimulation/components/Hierarchy/Hierarchy.tsx
@@ -0,0 +1,141 @@
+import { useMemo, useState } from 'react';
+import styles from './Hierarchy.module.css';
+import search from '../../../../assets/svgs/search.svg';
+import remove from '../../../../assets/svgs/remove.svg';
+import HierarchyItem from './HierarchyItem/HierarchyItem';
+import { usePolygonContext } from '../../../../contexts/PolygonContext';
+export interface HierarchyItemProps {
+ identifier: string;
+ properties: {
+ assigned: false;
+ childrenNumber: number;
+ geographicLevel: string;
+ name: string;
+ parentIdentifier: string;
+ simulationSearchResult: boolean;
+ };
+ isOpen?: boolean;
+ children?: HierarchyItemProps[];
+}
+
+interface HierarchyProps {
+ // data: HierarchyItemProps[];
+ clickHandler: (id: string) => void;
+}
+
+function Hierarchy({ clickHandler }: HierarchyProps) {
+ const { state } = usePolygonContext();
+
+ const data = state.polygons;
+
+ const [filteredData, setFilteredData] = useState(data);
+
+ const [searchTerm, setSearchTerm] = useState('');
+
+ const toggleExpanded = (id: string) => {
+ const updateIsOpen = (items: HierarchyItemProps[]): HierarchyItemProps[] => {
+ return items.map(item => {
+ if (item.identifier === id) {
+ return { ...item, isOpen: !item.isOpen };
+ }
+ if (item.children) {
+ return { ...item, children: updateIsOpen(item.children) };
+ }
+ return item;
+ });
+ };
+
+ setFilteredData(prevData => updateIsOpen(prevData));
+ };
+
+ useMemo(() => {
+ setFilteredData(data);
+ }, [data]);
+
+ useMemo(() => {
+ if (state.selected?.parentIdentifier) {
+ setFilteredData(prevData => {
+ const updateData = (items: HierarchyItemProps[]): HierarchyItemProps[] => {
+ return items.map(item => {
+ if (item.identifier === state.selected?.parentIdentifier) {
+ return { ...item, isOpen: true };
+ }
+ if (item.children) {
+ return { ...item, children: updateData(item.children) };
+ }
+ return item;
+ });
+ };
+
+ return updateData(prevData);
+ });
+ }
+ }, [state.selected?.parentIdentifier]);
+
+ const filterData = (items: HierarchyItemProps[], term: string): HierarchyItemProps[] => {
+ return items
+ .map(item => {
+ const itemName = item.properties.name.toLowerCase();
+ const searchTerm = term.toLowerCase();
+
+ // Check if the item matches the search term
+ if (itemName.startsWith(searchTerm)) {
+ return {
+ ...item,
+ isOpen: true, // Expand this item
+ children: item.children ? filterData(item.children, term) : undefined
+ };
+ }
+
+ // Check if any children match the search term
+ if (item.children) {
+ const filteredChildren = filterData(item.children, term);
+ if (filteredChildren.length > 0) {
+ return {
+ ...item,
+ isOpen: true, // Expand this item since a child matches
+ children: filteredChildren
+ };
+ }
+ }
+
+ return null;
+ })
+ .filter(Boolean) as HierarchyItemProps[];
+ };
+
+ const handleSearch = (term: string) => {
+ setSearchTerm(term);
+ if (term === '') {
+ setFilteredData(data.map((item: any) => ({ ...item, isOpen: false }))); // Collapse all items
+ } else {
+ setFilteredData(filterData(data, term));
+ }
+ };
+
+ return (
+
+
+
handleSearch(e.target.value)}
+ />
+
+ {searchTerm.length > 0 ? (
+
handleSearch('')} />
+ ) : (
+
+ )}
+
+
+ {filteredData.map(item => (
+
+ ))}
+
+ );
+}
+
+export default Hierarchy;
diff --git a/src/features/planSimulation/components/Hierarchy/HierarchyItem/HierarchyItem.module.css b/src/features/planSimulation/components/Hierarchy/HierarchyItem/HierarchyItem.module.css
new file mode 100644
index 00000000..85748886
--- /dev/null
+++ b/src/features/planSimulation/components/Hierarchy/HierarchyItem/HierarchyItem.module.css
@@ -0,0 +1,5 @@
+.selected {
+ background-color: #f0f0f0;
+ border-left: 3px solid rgb(3, 166, 13);
+ padding-left: 10px;
+}
diff --git a/src/features/planSimulation/components/Hierarchy/HierarchyItem/HierarchyItem.tsx b/src/features/planSimulation/components/Hierarchy/HierarchyItem/HierarchyItem.tsx
new file mode 100644
index 00000000..df99cf64
--- /dev/null
+++ b/src/features/planSimulation/components/Hierarchy/HierarchyItem/HierarchyItem.tsx
@@ -0,0 +1,74 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import styles from '../Hierarchy.module.css';
+import { HierarchyItemProps } from '../Hierarchy';
+import itemStyle from './HierarchyItem.module.css';
+
+// CONTEXT
+import { usePolygonContext } from '../../../../../contexts/PolygonContext';
+import { useMemo, useRef, useState } from 'react';
+
+const HierarchyItem = ({
+ item,
+ toggleExpanded,
+ clickHandler
+}: {
+ item: HierarchyItemProps;
+ toggleExpanded: (id: string) => void;
+ clickHandler: (id: string) => void;
+}) => {
+ const { isOpen, children, properties, identifier } = item;
+ const { state, dispatch } = usePolygonContext();
+ const itemRef = useRef(null);
+
+ const singleSelect = state.selected;
+
+ const [selectedState, setSelectedState] = useState(singleSelect);
+
+ useMemo(() => {
+ setSelectedState(singleSelect);
+ }, [singleSelect]);
+
+ if (selectedState?.id === identifier && itemRef.current) {
+ itemRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+ }
+ return (
+
+
{
+ clickHandler(identifier);
+ }}
+ >
+
{properties.name}
+ {selectedState?.id === identifier &&
}
+ {children && children.length > 0 && (
+
{
+ e.stopPropagation();
+ toggleExpanded(identifier);
+ }}
+ className={styles.expandButton}
+ >
+
+
+ )}
+
+ {isOpen && children && (
+
+ {children.map(child => (
+
+ ))}
+
+ )}
+
+ );
+};
+
+export default HierarchyItem;
diff --git a/src/features/planSimulation/components/MultiselectList/MultiselectList.module.css b/src/features/planSimulation/components/MultiselectList/MultiselectList.module.css
new file mode 100644
index 00000000..cb5b00e3
--- /dev/null
+++ b/src/features/planSimulation/components/MultiselectList/MultiselectList.module.css
@@ -0,0 +1,58 @@
+.multiselectItem {
+ display: flex;
+ width: 100%;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.5rem;
+ padding: 0.5rem;
+ border-radius: 5px;
+}
+
+.multiselectItem_info {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: row;
+ gap: 0.5rem;
+}
+
+.multiselectItem_info p {
+ margin: 0;
+}
+
+.selectedIcon {
+ filter: brightness(0) saturate(100%) invert(21%) sepia(91%) saturate(7190%) hue-rotate(358deg) brightness(109%)
+ contrast(115%);
+ width: 10px;
+ height: 10px;
+ margin-right: 0.2rem;
+}
+
+.selectedPolygonUl {
+ display: flex;
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+}
+
+.removeLocationButton {
+ border: none;
+ padding: 0 0.5rem;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ text-decoration: none;
+ border-radius: 7px;
+ background: transparent;
+}
+
+.removeLocationButton img {
+ width: 17px;
+ height: 17px;
+ opacity: 0.7;
+ transition: all 0.2s;
+}
+
+.removeLocationButton:hover img {
+ opacity: 1;
+}
diff --git a/src/features/planSimulation/components/MultiselectList/MultiselectList.tsx b/src/features/planSimulation/components/MultiselectList/MultiselectList.tsx
new file mode 100644
index 00000000..fb8dc332
--- /dev/null
+++ b/src/features/planSimulation/components/MultiselectList/MultiselectList.tsx
@@ -0,0 +1,24 @@
+import task from '../../../../assets/svgs/task.svg';
+import Delete from '../../../../assets/svgs/trash-bin.svg';
+import styles from './MultiselectList.module.css';
+import { SelectedPolygon, usePolygonContext } from '../../../../contexts/PolygonContext';
+
+function MultiselectList({ selectedPolygon }: { selectedPolygon: any }) {
+ const { dispatch } = usePolygonContext();
+
+ return (
+
+
+
+
{selectedPolygon.properties.name}
+
+
dispatch({ type: 'TOGGLE_MULTISELECT', payload: selectedPolygon })}
+ >
+
+
+
+ );
+}
+export default MultiselectList;
diff --git a/src/features/planSimulation/components/Simulation.module.css b/src/features/planSimulation/components/Simulation.module.css
new file mode 100644
index 00000000..6449a245
--- /dev/null
+++ b/src/features/planSimulation/components/Simulation.module.css
@@ -0,0 +1,119 @@
+.planSimulationForm {
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+}
+
+.customReactSelect {
+ font-size: 0.8rem;
+}
+
+/* Filter locations by a Parent Location */
+
+.customSwitchWrapper {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ grid-gap: 0;
+ gap: 1rem;
+ font-size: 1rem;
+ border: 1px solid #0000001a;
+ border-radius: 5px;
+ padding: 1rem;
+}
+
+.filterLoactionsWrapper {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.customSwitchForm {
+}
+
+.filterLoactionsWrapper__infoContainer {
+}
+
+.filterLoactionsWrapper__label {
+ color: black;
+ margin: 0;
+}
+
+.filterLoactionsWrapper__subLabel {
+ color: #73737398;
+ margin: 0;
+ font-size: 0.8rem;
+}
+
+.loadInactiveWrapper {
+}
+
+.parentLocationSelects {
+ border: 1px solid #0000001a;
+ border-radius: 5px;
+ padding: 1rem;
+ font-size: 0.8rem;
+}
+
+.parentLocationSelects select {
+ font-size: 0.8rem;
+}
+
+.formSubmitWrapper {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ gap: 1rem;
+}
+
+.formControlsWrapper label {
+ margin: 0;
+}
+
+.formControlsWrapper {
+ width: 100%;
+ display: flex;
+ gap: 1rem;
+ align-items: center;
+}
+
+.searchButton {
+ width: 100%;
+ padding-top: 1rem;
+ padding-bottom: 1rem;
+}
+
+.select {
+ width: 100%;
+}
+
+.WrapperDasasetsButton {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ gap: 1rem;
+ background-color: #ffffff;
+ border: 1px solid #0000001a;
+ border-radius: 0.375rem;
+ padding: 0.5rem;
+ margin-bottom: 0.5rem;
+}
+
+.dasasetsButton {
+ background-color: #f9f9f9;
+ border-radius: 0.375rem;
+ border: 1px solid #0000001a;
+ text-align: center;
+ text-decoration: none;
+ width: 100%;
+ color: black;
+ padding: 0.5rem;
+ font-weight: 500;
+ display: inline-block;
+ margin: 0;
+ transition: background-color 0.3s;
+}
+
+.dasasetsButton:hover {
+ background-color: #f0f0f0;
+}
diff --git a/src/features/planSimulation/components/Simulation.tsx b/src/features/planSimulation/components/Simulation.tsx
index 4dccc2fd..2da351b9 100644
--- a/src/features/planSimulation/components/Simulation.tsx
+++ b/src/features/planSimulation/components/Simulation.tsx
@@ -1,6 +1,5 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import React, { useCallback, useEffect, useRef, useState } from 'react';
-import { Button, Col, Container, Form, Modal, OverlayTrigger, Row, Spinner, Tooltip } from 'react-bootstrap';
+import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
+import { Button, Col, Container, Form, Modal, Row } from 'react-bootstrap';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { toast } from 'react-toastify';
@@ -9,13 +8,14 @@ import { ActionDialog } from '../../../components/Dialogs';
import { useWindowResize } from '../../../hooks/useWindowResize';
import { getGeneratedLocationHierarchyList, getLocationHierarchyList } from '../../location/api';
import { LocationHierarchyModel } from '../../location/providers/types';
-import { evaluate, isNumeric } from 'mathjs';
+import { electronMassDependencies, evaluate, isNumeric } from 'mathjs';
+import styles from './Simulation.module.css';
+
import {
getDataAssociatedEntityTags,
getEntityList,
getEventBasedEntityTags,
getFullLocationsSSE,
- getLocationList,
getLocationsSSE,
submitSimulationRequest,
updateSimulationRequest
@@ -34,15 +34,12 @@ import {
RevealFeature,
SearchLocationProperties
} from '../providers/types';
-import FormField from './FormField/FormField';
-import MultiFormField from './FormField/MultiFormField';
import SimulationModal from './SimulationModal';
-
-import Select, { MultiValue, SingleValue } from 'react-select';
+import { MultiValue, SingleValue } from 'react-select';
+import Select from 'react-select';
import PeopleDetailsModal from './PeopleDetailsModal';
-import { bbox, Feature, MultiPolygon, Point, Polygon } from '@turf/turf';
+import { bbox, Geometry, polygon } from '@turf/turf';
import { LngLatBounds, Map as MapBoxMap } from 'mapbox-gl';
-
import SimulationResultExpandingTable from '../../../components/Table/SimulationResultExpandingTable';
import DownloadSimulationResultsModal from './modals/DownloadSimulationResultsModal';
import UploadSimulationData from './modals/UploadSimulationData';
@@ -56,6 +53,47 @@ import { Color } from 'react-color-palette';
import { hex } from 'color-convert';
import { REVEAL_SIMULATION_EDIT } from '../../../constants';
import AuthorizedElement from '../../../components/AuthorizedElement';
+import { Drawer } from '../../location/components/drawer/Drawer';
+import Accordion from '../../location/components/accordion/Accordion';
+import { faUsers, faSitemap, faHouseUser, faDiceD20 } from '@fortawesome/free-solid-svg-icons';
+import { library } from '@fortawesome/fontawesome-svg-core';
+
+import Dashboard from '../components/Dashboard/Dashboard';
+
+import DrawerButton from '../../../components/DrawerButton/DrawerButton';
+
+import { CustomPopup } from '../../../components/CustomPopup/CustomPopup';
+import DatasetsAccordion from '../../location/components/DatasetsAccordion/DatasetsAccordion';
+
+import AddTargetAreaForm from './SimulationMapView/components/AddTargetAreaForm/AddTargetAreaForm';
+import AddDatasetForm from './SimulationMapView/components/AddDatasetForm/AddDatasetForm';
+
+import CampaignTotalsAccordion from './SimulationMapView/components/CampaignTotalsAccordion/CampaignTotalsAccordion';
+
+import {
+ getDefaultHierarchyData,
+ getHierarchy,
+ getHierarchyPolygon,
+ getPlanInfo
+} from './SimulationMapView/api/hierarchyAPI';
+import Hierarchy from './Hierarchy/Hierarchy';
+
+// CONTEXT
+import { usePolygonContext } from '../../../contexts/PolygonContext';
+import {
+ AddDatasetResponse,
+ addSearchRequest,
+ DataSetList,
+ deleteDataset,
+ filterDatasets,
+ getLocationPolygonsWithDatasets,
+ getSimulationData,
+ LocationData,
+ SimulationDatasetRequest
+} from './SimulationMapView/api/datasetsAPI';
+import { assignLocationsToPlan } from '../../assignment/api';
+
+library.add(faUsers, faSitemap, faHouseUser, faDiceD20);
interface SubmitValue {
fieldIdentifier: string;
@@ -114,9 +152,22 @@ export interface Children {
childrenList: string[];
}
+export interface Polygondata {
+ polygonData: any;
+ childrenLoaded: boolean;
+}
+
+interface PolygonsState {
+ [key: string]: Polygondata;
+}
+
+// const extractPolygonsFromPolysWithData = (polygonsWithData?: PolygonsState) => {
+// return polygonsWithData ? Object.values(polygonsWithData!).map((polygon: Polygondata) => polygon.polygonData) : [];
+// };
+
const Simulation = () => {
const { t } = useTranslation();
- const [showModal, setShowModal] = useState(false);
+ // const [showModal, setShowModal] = useState(false);
const [showDetails, setShowDetails] = useState(false);
const [showResult, setShowResult] = useState(false);
const [combinedHierarchyList, setCombinedHierarchyList] = useState();
@@ -125,7 +176,7 @@ const Simulation = () => {
const [selectedEntityConditionList, setSelectedEntityConditionList] = useState([]);
const divRef = useRef(null);
const divHeight = useWindowResize(divRef.current);
- const [mapFullScreen, setMapFullScreen] = useState(false);
+ const [mapFullScreen, setMapFullScreen] = useState(true);
const [mapData, setMapData] = useState();
const [parentMapData, setParentMapData] = useState();
const [mapDataLoad, setMapDataLoad] = useState({
@@ -137,8 +188,8 @@ const Simulation = () => {
source: undefined
});
const [parentMapDataLoad, setParentMapDataLoad] = useState();
- const [nodeList, setNodeList] = useState([]);
- const [completeGeographicList, setCompleteGeographicList] = useState([]);
+ // const [nodeList, setNodeList] = useState([]);
+ // const [completeGeographicList, setCompleteGeographicList] = useState([]);
const [locationList, setLocationList] = useState([]);
const [selectedHierarchy, setSelectedHierarchy] = useState();
const [selectedLocation, setSelectedLocation] = useState>();
@@ -148,7 +199,7 @@ const Simulation = () => {
const [entityTags, setEntityTags] = useState([]);
const [entityTagsOriginal, setEntityTagsOriginal] = useState([]);
const [showSummaryModal, setShowSummaryModal] = useState(false);
- const [highestLocations, setHighestLocations] = useState[]>();
+ const [highestLocations, setHighestLocations] = useState();
const [summary, setSummary] = useState({});
const [selectedMapData, setSelectedMapData] = useState();
const [showCountResponseModal, setShowCountResponseModal] = useState(false);
@@ -192,6 +243,226 @@ const Simulation = () => {
const [tooLargeOrSmall, setTooLargeOrSmall] = useState(0);
const [omitLayers, setOmitLayers] = useState(false);
+ const [leftOpen, setLeftOpen] = useState(false);
+ const [rightOpen, setRightOpen] = useState(false);
+
+ const [showModal, setShowModal] = useState(false);
+
+ const [openCustomModal, setOpenCustomModal] = useState();
+ // AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ // const [geometry, setGeometry] = useState | null>(null);
+ const [geometry, setGeometry] = useState();
+ const [currentLocationId, setCurrentLocationId] = useState();
+ // const [polygons, setPolygons] = useState([]);
+ const [polygonsWithData, setPolygonsWithData] = useState();
+
+ const [selectedLocationChildren, setSelectedLocationChildren] = useState([]);
+
+ const [datasetList, setDatasetList] = useState([]);
+ const [includeGeometry, setIncludeGeometry] = useState(true);
+
+ const [chartData, setChartData] = useState>({});
+ const [totals, setTotals] = useState>({});
+ const [labels, setLabels] = useState([]);
+ const [nodeOrderListVisible, setNodeOrderListVisible] = useState(false);
+ const [showDatasetsAgainstParentLevel, setShowDatasetsAgainstParentLevel] = useState(false);
+ const [selectedParentLevel, setSelectedParentLevel] = useState>();
+ const [showingParentLevelsMenu, setShowingParentLevelsMenu] = useState(false);
+ const [numberOfStructures, setNumberOfStructures] = useState(0);
+ // AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+
+ const { dispatch } = usePolygonContext();
+ const { state } = usePolygonContext();
+
+ const fetchSimulationAndData = async () => {
+ const simulationIdentifier = await fetchPlanInfo();
+
+ try {
+ const simulationData = await getSimulationData(simulationIdentifier);
+ dispatch({ type: 'SET_NEW_DATASETS', payload: simulationData.datasets });
+ dispatch({ type: 'SET_SIMULATION_ID', payload: simulationData.identifier });
+ dispatch({ type: 'SET_TARGET_AREAS', payload: simulationData.targetAreas });
+ } catch (error) {
+ console.error('Failed to fetch simulation:', error);
+ }
+ };
+
+ const handleAddDataset = (datasetResponse: AddDatasetResponse) => {
+ const dataset = {
+ identifier: datasetResponse.datasetId,
+ name: datasetResponse.datasetName,
+ hexColor: datasetResponse.hexColor,
+ lineWidth: datasetResponse.lineWidth,
+ borderColor: datasetResponse.borderColor
+ };
+
+ dispatch({ type: 'ADD_DATASET', payload: dataset });
+
+ //! LOOP LOCATIONS WITH METADA AND ATTACH DATASET DATA TO LOADED POLYGONS
+ setPolygonsWithData((prev: any) => {
+ const updatedPolygons = { ...prev };
+
+ Object.entries(datasetResponse.locationWithMetadata).forEach(([locationId, metadata]) => {
+ if (updatedPolygons[locationId]) {
+ const existingMetadata = updatedPolygons[locationId].polygonData.properties.metadata || [];
+ updatedPolygons[locationId].polygonData.properties.metadata = Array.from(
+ new Set([...existingMetadata, metadata])
+ );
+ }
+ });
+ return updatedPolygons;
+ });
+ };
+
+ const fetchHierarchy = async () => {
+ const hierarchyData = await getHierarchy();
+ try {
+ setHighestLocations(hierarchyData);
+ dispatch({ type: 'SET_HIERARCHY', payload: hierarchyData });
+ } catch (error) {
+ console.error('Failed to fetch hierarchy:', error);
+ }
+ };
+
+ const fetchPlanInfo = async () => {
+ try {
+ const planInfo = await getPlanInfo();
+ dispatch({ type: 'SET_PLANID', payload: planInfo.identifier });
+ return planInfo.identifier;
+ } catch (error) {
+ console.error('Failed to fetch plan info:', error);
+ }
+ };
+
+ const fetchDefaultHierarchyData = async () => {
+ const hierarchyData = await getDefaultHierarchyData();
+ try {
+ dispatch({ type: 'SET_DEFAULT_HIERARCHY_DATA', payload: hierarchyData });
+ } catch (error) {
+ console.error('Failed to fetch hierarchy data:', error);
+ }
+ };
+
+ useMemo(() => {
+ setDatasetList(state.datasets);
+ }, [state.datasets]);
+
+ //! UPDATE DATASETS LIST
+ const updateDatasetHandler = async (newDatasetList: string) => {
+ dispatch({ type: 'SET_DATASET', payload: newDatasetList });
+ };
+
+ const removeDatasetHandler = async (datasetId: string) => {
+ deleteDataset({
+ simulationId: state.simulationId,
+ datasetId
+ });
+ dispatch({ type: 'DELETE_DATASET', payload: datasetId });
+ //! remove dataset update metadata
+ setPolygonsWithData((prev: any) => {
+ const updatedPolygons = { ...prev };
+
+ Object.entries(updatedPolygons).forEach(([locationId, polygonData]: any) => {
+ const updatedMetadata = polygonData.polygonData.properties.metadata.filter(
+ (metadata: any) => metadata.datasetId !== datasetId
+ );
+ updatedPolygons[locationId].polygonData.properties.metadata = updatedMetadata;
+ });
+
+ return updatedPolygons;
+ });
+ };
+
+ // we are updating selectedLocationChildren whenever an assignment happens,
+ // because assigned flag on these locations is not updated (it is still the one we got on location fetch)
+ useEffect(() => {
+ setSelectedLocationChildren(prev =>
+ prev.map(obj => ({
+ ...obj,
+ properties: {
+ ...obj.properties,
+ assigned: state.assingedLocations[obj.identifier]
+ }
+ }))
+ );
+ }, [state.assingedLocations]);
+
+ useEffect(() => {
+ if (
+ currentLocationId &&
+ polygonsWithData &&
+ polygonsWithData[currentLocationId] &&
+ !showDatasetsAgainstParentLevel
+ ) {
+ const children = Object.values(polygonsWithData)
+ .map((polygon: any) => polygon.polygonData)
+ .filter((polygon: any) => polygon.properties.parentIdentifier === currentLocationId);
+
+ setSelectedLocationChildren(children);
+
+ // when locations loaded, we are setting their assigned flag values as default values in assignment map
+ // this way, state.assignedLocations is our single source of truth
+ const assignedMap = children.reduce(
+ (map, obj) => {
+ return {
+ ...map,
+ [obj.identifier]: map[obj.identifier] ?? obj.properties.assigned
+ };
+ },
+ { ...state.assingedLocations }
+ );
+ dispatch({ type: 'SET_ASSIGNED', payload: assignedMap });
+
+ const selectedLocation = polygonsWithData[currentLocationId].polygonData;
+
+ console.log('418 selectedLocation', selectedLocation?.properties.name);
+
+ if (selectedLocation && !showDatasetsAgainstParentLevel) {
+ setGeometry(selectedLocation);
+ setToLocation(JSON.parse(JSON.stringify(bbox(selectedLocation.geometry))));
+ const populationData = transformPopulationData(selectedLocation?.properties?.population);
+ setNumberOfStructures(selectedLocation?.properties?.numberOfStructures);
+ if (populationData !== null) {
+ setChartData(populationData?.chartData);
+ setLabels(populationData?.labels);
+ setTotals(populationData?.totals);
+ }
+ }
+ }
+ }, [currentLocationId, polygonsWithData, showDatasetsAgainstParentLevel]);
+
+ useEffect(() => {
+ let populationData: any;
+ if (state.selected) {
+ populationData = state.selected.population
+ ? transformPopulationData(JSON.parse(state.selected.population))
+ : null;
+ setNumberOfStructures(state.selected?.numberOfStructures);
+ if (populationData !== null) {
+ setChartData(populationData.chartData);
+ setLabels(populationData?.labels);
+ setTotals(populationData?.totals);
+ }
+ } else if (!state.selected && currentLocationId) {
+ const selectedLocation = polygonsWithData[currentLocationId].polygonData;
+ const populationData = transformPopulationData(selectedLocation?.properties?.population);
+ setNumberOfStructures(selectedLocation?.properties?.numberOfStructures);
+ if (populationData !== null) {
+ setChartData(populationData.chartData);
+ setLabels(populationData?.labels);
+ setTotals(populationData?.totals);
+ }
+ }
+ }, [state.selected, showDatasetsAgainstParentLevel]);
+
+ useEffect(() => {
+ fetchHierarchy();
+ fetchSimulationAndData();
+ fetchDefaultHierarchyData();
+ }, []);
+
+ // AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+
useEffect(() => {
Promise.all([
getLocationHierarchyList(50, 0, true),
@@ -1046,16 +1317,16 @@ const Simulation = () => {
}
});
- if (mapData.parents) {
- let highestLocations: any[] = Object.keys(mapData.parents)
- .filter(
- key =>
- mapData.parents[key].properties !== null &&
- mapData.parents[key].properties?.geographicLevelNodeNumber === min
- )
- .map(key => mapData.parents[key]);
- setHighestLocations(highestLocations);
- }
+ // if (mapData.parents) {
+ // let highestLocations: any[] = Object.keys(mapData.parents)
+ // .filter(
+ // key =>
+ // mapData.parents[key].properties !== null &&
+ // mapData.parents[key].properties?.geographicLevelNodeNumber === min
+ // )
+ // .map(key => mapData.parents[key]);
+ // // setHighestLocations(highestLocations);
+ // }
}
}
}, [mapData, resultsLoaded, parentsLoaded, getLocationHierarchyFromLowestLocation, markedLocations]);
@@ -1082,32 +1353,100 @@ const Simulation = () => {
}
}, [selectedHierarchy]);
- const loadLocationHandler = (locationId: string) => {
- let feature = mapData?.features[locationId] || mapData?.parents[locationId];
-
- if (feature && feature.geometry) {
- setToLocation(JSON.parse(JSON.stringify(bbox(feature))));
+ const checkifChildrenLoaded = (polygonsWithData: any, selectedLocationId: any) => {
+ if (polygonsWithData?.[selectedLocationId]?.childrenLoaded === undefined) {
+ return true;
+ } else if (polygonsWithData?.[selectedLocationId]?.childrenLoaded === true) {
+ return false;
} else {
+ return true;
}
};
- const showDetailsClickHandler = (locationId: string) => {
- let feature = mapData?.parents[locationId];
- //create deep copy of bounds object for triggering bounds event every time
- if (feature) {
- let val: any = feature;
- let valProps: SearchLocationProperties = {
- bounds: feature.geometry ? (bbox(val.geometry) as any) : undefined,
- identifier: val.identifier,
- metadata: val.properties?.metadata,
- name: val.properties?.name,
- persons: []
+ useEffect(() => {
+ fetchHierarchy();
+ fetchSimulationAndData();
+ }, []);
+
+ //! LOADING POLYGONS ON DEMAND
+ const loadLocationHandler = async (locationId: string) => {
+ setShowDatasetsAgainstParentLevel(false);
+ setSelectedParentLevel(null);
+ setNodeOrderListVisible(false);
+
+ setCurrentLocationId(locationId);
+ if (state.datasets.length === 0 && polygonsWithData?.[locationId]?.childrenLoaded) {
+ setIncludeGeometry(false);
+
+ const k = Object.values(polygonsWithData)
+ .map((polygon: any) => polygon.polygonData)
+ .filter(p => p.properties.parentIdentifier === locationId);
+
+ setSelectedLocationChildren(k);
+
+ const selectedLocation = polygonsWithData?.[locationId]?.polygonData;
+ if (selectedLocation) {
+ setGeometry(selectedLocation);
+ setToLocation(JSON.parse(JSON.stringify(bbox(selectedLocation.geometry))));
+ }
+ } else {
+ const includeGeometry: boolean = checkifChildrenLoaded(polygonsWithData, locationId);
+
+ let configObj: LocationData = {
+ datasetsIds: datasetList.map(dataset => dataset.identifier),
+ includeGeometry: includeGeometry,
+ parentLocationId: locationId, //current location identifier
+ simulationId: state.simulationId
};
- setSelectedRow(valProps);
- setShowDetails(true);
+
+ const polygonsWithDatasets = await getLocationPolygonsWithDatasets(configObj);
+ if (!polygonsWithDatasets || polygonsWithDatasets.length === 0) {
+ toast.error('Cannot get results. Please try again.');
+ return;
+ }
+
+ if (includeGeometry) {
+ setPolygonsWithData((prev: any) => {
+ const updatedPolygons = { ...prev };
+ polygonsWithDatasets.forEach((location: any) => {
+ updatedPolygons[location.identifier] = {
+ polygonData: location,
+ childrenLoaded: location.identifier === locationId
+ };
+ });
+
+ return updatedPolygons;
+ });
+ } else {
+ setPolygonsWithData((prev: any) => {
+ const updatedPolygons = { ...prev };
+ polygonsWithDatasets.forEach((location: any) => {
+ updatedPolygons[location.identifier].polygonData.properties.metadata = location.properties.metadata;
+ });
+
+ return updatedPolygons;
+ });
+ }
}
};
+ // const showDetailsClickHandler = (locationId: string) => {
+ // let feature = mapData?.parents[locationId];
+ // //create deep copy of bounds object for triggering bounds event every time
+ // if (feature) {
+ // let val: any = feature;
+ // let valProps: SearchLocationProperties = {
+ // bounds: feature.geometry ? (bbox(val.geometry) as any) : undefined,
+ // identifier: val.identifier,
+ // metadata: val.properties?.metadata,
+ // name: val.properties?.name,
+ // persons: []
+ // };
+ // setSelectedRow(valProps);
+ // setShowDetails(true);
+ // }
+ // };
+
const processChildren = useCallback(
(mapDataClone: any) => {
let geoLevel: string = mapDataClone.properties.geographicLevel;
@@ -1162,430 +1501,249 @@ const Simulation = () => {
setShowResult(true);
};
- const conditionalRender = (el: EntityTag, index: number) => {
- if (el.more && el.more.length) {
- return (
- {
- if (range) {
- el.more.splice(1);
- } else {
- unregister((el.tag + index + 'range') as any);
- el.more.splice(i, 1);
- }
- setSelectedEntityConditionList([...selectedEntityConditionList]);
- }}
- />
+ // const conditionalRender = (el: EntityTag, index: number) => {
+ // if (el.more && el.more.length) {
+ // return (
+ // {
+ // if (range) {
+ // el.more.splice(1);
+ // } else {
+ // unregister((el.tag + index + 'range') as any);
+ // el.more.splice(i, 1);
+ // }
+ // setSelectedEntityConditionList([...selectedEntityConditionList]);
+ // }}
+ // />
+ // );
+ // }
+ // return ;
+ // };
+
+ // const selectStyles = {
+ // dropdownIndicator: (baseStyles: object) => ({
+ // ...baseStyles,
+ // scale: '0.8'
+ // }),
+ // clearIndicator: (baseStyles: object) => ({
+ // ...baseStyles,
+ // scale: '0.8'
+ // })
+ // };
+
+ // const convertColor = (color: any) => {
+ // if (!color) return;
+ // let hexValue = color.toString();
+ // let rgbArr = hex.rgb(hexValue);
+ // let hsvArr = hex.hsv(hexValue);
+
+ // let convertedColor = {
+ // hex: '#009900',
+ // rgb: { r: rgbArr[0], g: rgbArr[1], b: rgbArr[2] },
+ // hsv: { h: hsvArr[0], s: hsvArr[1], v: hsvArr[2] }
+ // };
+
+ // return convertedColor as Color;
+ // };
+
+ const handleRemoveTargetArea = (id: string) => {
+ const assignedAreas = state.targetAreas?.flatMap((ta: any) => [...ta.ancestry, ta.identifier]) || [];
+ const targetArea = state.targetAreas?.find(ta => ta.identifier === id);
+ const toExcludeSet = new Set([...targetArea.ancestry, id]);
+ const filtered = assignedAreas.filter(item => !toExcludeSet.has(item));
+ assignLocationsToPlan(state.planid, filtered).then(async () => {
+ // update assignment map
+ dispatch({ type: 'SET_ASSIGNED', payload: { ...state.assingedLocations, [id]: false } });
+ // refetch target areas, so the map updates
+ const simulationData = await getSimulationData(state.planid);
+ dispatch({ type: 'SET_TARGET_AREAS', payload: simulationData.targetAreas });
+ dispatch({ type: 'CLEAR_SELECTION' });
+ });
+ };
+
+ const campaignTotals = [
+ {
+ label: 'Target Areas',
+ total: state.targetAreas.length,
+ targetAreasList: state.targetAreas,
+ remove: handleRemoveTargetArea
+ },
+ {
+ label: 'Total Population',
+ total: Math.round(state.targetAreas?.reduce((a, b) => a + b?.properties?.population?.sum, 0)) || 0,
+ targetAreasList: state.targetAreas,
+ type: 'population'
+ }
+ ];
+
+ const handleDatasetsButtonClick = () => {
+ setNodeOrderListVisible(!nodeOrderListVisible);
+ };
+
+ const filterDatasetsErrorHandler = () => {
+ //toast("An error occured. Cannot load requested data.")
+ //TODO
+ };
+ const filterDatasetsOpenHandler = () => {
+ //TODO
+ };
+ const filterDatasetsCloseHandler = () => {
+ //TODO
+ };
+
+ const filterDatasetsMessageHandler = (message: any) => {
+ const res: any = JSON.parse(message.data);
+ setSelectedLocationChildren(prev => {
+ return [...prev, ...res];
+ });
+ };
+
+ const handleParentSelectionChange = async (option: SingleValue<{ value: string; label: string }>) => {
+ setSelectedParentLevel(option);
+ if (option != null && option.value !== '') {
+ const searchRequest: SimulationDatasetRequest = {
+ simulationId: state.simulationId,
+ parentAdminLevel: option.value
+ };
+ const searchId = await addSearchRequest(searchRequest);
+ setSelectedLocationChildren([]);
+ setShowDatasetsAgainstParentLevel(true);
+ filterDatasets(
+ searchId,
+ filterDatasetsMessageHandler,
+ filterDatasetsCloseHandler,
+ filterDatasetsOpenHandler,
+ filterDatasetsErrorHandler
);
}
- return ;
};
return (
<>
-
- {!mapFullScreen && (
-
-
-
-
- Use Layers}>
- Omit Layers:
-
-
-
- setOmitLayers(!omitLayers)}
- />
-
-
-
-
-
-
- {t('simulationPage.hierarchy')}:
-
-
- {
- const selectedHierarchy = combinedHierarchyList?.find(el => el.identifier === e.target.value);
- if (selectedHierarchy) {
- setSelectedHierarchy(selectedHierarchy);
- setNodeList(selectedHierarchy.nodeOrder.filter(el => el !== 'structure'));
- setCompleteGeographicList(selectedHierarchy.nodeOrder);
- } else {
- setSelectedHierarchy(undefined);
- setNodeList([]);
- setSelectedLocation(null);
- setCompleteGeographicList([]);
- }
- }}
- >
- {t('simulationPage.selectHierarchy')}...
- {combinedHierarchyList?.map(el => (
-
- {el.name}
-
- ))}
-
-
-
-
-
-
-
-
- Filter locations by a Parent Location{' '}
-
- (Search results will be locations within this parent location)
-
-
-
-
-
- {t('simulationPage.selectParentToSearchWithin')}
- }
- >
- {t('simulationPage.geographicLevel')}:
-
-
-
- {
- if (e.target.value && selectedHierarchy && selectedHierarchy.type) {
- getLocationList(
- selectedHierarchy.identifier,
- selectedHierarchy.type,
- e.target.value
- ).then(res => {
- setLocationList(res);
- });
- } else {
- setLocationList([]);
- }
- setSelectedLocation(null);
- }}
- >
-
- {selectedHierarchy
- ? t('simulationPage.selectGeographicLevel')
- : t('simulationPage.selectHierarchy')}
- ...
-
- {nodeList.map(el => (
-
- {el}
-
- ))}
-
-
-
-
-
-
-
-
- {t('simulationPage.selectParentLocationToSearchWithin')}
-
- }
- >
- {t('simulationPage.location')}:
-
-
-
- {
- return [...prev, { label: current.name, value: current.identifier }];
- }, [])}
- onChange={newValue => setSelectedLocation(newValue)}
- />
-
-
-
-
-
-
-
-
-
- {t('simulationPage.filterSearchByLevel')}}
- >
- {t('simulationPage.filterGeographicLevel')}:
-
-
-
- {
- return { value: geo, label: geo };
- })
- .filter((geo: any) => !levelsLoaded.current.includes(geo.label))}
- value={geoFilterList}
- noOptionsMessage={obj => {
- if (obj.inputValue === '') {
- return 'Enter at least 1 char to display the results...';
- } else {
- return 'No location found.';
- }
- }}
- placeholder={
- completeGeographicList.length > 0
- ? t('simulationPage.search') + '...'
- : t('simulationPage.selectHierarchyFirst')
- }
- onInputChange={e => {}}
- onChange={newValues => {
- setGeoFilterList(newValues);
- if (newValues) {
- setSelectedFilterGeographicLevelList(newValues.map(value => value.label));
- }
- }}
- />
-
-
-
-
-
-
- {t('simulationPage.selectToLoadInactiveLocations')}
- }
- >
- {t('simulationPage.loadInactiveLocations')}:
-
-
-
- setLoadParentsToggle(e.target.checked)}
- />
-
-
-
-
- {loadParentsToggle && (
-
-
- {t('simulationPage.filterInactiveLocationsByLevel')}
- }
- >
- {t('simulationPage.filterGeographicLevel')}:
-
-
-
+
+
+ {/* {highestLocations && showResult && ( */}
+ {highestLocations && (
+
+
+ {/* setOpenCustomModal(0)}>Add Operational Area
+ setOpenCustomModal(undefined)} hasBackdrop>
+ setOpenCustomModal(undefined)} />
+ */}
+
+ )}
+ {/* {highestLocations && showResult && ( */}
+ {highestLocations && (
+
+ {state.datasets?.length !== 0 && (
+
+
+ Display datasets by parent level
+
+ {nodeOrderListVisible && (
+ <>
{
- return { value: geo, label: geo };
- })}
- value={inactiveGeoFilterList}
- noOptionsMessage={obj => {
- if (obj.inputValue === '') {
- return 'Enter at least 1 char to display the results...';
- } else {
- return 'No location found.';
- }
+ components={{
+ IndicatorSeparator: () => null
}}
- placeholder={
- completeGeographicList.length > 0
- ? t('simulationPage.search') + '...'
- : t('simulationPage.selectHierarchyFirst')
- }
- onChange={newValues => {
- setInactiveGeoFilterList(newValues);
- if (newValues) {
- setSelectedFilterInactiveGeographicLevelList(newValues.map(value => value.label));
- }
+ placeholder={'Select Parent Level'}
+ className={styles.select}
+ isClearable
+ onMenuOpen={() => setShowingParentLevelsMenu(true)}
+ onMenuClose={() => setShowingParentLevelsMenu(false)}
+ options={state.defaultHierarchyData?.nodeOrder.map((node: string) => {
+ return {
+ value: node,
+ label: node
+ };
+ })}
+ value={selectedParentLevel}
+ onChange={(selectedOption: SingleValue<{ value: string; label: string }>) => {
+ handleParentSelectionChange(selectedOption);
}}
/>
-
-
- )}
-
-
-
-
-
- {t('simulationPage.addQueryAttribute')}
-
-
-
-
-
- openModalHandler(true)}
- >
-
-
-
-
-
- {resultsLoadingState === 'error' && parentsLoadingState === 'error'
- ? 'Error loading active and inactive locations'
- : resultsLoadingState === 'error' && parentsLoadingState !== 'error'
- ? 'Error loading active locations'
- : 'Error loading inactive locations'}
-
- ) : (
- <>>
- )
- }
- >
-
- {(resultsLoadingState === 'notstarted' ||
- resultsLoadingState === 'complete' ||
- resultsLoadingState === 'error') &&
- (parentsLoadingState === 'notstarted' ||
- parentsLoadingState === 'complete' ||
- parentsLoadingState === 'error') ? (
- <>
- {resultsLoadingState === 'error' || parentsLoadingState === 'error' ? (
-
- ) : (
-
- )}
- {t('simulationPage.search')}
- >
- ) : (
- <>
-
- Loading
- >
- )}
-
- {/*)}*/}
-
-
-
-
-
-
- 900 ? '51vh' : '44vh' }}
- className="border rounded overflow-auto"
- >
- {selectedEntityConditionList.map((el, index) => {
- return (
-
- {conditionalRender(el, index)}
-
- {(el.valueType === 'integer' ||
- el.valueType === 'double' ||
- el.valueType === 'date' ||
- el.valueType === 'string') && (
-
- {
- if (el.more) {
- el.more.push(el);
- } else {
- el.more = [el];
- }
- setSelectedEntityConditionList([...selectedEntityConditionList]);
- }}
- >
-
-
-
- )}
-
- {
- selectedEntityConditionList.splice(index, 1);
- setSelectedEntityConditionList([...selectedEntityConditionList]);
- }}
- >
-
-
-
-
-
- );
- })}
-
-
-
- )}
-
- {
- setMapFullScreen(!mapFullScreen);
- }}
- fullScreen={mapFullScreen}
- toLocation={toLocation}
- entityTags={entityTags}
- parentMapData={parentMapData}
- setMapDataLoad={setMapDataLoad}
- chunkedData={mapDataLoad}
- resetMap={resetMap}
- setResetMap={setResetMap}
- stats={statsLayerMetadata}
- resultsLoadingState={resultsLoadingState}
- parentsLoadingState={parentsLoadingState}
- map={map}
- updateMarkedLocations={updateMarkedLocations}
- parentChild={parentChild}
- analysisLayerDetails={analysisLayerDetails}
- />
-
-
+
+ {showingParentLevelsMenu && (
+ <>
+
+
+
+ >
+ )}
+ >
+ )}
+
+ )}
+ {state.datasets?.map(dataset => (
+
+ ))}
+ setOpenCustomModal(1)} disabled={showDatasetsAgainstParentLevel}>
+ Add dataset
+
+ setOpenCustomModal(undefined)} hasBackdrop>
+
+
setOpenCustomModal(undefined)}
+ onDatasetAdded={handleAddDataset}
+ selectedLocationId={currentLocationId}
+ />
+
+
+
+ )}
+
+
setLeftOpen(!leftOpen)}
+ leftOpenState={leftOpen}
+ rightOpenState={rightOpen}
+ rightOpenHandler={() => setRightOpen(!rightOpen)}
+ fullScreenHandler={() => {
+ setMapFullScreen(!mapFullScreen);
+ }}
+ fullScreen={mapFullScreen}
+ toLocation={toLocation} // bbox
+ entityTags={entityTags}
+ parentMapData={parentMapData}
+ setMapDataLoad={setMapDataLoad}
+ chunkedData={mapDataLoad}
+ resetMap={resetMap}
+ setResetMap={setResetMap}
+ stats={statsLayerMetadata}
+ resultsLoadingState={resultsLoadingState}
+ parentsLoadingState={parentsLoadingState}
+ map={map}
+ updateMarkedLocations={updateMarkedLocations}
+ parentChild={parentChild}
+ analysisLayerDetails={analysisLayerDetails}
+ />
+
+ {Object.keys(chartData).length > 0 && (
+
+
+
+ )}
+
+ {campaignTotals.map((item, index) => (
+
+ ))}
+
+
+
<>
@@ -1645,21 +1803,6 @@ const Simulation = () => {
)}
-
-
- {highestLocations && showResult && (
-
- )}
-
-
>
{showModal && selectedEntity && (
@@ -1802,3 +1945,51 @@ const Simulation = () => {
);
};
export default Simulation;
+
+const transformPopulationData = (population: any) => {
+ const mergedPyramids = mergeAgeGroups(population?.Pyramids) || [];
+ if (!mergedPyramids || mergedPyramids.length === 0) {
+ return null;
+ }
+ const ageGroups = mergedPyramids.map((group: any) => group.AgeGroup.replace('_', '-'));
+ const summaryData = mergedPyramids.map((group: any) => Math.round(group.TotalPop));
+ const maleData = mergedPyramids.map((group: any) => Math.round(group.MalePop));
+ const femaleData = mergedPyramids.map((group: any) => Math.round(group.FemalePop));
+
+ return {
+ labels: ageGroups,
+ chartData: {
+ summary: summaryData,
+ male: maleData,
+ female: femaleData
+ },
+ totals: {
+ summary: Math.round(population.sum),
+ male: Math.round(population.male),
+ female: Math.round(population.female)
+ }
+ };
+};
+
+const mergeAgeGroups = (pyramids: any[]) => {
+ if (!pyramids || pyramids.length === 0) return [];
+ const mergedGroups: any[] = [];
+
+ for (let i = 0; i < pyramids.length; i += 2) {
+ const first = pyramids[i];
+ const second = pyramids[i + 1] || null;
+
+ const mergedGroup = {
+ AgeGroup: second
+ ? `${first.AgeGroup.split('_')[0]}-${second.AgeGroup.split('_')[1]}`
+ : first.AgeGroup.replace('_', '-'),
+ MalePop: Math.round(first.MalePop + (second?.MalePop || 0)),
+ FemalePop: Math.round(first.FemalePop + (second?.FemalePop || 0)),
+ TotalPop: Math.round(first.TotalPop + (second?.TotalPop || 0))
+ };
+
+ mergedGroups.push(mergedGroup);
+ }
+
+ return mergedGroups;
+};
diff --git a/src/features/planSimulation/components/SimulationMapView/Helper/MapInteractionsHelper.ts b/src/features/planSimulation/components/SimulationMapView/Helper/MapInteractionsHelper.ts
new file mode 100644
index 00000000..cf40afb6
--- /dev/null
+++ b/src/features/planSimulation/components/SimulationMapView/Helper/MapInteractionsHelper.ts
@@ -0,0 +1,141 @@
+export const DrawPolygonsFeature = (mapRef: any, selectedLoaction: any, featureName: any) => {
+ const SourceName = `${featureName}-source`;
+ if (!mapRef.getSource(SourceName)) {
+ mapRef.addSource(SourceName, {
+ type: 'geojson',
+ data: {
+ type: 'Feature',
+ geometry: selectedLoaction.geometry,
+ properties: selectedLoaction
+ }
+ });
+ } else {
+ mapRef.getSource(SourceName).setData({
+ type: 'Feature',
+ geometry: selectedLoaction.geometry,
+ properties: selectedLoaction
+ });
+ }
+};
+
+export const DrawPolygonsFeatureCollection = (mapRef: any, polygonArray: any, featureName: any) => {
+ const SourceName = `${featureName}-source`;
+ // attach ancestry to properties, as Mapbox strips down the features object to geometry and properties
+ const updatedArray = polygonArray.map((obj: any) => ({
+ ...obj,
+ properties: {
+ ...obj.properties,
+ ancestry: obj.ancestry,
+ teamsAssigned: obj.teams
+ }
+ }));
+ if (!mapRef.getSource(SourceName)) {
+ mapRef.addSource(SourceName, {
+ type: 'geojson',
+ data: {
+ type: 'FeatureCollection',
+ features: updatedArray
+ }
+ });
+ } else {
+ mapRef.getSource(SourceName).setData({
+ type: 'FeatureCollection',
+ features: updatedArray
+ });
+ }
+};
+
+export const AddLayer = (mapRef: any, layerName: string, sourceName: string, paintConfig: object) => {
+ if (!mapRef.getLayer(`${layerName}-layer`)) {
+ mapRef.addLayer({
+ id: `${layerName}-layer`,
+ type: 'fill',
+ source: `${sourceName}-source`,
+ paint: paintConfig
+ });
+ }
+};
+
+export const findAllIdentifiersToSend = (
+ searchedId: string,
+ locationObj: any,
+ selectedLocationForCampaign: Set = new Set([])
+) => {
+ selectedLocationForCampaign.add(searchedId);
+
+ const findClickedObjectByIdentifier = (searchedId: string, locObj: any): any => {
+ if (locObj.identifier === searchedId) {
+ selectedLocationForCampaign.add(locObj.identifier);
+ return locObj;
+ }
+
+ for (let i = 0; i < locObj.children.length; i++) {
+ const result = findClickedObjectByIdentifier(searchedId, locObj.children[i]);
+ if (result) {
+ return result;
+ }
+ }
+ };
+
+ function aStarSearch(root: any, target: any) {
+ const openSet = []; // Priority queue for A* (nodes to explore)
+ const cameFrom = new Map(); // Tracks paths
+ const gScore = new Map(); // Cost from start to this node
+
+ // Initialize
+ openSet.push({ node: root, path: [root.identifier] });
+ gScore.set(root.identifier, 0);
+
+ while (openSet.length > 0) {
+ // Sort openSet by gScore (or use a priority queue implementation)
+ openSet.sort((a, b) => gScore.get(a.node.identifier) - gScore.get(b.node.identifier));
+ const current: any = openSet.shift(); // Node with the lowest score
+ const { node, path } = current;
+
+ if (node.identifier === target) {
+ return path; // Found the target, return the path
+ }
+
+ // Explore children
+ for (const child of node.children || []) {
+ const tentativeGScore = gScore.get(node.identifier) + 1;
+
+ if (!gScore.has(child.identifier) || tentativeGScore < gScore.get(child.identifier)) {
+ // Update path and scores
+ cameFrom.set(child.identifier, node.identifier);
+ gScore.set(child.identifier, tentativeGScore);
+
+ // Add to openSet if not already present
+ if (!openSet.some(entry => entry.node.identifier === child.identifier)) {
+ openSet.push({ node: child, path: [...path, child.identifier] });
+ }
+ }
+ }
+ }
+
+ return null; // No path found
+ }
+
+ const getChildrenIdentifiers = (locObj: any) => {
+ selectedLocationForCampaign.add(locObj?.identifier);
+ locObj?.children.forEach((child: any) => {
+ if (child) {
+ selectedLocationForCampaign.add(child.identifier);
+ getChildrenIdentifiers(child);
+ }
+ });
+ };
+
+ const getParentIdentifiers = (searchedId: string) => {
+ const result = aStarSearch(locationObj, searchedId);
+ if (result) {
+ result.forEach((id: string) => {
+ selectedLocationForCampaign.add(id);
+ });
+ }
+ };
+
+ const clickedLevel = findClickedObjectByIdentifier(searchedId, locationObj);
+ getChildrenIdentifiers(clickedLevel);
+ getParentIdentifiers(searchedId);
+};
diff --git a/src/features/planSimulation/components/SimulationMapView/SimulationMapView.module.css b/src/features/planSimulation/components/SimulationMapView/SimulationMapView.module.css
new file mode 100644
index 00000000..9fb38688
--- /dev/null
+++ b/src/features/planSimulation/components/SimulationMapView/SimulationMapView.module.css
@@ -0,0 +1,277 @@
+.backDrop {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.5);
+ z-index: 1;
+}
+
+.spinner {
+ width: 3rem;
+ height: 3rem;
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ margin-left: -1.5rem;
+}
+
+.buttonDrawer {
+ background-color: white;
+ border: 1px solid #0000001a;
+ z-index: 2000;
+ padding: 1rem 0.5rem 1rem 0.5rem;
+ position: absolute;
+ transform: translate(-50%, -50%);
+ top: 50%;
+}
+
+.left {
+ border-top-right-radius: 10px;
+ border-bottom-right-radius: 10px;
+ left: 11.1px;
+}
+
+.right {
+ border-top-left-radius: 10px;
+ border-bottom-left-radius: 10px;
+ right: -12px;
+}
+
+.customIcon {
+ color: rgba(0, 0, 0, 0.623);
+ font-size: 1.5rem;
+}
+
+.langLatContainer {
+ position: absolute;
+ z-index: 2;
+ bottom: 15px;
+ left: 15px;
+ text-align: center;
+}
+
+.langLatContent {
+ display: flex;
+ gap: 1rem;
+ padding: 0.5rem 0.7rem;
+ margin: 0;
+ background-color: #606060da;
+ color: white;
+ border-radius: 5px;
+ font-size: 0.8rem;
+}
+
+.langLatContent p {
+ margin: 0;
+}
+
+.mapboxgl-ctrl-bottom-left {
+ top: 0;
+ right: 0;
+}
+
+.popupWrapper {
+ padding: 1rem;
+}
+
+.locationImage {
+ width: 20px;
+ height: 20px;
+ margin-right: 0.3rem;
+}
+
+.tagIcon {
+ width: 15px;
+ height: 15px;
+ margin-top: 0.3rem;
+}
+
+.tagInfo {
+ display: flex;
+ flex-direction: row;
+}
+
+.paragraphPopup p {
+ max-width: fit-content !important;
+ margin: 0 !important;
+ padding: 0 !important;
+}
+
+.popupHeadingContainer {
+ display: flex;
+ margin-bottom: 1rem;
+}
+
+.popupHeadingContainer p {
+ font-weight: 600;
+}
+
+.multiselectPanel {
+ position: absolute;
+ z-index: 2;
+ top: 15px;
+ right: 15px;
+ background-color: rgba(255, 255, 255, 0.815);
+ border-radius: 5px;
+ box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
+ min-width: 200px;
+}
+
+/*! Custom popup E.G. */
+.card {
+ font-family: 'BRFirma', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell',
+ 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif !important;
+ background-color: white;
+ border-radius: 0.5rem;
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+ padding: 1rem;
+ width: 20rem;
+}
+
+.header {
+ display: flex;
+ align-items: center;
+ gap: 0.1rem;
+ margin-bottom: 0.7rem;
+}
+
+.locationIcon {
+ color: #059669;
+ width: 1.25rem;
+ height: 1.25rem;
+}
+
+.title {
+ font-size: 1.3rem;
+ margin-bottom: 0;
+ font-weight: 600;
+ color: #1f2937;
+}
+
+.adminLevel {
+ background-color: #f9fafb;
+ border-radius: 0.375rem;
+ padding: 0.5rem;
+ margin-left: auto;
+ font-weight: 600;
+ font-size: 0.9rem;
+}
+
+.content {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.populationCard {
+ background-color: #f9fafb;
+ border-radius: 0.375rem;
+ padding: 0.75rem;
+ display: flex;
+ flex-direction: column;
+}
+
+.label {
+ font-size: 0.875rem;
+ font-weight: 500;
+ color: #4b5563;
+ margin-bottom: 0.25rem;
+}
+
+.value {
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: #111827;
+}
+
+.subtotalValueContainer {
+ display: flex;
+ flex-direction: column;
+ line-height: 1.2rem;
+}
+
+.sublabel {
+ font-size: 0.75rem;
+ color: #6b7280;
+ margin-top: 0.5rem;
+}
+
+.sublabelValue {
+ color: #000000;
+ font-size: 0.8rem;
+}
+
+.scoreContainer {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.scoreItem {
+ display: flex;
+ align-items: flex-start;
+ gap: 0.75rem;
+}
+
+.dropletIcon {
+ margin-top: 0.25rem;
+ width: 1.25rem;
+ height: 1.25rem;
+}
+
+.scoreInfo {
+ display: flex;
+ flex-direction: column;
+}
+
+.scoreLabel {
+ font-size: 0.875rem;
+ font-weight: 500;
+ color: #374151;
+}
+
+.scoreValue {
+ font-size: 1.125rem;
+ font-weight: 600;
+}
+
+.addToCampaignButton {
+ background-color: #059669;
+ color: white;
+ border: none;
+ border-radius: 0.375rem;
+ padding: 0.5rem 1rem;
+ font-size: 0.875rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background-color 0.3s;
+ text-decoration: none;
+ text-align: center;
+}
+
+.RemoveFromCampaignButton {
+ background-color: #e70101;
+ color: white;
+ border: none;
+ border-radius: 0.375rem;
+ padding: 0.5rem 1rem;
+ font-size: 0.875rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background-color 0.3s;
+ text-decoration: none;
+ text-align: center;
+}
+
+.RemoveFromCampaignButton:hover {
+ background-color: #ca0303;
+ color: white;
+}
+
+.addToCampaignButton:hover {
+ background-color: #047857;
+ color: white;
+}
diff --git a/src/features/planSimulation/components/SimulationMapView/SimulationMapView.tsx b/src/features/planSimulation/components/SimulationMapView/SimulationMapView.tsx
index 4f943859..4aae3ab2 100644
--- a/src/features/planSimulation/components/SimulationMapView/SimulationMapView.tsx
+++ b/src/features/planSimulation/components/SimulationMapView/SimulationMapView.tsx
@@ -1,94 +1,96 @@
-import { EventData, GeoJSONSource, LngLatBounds, Map as MapBoxMap, MapLayerEventType, Popup } from 'mapbox-gl';
-import React, { MutableRefObject, useCallback, useEffect, useRef, useState } from 'react';
-import { Accordion, Button, Col, Container, Form, FormGroup, Row, Tab, Tabs } from 'react-bootstrap';
+import { EventData, Expression, GeoJSONSource, MapLayerEventType, Popup } from 'mapbox-gl';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { Button, Col, Container, Form, Row } from 'react-bootstrap';
import { MAPBOX_STYLE_STREETS } from '../../../../constants';
import {
+ createLocationLabel,
disableMapInteractions,
fitCollectionToBounds,
getFeatureCentresFromLocation,
getGeoListFromMapData,
+ getPolygonCenter,
getTagStats,
initSimulationMap,
PARENT_LABEL_SOURCE,
PARENT_SOURCE
} from '../../../../utils';
-import {
- EntityTag,
- PlanningLocationResponse,
- PlanningParentLocationResponse,
- RevealFeature
-} from '../../providers/types';
-import { hex, hsv } from 'color-convert';
-import {
- Feature,
- FeatureCollection,
- MultiPoint,
- MultiPolygon,
- Point,
- pointsWithinPolygon,
- Polygon,
- Properties
-} from '@turf/turf';
-import { ColorPicker, Color, useColor } from 'react-color-palette';
+import { PlanningLocationResponse, PlanningParentLocationResponse } from '../../providers/types';
+import { bbox, Feature, MultiPoint, MultiPolygon, Point, pointsWithinPolygon, Polygon, Properties } from '@turf/turf';
+import { Color, useColor } from 'react-color-palette';
import 'react-color-palette/lib/css/styles.css';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Children, StatsLayer } from '../Simulation';
+import { faCaretRight, faCaretLeft } from '@fortawesome/free-solid-svg-icons';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { Children } from '../Simulation';
import ActionDialog from '../../../../components/Dialogs/ActionDialog';
import MapboxDraw from '@mapbox/mapbox-gl-draw';
-import { AnalysisLayer } from '../Simulation';
-import { isNumeric } from 'mathjs';
-
-interface Props {
- fullScreenHandler: () => void;
- fullScreen: boolean;
- toLocation: LngLatBounds | undefined;
- entityTags: EntityTag[];
- parentMapData: PlanningParentLocationResponse | undefined;
- setMapDataLoad: (data: PlanningLocationResponse) => void;
- chunkedData: PlanningLocationResponse;
- resetMap: boolean;
- setResetMap: (resetMap: boolean) => void;
- resultsLoadingState: 'notstarted' | 'error' | 'started' | 'complete';
- parentsLoadingState: 'notstarted' | 'error' | 'started' | 'complete';
- stats: StatsLayer;
- map: MutableRefObject;
- updateMarkedLocations: (identifier: string, ancestry: string[], marked: boolean) => void;
- parentChild: { [parent: string]: Children };
- analysisLayerDetails: AnalysisLayer[];
-}
-
-const lineParameters: any = {
- countryx: { col: 'red', num: 1, offset: 2.5 },
- countyx: { col: 'blue', num: 1, offset: 2.5 },
- subcountyx: { col: 'darkblue', num: 1, offset: 6 },
- wardx: { col: 'yellow', num: 2, offset: 3 },
- catchmentx: { col: 'purple', num: 3, offset: 1 }
-};
+import styles from './SimulationMapView.module.css';
+import Spinner from 'react-bootstrap/Spinner';
+
+import { FeatureCollection, Geometry } from 'geojson';
+
+import locationTag from '../../../../assets/svgs/placeMarker.svg';
+import tagIcon from '../../../../assets/svgs/tag-2.svg';
+// UTISLS
+import {
+ getLineParameters,
+ updateFeaturesWithTagStatsAndColorAndTransparency,
+ updateSelectedTag,
+ updateLayerProperty,
+ updateSelectedLayerProperty
+} from './SimulationMapViewUtils';
+// INTERFACE
+import { SimulationMapViewProps, UserDefinedLayer, UserDefinedNames } from './SimulationMapViewModels';
+// CONTANTS
+import {
+ INITIAL_FILL_COLOR,
+ INITIAL_HEAT_MAP_OPACITY,
+ INITIAL_HEAT_MAP_RADIUS,
+ INITIAL_LINE_COLOR
+} from './SimulationMapViewConstants';
+import StatisticsPanel from './components/StatisticsPanel/StatisticsPanel';
+import DataSetPanel from './components/DataSetPanel/DataSetPanel';
+import mapboxgl from 'mapbox-gl';
+
+import * as turf from '@turf/turf';
+import {
+ AddLayer,
+ DrawPolygonsFeature,
+ DrawPolygonsFeatureCollection,
+ findAllIdentifiersToSend
+} from './Helper/MapInteractionsHelper';
+
+// CONTEXT
+import { SelectedPolygon, usePolygonContext } from '../../../../contexts/PolygonContext';
+import TargetsSelectedList from '../TargetsSelectedList/TargetsSelectedList';
+import MapLegend from './components/MapLegend/MapLegend';
+import { assignLocationsToPlan } from './api/planAPI';
+import { set } from 'react-hook-form';
+import { getSimulationData } from './api/datasetsAPI';
+import { getPlanInfo } from './api/hierarchyAPI';
+import { findNodeById, getIdsByGeographicLevel } from './util';
+import { AssignToTeamsDialog } from '../AssignToTeamsDialog/AssignToTeamsDialog';
+
+library.add(faCaretRight, faCaretLeft);
+
+// DATASET REFACTORED GET COLOR FUNCTION
export const getBackgroundStyle = (value: { r: number; g: number; b: number } | null) => {
if (value != null) {
- return (
- 'linear-gradient(to right, rgba(' +
- value.r +
- ', ' +
- value.g +
- ', ' +
- value.b +
- ', 0), rgba(' +
- value.r +
- ',' +
- value.g +
- ', ' +
- value.b +
- ', 1)'
- );
+ return `rgb(${value.r}, ${value.g}, ${value.b})`;
} else {
- return 'linear-gradient(to right, rgba(255,255,255, 0.5), rgba(255,255,255, 0.5)';
+ return 'rgb(255, 255, 255)'; // Default solid white
}
};
const SimulationMapView = ({
- fullScreenHandler,
+ teamsList,
+ currentLocationChildren,
+ loading,
+ leftOpenHandler,
+ rightOpenHandler,
+ leftOpenState,
+ rightOpenState,
fullScreen,
toLocation,
entityTags,
@@ -102,23 +104,21 @@ const SimulationMapView = ({
map,
updateMarkedLocations,
parentChild,
- analysisLayerDetails
-}: Props) => {
- const INITIAL_HEAT_MAP_RADIUS = 50;
- const INITIAL_HEAT_MAP_OPACITY = 0.2;
- const INITIAL_FILL_COLOR = '#005512';
- const INITIAL_LINE_COLOR = '#000000';
-
+ analysisLayerDetails,
+ selectedLoaction,
+ showDatasetsAgainstParentLevel = false
+}: SimulationMapViewProps) => {
const [defColor] = useColor('hex', INITIAL_FILL_COLOR);
const mapContainer = useRef();
const [color, setColor] = useColor('hex', INITIAL_FILL_COLOR);
const [initialLineColor] = useColor('hex', INITIAL_LINE_COLOR);
- const [lng, setLng] = useState(28.33);
- const [lat, setLat] = useState(-15.44);
- const [zoom, setZoom] = useState(10);
- const [showUserDefineLayerSelector, setShowUserDefineLayerSelector] = useState(false);
+ const [lng, setLng] = useState(20.33);
+ const [lat, setLat] = useState(4.44);
+ const [zoom, setZoom] = useState(2.5);
+
+ // DATASET MIGRATION TO PARENT
const hoverPopup = useRef(
new Popup({
@@ -127,37 +127,25 @@ const SimulationMapView = ({
offset: 20
})
);
- const [showMapControls, setShowMapControls] = useState(true);
+
+ const polygonClickPopup = useRef(
+ new Popup({
+ closeOnClick: false,
+ closeButton: false,
+ offset: 20,
+ className: styles.paragraphPopup
+ })
+ );
+ const zoomRef = useRef(0);
+
+ // we are using ref for these two, as state won't do for event listener handlers
+ const targetAreasRef = useRef([]);
+ const assignedLocationsRef = useRef({});
+
const [parentMapStateData, setParentMapStateData] = useState();
- const [showStats, setShowStats] = useState(true);
- const [userDefinedLayers, setUserDefinedLayers] = useState<
- {
- layer: string;
- key: string;
- geo: string;
- layerName: string;
- active: boolean;
- col: Color;
- tagList?: Set;
- selectedTag?: string;
- transparency?: number;
- size?: number;
- lineWidth?: number;
- lineColor?: string;
- }[]
- >([]);
- const [userDefinedNames, setUserDefinedNames] = useState<
- {
- layer: string;
- key: string;
- layerName: string;
- active: boolean;
- col: Color;
- tagList?: Set;
- selectedTag?: string;
- }[]
- >([]);
+ const [userDefinedLayers, setUserDefinedLayers] = useState([]);
+ const [userDefinedNames, setUserDefinedNames] = useState([]);
const [showMapDrawnModal, setShowMapDrawnModal] = useState(false);
@@ -166,76 +154,54 @@ const SimulationMapView = ({
const [drawnMapLevel, setDrawnMapLevel] = useState();
const [shouldApplyToChildren, setShouldApplyToChildren] = useState(true);
const [shouldApplyToAll, setShouldApplyToAll] = useState(false);
- const [selectedUserDefinedLayer, setSelectedUserDefinedLayer] = useState<
- { key: string; col: Color; transparency?: number; lineColor: Color } | undefined
- >();
+ const [selectedUserDefinedLayer, setSelectedUserDefinedLayer] = useState();
+
const [showUserDefinedSettingsPanel, setShowUserDefinedSettingsPanel] = useState(false);
- const getLineParameters = (level: string) => {
- let newlevel = level
- .split('-')
- .filter((item, index) => index > 0)
- .join('-');
- if (lineParameters[newlevel]) {
- return lineParameters[newlevel];
- } else {
- return { col: 'black', num: 1, offset: 0 };
- }
- };
- const getTransparencyValue = (
- value: {
- layer: string;
- key: string;
- geo: string;
- layerName: string;
- active: boolean;
- col: Color;
- tagList?: Set;
- selectedTag?: string;
- transparency?: number;
- }[],
- selectedValue: { key: string; col: Color; transparency?: number } | undefined
- ) => {
- let transparency = 10;
- if (value) {
- const filterElement = value.filter(userDefinedLayer => userDefinedLayer.layerName === selectedValue?.key)[0];
- if (filterElement && filterElement.transparency !== undefined && filterElement.transparency !== null) {
- transparency = filterElement.transparency;
- }
- }
- return transparency;
- };
+ // SELECTIONS ON MAP
+ const [singleSelected, setSingleSelected] = useState(null);
+ const [multiSelected, setMultiSelected] = useState([]);
- const getLineWidthValue = (
- value: {
- layer: string;
- key: string;
- geo: string;
- layerName: string;
- active: boolean;
- col: Color;
- tagList?: Set;
- selectedTag?: string;
- transparency?: number;
- lineWidth?: number;
- }[],
- selectedValue: { key: string; col: Color; transparency?: number } | undefined
- ) => {
- let lineWidth = 1;
- if (value) {
- const filterElement = value.filter(userDefinedLayer => userDefinedLayer.layerName === selectedValue?.key)[0];
- if (filterElement && filterElement.lineWidth !== undefined && filterElement.lineWidth !== null) {
- lineWidth = filterElement.lineWidth;
- }
- }
- return lineWidth;
- };
+ const [singleSelectedColor, setSingleSelectedColor] = useState('rgba(3, 166, 13, 0.4)');
+ const [multiSelectedColor, setMultiSelectedColor] = useState('rgba(0, 0, 102, 0.4)');
+
+ const [datasets, setDatasets] = useState();
+ const [datasetsDataMap, setDatasetsDataMap] = useState({});
+
+ // DATASET OPACITY IDS
+ const [datasetLayersId, setDatasetLayersId] = useState([]);
+
+ // Assign location to team
+ const [assignToTeamPopup, setAssignToTeamPopup] = useState(false);
+ const [locationForTeamAssignment, setLocationForTeamAssignment] = useState();
+
+ const [toggleAssignedLayer, setToggleAssignedLayer] = useState(false);
+ // CONTEXT
+ const { dispatch } = usePolygonContext();
+ const { state } = usePolygonContext();
+ const selectedState = state.selected;
+ const multiselectState = state.multiselect;
+ const locationsObject = state.polygons?.[0];
+ const planId = state.planid;
+
+ useMemo(() => {
+ setSingleSelected(selectedState?.id ?? null);
+ setMultiSelected(multiselectState as any[]);
+ }, [selectedState, multiselectState]);
useEffect(() => {
if (map.current) return;
initializeMap();
});
+ useEffect(() => {
+ targetAreasRef.current = state.targetAreas;
+ }, [state.targetAreas]);
+
+ useEffect(() => {
+ assignedLocationsRef.current = state.assingedLocations;
+ }, [state.assingedLocations]);
+
useEffect(() => {
if (map.current) {
if (
@@ -250,6 +216,70 @@ const SimulationMapView = ({
}
}, [resultsLoadingState, parentsLoadingState, map]);
+ const handleCampaignClick = (clickedFeature: any) => {
+ // ancestry contains a list of all ids of levels above this polygon
+ const ancestry = JSON.parse(clickedFeature.properties?.ancestry);
+ // find polygon as object from hierarchy, as it contains a list of its children
+ const currentLoc = findNodeById(state.polygons, clickedFeature.properties?.id);
+
+ // get all children (except structures) ids
+ const results = getIdsByGeographicLevel(currentLoc.children);
+
+ // as target areas lowest possible level for operational area, we just need their id and their ancestry (children are just structures)
+ const assignedAreas = targetAreasRef.current?.flatMap(ta => [...ta.ancestry, ta.identifier]) || [];
+
+ const allLocationsIdsToBeAssigned = new Set([
+ ...results,
+ ...ancestry,
+ ...assignedAreas,
+ clickedFeature.properties?.id
+ ]);
+ const identifiersToSendArray = Array.from(allLocationsIdsToBeAssigned);
+
+ // check assignment map - if there is no assignment map, use polygon's properties.assigned flag
+ // if assignment map, use the value for assigned flag thats under the key = clickedFeature.identifier
+ const condition = assignedLocationsRef.current
+ ? !assignedLocationsRef.current?.[clickedFeature.properties?.id]
+ : !clickedFeature.properties?.assigned;
+ if (planId) {
+ if (condition) {
+ assignLocationsToPlan(planId, identifiersToSendArray).then(async (res: any) => {
+ // update assignment map
+ dispatch({
+ type: 'SET_ASSIGNED',
+ payload: { ...assignedLocationsRef?.current, [clickedFeature.properties?.id]: true }
+ });
+ // refetch target areas, so the map updates
+ const simulationData = await getSimulationData(state.planid);
+ dispatch({ type: 'SET_TARGET_AREAS', payload: simulationData.targetAreas });
+ polygonClickPopup.current.remove();
+ dispatch({ type: 'CLEAR_SELECTION' });
+ });
+ } else {
+ // if remove, find the ids bound to the clicked property - its ancestry and children and its own id and remove from assignedAreas
+ const toExcludeSet = new Set([...results, ...ancestry, clickedFeature.properties?.id]);
+ const filtered = assignedAreas.filter(item => !toExcludeSet.has(item));
+ assignLocationsToPlan(planId, filtered).then(async () => {
+ // update assignment map
+ dispatch({
+ type: 'SET_ASSIGNED',
+ payload: { ...assignedLocationsRef?.current, [clickedFeature.properties?.id]: false }
+ });
+ // refetch target areas, so the map updates
+ const simulationData = await getSimulationData(state.planid);
+ dispatch({ type: 'SET_TARGET_AREAS', payload: simulationData.targetAreas });
+ polygonClickPopup.current.remove();
+ dispatch({ type: 'CLEAR_SELECTION' });
+ });
+ }
+ }
+ };
+
+ const handleTeamAssignment = async (location: any) => {
+ setLocationForTeamAssignment(location);
+ setAssignToTeamPopup(true);
+ };
+
const updateChildrenOfSelectedLocation = useCallback(
(identifier: string) => {
let children: Children = parentChild[identifier];
@@ -383,7 +413,7 @@ const SimulationMapView = ({
]);
const initializeMap = useCallback(() => {
- map.current = initSimulationMap(mapContainer, [lng, lat], zoom, 'bottom-left', undefined, e => {
+ map.current = initSimulationMap(mapContainer, [lng, lat], zoom, 'bottom-right', undefined, e => {
if (map.current) {
let source: any = map.current?.getSource('mark-source') as GeoJSONSource;
@@ -433,8 +463,8 @@ const SimulationMapView = ({
mapBoxDraw.current = new MapboxDraw({
controls: {
- trash: true,
- polygon: true,
+ trash: false,
+ polygon: false,
point: false,
uncombine_features: false,
combine_features: false,
@@ -442,7 +472,7 @@ const SimulationMapView = ({
}
});
- map.current.addControl(mapBoxDraw.current, 'bottom-left');
+ map.current.addControl(mapBoxDraw.current, 'bottom-right');
map.current?.on(
'mouseover',
@@ -450,6 +480,7 @@ const SimulationMapView = ({
(e: MapLayerEventType['mouseover'] & EventData) => {
if (e.features) {
let acc: Feature[] = [];
+
userDefinedLayers
.filter(layer => layer.active)
.forEach(layer => {
@@ -511,6 +542,16 @@ const SimulationMapView = ({
}
);
map.current?.setStyle(MAPBOX_STYLE_STREETS);
+
+ const handleZoom = () => {
+ zoomRef.current = map.current?.getZoom() || 0;
+ };
+
+ map.current?.on('zoom', handleZoom);
+
+ return () => {
+ map.current?.off('zoom', handleZoom);
+ };
}, [lat, lng, map, zoom, userDefinedLayers]);
useEffect(() => {
@@ -523,8 +564,656 @@ const SimulationMapView = ({
}, [resetMap, initializeMap, setResetMap]);
useEffect(() => {
- if (toLocation && map && map.current) map.current?.fitBounds(toLocation);
- }, [toLocation, map]);
+ if (currentLocationChildren) {
+ const groupedById: any = {};
+ currentLocationChildren.forEach((location: any) => {
+ location.properties?.metadata?.forEach((meta: any) => {
+ if (!groupedById[meta.datasetId]) {
+ groupedById[meta.datasetId] = [];
+ }
+ groupedById[meta.datasetId].push({
+ type: 'Feature',
+ properties: {
+ locationId: location.identifier,
+ value: meta.value,
+ tagName: meta.type,
+ id: meta.datasetId,
+ name: location.properties.name,
+ geographicLevel: location.properties.geographicLevel || ''
+ },
+ geometry: location.geometry
+ });
+ });
+ });
+ setDatasetsDataMap(groupedById);
+ }
+ }, [currentLocationChildren]);
+
+ useEffect(() => {
+ if (datasetsDataMap) {
+ Object.entries(datasetsDataMap).forEach(([layerId, features]) => {
+ const { maxValue, minValue } = (features as any[]).reduce(
+ (acc, feature) => {
+ const value = feature.properties?.value;
+ if (typeof value === 'number' && !isNaN(value)) {
+ acc.maxValue = acc.maxValue === undefined ? value : Math.max(acc.maxValue, value);
+ acc.minValue = acc.minValue === undefined ? value : Math.min(acc.minValue, value);
+ }
+ return acc;
+ },
+ { maxValue: undefined, minValue: undefined }
+ );
+
+ dispatch({
+ type: 'UPDATE_DATASET',
+ payload: {
+ datasetId: layerId,
+ filter: {
+ maxValue: maxValue,
+ minValue: minValue
+ }
+ }
+ });
+ });
+ }
+ }, [datasetsDataMap]);
+
+ // useEffect(() => {
+
+ // }, [
+ // datasetLayersId,
+ // datasetsDataMap,
+ // map,
+ // state.datasets,
+ // state.opacitySliderValue.id,
+ // state.opacitySliderValue.value
+ // ]);
+
+ useEffect(() => {
+ if (datasetsDataMap && map && map.current && state.datasets && state.datasets.length !== 0) {
+ const colors = generateColors(state.datasets);
+
+ const borderConfig = generateBorderDetails(state.datasets);
+
+ // const layers = map.current?.getStyle().layers;
+ let layers;
+
+ try {
+ layers = map.current?.getStyle().layers;
+ } catch (error) {
+ return;
+ }
+
+ const matchingLayers = layers?.filter(
+ layer =>
+ layer.id.startsWith(`ds-`) &&
+ (!layer.id.endsWith(selectedLoaction.properties.name) || showDatasetsAgainstParentLevel)
+ );
+ matchingLayers?.forEach(layer => {
+ if (map.current?.getLayer(layer.id)) {
+ map.current?.removeLayer(layer.id);
+ }
+ });
+
+ Object.entries(datasetsDataMap).forEach(([layerId, features]) => {
+ const sourceId = `ds-${layerId}-${selectedLoaction.properties.name}`;
+
+ if (!map.current?.getSource(sourceId)) {
+ map.current?.addSource(sourceId, {
+ type: 'geojson',
+ data: {
+ type: 'FeatureCollection',
+ features: features as any[]
+ }
+ });
+ } else {
+ let source = map.current?.getSource(sourceId) as any;
+ source.setData({
+ type: 'FeatureCollection',
+ features: features as any[]
+ });
+ }
+
+ setDatasetLayersId((pre: string[]) => {
+ const sourceIdForState = `ds@${layerId}@${selectedLoaction.properties.name}`;
+ if (!pre.includes(sourceIdForState)) {
+ return [...pre, sourceIdForState];
+ }
+ return pre;
+ });
+
+ const matchingDataset = state.datasets.find((d: any) => d.identifier === layerId);
+ if (!matchingDataset && map.current?.getLayer(`ds-${layerId}-${selectedLoaction.properties.name}`)) {
+ map.current?.removeLayer(`ds-${layerId}-${selectedLoaction.properties.name}`);
+ return;
+ }
+
+ const fillColorConfig: Expression = [
+ 'case',
+ ['==', ['literal', matchingDataset?.hidden], true],
+ 'transparent',
+ ['<', ['get', 'value'], matchingDataset?.selectedRange?.minValue],
+ 'transparent',
+ ['>', ['get', 'value'], matchingDataset?.selectedRange?.maxValue],
+ 'transparent',
+ colors[layerId]
+ ];
+
+ const lineWidthConfig: Expression = [
+ 'case',
+ ['==', ['literal', matchingDataset?.hidden], true],
+ 0,
+ borderConfig[layerId]?.lineWidth
+ ];
+
+ const borderColorConfig: Expression = [
+ 'case',
+ ['==', ['literal', matchingDataset?.hidden], true],
+ 'transparent',
+ borderConfig[layerId]?.borderColor
+ ];
+
+ const fillOpacityConfig: Expression | number =
+ matchingDataset?.filter?.maxValue && matchingDataset.filter.maxValue > 0
+ ? ['interpolate', ['linear'], ['get', 'value'], 0, 0, matchingDataset.filter.maxValue, 1]
+ : 0;
+
+ if (!map.current?.getLayer(`ds-${layerId}-${selectedLoaction.properties.name}-outline`)) {
+ map.current?.addLayer({
+ id: `ds-${layerId}-${selectedLoaction.properties.name}-outline`,
+ type: 'line',
+ source: sourceId,
+ layout: {},
+ paint: {
+ 'line-color': borderColorConfig,
+ 'line-width': lineWidthConfig
+ }
+ });
+ } else {
+ map.current?.setPaintProperty(
+ `ds-${layerId}-${selectedLoaction.properties.name}-outline`,
+ 'line-width',
+ lineWidthConfig
+ );
+ map.current?.setPaintProperty(
+ `ds-${layerId}-${selectedLoaction.properties.name}-outline`,
+ 'line-color',
+ borderColorConfig
+ );
+ }
+
+ if (map.current?.getLayer(`ds-${layerId}-${selectedLoaction.properties.name}`)) {
+ map.current?.setPaintProperty(
+ `ds-${layerId}-${selectedLoaction.properties.name}`,
+ 'fill-color',
+ fillColorConfig
+ );
+
+ map.current?.setPaintProperty(
+ `ds-${layerId}-${selectedLoaction.properties.name}`,
+ 'fill-opacity',
+ fillOpacityConfig
+ );
+ } else {
+ map.current?.addLayer({
+ id: `ds-${layerId}-${selectedLoaction.properties.name}`,
+ type: 'fill',
+ source: sourceId,
+ filter: ['==', ['get', 'id'], layerId],
+ paint: {
+ 'fill-color': fillColorConfig,
+ 'fill-opacity': fillOpacityConfig
+ }
+ });
+ }
+ });
+
+ if (datasetsDataMap && map && map.current && state.datasets && state.datasets.length !== 0 && datasetLayersId) {
+ const datasetIds = datasetLayersId.filter((sourceId: string) => {
+ const layerId = sourceId.split('@')[1];
+
+ return Object.keys(state.opacitySliderValue).includes(layerId);
+ });
+
+ datasetIds.forEach((datasetId: string) => {
+ if (datasetId) {
+ const newSourceId = datasetId.replaceAll('@', '-');
+ const layerIdNew = datasetId.split('@')[1];
+ if (map.current?.getLayer(newSourceId) && newSourceId) {
+ const matchingDataset = state.datasets.find((d: any) => d.identifier === layerIdNew);
+
+ map.current?.setPaintProperty(newSourceId, 'fill-opacity', [
+ 'interpolate',
+ ['linear'],
+ ['get', 'value'],
+ 0,
+ 0,
+ matchingDataset.filter.maxValue,
+ state.opacitySliderValue[layerIdNew] / 100
+ ]);
+ }
+ }
+ });
+ }
+
+ if (map.current.getLayer('children-layer')) {
+ map.current.moveLayer('children-layer');
+ }
+ if (map.current.getLayer('multi-selected-layer')) {
+ map.current.moveLayer('multi-selected-layer');
+
+ if (map.current.getLayer('target-areas-layer')) {
+ map.current.moveLayer('target-areas-layer');
+ }
+ }
+ if (map.current.getLayer('labels-layer')) {
+ map.current.moveLayer('labels-layer');
+ }
+ }
+ if (
+ map &&
+ map.current &&
+ datasetsDataMap &&
+ Object.values(datasetsDataMap).length !== 0 &&
+ state.datasets &&
+ state.datasets.length === 0
+ ) {
+ const layers = map.current?.getStyle().layers;
+ const matchingLayers = layers?.filter(layer => layer.id.startsWith(`ds-`));
+ matchingLayers?.forEach(layer => {
+ if (map.current?.getLayer(layer.id)) {
+ map.current?.removeLayer(layer.id);
+ }
+ });
+ }
+ }, [map, map.current, state.datasets, datasetsDataMap, state.opacitySliderValue]);
+
+ useEffect(() => {
+ // console.log('selectedLoaction', currentLocationChildren);
+
+ if (map && map.current && currentLocationChildren && selectedLoaction) {
+ if (!showDatasetsAgainstParentLevel) {
+ map.current?.fitBounds(JSON.parse(JSON.stringify(bbox(selectedLoaction.geometry))));
+ }
+
+ // Add or update the "parent-source" {REFACTORED}
+ DrawPolygonsFeature(map.current, selectedLoaction, 'parent');
+
+ // Add or update the "children-source" {REFACTORED}
+
+ DrawPolygonsFeatureCollection(map.current, currentLocationChildren, 'children');
+
+ // Add or update the "parent-layer" {REFACTORED}
+ AddLayer(map.current, 'parent', 'parent', {
+ 'fill-color': 'rgba(57, 62, 65, 0)',
+ 'fill-outline-color': 'rgba(57, 62, 65, 1)'
+ });
+
+ // Add or update the "children-layer" with individual polygon colors
+ if (!map.current.getLayer('children-layer')) {
+ // Add or update the "children-layer" {REFACTORED}
+ // const paintConfig = {
+ // 'fill-color': [
+ // 'case',
+ // ['==', ['get', 'id'], singleSelected],
+ // singleSelectedColor,
+ // 'rgba(239, 239, 240, 0)' // Default color
+ // ],
+ // 'fill-outline-color': 'rgba(000, 000, 000, 0.5)'
+ // };
+
+ const paintConfig = {
+ 'fill-color': [
+ 'case',
+ ['==', ['get', 'id'], singleSelected],
+ singleSelectedColor, // Highlight selected
+
+ ['==', ['get', 'businessStatus'], 'Not Visited'],
+ 'rgba(255, 255, 0, 1)', // Yellow
+ ['==', ['get', 'businessStatus'], 'Not Eligible'],
+ 'rgba(0, 0, 0, 1)', // Black
+ ['==', ['get', 'businessStatus'], 'Family / structure registered'],
+ 'rgba(255, 192, 203, 1)', // Pink
+ ['==', ['get', 'businessStatus'], 'In Progress'],
+ 'rgba(255, 165, 0, 1)', // Orange
+ ['==', ['get', 'businessStatus'], 'Unable to complete / referral'],
+ 'rgba(255, 0, 0, 1)', // Red
+ ['==', ['get', 'businessStatus'], 'Complete'],
+ 'rgba(0, 128, 0, 1)', // Green
+
+ 'rgba(239, 239, 240, 0)' // Default transparent color
+ ],
+ 'fill-outline-color': 'rgba(0, 0, 0, 0.5)'
+ };
+ AddLayer(map.current, 'children', 'children', paintConfig);
+
+ //! Add a new source for multi-selected polygons
+ DrawPolygonsFeatureCollection(map.current, multiSelected, 'multi-selected');
+
+ //! Add a new layer for multi-selected polygons
+ AddLayer(map.current, 'multi-selected', 'multi-selected', {
+ 'fill-color': multiSelectedColor,
+ 'fill-outline-color': 'rgba(255, 0, 0, 1)'
+ });
+
+ map.current.on('click', 'children-layer', e => {
+ const clickedFeature = e.features && e.features[0] ? e.features[0] : null;
+
+ if (e.originalEvent.ctrlKey || e.originalEvent.metaKey) {
+ dispatch({ type: 'TOGGLE_MULTISELECT', payload: clickedFeature });
+
+ // Update multi-selected source data
+ const updatedFeatures = multiSelected.map(feature => ({
+ type: 'Feature',
+ geometry: feature.geometry,
+ properties: feature.properties
+ }));
+ (map.current?.getSource('multi-selected-source') as GeoJSONSource).setData({
+ type: 'FeatureCollection',
+ features: updatedFeatures.map(feature => ({
+ ...feature,
+ type: 'Feature'
+ }))
+ });
+ } else {
+ dispatch({ type: 'SELECT_SINGLE', payload: clickedFeature });
+
+ if (map.current && clickedFeature) {
+ const createPopupContent = () => {
+ const tagData = clickedFeature.properties?.metadata
+ ? JSON.parse(clickedFeature.properties.metadata)
+ : [];
+
+ // Create the popup container
+ const container = document.createElement('div');
+ container.className = styles.card;
+
+ // Header section
+ const header = document.createElement('div');
+ header.className = styles.header;
+ const img = document.createElement('img');
+ img.className = styles.locationImage;
+ img.src = locationTag;
+ img.alt = 'location';
+ const title = document.createElement('h2');
+ const adminLevel = document.createElement('span');
+ adminLevel.className = styles.adminLevel;
+ title.className = styles.title;
+ title.textContent = clickedFeature.properties?.name;
+ adminLevel.textContent = clickedFeature.properties?.geographicLevel;
+ header.appendChild(img);
+ header.appendChild(title);
+ header.appendChild(adminLevel);
+
+ // Content section
+ const content = document.createElement('div');
+ content.className = styles.content;
+
+ // Population card
+ if (clickedFeature.properties?.population && clickedFeature.properties?.childrenNumber) {
+ const populationCard = document.createElement('div');
+ populationCard.className = styles.populationCard;
+ populationCard.innerHTML = `
+ Population
+ ${
+ Math.round(JSON.parse(clickedFeature.properties?.population)?.sum)?.toLocaleString() ??
+ 'Not Available'
+ }
+
+
Children Number
+
${
+ clickedFeature.properties?.childrenNumber ?? 'Not Available'
+ }
+
+ `;
+ content.appendChild(populationCard);
+ }
+
+ // Score container
+ const scoreContainer = document.createElement('div');
+ scoreContainer.className = styles.scoreContainer;
+
+ tagData.forEach((tag: any) => {
+ const scoreItem = document.createElement('div');
+ scoreItem.className = styles.scoreItem;
+
+ scoreItem.innerHTML = `
+
+
+
${tag.type}
+
+ ${Math.round(tag.value * 1000) / 1000}
+
+
+ `;
+ scoreContainer.appendChild(scoreItem);
+ });
+
+ // Button with event listener
+ const button = document.createElement('a');
+ const assignToATeamButton = document.createElement('a');
+ const condition = assignedLocationsRef.current
+ ? !assignedLocationsRef.current?.[clickedFeature.properties?.id]
+ : !clickedFeature.properties?.assigned;
+ if (condition) {
+ button.textContent = 'Add to campaign';
+ button.className = styles.addToCampaignButton;
+ } else {
+ if ((teamsList ?? []).length > 0) {
+ assignToATeamButton.textContent = 'Assign to a team';
+ assignToATeamButton.className = styles.addToCampaignButton;
+ }
+ button.textContent = 'Remove from campaign';
+ button.className = styles.RemoveFromCampaignButton;
+ }
+ button.addEventListener('click', () => handleCampaignClick(clickedFeature));
+ if ((teamsList ?? []).length > 0) {
+ assignToATeamButton.addEventListener('click', () => {
+ console.log('clickedFeature', clickedFeature);
+
+ handleTeamAssignment(clickedFeature);
+ });
+ scoreContainer.appendChild(assignToATeamButton);
+ }
+ scoreContainer.appendChild(button);
+
+ content.appendChild(scoreContainer);
+
+ // Append header and content to the container
+ container.appendChild(header);
+ container.appendChild(content);
+
+ return container;
+ };
+
+ //! TESTING MUTIRAJUCI SET
+ // Add popup to the map
+ polygonClickPopup.current
+ .setLngLat(e.lngLat)
+ .setDOMContent(createPopupContent()) // Use DOM content instead of raw HTML
+ .setOffset([150, -25])
+ .addTo(map.current);
+ }
+ }
+ });
+ } else {
+ // map.current?.setPaintProperty('children-layer', 'fill-color', [
+ // 'case',
+ // ['==', ['get', 'id'], singleSelected],
+ // singleSelectedColor,
+ // 'rgba(57, 62, 65, 0.05)' // Default color
+ // ]);
+
+ map.current?.setPaintProperty('children-layer', 'fill-color', [
+ 'case',
+ ['==', ['get', 'id'], singleSelected],
+ singleSelectedColor, // Highlight selected
+
+ ['==', ['get', 'businessStatus'], 'Not Visited'],
+ 'rgba(255, 255, 0, 1)', // Yellow
+ ['==', ['get', 'businessStatus'], 'Not Eligible'],
+ 'rgba(0, 0, 0, 1)', // Black
+ ['==', ['get', 'businessStatus'], 'Family / structure registered'],
+ 'rgba(255, 192, 203, 1)', // Pink
+ ['==', ['get', 'businessStatus'], 'In Progress'],
+ 'rgba(255, 165, 0, 1)', // Orange
+ ['==', ['get', 'businessStatus'], 'Unable to complete / referral'],
+ 'rgba(255, 0, 0, 1)', // Red
+ ['==', ['get', 'businessStatus'], 'Complete'],
+ 'rgba(0, 128, 0, 1)', // Green
+
+ 'rgba(57, 62, 65, 0.05)' // Default color
+ ]);
+
+ map.current?.setPaintProperty('children-layer', 'fill-opacity', [
+ 'case',
+ ['==', ['get', 'id'], singleSelected],
+ 1,
+ 0.2
+ ]);
+
+ const updatedFeatures = multiSelected.map(feature => ({
+ type: 'Feature',
+ geometry: feature.geometry,
+ properties: feature.properties
+ }));
+ (map.current?.getSource('multi-selected-source') as GeoJSONSource).setData({
+ type: 'FeatureCollection',
+ features: updatedFeatures.map(feature => ({
+ ...feature,
+ type: 'Feature'
+ }))
+ });
+ }
+
+ addLabelsLayer(showDatasetsAgainstParentLevel && zoomRef.current ? zoomRef.current : null);
+
+ if (!selectedState && polygonClickPopup.current.isOpen()) {
+ polygonClickPopup.current.remove();
+ }
+ }
+ }, [
+ map,
+ selectedState,
+ currentLocationChildren,
+ selectedLoaction,
+ singleSelected,
+ multiSelected,
+ singleSelectedColor,
+ multiSelectedColor,
+ showDatasetsAgainstParentLevel,
+ state.targetAreas,
+ dispatch
+ ]);
+
+ useEffect(() => {
+ addLabelsLayer(showDatasetsAgainstParentLevel && zoomRef.current ? zoomRef.current : null);
+ }, [zoomRef.current]);
+
+ useEffect(() => {
+ if (map.current?.getLayer('target-areas-layer') && toggleAssignedLayer) {
+ map.current?.setLayoutProperty('target-areas-layer', 'visibility', 'visible');
+ } else if (map.current?.getLayer('target-areas-layer') && !toggleAssignedLayer) {
+ map.current?.setLayoutProperty('target-areas-layer', 'visibility', 'none');
+ }
+ });
+
+ useEffect(() => {
+ if (
+ map &&
+ map.current &&
+ currentLocationChildren.length > 0 &&
+ state.targetAreas &&
+ state.targetAreas.length !== 0
+ ) {
+ DrawPolygonsFeatureCollection(map.current, state.targetAreas, 'target-areas');
+ console.log('target areas', state.targetAreas);
+
+ if (!map.current.getLayer('target-areas-layer')) {
+ map.current.addLayer({
+ id: `target-areas-layer`,
+ type: 'fill',
+ source: `target-areas-source`,
+ paint: {
+ // 'fill-color': 'rgba(255, 0, 74, 0.5)',
+ 'fill-color': [
+ 'case',
+ ['!=', ['get', 'numberOfTeams'], 0], // Corrected condition
+ 'rgba(128, 128, 128, 0.7)', // Yellow
+ 'rgba(255, 0, 74, 0.7)' // Default color
+ ]
+ },
+ layout: {
+ visibility: 'visible'
+ }
+ });
+ }
+
+ const taLabelFeatures = state.targetAreas?.map(child => {
+ const center = turf.centroid(child.geometry);
+ return {
+ type: 'Feature' as const,
+ geometry: center.geometry,
+ properties: {
+ name: child.properties.name,
+ geographicLevel: child.properties.geographicLevel,
+ childrenNumber: child.properties.childrenNumber > 0 ? ` (${child.properties.childrenNumber})` : ''
+ }
+ };
+ });
+
+ if (!map.current.getSource('ta-labels-source')) {
+ map.current.addSource('ta-labels-source', {
+ type: 'geojson',
+ data: {
+ type: 'FeatureCollection',
+ features: taLabelFeatures
+ }
+ });
+ } else {
+ const taLabelsSource = map.current.getSource('ta-labels-source') as mapboxgl.GeoJSONSource;
+ taLabelsSource.setData({
+ type: 'FeatureCollection',
+ features: taLabelFeatures
+ });
+ }
+
+ if (!map.current.getLayer('ta-labels-layer') && (zoomRef.current || map.current.getZoom()) >= 8) {
+ map.current.addLayer({
+ id: 'ta-labels-layer',
+ type: 'symbol',
+ source: 'ta-labels-source',
+ layout: {
+ 'text-field': [
+ 'concat',
+ ['get', 'name'],
+ [
+ 'case',
+ ['==', ['get', 'geographicLevel'], 'structure'],
+ '',
+ ['concat', ['to-string', ['get', 'childrenNumber']]]
+ ]
+ ],
+ 'text-size': 13,
+ 'text-anchor': 'center'
+ },
+ paint: {
+ 'text-color': [
+ 'case',
+ ['in', ['get', 'name'], ['literal', multiSelected?.map(p => p.properties.name)]],
+ '#FF0000', // Multi-selected label color
+ '#000000' // Default color for other labels
+ ],
+ 'text-halo-color': '#fff', // Black border color
+ 'text-halo-width': 2, // Width of the border
+ 'text-halo-blur': 1 // Optional: smooth edges
+ }
+ });
+ } else if (map.current.getLayer('ta-labels-layer') && (zoomRef.current || map.current.getZoom()) < 8) {
+ map.current.removeLayer('ta-labels-layer');
+ }
+ }
+ }, [state.targetAreas, map.current, currentLocationChildren, toggleAssignedLayer]);
useEffect(() => {
if (chunkedData) {
@@ -648,18 +1337,6 @@ const SimulationMapView = ({
['get', 'selectedColor']
],
- // 'fill-color': [
- // 'case',
- // ['>', ['length', ['get', 'metadata']], 0],
- // ['case', ['==', ['get', 'selectedColor'], null], geoColor.hex, ['get', 'selectedColor']],
- // ['all', ['==', ['get', 'selectedTagValue'], null], ['!=', ['get', 'selectedTag'], null]],
- // 'black',
- // ['<', ['get', 'selectedTagValue'], 0],
- // geoColor.hex,
- // ['==', ['get', 'selectedTagValue'], 0],
- // geoColor.hex,
- // ['get', 'selectedColor']
- // ],
'fill-opacity': [
'case',
@@ -668,13 +1345,6 @@ const SimulationMapView = ({
['any', ['==', ['get', 'selectedTagValue'], null], ['==', ['get', 'selectedTagValue'], 0]],
0,
['/', ['get', 'selectedTransparency'], 100]
- // ['all', ['==', ['get', 'selectedTagValue'], null], ['<=', ['length', ['get', 'metadata']], 0]],
- // 0,
- // ['==', ['get', 'selectedTagValue'], 0],
- // 0,
- // ['==', ['get', 'selectedTransparency'], null],
- // 0,
- // ['/', ['get', 'selectedTransparency'], 100]
]
}
},
@@ -682,74 +1352,6 @@ const SimulationMapView = ({
);
}
- // if (!map.current?.getLayer(finalLayer.concat('-fill-extrusion'))) {
- // map.current?.addLayer(
- // {
- // id: finalLayer.concat('-fill-extrusion'),
- // type: 'fill-extrusion',
- // source: finalLayer,
- // filter: ['!=', ['geometry-type'], 'Point'],
- // paint: {
- // 'fill-extrusion-color': [
- // 'case',
- // ['==', ['get', 'selectedTagValue'], null],
- // geoColor.hex,
- // ['<', ['get', 'selectedTagValue'], 0],
- // geoColor.hex,
- // ['==', ['get', 'selectedTagValue'], 0],
- // geoColor.hex,
- // ['get', 'selectedColor']
- // ],
- // 'fill-extrusion-base': 0,
- // 'fill-extrusion-height': [
- // 'case',
- // ['==', ['get', 'selectedTagValuePercent'], null],
- // 0,
- // ['<', ['get', 'selectedTagValuePercent'], 0],
- // 0,
- // ['==', ['get', 'selectedTagValuePercent'], 0],
- // 0,
- // ['*', ['get', 'selectedTagValuePercent'], 10000]
- // ],
- // 'fill-extrusion-opacity': 1
- // }
- // },
- // finalLayer.concat('-line')
- // );
- // }
-
- // if (!map.current?.getLayer(finalLayer.concat('-null-symbol'))) {
- // map.current?.loadImage(logo, (error: any, image: any) => {
- // map.current?.addImage('store-icon', image);
- // map.current?.addLayer(
- // {
- // id: finalLayer.concat('-null-symbol'),
- // type: 'symbol',
- // filter: ['==', ['get', 'selectedTagValue'], null],
- // source: finalLayer.concat('-centers'),
- // layout: {
- // 'text-field': [
- // 'format',
- // ['get', 'name'],
- // {
- // 'text-font': ['literal', ['Open Sans Bold', 'Open Sans Semibold']]
- // }
- // ],
- // 'text-size': ['interpolate', ['linear'], ['zoom'], 5, 5, 7, 10, 10, 12, 18, 20],
- // 'text-anchor': 'top',
- // 'text-justify': 'center',
- // 'icon-image': 'store-icon',
- // 'icon-size': 0.1,
- // 'icon-anchor': 'bottom'
- // },
- // paint: {
- // 'icon-color': ['case', ['==', ['get', 'selectedColor'], null], 'blue', 'red']
- // }
- // },
- // finalLayer.concat('-line')
- // );
- // });
- // }
if (!map.current?.getLayer(finalLayer.concat('-symbol'))) {
map.current?.addLayer(
{
@@ -1079,7 +1681,6 @@ const SimulationMapView = ({
col: geoColor,
size: layerSize
});
-
return userDefinedLayers;
} else {
userDefinedLayers.forEach(userDefinedLayer => {
@@ -1135,129 +1736,6 @@ const SimulationMapView = ({
}
}, [chunkedData, map, analysisLayerDetails, userDefinedLayers]);
- const getProcessedUserDefinedLayers = (
- userDefinedLayers: {
- layer: string;
- key: string;
- geo: string;
- layerName: string;
- active: boolean;
- col: Color;
- size?: number;
- }[]
- ): {
- list:
- | { layer: string; key: string; geo: string; layerName: string; active: boolean; col: Color; size?: number }[]
- | undefined;
- key: string;
- color: Color;
- }[] => {
- let processedUserDefinedLayerSet = new Map<
- string,
- { layer: string; key: string; geo: string; layerName: string; active: boolean; col: Color; size?: number }[]
- >();
- userDefinedLayers.forEach(layer => {
- let arr;
- let arr2: {
- layer: string;
- key: string;
- geo: string;
- layerName: string;
- active: boolean;
- col: Color;
- size?: number;
- }[] = [];
- if (processedUserDefinedLayerSet.has(layer.layerName) && processedUserDefinedLayerSet.get(layer.layerName)) {
- arr = processedUserDefinedLayerSet.get(layer.layerName);
- if (arr) {
- arr.forEach(item => arr2.push(item));
- }
- }
-
- arr2.push(layer);
- processedUserDefinedLayerSet.set(layer.layerName, arr2);
- });
-
- const map1 = Array.from(processedUserDefinedLayerSet.keys())
- .filter(key => processedUserDefinedLayerSet.has(key))
- .map((key: string) => {
- let col: Color = defColor;
- // @ts-ignore
- let item = processedUserDefinedLayerSet.get(key) ? processedUserDefinedLayerSet.get(key)[0] : undefined;
- if (item) {
- col = item.col;
- }
- return {
- key: key,
- list: processedUserDefinedLayerSet.get(key),
- color: col
- };
- });
- return map1;
- };
-
- const getProcessedUserDefinedLayersReact = useCallback((): {
- list:
- | {
- layer: string;
- key: string;
- geo: string;
- layerName: string;
- active: boolean;
- col: Color;
- size?: number;
- tagList?: Set;
- }[]
- | undefined;
- key: string;
- color: Color;
- }[] => {
- let processedUserDefinedLayerSet = new Map<
- string,
- { layer: string; key: string; geo: string; layerName: string; active: boolean; col: Color; size?: number }[]
- >();
-
- userDefinedLayers.forEach(layer => {
- let arra;
- let arr2: {
- layer: string;
- key: string;
- geo: string;
- layerName: string;
- active: boolean;
- col: Color;
- size?: number;
- tagList?: Set;
- }[] = [];
- if (processedUserDefinedLayerSet.has(layer.layerName) && processedUserDefinedLayerSet.get(layer.layerName)) {
- arra = processedUserDefinedLayerSet.get(layer.layerName);
- if (arra) {
- arra.forEach(item => arr2.push(item));
- }
- }
-
- arr2.push(layer);
- processedUserDefinedLayerSet.set(layer.layerName, arr2);
- });
-
- const map1 = Array.from(processedUserDefinedLayerSet.keys())
- .filter(key => processedUserDefinedLayerSet.has(key))
- .map((key: string) => {
- let col: Color = defColor;
- // @ts-ignore
- let item = processedUserDefinedLayerSet.get(key) ? processedUserDefinedLayerSet.get(key)[0] : undefined;
- if (item) {
- col = item.col;
- }
- return {
- key: key,
- list: processedUserDefinedLayerSet.get(key),
- color: col
- };
- });
- return map1;
- }, [userDefinedLayers, defColor]);
-
useEffect(() => {
if (map.current) {
if (selectedUserDefinedLayer) {
@@ -1377,6 +1855,81 @@ const SimulationMapView = ({
});
}, [userDefinedLayers, showLayer]);
+ const addLabelsLayer = (zoomLevel: number | null) => {
+ if (map && map.current && selectedLoaction && currentLocationChildren) {
+ // Generate label data PARENT / SHILDREN
+ const labelFeatures = (currentLocationChildren.length > 0 ? currentLocationChildren : [selectedLoaction]).map(
+ child => {
+ const center = turf.centroid(child.geometry); // Use turf.js to calculate the center
+ return {
+ type: 'Feature' as const,
+ geometry: center.geometry,
+ properties: {
+ name: child.properties.name,
+ geographicLevel: child.properties.geographicLevel,
+ childrenNumber: child.properties.childrenNumber > 0 ? ` (${child.properties.childrenNumber})` : ''
+ }
+ };
+ }
+ );
+
+ if (!map.current.getSource('labels-source')) {
+ map.current.addSource('labels-source', {
+ type: 'geojson',
+ data: {
+ type: 'FeatureCollection',
+ features: labelFeatures
+ }
+ });
+ } else {
+ const labelsSource = map.current.getSource('labels-source') as mapboxgl.GeoJSONSource;
+ labelsSource.setData({
+ type: 'FeatureCollection',
+ features: labelFeatures
+ });
+ }
+
+ const zoom = zoomLevel && currentLocationChildren.length > 100 ? zoomLevel : Infinity;
+
+ if (!map.current.getLayer('labels-layer')) {
+ map.current.addLayer({
+ id: 'labels-layer',
+ type: 'symbol',
+ source: 'labels-source',
+ layout: {
+ // Dynamically set the text field
+ 'text-field': [
+ 'concat',
+ ['get', 'name'], // Name property
+ [
+ 'case',
+ ['==', ['get', 'geographicLevel'], 'structure'], // Condition for 'structure'
+ '',
+ ['concat', ['to-string', ['get', 'childrenNumber']]] // Append childrenNumber if not 'structure'
+ ]
+ ],
+ 'text-size': 13,
+ 'text-anchor': 'center'
+ },
+ paint: {
+ // 'text-color': '#000', // White font color
+ 'text-color': [
+ 'case',
+ ['in', ['get', 'name'], ['literal', multiSelected.map(p => p.properties.name)]],
+ '#FF0000', // Multi-selected label color
+ '#000000' // Default color for other labels
+ ],
+ 'text-halo-color': '#fff', // Black border color
+ 'text-halo-width': 2, // Width of the border
+ 'text-halo-blur': 1 // Optional: smooth edges
+ }
+ });
+ } else {
+ map.current.setLayoutProperty('labels-layer', 'visibility', zoom >= 7 ? 'visible' : 'none');
+ }
+ }
+ };
+
const addParentMapData = useCallback(
(filteredData: PlanningParentLocationResponse) => {
if (map.current) {
@@ -1413,84 +1966,6 @@ const SimulationMapView = ({
}
}, [parentMapStateData, addParentMapData]);
- const updateFeaturesWithTagStatsAndColorAndTransparency = (
- feature: RevealFeature | Feature,
- tagStats: any,
- tag: string | undefined,
- percentageField: string,
- valueField: string,
- tagField: string,
- colorField: string,
- color: Color
- ) => {
- if (feature) {
- feature.properties?.metadata?.forEach((element: any) => {
- if (feature?.properties) {
- if (tag) {
- if (element.type === tag && tagStats.max && tagStats.max[tag]) {
- feature.properties[tagField] = element.type;
- delete feature.properties['reachedMax'];
- if (!(element.value >= 0x10000000000000000 || element.value < -0x10000000000000000)) {
- feature.properties[valueField] = element.value;
- } else {
- feature.properties['reachedMax'] = 'true';
- }
-
- let percentage: any;
- if (element.value < 0) {
- percentage = (-1 * element.value) / (-1 * tagStats.min[tag]);
- } else {
- percentage = element.value / tagStats.max[tag];
- }
-
- feature.properties[percentageField] = percentage;
-
- if (!(tagStats.min[tag] >= 0x10000000000000000 || tagStats.min[tag] < -0x10000000000000000)) {
- feature.properties.selectedTagValueMin = tagStats.min[tag];
- }
-
- if (!(tagStats.max[tag] >= 0x10000000000000000 || tagStats.max[tag] < -0x10000000000000000)) {
- feature.properties.selectedTagValueMax = tagStats.min[tag];
- }
-
- let [h, s, v] = hex.hsv(color.hex);
- const colorField1 = s * feature.properties[percentageField];
-
- let hexVal = hsv.hex([h, colorField1, v]);
-
- feature.properties[colorField] = '#'.concat(hexVal);
- }
- } else {
- delete feature.properties[valueField];
- delete feature.properties[tagField];
- delete feature.properties[percentageField];
- delete feature.properties.selectedTagValueMin;
- delete feature.properties.selectedTagValueMax;
- delete feature.properties[colorField];
- }
- }
- });
- if (
- !feature.properties?.metadata?.some((element: any) => {
- if (feature?.properties) {
- return element.type === tag;
- }
- return false;
- })
- ) {
- if (feature?.properties) {
- delete feature.properties[valueField];
- delete feature.properties[tagField];
- delete feature.properties[percentageField];
- delete feature.properties.selectedTagValueMin;
- delete feature.properties.selectedTagValueMax;
- feature['properties'][tagField] = tag;
- }
- }
- }
- return feature;
- };
-
const getOptions = useCallback(() => {
return (
<>
@@ -1508,522 +1983,135 @@ const SimulationMapView = ({
);
}, [markedMapBoxFeatures]);
- return (
-
-
-
- {/*
*/}
- {
- setShowMapControls(!showMapControls);
- }}
- className="rounded"
- size="sm"
- variant="primary"
- >
- {showMapControls ? 'Hide' : 'Show'}{' '}
-
-
- {showMapControls && userDefinedLayers.length > 0 && (
-
-
{
- setShowUserDefineLayerSelector(!showUserDefineLayerSelector);
- }}
- >
- ResultSets{' '}
- {showUserDefineLayerSelector ? (
-
- ) : (
-
- )}
-
-
- {showUserDefineLayerSelector && (
-
- {getProcessedUserDefinedLayers(userDefinedLayers).map(layerObj => {
- return (
-
-
-
- <>
- {layerObj.key}
-
- >
-
-
- <>
- {layerObj?.list?.map(layer => {
- return (
- alert('hello')}>
- {layer.geo}
-
- }
- value={layer.layer}
- type="checkbox"
- checked={layer.active}
- onChange={e => {
- setUserDefinedLayers(layerItems => {
- let newItems: {
- layer: string;
- active: boolean;
- layerName: string;
- geo: string;
- key: string;
- col: Color;
- }[] = [];
- layerItems.forEach(newItem => {
- newItems.push(newItem);
- });
- let item = newItems.find(layerItem => layerItem.layer === layer.layer);
- if (item) {
- item.active = e.target.checked;
- }
- return newItems;
- });
- }}
- />
- );
- })}
-
-
- {
- setShowUserDefinedSettingsPanel(
- !showUserDefinedSettingsPanel || selectedUserDefinedLayer?.key !== layerObj.key
- );
- if (selectedUserDefinedLayer?.key !== layerObj.key) {
- setSelectedUserDefinedLayer({
- key: layerObj.key,
- col: layerObj.color,
- lineColor: selectedUserDefinedLayer
- ? selectedUserDefinedLayer.lineColor
- : initialLineColor
- });
- setColor(layerObj.color);
- }
- }}
- >
-
-
-
- {!showUserDefinedSettingsPanel || selectedUserDefinedLayer?.key !== layerObj.key
- ? ''
- : 'Hide '}
- Settings
-
-
- >
-
-
-
- );
- })}
-
- )}
-
- )}
+ const handleTagChange = (e: React.ChangeEvent
, layerName: string) => {
+ const newSelectedTag = e.target.value;
- {showMapControls &&
- userDefinedLayers.length > 0 &&
- showUserDefinedSettingsPanel &&
- analysisLayerDetails.length > 0 &&
- selectedUserDefinedLayer && (
-
-
Settings - {selectedUserDefinedLayer.key}
-
-
-
-
- {userDefinedNames
- ?.filter(layer => layer.layerName === selectedUserDefinedLayer.key)
- .map(layer => (
- <>
-
{
- setUserDefinedNames(userDefinedNames => {
- let newUserDefinedNames: {
- layer: string;
- key: string;
- layerName: string;
- active: boolean;
- col: Color;
- tagList?: Set;
- selectedTag?: string;
- }[] = [];
- userDefinedNames.forEach(userDefinedName => {
- if (userDefinedName.layerName === layer.layerName) {
- userDefinedName.selectedTag = e.target.value;
- }
- newUserDefinedNames.push(userDefinedName);
- });
- return newUserDefinedNames;
- });
- setUserDefinedLayers(userDefinedLayers => {
- let newUserDefinedNames: {
- layer: string;
- key: string;
- geo: string;
- layerName: string;
- active: boolean;
- col: Color;
- tagList?: Set;
- selectedTag?: string;
- }[] = [];
- userDefinedLayers.forEach(userDefinedName => {
- if (userDefinedName.layerName === layer.layerName) {
- userDefinedName.selectedTag = e.target.value;
- }
- newUserDefinedNames.push(userDefinedName);
- });
- return newUserDefinedNames;
- });
- }}
- >
- Select Metadata Tag...
- {layer.tagList &&
- Array.from(layer.tagList).map(metaDataItem => {
- return (
-
- {metaDataItem}
-
- );
- })}
-
- >
- ))}
-
-
Opacity ({getTransparencyValue(userDefinedLayers, selectedUserDefinedLayer)})
-
{
- setUserDefinedLayers(userDefinedLayers => {
- let newUserDefinedLayers: {
- layer: string;
- key: string;
- geo: string;
- layerName: string;
- active: boolean;
- col: Color;
- tagList?: Set;
- selectedTag?: string;
- transparency?: number;
- }[] = [];
- userDefinedLayers.forEach(userDefinedLayer => {
- if (userDefinedLayer.layerName === selectedUserDefinedLayer.key) {
- userDefinedLayer.transparency = Number(e.target.value);
- }
+ setUserDefinedNames(prevNames => updateSelectedTag(prevNames, layerName, newSelectedTag));
+ setUserDefinedLayers(prevLayers => updateSelectedTag(prevLayers, layerName, newSelectedTag));
+ };
- newUserDefinedLayers.push(userDefinedLayer);
- });
- return newUserDefinedLayers;
- });
- }}
- />
+ // Handlers for each specific property change
+ const handleTransparencyChange = (e: React.ChangeEvent) => {
+ const newValue = Number(e.target.value);
+ setUserDefinedLayers(userDefinedLayers =>
+ updateLayerProperty(userDefinedLayers, selectedUserDefinedLayer?.key || '', 'transparency', newValue)
+ );
+ };
- {
-
-
- Line Control
-
- Line Width ({getLineWidthValue(userDefinedLayers, selectedUserDefinedLayer)})
- {
- setUserDefinedLayers(userDefinedLayers => {
- let newUserDefinedLayers: {
- layer: string;
- key: string;
- geo: string;
- layerName: string;
- active: boolean;
- col: Color;
- tagList?: Set;
- selectedTag?: string;
- transparency?: number;
- lineWidth?: number;
- }[] = [];
- userDefinedLayers.forEach(userDefinedLayer => {
- if (userDefinedLayer.layerName === selectedUserDefinedLayer?.key) {
- userDefinedLayer.lineWidth = Number(e.target.value);
- }
-
- newUserDefinedLayers.push(userDefinedLayer);
- });
- return newUserDefinedLayers;
- });
- }}
- />
-
- {
- setUserDefinedLayers(userDefinedLayers => {
- let newUserDefinedLayers: {
- layer: string;
- key: string;
- geo: string;
- layerName: string;
- active: boolean;
- col: Color;
- tagList?: Set;
- selectedTag?: string;
- transparency?: number;
- lineColor?: string;
- }[] = [];
- userDefinedLayers.forEach(userDefinedLayer => {
- if (
- selectedUserDefinedLayer &&
- userDefinedLayer.layerName === selectedUserDefinedLayer.key
- ) {
- userDefinedLayer.lineColor = color.hex;
- }
-
- newUserDefinedLayers.push(userDefinedLayer);
- });
- return newUserDefinedLayers;
- });
-
- setSelectedUserDefinedLayer(selectedUserDefinedLayer => {
- if (selectedUserDefinedLayer) {
- return {
- key: selectedUserDefinedLayer?.key,
- col: selectedUserDefinedLayer.col,
- transparency: selectedUserDefinedLayer.transparency,
- lineColor: color
- };
- } else {
- return undefined;
- }
- });
-
- setColor(color);
- }}
- hideHEX={true}
- hideHSV={true}
- hideRGB={true}
- />
-
-
-
- }
-
- )}
+ const handleLineWidthChange = (e: React.ChangeEvent) => {
+ const newValue = Number(e.target.value);
+ setUserDefinedLayers(userDefinedLayers =>
+ updateLayerProperty(userDefinedLayers, selectedUserDefinedLayer?.key || '', 'lineWidth', newValue)
+ );
+ };
+
+ const handleColorChange = (color: any) => {
+ const newColor = color.hex;
+ setUserDefinedLayers(userDefinedLayers =>
+ updateLayerProperty(userDefinedLayers, selectedUserDefinedLayer?.key || '', 'lineColor', newColor)
+ );
+ setSelectedUserDefinedLayer(prevLayer => updateSelectedLayerProperty(prevLayer, 'lineColor', color.toString()));
+ setColor(color); // Update color state
+ };
+
+ const handleAssign = (teamId: number) => {
+ // console.log('SELECTED TEAM ', teamId);
+ };
+
+ const teams: any[] = [
+ { id: 1, name: 'Team Alpha', members: 8, active: true },
+ { id: 2, name: 'Team Beta', members: 6, active: true },
+ { id: 3, name: 'Team Gamma', members: 5, active: false },
+ { id: 4, name: 'Team Delta', members: 7, active: true },
+ { id: 5, name: 'Team Epsilon', members: 4, active: true }
+ ];
+
+ return (
+
+
+ {loading === 'started' && (
+
+
+
+ )}
+
+
+
+
+
+
+
+ {/* MULTISELECTED POLYGONS LIST */}
+ {multiselectState.length > 0 && }
+ {/* MAP LEGEND */}
+
+
+
+
+ {/* DATA SETS RESULT PANEL */}
+ {userDefinedLayers.length > 0 && (
+
+ )}
-
-
-
- Lat: {lat} Lng: {lng} Zoom: {zoom}
-
-
{
- fullScreenHandler();
- setTimeout(() => {
- map.current?.resize();
- document.getElementById('mapRow')?.scrollIntoView({ behavior: 'smooth' });
- }, 0);
- }}
- className="mb-2 float-end"
- >
- {fullScreen ? 'Show Controls' : 'Full Screen'}
-
+
+ {/* LANG LAT ZOOM */}
+
+
+
Lat: {lat}
Lng: {lng}
Zoom: {zoom}
- {/* enable when needed by setting false to true */}
+ {/* STATISTICS PANEL ON THE RIGHT */}
{userDefinedLayers && userDefinedLayers.length > 0 && (
-
{
- if (!showStats) {
- setShowStats(!showStats);
- }
- }}
- >
- {userDefinedLayers.length && !showStats && 'Show Stats'}
-
- {showStats &&
- getProcessedUserDefinedLayersReact()?.map(userDefinedLayer => {
- return (
-
- <>
- {userDefinedLayer &&
- userDefinedLayer.list &&
- userDefinedLayer.list.map(item => (
-
- {item.geo}: {item.size}
-
- ))}
-
- {userDefinedNames &&
- userDefinedNames
- .filter(layer => layer.key === userDefinedLayer.key)
-
- .map(locItem => {
- return Array.from(locItem.tagList ? locItem.tagList : new Set())
- .filter((meta: any) => {
- let tagItem = entityTags.filter(tag => tag.tag === meta);
- if (tagItem && tagItem[0]) {
- return tagItem[0].simulationDisplay;
- }
- return false;
- })
- .filter((meta: any) => stats[userDefinedLayer.key] && stats[userDefinedLayer.key][meta])
- .map((meta: any) => {
- let num: any = stats[userDefinedLayer.key][meta];
- let val = isNumeric(num)
- ? (num as number) > 0
- ? Math.round(num as number).toLocaleString('en-US')
- : num
- : stats[userDefinedLayer.key][meta];
- return (
-
- {meta}: {stats[userDefinedLayer.key] ? val : ''}
-
- );
- });
- })}
- >
-
- );
- })}
-
- {showStats && (
- setShowStats(!showStats)}>
- {showStats ? 'Close' : 'Open'}
-
- )}
-
- )}
-
- {showMapDrawnModal && (
-
setShowMapDrawnModal(false)}
- title={'Selected Locations'}
- footer={
- <>
- {
- if (mapBoxDraw.current) {
- mapBoxDraw.current?.deleteAll();
- }
- setShowMapDrawnModal(false);
- }}
- >
-
-
- {
- updateSelectedLocations3();
-
- if (mapBoxDraw.current) {
- mapBoxDraw.current?.deleteAll();
- }
-
- setShowMapDrawnModal(false);
- }}
- >
- update
-
- setShowMapDrawnModal(false)}>close
- >
- }
- element={
-
-
-
-
- setShouldApplyToAll(e.target.checked)}
- />
-
- {!shouldApplyToAll && (
-
- {'Select Location for which the Drawn Polygon to apply to'}
-
- {
- setDrawnMapLevel(e.target.value);
- }}
- >
-
- {'Select layer...'}
-
- {getOptions()}
-
-
- )}
-
-
-
-
-
- setShouldApplyToChildren(e.target.checked)}
- />
-
-
-
-
- }
- size={'lg'}
+
)}
-
+
);
};
export default SimulationMapView;
+
+const generateColors = (datasets: any[]) => {
+ return datasets.reduce((acc, dataset) => {
+ acc[dataset.identifier] = dataset.hexColor;
+ return acc;
+ }, {});
+};
+
+const generateBorderDetails = (datasets: any[]) => {
+ return datasets.reduce((acc, dataset) => {
+ acc[dataset.identifier] = {
+ lineWidth: dataset.lineWidth,
+ borderColor: dataset.borderColor || 'black' // Default to black if not provided
+ };
+ return acc;
+ }, {});
+};
diff --git a/src/features/planSimulation/components/SimulationMapView/SimulationMapViewConstants.ts b/src/features/planSimulation/components/SimulationMapView/SimulationMapViewConstants.ts
new file mode 100644
index 00000000..385bf154
--- /dev/null
+++ b/src/features/planSimulation/components/SimulationMapView/SimulationMapViewConstants.ts
@@ -0,0 +1,11 @@
+export const INITIAL_HEAT_MAP_RADIUS = 50;
+export const INITIAL_HEAT_MAP_OPACITY = 0.2;
+export const INITIAL_FILL_COLOR = '#005512';
+export const INITIAL_LINE_COLOR = '#000000';
+export const lineParameters: any = {
+ countryx: { col: 'red', num: 1, offset: 2.5 },
+ countyx: { col: 'blue', num: 1, offset: 2.5 },
+ subcountyx: { col: 'darkblue', num: 1, offset: 6 },
+ wardx: { col: 'yellow', num: 2, offset: 3 },
+ catchmentx: { col: 'purple', num: 3, offset: 1 }
+};
diff --git a/src/features/planSimulation/components/SimulationMapView/SimulationMapViewModels.ts b/src/features/planSimulation/components/SimulationMapView/SimulationMapViewModels.ts
new file mode 100644
index 00000000..731daafb
--- /dev/null
+++ b/src/features/planSimulation/components/SimulationMapView/SimulationMapViewModels.ts
@@ -0,0 +1,99 @@
+import { LngLatBounds, Map as MapBoxMap } from 'mapbox-gl';
+import { MutableRefObject } from 'react';
+import { Color } from 'react-color-palette';
+import { EntityTag, PlanningParentLocationResponse, PlanningLocationResponse } from '../../providers/types';
+import { StatsLayer, Children, AnalysisLayer } from '../Simulation';
+
+export interface SimulationMapViewProps {
+ teamsList?: any[];
+ currentLocationChildren: any[]; //
+ polygons?: any[]; //
+ loading: string;
+ leftOpenHandler: () => void;
+ rightOpenHandler: () => void;
+ leftOpenState: boolean;
+ rightOpenState: boolean;
+ fullScreenHandler: () => void;
+ fullScreen: boolean;
+ toLocation: LngLatBounds | undefined;
+ entityTags: EntityTag[];
+ parentMapData: PlanningParentLocationResponse | undefined;
+ setMapDataLoad: (data: PlanningLocationResponse) => void;
+ chunkedData: PlanningLocationResponse;
+ resetMap: boolean;
+ setResetMap: (resetMap: boolean) => void;
+ resultsLoadingState: 'notstarted' | 'error' | 'started' | 'complete';
+ parentsLoadingState: 'notstarted' | 'error' | 'started' | 'complete';
+ stats: StatsLayer;
+ map: MutableRefObject;
+ updateMarkedLocations: (identifier: string, ancestry: string[], marked: boolean) => void;
+ parentChild: { [parent: string]: Children };
+ analysisLayerDetails: AnalysisLayer[];
+ selectedLoaction?: any;
+ showDatasetsAgainstParentLevel?: boolean;
+}
+
+export interface LineWidth {
+ layer: string;
+ key: string;
+ geo: string;
+ layerName: string;
+ active: boolean;
+ col: Color;
+ tagList?: Set;
+ selectedTag?: string;
+ transparency?: number;
+ lineWidth?: number;
+}
+
+export interface SelectedUserDefinedLayer {
+ key: string;
+ col: Color;
+ transparency?: number;
+}
+
+export interface UserDefinedLayer {
+ layer: string;
+ key: string;
+ layerName: string;
+ active: boolean;
+ col: Color;
+ tagList?: Set;
+ selectedTag?: string;
+ size?: number;
+ geo: string;
+ lineColor?: string;
+ lineWidth?: number;
+ transparency?: number;
+}
+
+export interface UserDefinedNames {
+ layer: string;
+ key: string;
+ layerName: string;
+ active: boolean;
+ col: Color;
+ tagList?: Set;
+ selectedTag?: string;
+}
+
+export interface ProcessedLayer {
+ list: UserDefinedLayer[] | undefined;
+ key: string;
+ color: Color;
+}
+
+export interface SingleLayer {
+ layer: string;
+ active: boolean;
+ layerName: string;
+ geo: string;
+ key: string;
+ col: Color;
+}
+
+export interface ProcessedLayer {
+ key: string;
+ list: UserDefinedLayer[] | undefined;
+ color: Color;
+}
diff --git a/src/features/planSimulation/components/SimulationMapView/SimulationMapViewUtils.ts b/src/features/planSimulation/components/SimulationMapView/SimulationMapViewUtils.ts
new file mode 100644
index 00000000..f4ac05c0
--- /dev/null
+++ b/src/features/planSimulation/components/SimulationMapView/SimulationMapViewUtils.ts
@@ -0,0 +1,210 @@
+import { Color } from 'react-color-palette';
+import { lineParameters } from './SimulationMapViewConstants';
+import {
+ LineWidth,
+ ProcessedLayer,
+ SelectedUserDefinedLayer,
+ SingleLayer,
+ UserDefinedLayer
+} from './SimulationMapViewModels';
+import { Feature, MultiPolygon, Point, Polygon } from '@turf/turf';
+import { hex, hsv } from 'color-convert';
+import { RevealFeature } from '../../providers/types';
+
+export const getTransparencyValue = (value: LineWidth[], selectedValue: SelectedUserDefinedLayer | undefined) => {
+ let transparency = 10;
+ if (value) {
+ const filterElement = value.filter(userDefinedLayer => userDefinedLayer.layerName === selectedValue?.key)[0];
+ if (filterElement && filterElement.transparency !== undefined && filterElement.transparency !== null) {
+ transparency = filterElement.transparency;
+ }
+ }
+ return transparency;
+};
+
+export const getLineWidthValue = (value: LineWidth[], selectedValue?: SelectedUserDefinedLayer) => {
+ let lineWidth = 1;
+ if (value) {
+ const filterElement = value.filter(userDefinedLayer => userDefinedLayer.layerName === selectedValue?.key)[0];
+ if (filterElement && filterElement.lineWidth !== undefined && filterElement.lineWidth !== null) {
+ lineWidth = filterElement.lineWidth;
+ }
+ }
+ return lineWidth;
+};
+
+export const getLineParameters = (level: string) => {
+ let newlevel = level
+ .split('-')
+ .filter((item, index) => index > 0)
+ .join('-');
+ if (lineParameters[newlevel]) {
+ return lineParameters[newlevel];
+ } else {
+ return { col: 'black', num: 1, offset: 0 };
+ }
+};
+
+export const getProcessedUserDefinedLayers = (
+ userDefinedLayers: UserDefinedLayer[],
+ defColor: Color
+): ProcessedLayer[] => {
+ const layerMap = userDefinedLayers.reduce((acc, layer) => {
+ // Ensure layerName is defined
+ if (!layer.layerName) {
+ console.warn('Layer is missing layerName:', layer);
+ return acc; // Skip invalid layers
+ }
+
+ // Ensure the entry for the `layerName` exists in `acc`
+ if (!acc[layer.layerName]) {
+ acc[layer.layerName] = {
+ key: layer.layerName,
+ list: [],
+ color: layer.col || defColor
+ };
+ }
+
+ // Add the current layer to the corresponding group
+ acc[layer.layerName]?.list?.push(layer);
+ return acc;
+ }, {} as Record);
+
+ return Object.values(layerMap);
+};
+
+export const updateFeaturesWithTagStatsAndColorAndTransparency = (
+ // feature: RevealFeature | Feature,
+ feature: RevealFeature | Feature,
+ tagStats: any,
+ tag: string | undefined,
+ percentageField: string,
+ valueField: string,
+ tagField: string,
+ colorField: string,
+ color: Color
+) => {
+ if (feature) {
+ feature.properties?.metadata?.forEach((element: any) => {
+ if (feature?.properties) {
+ if (tag) {
+ if (element.type === tag && tagStats.max && tagStats.max[tag]) {
+ feature.properties[tagField] = element.type;
+ delete feature.properties['reachedMax'];
+ if (!(element.value >= 0x10000000000000000 || element.value < -0x10000000000000000)) {
+ feature.properties[valueField] = element.value;
+ } else {
+ feature.properties['reachedMax'] = 'true';
+ }
+
+ let percentage: any;
+ if (element.value < 0) {
+ percentage = (-1 * element.value) / (-1 * tagStats.min[tag]);
+ } else {
+ percentage = element.value / tagStats.max[tag];
+ }
+
+ feature.properties[percentageField] = percentage;
+
+ if (!(tagStats.min[tag] >= 0x10000000000000000 || tagStats.min[tag] < -0x10000000000000000)) {
+ feature.properties.selectedTagValueMin = tagStats.min[tag];
+ }
+
+ if (!(tagStats.max[tag] >= 0x10000000000000000 || tagStats.max[tag] < -0x10000000000000000)) {
+ feature.properties.selectedTagValueMax = tagStats.min[tag];
+ }
+
+ let [h, s, v] = hex.hsv(color.hex);
+ const colorField1 = s * feature.properties[percentageField];
+
+ let hexVal = hsv.hex([h, colorField1, v]);
+
+ feature.properties[colorField] = '#'.concat(hexVal);
+ }
+ } else {
+ delete feature.properties[valueField];
+ delete feature.properties[tagField];
+ delete feature.properties[percentageField];
+ delete feature.properties.selectedTagValueMin;
+ delete feature.properties.selectedTagValueMax;
+ delete feature.properties[colorField];
+ }
+ }
+ });
+ if (
+ !feature.properties?.metadata?.some((element: any) => {
+ if (feature?.properties) {
+ return element.type === tag;
+ }
+ return false;
+ })
+ ) {
+ if (feature?.properties) {
+ delete feature.properties[valueField];
+ delete feature.properties[tagField];
+ delete feature.properties[percentageField];
+ delete feature.properties.selectedTagValueMin;
+ delete feature.properties.selectedTagValueMax;
+ feature['properties'][tagField] = tag;
+ }
+ }
+ }
+ return feature;
+};
+
+export const handleOpenSettingsMenu = (
+ layerObj: ProcessedLayer,
+ setShowUserDefinedSettingsPanel: Function,
+ showUserDefinedSettingsPanel: Boolean,
+ selectedUserDefinedLayer: UserDefinedLayer | undefined,
+ setSelectedUserDefinedLayer: Function,
+ setColor: Function,
+ initialLineColor: Color
+) => {
+ setShowUserDefinedSettingsPanel(!showUserDefinedSettingsPanel || selectedUserDefinedLayer?.key !== layerObj.key);
+ if (selectedUserDefinedLayer?.key !== layerObj.key) {
+ setSelectedUserDefinedLayer({
+ key: layerObj.key,
+ col: layerObj.color,
+ lineColor: selectedUserDefinedLayer ? selectedUserDefinedLayer.lineColor : initialLineColor
+ });
+ setColor(layerObj.color);
+ }
+};
+
+export const updateLayerActiveState = (
+ layers: SingleLayer[],
+ targetLayer: string,
+ isActive: boolean
+): SingleLayer[] => {
+ return layers.map(layer => (layer.layer === targetLayer ? { ...layer, active: isActive } : layer));
+};
+
+export const updateSelectedTag = (
+ items: T[],
+ layerName: string,
+ selectedTag: string
+): T[] => {
+ return items.map(item => (item.layerName === layerName ? { ...item, selectedTag } : item));
+};
+
+export const updateLayerProperty = (
+ layers: UserDefinedLayer[],
+ selectedLayerKey: string,
+ property: string,
+ value: any
+): UserDefinedLayer[] => {
+ return layers.map(layer => (layer.layerName === selectedLayerKey ? { ...layer, [property]: value } : layer));
+};
+
+export const updateSelectedLayerProperty = (
+ // Utility function to update the selected layer's property dynamically
+ selectedLayer: UserDefinedLayer | undefined,
+ property: string,
+ value: any
+): UserDefinedLayer | undefined => {
+ if (selectedLayer) {
+ return { ...selectedLayer, [property]: value };
+ }
+ return undefined;
+};
diff --git a/src/features/planSimulation/components/SimulationMapView/api/datasetsAPI.ts b/src/features/planSimulation/components/SimulationMapView/api/datasetsAPI.ts
new file mode 100644
index 00000000..dc878387
--- /dev/null
+++ b/src/features/planSimulation/components/SimulationMapView/api/datasetsAPI.ts
@@ -0,0 +1,158 @@
+import api from '../../../../../api/axios';
+
+export interface DataSet {
+ simulationId: string;
+ tagId: string;
+ hexColor: string;
+ lineWidth: number;
+ parentLocationId: string;
+}
+
+export interface DataSetList {
+ identifier: string;
+ name: string;
+ hexColor: string;
+ lineWidth: number;
+ borderColor: string;
+ hidden: boolean;
+ filter: {
+ minValue: number;
+ maxValue: number;
+ };
+ selectedRange: {
+ minValue: number;
+ maxValue: number;
+ };
+}
+
+export interface DataSetDelete {
+ simulationId: string;
+ datasetId: string;
+}
+
+export interface DataSetUpdate {
+ simulationId: string;
+ datasetId: string;
+ name: string;
+ hexColor: string;
+ lineWidth: number;
+ borderColor: string;
+}
+
+export interface LocationData {
+ datasetsIds: string[];
+ includeGeometry: boolean;
+ parentLocationId: string;
+ simulationId: string;
+}
+
+export interface AddDatasetResponse {
+ simulationId: string;
+ datasetId: string;
+ datasetName: string;
+ borderColor: string;
+ hexColor: string;
+ lineWidth: number;
+ tagId: string;
+ locationWithMetadata: any;
+}
+
+export interface SimulationDatasetRequest {
+ simulationId: string;
+ parentAdminLevel: string;
+}
+
+export const getEntityTags = async () => {
+ try {
+ const response = await api.get(`/entityTag/default-hierarchy`);
+ return response.data;
+ } catch (error) {
+ console.error(error);
+ }
+};
+
+export const getSimulationData = async (simulationId: string) => {
+ try {
+ const response = await api.get(`/simulation/${simulationId}`);
+
+ return response.data;
+ } catch (error) {
+ console.error(error);
+ }
+};
+
+//! POST
+export const setDataset = async (data: DataSet) => {
+ try {
+ const response = await api.post(`/simulation/dataset`, data);
+ return response.data;
+ } catch (error) {
+ console.error(error);
+ }
+};
+
+//! PUT
+export const updateDataset = async (data: DataSetUpdate) => {
+ try {
+ const response = await api.put(`/simulation/dataset`, data);
+ return response.data;
+ } catch (error) {
+ console.error(error);
+ }
+};
+
+//! DELETE
+export const deleteDataset = async (data: DataSetDelete) => {
+ try {
+ const response = await api.delete(`/simulation/dataset`, { data });
+ return response.data;
+ } catch (error) {
+ console.error(error);
+ }
+};
+
+export const getLocationPolygonsWithDatasets = async (data: LocationData) => {
+ try {
+ const response = await api.post('/simulation/dataset/location-data', data);
+ return response.data;
+ } catch (error) {
+ console.error(error);
+ }
+};
+
+export const addSearchRequest = async (data: SimulationDatasetRequest) => {
+ try {
+ const response = await api.post('/simulation/add-search-request', data);
+ return response.data;
+ } catch (error) {
+ console.error(error);
+ }
+};
+
+export const filterDatasets = async (
+ searchId: string,
+ messageHandler: (e: MessageEvent) => void,
+ closeHandler: () => any,
+ openHandler: () => any,
+ resultsErrorHandler: (e: any) => any
+) => {
+ try {
+ const events = new EventSource(
+ `${process.env.REACT_APP_API_URL}/simulation/datasets/filter-sse?searchId=${searchId}`
+ );
+ events.addEventListener('message', messageHandler);
+ events.addEventListener('open', _ => {
+ openHandler();
+ });
+ events.addEventListener('error', e => {
+ resultsErrorHandler(e);
+ return events.close();
+ });
+ events.addEventListener('close', _ => {
+ closeHandler();
+ return events.close();
+ });
+ } catch (error) {
+ console.error(error);
+ }
+};
diff --git a/src/features/planSimulation/components/SimulationMapView/api/hierarchyAPI.ts b/src/features/planSimulation/components/SimulationMapView/api/hierarchyAPI.ts
new file mode 100644
index 00000000..4f8d6893
--- /dev/null
+++ b/src/features/planSimulation/components/SimulationMapView/api/hierarchyAPI.ts
@@ -0,0 +1,51 @@
+import axios from 'axios';
+import api from '../../../../../api/axios';
+
+// export const getSimulationInstance = async (simulationId: string) => {
+
+export const getHierarchy = async () => {
+ try {
+ const response = await axios.get(`http://localhost:8080/api/v1/locationHierarchy/default/location`);
+ return response.data;
+ } catch (error) {
+ console.error(error);
+ }
+};
+
+export const getLocation = async () => {
+ try {
+ const response = await axios.get('http://localhost:8080/api/v1/location/627e0983-a64b-4db4-877f-d3b3ed0c3c21');
+ return response.data;
+ } catch (error) {
+ console.error(error);
+ }
+};
+
+export const getDefaultHierarchyData = async () => {
+ try {
+ const response = await axios.get(`http://localhost:8080/api/v1/locationHierarchy/default`);
+ return response.data;
+ } catch (error) {
+ console.error(error);
+ }
+}
+
+export const getHierarchyPolygon = async (locationId: string, page: number, size: number) => {
+ try {
+ const response = await axios.get(
+ `http://localhost:8080/api/v1/location/${locationId}/children-included?page=${page}&size=${size}`
+ );
+ return response.data;
+ } catch (error) {
+ console.error(error);
+ }
+};
+
+export const getPlanInfo = async () => {
+ try {
+ const response = await api.get(`/plan?_summary=false&search=&size=1&page=0&sort=,desc`);
+ return response.data.content[0];
+ } catch (error) {
+ console.error(error);
+ }
+};
diff --git a/src/features/planSimulation/components/SimulationMapView/api/planAPI.ts b/src/features/planSimulation/components/SimulationMapView/api/planAPI.ts
new file mode 100644
index 00000000..9fef419a
--- /dev/null
+++ b/src/features/planSimulation/components/SimulationMapView/api/planAPI.ts
@@ -0,0 +1,13 @@
+import axios from 'axios';
+import api from '../../../../../api/axios';
+
+export const assignLocationsToPlan = async (planId: string, selectedlocations: string[]) => {
+ try {
+ const response = await api.post(`/plan/${planId}/assignLocations`, {
+ locations: selectedlocations
+ });
+ return response.data;
+ } catch (error) {
+ console.error(error);
+ }
+};
diff --git a/src/features/planSimulation/components/SimulationMapView/components/AddDatasetForm/AddDatasetForm.module.css b/src/features/planSimulation/components/SimulationMapView/components/AddDatasetForm/AddDatasetForm.module.css
new file mode 100644
index 00000000..8d705141
--- /dev/null
+++ b/src/features/planSimulation/components/SimulationMapView/components/AddDatasetForm/AddDatasetForm.module.css
@@ -0,0 +1,25 @@
+.step {
+ border-radius: 4px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+}
+
+.previewWrapper {
+ width: 100%;
+}
+
+.select {
+ width: 100%;
+}
+
+.stepParagraph {
+ margin: 0.5rem 0;
+ letter-spacing: 0.5px;
+}
+
+.downloadTemplate {
+ color: rgb(62, 58, 252);
+ text-decoration: underline;
+}
diff --git a/src/features/planSimulation/components/SimulationMapView/components/AddDatasetForm/AddDatasetForm.tsx b/src/features/planSimulation/components/SimulationMapView/components/AddDatasetForm/AddDatasetForm.tsx
new file mode 100644
index 00000000..d49db0d7
--- /dev/null
+++ b/src/features/planSimulation/components/SimulationMapView/components/AddDatasetForm/AddDatasetForm.tsx
@@ -0,0 +1,151 @@
+import React, { useEffect, useState } from 'react';
+import CustomStepper from '../../../../../../components/CustomStepper/CustomStepper';
+import styles from './AddDatasetForm.module.css';
+import FileDrop from '../../../../../../components/FileDrop/FileDrop';
+import Select from 'react-select';
+import RangeInput from '../../../../../../components/RangeInput/RangeInput';
+import { ColorPicker, useColor } from 'react-color-palette';
+import { AddDatasetResponse, DataSet, getEntityTags, setDataset } from '../../api/datasetsAPI';
+import { usePolygonContext } from '../../../../../../contexts/PolygonContext';
+
+function AddDatasetForm({
+ onClose,
+ onDatasetAdded,
+ selectedLocationId
+}: {
+ onClose: () => void;
+ onDatasetAdded: (dataset: AddDatasetResponse) => void;
+ selectedLocationId?: string;
+}) {
+ const { state } = usePolygonContext();
+ const [datasetColor, setDatasetColor] = useColor('hex', '#000000');
+ const [borderValue, setBorderValue] = useState(1);
+ const [entityTags, setEntityTags] = useState([]);
+ const [formValue, setFormValue] = useState({
+ simulationId: state.simulationId,
+ tagId: '',
+ hexColor: datasetColor.hex,
+ lineWidth: borderValue,
+ parentLocationId: selectedLocationId || state.admin0LocationId
+ });
+ const [isLoading, setIsLoading] = useState(true);
+ const [validation, setValidation] = useState(false);
+
+ useEffect(() => {
+ setValidation(!!formValue.tagId);
+ }, [formValue]);
+
+ useEffect(() => {
+ const fetchEntityTags = async () => {
+ try {
+ const res = await getEntityTags();
+
+ const filteredEntityTags = res.entityTagResponses.filter(
+ (resTag: any) => !state.datasets.some((stateTag: any) => stateTag.name === resTag.tag)
+ );
+
+ setEntityTags(filteredEntityTags);
+ } catch (error) {
+ console.error('Failed to fetch entity tags:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchEntityTags();
+ }, []);
+
+ useEffect(() => {
+ setFormValue((prevValue: any) => ({
+ ...prevValue,
+ hexColor: datasetColor.hex,
+ lineWidth: borderValue
+ }));
+ }, [datasetColor, borderValue]);
+
+ const handleFinish = async () => {
+ try {
+ const newDataset = await setDataset(formValue);
+ onDatasetAdded(newDataset);
+ onClose();
+ } catch (error) {
+ console.error('Failed to add dataset:', error);
+ }
+ };
+
+ return (
+
+
+ {/*
+ Make sure you upload a JSON file. You can dowload JSON sample link{' '}
+ here
+
+ {
+ // setFormValue(prevValue => ({
+ // ...prevValue,
+ // uploadedFile: file
+ // }));
+ // }}
+ />
+ or
*/}
+ {isLoading ? (
+ Loading options...
+ ) : (
+ null
+ }}
+ placeholder={'Select Dataset'}
+ className={styles.select}
+ isClearable
+ options={entityTags.map(tag => ({
+ value: tag.identifier,
+ label: tag.tag
+ }))}
+ onChange={selectedOption => {
+ setFormValue((prevValue: any) => ({
+ ...prevValue,
+ tagId: selectedOption?.value || ''
+ }));
+ }}
+ />
+ )}
+
+
+
+ );
+}
+
+export default AddDatasetForm;
diff --git a/src/features/planSimulation/components/SimulationMapView/components/AddTargetAreaForm/AddTargetAreaForm.module.css b/src/features/planSimulation/components/SimulationMapView/components/AddTargetAreaForm/AddTargetAreaForm.module.css
new file mode 100644
index 00000000..03e9ea6c
--- /dev/null
+++ b/src/features/planSimulation/components/SimulationMapView/components/AddTargetAreaForm/AddTargetAreaForm.module.css
@@ -0,0 +1,25 @@
+.templateDowloadWrapper {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 1rem;
+ width: 100%;
+ border-radius: 5px;
+ cursor: pointer;
+ transition: background-color 0.3s;
+}
+
+.stepControls {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.step {
+ border-radius: 4px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
diff --git a/src/features/planSimulation/components/SimulationMapView/components/AddTargetAreaForm/AddTargetAreaForm.tsx b/src/features/planSimulation/components/SimulationMapView/components/AddTargetAreaForm/AddTargetAreaForm.tsx
new file mode 100644
index 00000000..0ed4442a
--- /dev/null
+++ b/src/features/planSimulation/components/SimulationMapView/components/AddTargetAreaForm/AddTargetAreaForm.tsx
@@ -0,0 +1,26 @@
+import CustomStepper from '../../../../../../components/CustomStepper/CustomStepper';
+import DrawerButton from '../../../../../../components/DrawerButton/DrawerButton';
+import FileDrop from '../../../../../../components/FileDrop/FileDrop';
+import styles from './AddTargetAreaForm.module.css';
+
+function AddTargetAreaForm({ onClose }: { onClose: () => void }) {
+ return (
+ {} }}
+ >
+
+
+ {}}>Export Template
+
+
+
+
+ );
+}
+
+export default AddTargetAreaForm;
diff --git a/src/features/planSimulation/components/SimulationMapView/components/CampaignTotalsAccordion/CampaignTotalsAccordion.module.css b/src/features/planSimulation/components/SimulationMapView/components/CampaignTotalsAccordion/CampaignTotalsAccordion.module.css
new file mode 100644
index 00000000..d718feed
--- /dev/null
+++ b/src/features/planSimulation/components/SimulationMapView/components/CampaignTotalsAccordion/CampaignTotalsAccordion.module.css
@@ -0,0 +1,59 @@
+.total {
+ margin-left: auto;
+}
+
+.targetAreaItem {
+ display: flex;
+ justify-content: space-between;
+ position: relative;
+ color: #444444;
+ margin-left: 2.2rem;
+}
+
+.paragraph {
+ margin-bottom: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+ line-height: 35px;
+}
+
+.hoverButton {
+ border: none;
+ cursor: pointer;
+ padding: 0;
+ background-color: transparent;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.itemDot {
+ position: absolute;
+ left: -18px;
+ width: 5px;
+ height: 5px;
+ top: 50%;
+ transform: translateY(-50%);
+ border-radius: 50%;
+ background-color: #444444;
+ margin-right: 0.5rem;
+}
+
+.hoverButton img {
+ transition: width 0.2s;
+ width: 0rem;
+ opacity: 0.7;
+ height: 17px;
+}
+
+.targetAreaItem:hover .hoverButton img {
+ visibility: visible;
+ width: 17px;
+ height: 17px;
+}
+
+.targetAreaItem:hover .hoverButton {
+ margin-left: 0.5rem;
+}
diff --git a/src/features/planSimulation/components/SimulationMapView/components/CampaignTotalsAccordion/CampaignTotalsAccordion.tsx b/src/features/planSimulation/components/SimulationMapView/components/CampaignTotalsAccordion/CampaignTotalsAccordion.tsx
new file mode 100644
index 00000000..68af103f
--- /dev/null
+++ b/src/features/planSimulation/components/SimulationMapView/components/CampaignTotalsAccordion/CampaignTotalsAccordion.tsx
@@ -0,0 +1,50 @@
+import { useState } from 'react';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import Delete from '../../../../../../assets/svgs/trash-bin.svg';
+
+import CampaignStyles from './CampaignTotalsAccordion.module.css';
+import styles from '../../../../../location/components/accordion/Accordion.module.css';
+
+function CampaignTotalsAccordion({ open = false, campaignTotals }: any) {
+ const [isOpen, setOpen] = useState(open);
+
+
+ return (
+
+ {/* Accordion Header */}
+
setOpen(!isOpen)}
+ style={{ position: 'relative' }}
+ >
+ {campaignTotals.total}
+ {campaignTotals.label}
+
+
+
+ {/* Accordion Content */}
+
+
+ {campaignTotals.targetAreasList.map((area: any, index: number) => (
+
+
+
{area?.properties?.name || ''}
+
+
{campaignTotals.type === 'population' ? Math.round(area?.properties?.population?.sum) ?? '' : '' }
+
+ {campaignTotals.remove && campaignTotals.remove(area.identifier)} />}
+
+
+
+ ))}
+
+
+
+ );
+}
+
+export default CampaignTotalsAccordion;
diff --git a/src/features/planSimulation/components/SimulationMapView/components/DataSetPanel/DataSetPanel.module.css b/src/features/planSimulation/components/SimulationMapView/components/DataSetPanel/DataSetPanel.module.css
new file mode 100644
index 00000000..1dd08e21
--- /dev/null
+++ b/src/features/planSimulation/components/SimulationMapView/components/DataSetPanel/DataSetPanel.module.css
@@ -0,0 +1,7 @@
+.datasetContainer {
+ display: flex;
+ flex-direction: column;
+ margin: 3rem;
+ background-color: #fff;
+ padding: 1rem;
+}
diff --git a/src/features/planSimulation/components/SimulationMapView/components/DataSetPanel/DataSetPanel.tsx b/src/features/planSimulation/components/SimulationMapView/components/DataSetPanel/DataSetPanel.tsx
new file mode 100644
index 00000000..7a425daf
--- /dev/null
+++ b/src/features/planSimulation/components/SimulationMapView/components/DataSetPanel/DataSetPanel.tsx
@@ -0,0 +1,235 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import React, { useState } from 'react';
+import { Accordion, Form, FormGroup, Button } from 'react-bootstrap';
+import { getBackgroundStyle } from '../../SimulationMapView';
+import {
+ getProcessedUserDefinedLayers,
+ updateLayerActiveState,
+ handleOpenSettingsMenu,
+ getTransparencyValue,
+ getLineWidthValue
+} from '../../SimulationMapViewUtils';
+import styles from './DataSetPanel.module.css';
+import { ColorPicker } from 'react-color-palette';
+
+interface DataSetPanelProps {
+ userDefinedLayers: any[];
+ setUserDefinedLayers: React.Dispatch>;
+ defColor: any;
+ setColor: React.Dispatch>;
+ initialLineColor: any;
+ showUserDefinedSettingsPanel: boolean;
+ setShowUserDefinedSettingsPanel: React.Dispatch>;
+ selectedUserDefinedLayer: any;
+ setSelectedUserDefinedLayer: React.Dispatch>;
+ userDefinedNames: any;
+ handleTagChange: any;
+ handleLineWidthChange: any;
+ handleTransparencyChange: any;
+ handleColorChange: any;
+ color: any;
+}
+
+function DataSetPanel({
+ userDefinedLayers,
+ setUserDefinedLayers,
+ defColor,
+ setColor,
+ initialLineColor,
+ showUserDefinedSettingsPanel,
+ setShowUserDefinedSettingsPanel,
+ selectedUserDefinedLayer,
+ setSelectedUserDefinedLayer,
+ userDefinedNames,
+ handleTagChange,
+ handleLineWidthChange,
+ handleTransparencyChange,
+ handleColorChange,
+ color
+}: DataSetPanelProps) {
+ const [showUserDefineLayerSelector, setShowUserDefineLayerSelector] = useState(false);
+
+ return (
+
+
+
{
+ // BOOLEAN FOR OPENING AND CLOSING THE RESULT SETS
+ setShowUserDefineLayerSelector(!showUserDefineLayerSelector);
+ }}
+ >
+ ResultSets{' '}
+ {showUserDefineLayerSelector ? (
+
+ ) : (
+
+ )}
+
+
+
+ {showUserDefineLayerSelector && (
+
+ {/* REFACTORED FUNCITION THAT RETURNS THE ARRAY OF LAYERS */}
+ {getProcessedUserDefinedLayers(userDefinedLayers, defColor).map(layerObj => {
+ return (
+
+
+
+ <>
+ {layerObj.key}
+
+ >
+
+
+ {/* CHECK BOX LAYERS */}
+ <>
+ {layerObj?.list?.map(layer => {
+ return (
+ alert('hello')}>
+ {layer.geo}
+
+ }
+ value={layer.layer}
+ type="checkbox"
+ checked={layer.active}
+ onChange={e => {
+ setUserDefinedLayers((layerItems: any) =>
+ updateLayerActiveState(layerItems, layer.layer, e.target.checked)
+ );
+ }}
+ />
+ );
+ })}
+
+
+ {/* SETTINGS BUTTON */}
+
+ handleOpenSettingsMenu(
+ layerObj,
+ setShowUserDefinedSettingsPanel,
+ showUserDefinedSettingsPanel,
+ selectedUserDefinedLayer,
+ setSelectedUserDefinedLayer,
+ setColor,
+ initialLineColor
+ )
+ }
+ >
+
+
+
+ {!showUserDefinedSettingsPanel || selectedUserDefinedLayer?.key !== layerObj.key
+ ? ''
+ : 'Hide '}
+ Settings
+
+
+ >
+
+
+
+ );
+ })}
+
+ )}
+
+ {/* SETTINGS PANEL WITH LINE AND OPACITY */}
+
+ {userDefinedLayers.length > 0 && showUserDefinedSettingsPanel && selectedUserDefinedLayer && (
+
+
Settings - {selectedUserDefinedLayer.key}
+
+
+
+
+ {userDefinedNames
+ ?.filter((layer: { layerName: any }) => layer.layerName === selectedUserDefinedLayer.key)
+ .map(
+ (layer: {
+ selectedTag: string | number | readonly string[] | undefined;
+ layerName: any;
+ tagList: Iterable | ArrayLike;
+ }) => (
+ <>
+ handleTagChange(e, layer.layerName)}
+ >
+ Select Metadata Tag...
+ {layer.tagList &&
+ Array.from(layer.tagList).map((metaDataItem: any) => {
+ return (
+
+ {metaDataItem}
+
+ );
+ })}
+
+ >
+ )
+ )}
+
+
Opacity ({getTransparencyValue(userDefinedLayers, selectedUserDefinedLayer)})
+
+
+ {
+
+
+ Line Control
+
+ Line Width ({getLineWidthValue(userDefinedLayers, selectedUserDefinedLayer)})
+
+
+
+
+
+
+ }
+
+ )}
+
+ );
+}
+
+export default DataSetPanel;
diff --git a/src/features/planSimulation/components/SimulationMapView/components/MapLegend/MapLegend.module.css b/src/features/planSimulation/components/SimulationMapView/components/MapLegend/MapLegend.module.css
new file mode 100644
index 00000000..25274d81
--- /dev/null
+++ b/src/features/planSimulation/components/SimulationMapView/components/MapLegend/MapLegend.module.css
@@ -0,0 +1,75 @@
+.legendBody {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ position: absolute;
+ z-index: 1000;
+ list-style: none;
+ background: rgba(255, 255, 255, 0.863);
+ border: 1px solid #ccc;
+ right: 50px;
+ bottom: 30px;
+ max-width: 320px;
+ width: 320px;
+ max-height: 500px;
+ overflow: auto;
+}
+
+.legendName {
+ margin-bottom: 0;
+}
+
+.legendTitleWrapper {
+ font-weight: bold;
+ display: flex;
+ align-items: center;
+}
+
+.legendTitle {
+ font-size: 1.1rem;
+}
+
+.legendList {
+ font-size: 0.9rem;
+ font-weight: bold;
+ margin-bottom: 0;
+}
+
+.legendItem {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ margin-top: 10px;
+ gap: 0.75rem;
+}
+
+.legendIcon {
+ width: 20px;
+ height: 20px;
+ margin-right: 7px;
+ opacity: 0.5;
+}
+
+.colorBox {
+ width: 1rem;
+ height: 1rem;
+ border-radius: 0.25rem;
+}
+
+.assignedItemInfo {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: row;
+}
+
+.assignedItemToggle {
+ padding: 0.7rem;
+ border: 1px solid #0000001a;
+ border-radius: 5px;
+ background-color: white;
+ justify-content: space-between;
+ display: flex;
+ align-items: center;
+ list-style: none;
+}
diff --git a/src/features/planSimulation/components/SimulationMapView/components/MapLegend/MapLegend.tsx b/src/features/planSimulation/components/SimulationMapView/components/MapLegend/MapLegend.tsx
new file mode 100644
index 00000000..f1623fe1
--- /dev/null
+++ b/src/features/planSimulation/components/SimulationMapView/components/MapLegend/MapLegend.tsx
@@ -0,0 +1,61 @@
+import { useMemo, useState } from 'react';
+import { usePolygonContext } from '../../../../../../contexts/PolygonContext';
+import style from './MapLegend.module.css';
+import SwitchButton from '../../../../../../components/SwitchButton/SwitchButton';
+import Accordion from '../../../../../location/components/accordion/Accordion';
+import LayerIcon from '../../../../../../assets/svgs/layers-icon.svg';
+
+interface Dataset {
+ identifier: string;
+ name: string;
+ hexColor: string;
+}
+
+function MapLegend({
+ handleClickedSwitchOnMap,
+ assigned
+}: {
+ handleClickedSwitchOnMap: (toggle: any) => void;
+ assigned: boolean;
+}) {
+ const { state } = usePolygonContext();
+
+ const datasets = useMemo(() => state.datasets, [state.datasets]);
+
+ return (
+
+
+
+ Legend
+
+ }
+ >
+
+
+ Toggle assigned
+ handleClickedSwitchOnMap(e.target.checked)}
+ />
+
+ {datasets.map((dataset: Dataset) => (
+
+
+ {dataset.name}
+
+ ))}
+
+
+
+ );
+}
+
+export default MapLegend;
diff --git a/src/features/planSimulation/components/SimulationMapView/components/StatisticsPanel/StatisticsPanel.module.css b/src/features/planSimulation/components/SimulationMapView/components/StatisticsPanel/StatisticsPanel.module.css
new file mode 100644
index 00000000..54ab1492
--- /dev/null
+++ b/src/features/planSimulation/components/SimulationMapView/components/StatisticsPanel/StatisticsPanel.module.css
@@ -0,0 +1,15 @@
+.statisticsPanel {
+ right: 5%;
+ bottom: 3%;
+ position: absolute;
+ z-index: 2;
+ min-width: 15%;
+ width: 15%;
+ max-width: 30%;
+ background-color: #fff;
+ padding: 1rem;
+}
+
+.statisticsPanelBody {
+ padding: 1rem;
+}
diff --git a/src/features/planSimulation/components/SimulationMapView/components/StatisticsPanel/StatisticsPanel.tsx b/src/features/planSimulation/components/SimulationMapView/components/StatisticsPanel/StatisticsPanel.tsx
new file mode 100644
index 00000000..d4276393
--- /dev/null
+++ b/src/features/planSimulation/components/SimulationMapView/components/StatisticsPanel/StatisticsPanel.tsx
@@ -0,0 +1,108 @@
+import { isNumeric } from 'mathjs';
+import { useState, useMemo } from 'react';
+import { Tabs, Tab, Button } from 'react-bootstrap';
+import { UserDefinedLayer, UserDefinedNames } from '../../SimulationMapViewModels';
+import { EntityTag } from '../../../../providers/types';
+import { StatsLayer } from '../../../Simulation';
+
+import styles from './StatisticsPanel.module.css';
+
+interface StatisticsPanelProps {
+ userDefinedLayers: UserDefinedLayer[];
+ userDefinedNames: UserDefinedNames[];
+ entityTags: EntityTag[];
+ stats: StatsLayer;
+ defaultColor?: string; // Add defaultColor as a prop
+}
+
+function StatisticsPanel({
+ userDefinedLayers,
+ userDefinedNames,
+ entityTags,
+ stats,
+ defaultColor
+}: StatisticsPanelProps) {
+ const [showStats, setShowStats] = useState(true);
+
+ const processedUserDefinedLayers = useMemo(() => {
+ // Group layers by 'layerName'
+ const groupLayersByKey = (layers: UserDefinedLayer[]) => {
+ return layers.reduce((acc, layer) => {
+ acc[layer.layerName] = acc[layer.layerName] || [];
+ acc[layer.layerName].push(layer);
+ return acc;
+ }, {} as Record
);
+ };
+
+ // Convert grouped layers into desired format
+ const groupedLayers = groupLayersByKey(userDefinedLayers);
+ return Object.entries(groupedLayers).map(([key, layers]) => ({
+ key,
+ list: layers,
+ color: layers[0]?.col || defaultColor // Use layer color or default
+ }));
+ }, [userDefinedLayers, defaultColor]);
+
+ const handleToggleStats = () => {
+ setShowStats(prev => !prev);
+ };
+
+ const renderLayerList = (layerList: UserDefinedLayer[] | undefined) =>
+ layerList?.map(item => (
+
+ {item.geo}: {item.size}
+
+ ));
+
+ const renderTagStats = (userDefinedLayerKey: string) => {
+ const relevantNames = userDefinedNames.filter(layer => layer.key === userDefinedLayerKey);
+
+ return relevantNames.flatMap(locItem =>
+ Array.from(locItem.tagList || []).flatMap(meta => {
+ const tagItem = entityTags.find(tag => tag.tag === meta);
+ const shouldDisplay = tagItem?.simulationDisplay && stats[userDefinedLayerKey]?.[meta];
+
+ if (!shouldDisplay) return null;
+
+ const num = stats[userDefinedLayerKey][meta];
+ const val = isNumeric(num) && typeof num === 'number' ? Math.round(num).toLocaleString('en-US') : num;
+
+ return (
+
+ {meta}: {val}
+
+ );
+ })
+ );
+ };
+
+ const renderTabs = () =>
+ processedUserDefinedLayers.map(userDefinedLayer => (
+
+ <>
+ {renderLayerList(userDefinedLayer.list)}
+ {renderTagStats(userDefinedLayer.key)}
+ >
+
+ ));
+
+ return (
+ !showStats && setShowStats(true)}>
+ {userDefinedLayers.length > 0 && !showStats && 'Show Stats'}
+ {showStats && renderTabs()}
+ {showStats && (
+
+ {showStats ? 'Close' : 'Open'}
+
+ )}
+
+ );
+}
+
+export default StatisticsPanel;
diff --git a/src/features/planSimulation/components/SimulationMapView/util.ts b/src/features/planSimulation/components/SimulationMapView/util.ts
new file mode 100644
index 00000000..ffa99960
--- /dev/null
+++ b/src/features/planSimulation/components/SimulationMapView/util.ts
@@ -0,0 +1,20 @@
+// finds ids of all the levels of provided node list, except the ones with geoLevel = structure
+export const getIdsByGeographicLevel = (nodes: any[] | undefined): string[] => {
+ if (!Array.isArray(nodes)) return [];
+ return nodes.reduce((ids, node) => {
+ if (node.properties?.geographicLevel !== "structure") {
+ ids.push(node.identifier);
+ }
+ return ids.concat(getIdsByGeographicLevel(node.children ?? []));
+ }, []);
+ }
+
+ export const findNodeById = (nodes: any[] | undefined, targetId: string): any | undefined => {
+ if (!Array.isArray(nodes)) return undefined;
+ for (const node of nodes) {
+ if (node.identifier === targetId) return node;
+ const found = findNodeById(node.children, targetId);
+ if (found) return found;
+ }
+ return undefined;
+ }
\ No newline at end of file
diff --git a/src/features/planSimulation/components/TargetsSelectedList/TargetsSelectedList.module.css b/src/features/planSimulation/components/TargetsSelectedList/TargetsSelectedList.module.css
new file mode 100644
index 00000000..c1e339a6
--- /dev/null
+++ b/src/features/planSimulation/components/TargetsSelectedList/TargetsSelectedList.module.css
@@ -0,0 +1,128 @@
+.targetsSelectedList {
+ position: absolute;
+ z-index: 3;
+ right: 0;
+ width: 250px;
+ background: rgba(255, 255, 255, 0.863);
+ border: 1px solid #ccc;
+ max-height: 500px;
+ overflow: auto;
+ font-size: 0.9rem;
+}
+
+.listWrapper {
+ list-style: none;
+ margin-bottom: 0;
+}
+
+.listWrapper p {
+ margin: 0;
+}
+
+.divider {
+ margin: 0.8rem 0;
+}
+
+.listItem {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.5rem;
+ transition: background-color 0.2s;
+}
+
+.listItem:hover {
+ background-color: #f5f5f580;
+}
+
+.listItemInfo {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: row;
+ gap: 0.5rem;
+}
+
+.selectedIcon {
+ filter: brightness(0) saturate(100%) invert(21%) sepia(91%) saturate(7190%) hue-rotate(358deg) brightness(109%)
+ contrast(115%);
+ width: 10px;
+ height: 10px;
+ margin-right: 0.2rem;
+}
+
+.removeLocationButton {
+ border: none;
+ padding: 0 0.5rem;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ text-decoration: none;
+ border-radius: 7px;
+ background: transparent;
+}
+
+.removeLocationButton img {
+ width: 17px;
+ height: 17px;
+ opacity: 0.7;
+ transition: all 0.2s;
+}
+
+.removeLocationButton:hover img {
+ opacity: 1;
+}
+
+.targetsSelectedList::-webkit-scrollbar {
+ width: 5px;
+ display: none;
+}
+
+.targetsSelectedList:hover::-webkit-scrollbar {
+ display: block;
+}
+
+.targetsSelectedList::-webkit-scrollbar-track {
+ background: #0000001a;
+ margin: 4px;
+}
+
+.targetsSelectedList::-webkit-scrollbar-thumb {
+ background-color: #949494;
+ border-radius: 10px;
+}
+
+.targetsSelectedList::-webkit-scrollbar-button {
+ display: none !important;
+}
+
+.targetsSelectedList::-webkit-scrollbar-thumb:hover {
+ background-color: #949494a1;
+}
+
+.buttonWrapper {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.assignmentButton {
+ background-color: #059669;
+ width: 100%;
+ color: white;
+ border: none;
+ border-radius: 0.375rem;
+ padding: 0.5rem 1rem;
+ font-size: 0.875rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background-color 0.3s;
+ text-decoration: none;
+ text-align: center;
+}
+
+.assignmentButton:hover {
+ background-color: #047857;
+ color: white;
+}
diff --git a/src/features/planSimulation/components/TargetsSelectedList/TargetsSelectedList.tsx b/src/features/planSimulation/components/TargetsSelectedList/TargetsSelectedList.tsx
new file mode 100644
index 00000000..7e550e78
--- /dev/null
+++ b/src/features/planSimulation/components/TargetsSelectedList/TargetsSelectedList.tsx
@@ -0,0 +1,113 @@
+import React, { useMemo, useCallback } from 'react';
+import styles from './TargetsSelectedList.module.css';
+import { usePolygonContext } from '../../../../contexts/PolygonContext';
+import Accordion from '../../../location/components/accordion/Accordion';
+import task from '../../../../assets/svgs/task.svg';
+import Delete from '../../../../assets/svgs/trash-bin.svg';
+import { assignLocationsToPlan } from '../SimulationMapView/api/planAPI';
+import { findNodeById, getIdsByGeographicLevel } from '../SimulationMapView/util';
+import { getSimulationData } from '../SimulationMapView/api/datasetsAPI';
+
+function TargetsSelectedList() {
+ const { state, dispatch } = usePolygonContext();
+ const { multiselect, polygons } = state;
+
+ // Helper function to find a parent by its identifier
+ const findParent = useCallback((data: any[], parentId: string): any => {
+ for (const polygon of data) {
+ if (polygon.identifier === parentId) {
+ return polygon;
+ }
+ if (polygon.children.length > 0) {
+ const found = findParent(polygon.children, parentId);
+ if (found) return found;
+ }
+ }
+ return undefined;
+ }, []);
+
+ // Group selected polygons by their parent (memoized for optimization)
+ const groupedData = useMemo(() => {
+ const grouped: Record = {};
+
+ multiselect.forEach(polygon => {
+ const parent = findParent(polygons, polygon.properties.parentIdentifier);
+ const parentName = parent?.properties.name || 'Unknown Parent';
+
+ if (!grouped[parentName]) {
+ grouped[parentName] = [];
+ }
+ grouped[parentName].push(polygon);
+ });
+
+ return grouped;
+ }, [multiselect, findParent, polygons]);
+
+ // Function to render nested children recursively (memoized)
+ const renderChildren = useCallback(
+ (polygon: any, key) => {
+ return (
+
+
+
+
+ {polygon.properties.name} ({polygon.properties.childrenNumber})
+
+
+ dispatch({ type: 'TOGGLE_MULTISELECT', payload: polygon })}
+ >
+
+
+
+ );
+ },
+ [dispatch]
+ );
+
+ // Handler for logging polygon identifiers
+ const handleLogIdentifiers = (polygons: any) => {
+ // same logic as for single select assignment, check SimulationMapView.tsx handleCampaignClick
+ const ancestry: any[] = [];
+ const results: any[] = [];
+ polygons.forEach((polygon: any) => {
+ ancestry.push(...JSON.parse(polygon.properties?.ancestry) || []);
+ const currentLocWithChildren = findNodeById(state.polygons, polygon.properties?.id);
+ results.push(...getIdsByGeographicLevel(currentLocWithChildren.children));
+ });
+ const assignedAreas = state.targetAreas?.flatMap(ta => [...ta.ancestry, ta.identifier]) || [];
+
+ const allLocationsIdsToBeAssigned = new Set([...results, ...ancestry, ...assignedAreas, ...polygons.map((p: any) => p.properties?.id)]);
+ const identifiersToSendArray = Array.from(allLocationsIdsToBeAssigned);
+ assignLocationsToPlan(state.planid, identifiersToSendArray).then(async () => {
+ const simulationData = await getSimulationData(state.planid);
+ dispatch({ type: 'SET_TARGET_AREAS', payload: simulationData.targetAreas });
+ dispatch({ type: 'CLEAR_SELECTION' });
+ dispatch({
+ type: 'SET_ASSIGNED', payload: {
+ ...state.assingedLocations, ...Object.fromEntries(polygons.map((p: any) => [p.properties?.id, true]))
+ }
+ });
+
+ });
+ };
+
+ return (
+
+ {Object.entries(groupedData).map(([parentName, polygons], index) => (
+
+ {polygons.map((polygon, index) => renderChildren(polygon, index))}
+
+
+ handleLogIdentifiers(polygons)} className={styles.assignmentButton}>
+ Add all to campaign
+
+
+
+ ))}
+
+ );
+}
+
+export default React.memo(TargetsSelectedList);
diff --git a/src/features/planSimulation/components/Teams/Teams.module.css b/src/features/planSimulation/components/Teams/Teams.module.css
new file mode 100644
index 00000000..afe68b05
--- /dev/null
+++ b/src/features/planSimulation/components/Teams/Teams.module.css
@@ -0,0 +1,100 @@
+.container {
+ border-radius: 8px;
+}
+
+.header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.usersIcon {
+ width: 15px;
+ height: 15px;
+}
+
+.title {
+ font-size: 14px;
+ font-weight: 600;
+ color: #4b5563;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.teamGroup {
+ margin-bottom: 1px;
+}
+
+.teamHeader {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ cursor: pointer;
+ user-select: none;
+ transition: background-color 0.2s;
+}
+
+.teamName {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: #111827;
+ font-weight: 500;
+}
+
+.chevron {
+ color: #6b7280;
+ transition: transform 0.2s;
+}
+
+.membersList {
+ overflow: hidden;
+ transition: height 0.2s ease-out;
+}
+
+.member {
+ display: flex;
+ padding: 0.3rem 0rem;
+ align-items: center;
+ font-size: 12px;
+ justify-content: space-between;
+}
+
+.memberInfo {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ justify-content: space-between;
+}
+
+.memberName {
+ color: #374151;
+}
+
+.memberUsername {
+ color: #6b7280;
+ font-size: 12px;
+ font-style: italic;
+ margin-right: 1px;
+}
+
+.emptyTeam {
+ color: #6b7280;
+ font-size: 12px;
+ font-style: italic;
+}
+
+.statusIndicator {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+}
+
+.statusIndicator.active {
+ background-color: #10b981;
+}
+
+.statusIndicator.inactive {
+ background-color: #e5e7eb;
+}
diff --git a/src/features/planSimulation/components/Teams/Teams.tsx b/src/features/planSimulation/components/Teams/Teams.tsx
new file mode 100644
index 00000000..fef346f1
--- /dev/null
+++ b/src/features/planSimulation/components/Teams/Teams.tsx
@@ -0,0 +1,72 @@
+import { useEffect } from 'react';
+import style from './Teams.module.css';
+import Accordion from '../../../location/components/accordion/Accordion';
+import Users from '../../../../assets/svgs/users.svg';
+
+interface TeamsProps {
+ teamsList: Team[];
+ fetchTeamsData: () => void;
+}
+
+interface Member {
+ identifier: string;
+ firstName: string;
+ lastName: string;
+ username: string;
+}
+
+interface Team {
+ identifier: string;
+ name: string;
+ members: Member[];
+ active?: boolean;
+}
+
+const Teams = ({ teamsList, fetchTeamsData }: TeamsProps) => {
+ useEffect(() => {
+ fetchTeamsData();
+ }, []);
+
+ return (
+ <>
+
+ {teamsList.map(team => (
+
+
+
+
+
{team.name}
+
+ {/* */}
+
+ }
+ >
+
+ {team.members.length > 0 ? (
+ team.members.map(member => (
+
+
+
+ {member.firstName} {member.lastName}
+
+ {member.username}
+
+
+ ))
+ ) : (
+
No members
+ )}
+
+
+ ))}
+
+ >
+ );
+};
+
+export default Teams;
diff --git a/src/features/planSimulation/components/Teams/api/teamAPI.ts b/src/features/planSimulation/components/Teams/api/teamAPI.ts
new file mode 100644
index 00000000..36217079
--- /dev/null
+++ b/src/features/planSimulation/components/Teams/api/teamAPI.ts
@@ -0,0 +1,36 @@
+import api from '../../../../../api/axios';
+import { toast } from 'react-toastify';
+
+import { OrganizationModel, Groups } from '../../../../../features/organization/providers/types';
+import { PageableModel } from '../../../../../api/providers';
+import { ORGANIZATION } from '../../../../../constants';
+
+export const getOrganizationList = async (
+ size: number,
+ page: number,
+ search?: string,
+ sortField?: string,
+ direction?: boolean
+): Promise> => {
+ const data = await api
+ .get>(
+ ORGANIZATION +
+ `?search=${search !== undefined ? search : ''}&size=${size}&page=${page}&_summary=FALSE&root=true&sort=${
+ sortField !== undefined ? sortField : ''
+ },${direction ? 'asc' : 'desc'}`
+ )
+ .then(response => response.data);
+ return data;
+};
+
+export const getOrganizationListSummary = async (): Promise> => {
+ const data = await api
+ .get>(ORGANIZATION + '?_summary=TRUE&root=false&size=500&page=0')
+ .then(response => response.data);
+ return data;
+};
+
+export const getOrganizatonsWithMembers = async (): Promise => {
+ const data = await api.get('/organization/members').then(response => response.data);
+ return data;
+};
diff --git a/src/features/planSimulation/components/User/api/userAPI.ts b/src/features/planSimulation/components/User/api/userAPI.ts
new file mode 100644
index 00000000..8e5fdf9e
--- /dev/null
+++ b/src/features/planSimulation/components/User/api/userAPI.ts
@@ -0,0 +1,157 @@
+import api from '../../../../../api/axios';
+import { toast } from 'react-toastify';
+import { OrganizationModel } from '../../../../../features/organization/providers/types';
+import { ORGANIZATION, USER } from '../../../../../constants';
+import axios, { AxiosResponse } from 'axios';
+export interface CreateUserRequest {
+ email: string;
+ firstName: string;
+ lastName: string;
+ organizations: string[];
+ password: string;
+ securityGroups: string[];
+ username: string;
+
+}
+export interface MemberModel {
+ identifier: string;
+ firstName: string;
+ lastName: string;
+ email: string;
+ role: string;
+ username: string;
+}
+
+export interface UserModel {
+ identifier: string;
+ sid: string;
+ username: string;
+ firstName: string;
+ lastName: string;
+ email: string;
+ password?: string;
+ tempPassword?: boolean;
+ organizations: OrganizationModel[];
+ securityGroups: string[];
+ selectedAll?: boolean;
+}
+export interface UserListResponse {
+ content: UserModel[];
+ totalElements: number;
+ totalPages: number;
+ pageable: {
+ sort: {
+ sorted: boolean;
+ unsorted: boolean;
+ empty: boolean;
+ };
+ pageNumber: number;
+ pageSize: number;
+ offset: number;
+ paged: boolean;
+ unpaged: boolean;
+ };
+ numberOfElements: number;
+ number: number;
+ size: number;
+ empty: boolean;
+ first: boolean;
+ last: boolean;
+ sort: {
+ sorted: boolean;
+ unsorted: boolean;
+ empty: boolean;
+ };
+}
+
+export interface CreateUserResponse {
+ userId: string;
+ email: string;
+ firstName: string;
+ lastName: string;
+ organizations: string[];
+ securityGroups: string[];
+ username: string;
+ success?: boolean;
+}
+
+export interface AddUserToOrganizationRequest {
+ username: string;
+}
+
+export interface AddUserToOrganizationResponse {
+ success: boolean;
+ message: string;
+}
+
+export const getUserList = async (
+ search?: string,
+ filters?: { firstName?: string; lastName?: string; email?: string },
+ sortField?: string,
+ direction?: boolean
+): Promise => {
+ const params = new URLSearchParams();
+
+ if (search) params.append('search', search);
+
+ if (filters) {
+ if (filters.firstName) params.append('firstName', filters.firstName);
+ if (filters.lastName) params.append('lastName', filters.lastName);
+ if (filters.email) params.append('email', filters.email);
+ }
+
+ if (sortField) {
+ params.append('sort', `${sortField},${direction ? 'asc' : 'desc'}`);
+ }
+
+ const response = await api.get(`${USER}?${params.toString()}`);
+
+ return response.data.content;
+};
+
+export const createUser = async (data: CreateUserRequest): Promise => {
+ try {
+ const response: AxiosResponse = await api.post('/user', data);
+ return response;
+ } catch (error) {
+ console.error('Error creating user:', error);
+ return null;
+ }
+};
+
+// create
+
+export const createOrganization = async (organization: any): Promise => {
+ const data = await api.post(ORGANIZATION, organization).then(response => response.data);
+ return data;
+};
+
+export const getOrganizationMembers = async (organizationId: string): Promise => {
+ try {
+ const response = await api.get(`/organization/${organizationId}/members`);
+ return response.data;
+ } catch (error) {
+ console.error('Error fetching organization members:', error);
+ throw error;
+ }
+};
+
+export const addUserToOrganization = async (organizationId: string, username: string) => {
+ try {
+ const response = await api.post(`/organization/${organizationId}/members`, username);
+ return response.data;
+ } catch (error) {
+ console.error('Error adding user to organization:', error);
+ throw error;
+ }
+};
+
+export const deleteUserFromOrganization = async (organizationId: string, username: string) => {
+ try {
+ const response = await api.delete(`/organization/${organizationId}/members/${username}`);
+ return response.data;
+ } catch (error) {
+ console.error('Error deleting user from organization:', error);
+ throw error;
+ }
+};
diff --git a/src/features/planSimulation/components/Users.module.css b/src/features/planSimulation/components/Users.module.css
new file mode 100644
index 00000000..a2730174
--- /dev/null
+++ b/src/features/planSimulation/components/Users.module.css
@@ -0,0 +1,511 @@
+.modalContainer {
+ display: flex;
+ flex-direction: row;
+ height: 500px;
+}
+
+.sidebar {
+ width: 200px;
+ background-color: #f8f9fa;
+ padding: 15px;
+ border-right: 1px solid #dee2e6;
+}
+.fullModal {
+ width: 90vw;
+ max-width: 1200px;
+ height: auto;
+ min-height: 400px;
+ max-height: 90vh;
+ margin: auto;
+ background-color: #ffffff;
+ border-radius: 12px;
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ padding-left: 15px;
+ padding-right: 18px !important;
+ padding:2px;
+}
+.fullModal h1 {
+ font-size: 24px;
+ font-weight: 600;
+ color: black;
+ padding: 10px;
+ border-bottom: 1px solid #dee2e6;
+ margin: 0;
+}
+
+.sidebarButton {
+ display: block;
+ width: 100%;
+ background: none;
+ border: none;
+ text-align: left;
+ padding: 10px;
+ font-weight: 500;
+ cursor: pointer;
+ color: #333;
+}
+
+.sidebarButton.active {
+ background-color: #e9ecef;
+}
+
+.content {
+ flex-grow: 1;
+ padding: 20px;
+}
+
+.formContainer {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 20px;
+}
+
+.avatar {
+ width: 100px;
+ height: 100px;
+ border-radius: 50%;
+ background-color: blue;
+ margin-bottom: 20px;
+}
+
+.formGroup {
+ width: 100%;
+ margin-bottom: 40px;
+}
+
+.formButtons {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 20px;
+ gap: 10px;
+}
+.fullForm {
+ width: 100%;
+ background-color: white;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ border-radius: 8px;
+ padding: 20px;
+}
+
+.buttonPrimary {
+ background-color: #254c8b;
+ color: white;
+ border: none;
+ padding: 8px 12px;
+ border-radius: 5px;
+ cursor: pointer;
+}
+
+.buttonSecondary {
+ background-color: #6c757d;
+ color: white;
+ border: none;
+ padding: 8px 12px;
+ border-radius: 5px;
+ cursor: pointer;
+}
+
+.buttonContainer {
+ display: flex;
+ justify-content: center;
+ margin-top: 20px;
+ gap: 20px;
+}
+
+.sidebarNav {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ background-color: white;
+ border-right: #d4d1d1;
+ border-right-width: 1px;
+ border-right-style: solid;
+ height: auto;
+ min-height: 420px;
+ overflow-y: auto;
+}
+
+
+.modal-dialog {
+ min-height: 400px;
+}
+
+.modal-content {
+ height: 100%;
+}
+
+.modalFull {
+ width: 100%;
+ min-height: 400px;
+}
+
+.title {
+ font-size: 20px;
+ font-weight: 500;
+ margin-bottom: 20px;
+}
+
+.settings {
+ display: flex;
+}
+.navsettingItemMenu {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: black !important;
+ font-size: 17px;
+ white-space: nowrap;
+}
+.settingIcon {
+ position: relative;
+ width: 20px;
+ height: 16px;
+ margin-right: 10px;
+ font-size: 16px;
+}
+
+.manageIcon {
+ position: relative;
+ width: 20px;
+ height: 16px;
+ margin-right: 10px;
+ font-size: 16px;
+}
+
+.manageItemMenu {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: black !important;
+ font-size: 17px;
+ white-space: nowrap;
+ transition: background-color 0.3s, color 0.3s;
+ width: 100%;
+ padding: 12px 20px;
+ border-radius: 8px;
+}
+
+.manageTeams {
+ display: flex;
+ width: 100%;
+ color: black;
+}
+
+.manageItemMenu:hover {
+ background-color: #085ED6 !important;
+ color: white !important;
+}
+
+/* .manageItemMenu.active {
+ background-color: #007bff;
+ color: white !important;
+} */
+
+.viewIcon {
+ position: relative;
+ width: 20px;
+ height: 16px;
+ margin-right: 10px;
+ font-size: 16px;
+}
+
+.viewItemMenu {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: black !important;
+ font-size: 17px;
+ white-space: nowrap;
+ transition: background-color 0.3s, color 0.3s;
+ width: 100%;
+ padding: 12px 20px;
+ border-radius: 8px;
+}
+
+.viewUsers {
+ display: flex;
+ width: 100%;
+ color: black;
+}
+
+
+.viewItemMenu:hover {
+ background-color: #085ED6 !important;
+ color: white !important;
+}
+
+
+
+
+.userIcon {
+ position: relative;
+ width: 20px;
+ height: 16px;
+ margin-right: 10px;
+ font-size: 17px;
+}
+
+.userItemMenu {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: black !important;
+ font-size: 17px;
+ white-space: nowrap;
+ transition: background-color 0.3s, color 0.3s;
+ width: 100%;
+ padding: 12px 20px;
+ border-radius: 8px;
+}
+
+.userPlus {
+ display: flex;
+ width: 100%;
+ color: black;
+}
+
+.userItemMenu:hover {
+ background-color: #085ED6 !important;
+ color: white !important;
+}
+
+.userIcon:hover {
+ color: white !important;
+}
+
+
+
+
+
+.viewItemMenu.active,
+.userItemMenu.active,
+.manageItemMenu.active {
+ background-color: #085ED6 !important;
+ color: white !important;
+}
+
+.viewItemMenu,
+.userItemMenu,
+.manageItemMenu {
+ color: black !important;
+}
+
+.nav-item .viewItemMenu.active,
+.nav-item .userItemMenu.active,
+.nav-item .manageItemMenu.active {
+ background-color: #085ED6 !important;
+ color: white !important;
+}
+
+.nav-item .viewItemMenu:hover .viewIcon,
+.nav-item .userItemMenu:hover .userIcon,
+.nav-item .manageItemMenu:hover .manageIcon {
+ color: white !important;
+}
+
+
+
+/* .searchContainer {
+ margin-top: 10px;
+ margin-bottom: 10px;
+} */
+
+.bodyClass {
+ height: 100%;
+ background-color: #f9fafb;
+ padding-left: 10px;
+ padding-top: 0 !important;
+ padding-bottom: 0;
+}
+
+.contentArea {
+ padding: 20px;
+}
+
+.dropDown {
+ margin-bottom: 16px;
+}
+
+.columnField {
+ margin-top: 5px;
+}
+
+.userColumn {
+ border: 1px solid #dee2e6;
+ background-color: white;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ min-height: 350px;
+ padding: 10px;
+}
+
+.arrowClass {
+ text-align: center;
+ margin-top: 80px;
+}
+
+.openModalButton {
+ background-color: white !important;
+ color: black !important;
+ border: 1px solid black;
+ padding: 10px 20px;
+ border-radius: 5px;
+ cursor: pointer;
+ width: 200px;
+ transition: background 0.3s ease !important;
+}
+
+.userColumn {
+ border: 1px solid #dee2e6;
+ background-color: white;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ min-height: 350px;
+ padding: 10px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.columnHeader {
+ text-align: center;
+ font-size: 18px;
+ font-weight: bold;
+ width: 100%;
+ margin-bottom: 10px;
+ padding: 5px 0;
+ border-bottom: 2px solid #dee2e6;
+}
+
+.userList {
+ list-style: none;
+ padding: 0;
+ width: 100%;
+}
+
+.userItem {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px;
+ cursor: pointer;
+ transition: background 0.3s;
+}
+
+.userItem:hover {
+ background-color: #f8f9fa;
+}
+
+.selected {
+ background-color: #e9ecef;
+ font-weight: bold;
+}
+
+
+
+.arrowClass {
+ display: flex !important;
+ flex-direction: column !important;
+
+ align-items: center !important;
+ justify-content: center !important;
+ height: 100%;
+ margin-top: 100px !important;
+}
+
+.viewUserContainer {
+ margin-top: 20px;
+ padding-left: 10px;
+}
+
+.usersSection,
+.teamsSection {
+ border: 1px solid #dee2e6;
+ background-color: white;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ min-height: 350px;
+ padding: 10px;
+ padding-left: 20px;
+}
+
+
+.sectionHeader {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-bottom: 10px;
+ border-bottom: 2px solid #ddd;
+ text-align: left;
+}
+
+.controls {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+
+.searchInput {
+ height: 35px;
+ padding: 0 10px;
+ font-size: 16px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ width: 200px;
+ box-sizing: border-box;
+ outline: none;
+ transition: border-color 0.3s ease;
+}
+
+
+.searchInput:focus {
+ border-color: #085ED6;
+}
+
+
+.controlIcon {
+ cursor: pointer;
+ color: #555;
+ font-size: 18px;
+ transition: color 0.3s ease;
+}
+
+
+.controlIcon:hover {
+ color: #085ED6;
+}
+
+
+.usersList,
+.teamsList {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.userEntry,
+.teamEntry {
+ display: flex;
+ align-items: center;
+ padding: 10px;
+ cursor: pointer;
+ transition: background 0.3s;
+}
+
+.userEntry:hover,
+.teamEntry:hover {
+ background-color: #f8f9fa;
+}
+
+.activeEntry {
+ background-color: #e9ecef;
+ font-weight: bold;
+}
+
+.userAvatar,
+.teamAvatar {
+ margin-right: 10px;
+ color: #007bff;
+}
diff --git a/src/features/planSimulation/components/UsersModal.tsx b/src/features/planSimulation/components/UsersModal.tsx
new file mode 100644
index 00000000..c7ee2a13
--- /dev/null
+++ b/src/features/planSimulation/components/UsersModal.tsx
@@ -0,0 +1,817 @@
+import React, { useState, useCallback, useEffect } from 'react';
+import { Button, Col, Container, Form, Modal, Row, Nav, Dropdown } from 'react-bootstrap';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import Select, { MultiValue, SingleValue } from 'react-select';
+import { DebounceInput } from 'react-debounce-input';
+
+import {
+ CreateUserRequest,
+ CreateUserResponse,
+ createUser,
+ UserModel,
+ getOrganizationMembers,
+ getUserList,
+ addUserToOrganization,
+ MemberModel,
+ createOrganization,
+ deleteUserFromOrganization
+} from '../../planSimulation/components/User/api/userAPI';
+import { getOrganizationList } from './Teams/api/teamAPI';
+import { getOrganizationListSummary, getSecurityGroups, getOrganizationById } from '../../organization/api';
+import { Controller, useForm } from 'react-hook-form';
+import { toast } from 'react-toastify';
+import { REGEX_EMAIL_VALIDATION, REGEX_NAME_VALIDATION, REGEX_USERNAME_VALIDATION } from '../../../constants/constants';
+import {
+ faUserPlus,
+ faUsers,
+ faUser,
+ faSort,
+ faFilter,
+ faCogs,
+ faUsersCog,
+ faArrowRight,
+ faArrowLeft,
+ faArrowUp,
+ faArrowDown
+} from '@fortawesome/free-solid-svg-icons';
+import style from './Users.module.css';
+import CreateUser from '../../user/components/UsersPage/create/CreateUser';
+import { OrganizationModel } from '../../organization/providers/types';
+import { FieldValidationError } from '../../../api/providers';
+import Accordion from '../../location/components/accordion/Accordion';
+import { string } from 'mathjs';
+import { AxiosResponse } from 'axios';
+interface RegisterValues {
+ username: string;
+ firstname: string;
+ lastname: string;
+ email: string;
+ password: string;
+ securityGroups: string[];
+ organizations: string[];
+ bulk: File[];
+}
+interface Option {
+ value: string;
+ label: string;
+}
+
+interface Options {
+ value: string;
+ label: string;
+}
+interface UserModalProps {
+ className?: string;
+ fetchTeamsData: () => void;
+}
+
+interface TeamsRegisterValues {
+ name: string;
+ type: string;
+ partOf: string;
+ active: boolean;
+}
+
+export default function UserModal({ fetchTeamsData }: UserModalProps) {
+ const [selectedSecurityGroups, setSelectedSecurityGroups] = useState();
+ const [selectedTeamSecurityGroups, setSelectedTeamSecurityGroups] = useState>();
+ const [teamOrganization, setteamOrganization] = useState([]);
+ const [selectedOrganizations, setSelectedOrganizations] = useState();
+ const [organizations, setOrganizations] = useState([]);
+ const [groups, setGroups] = useState();
+ const [users, setUsers] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [isFilterOpen, setIsFilterOpen] = useState(false);
+ const [sortField, setSortField] = useState('firstName');
+ const [sortDirection, setSortDirection] = useState(true);
+
+ const [teamSortField, setTeamSortField] = useState('name');
+ const [teamSortDirection, setTeamSortDirection] = useState(true);
+
+ const [teams, setTeams] = useState([]);
+ const [direction, setDirection] = useState(true);
+ const [teamSearchTerm, setTeamSearchTerm] = useState('');
+ const [searchTerm, setSearchTerm] = useState('');
+ const [showModal, setShowModal] = useState(false);
+ const [filters, setFilters] = useState({ firstName: '', lastName: '', email: '' });
+ const [activeTab, setActiveTab] = useState('viewUsers');
+ // const [show, setShow] = useState(false);
+ const handleClose = () => setShowModal(false);
+ const handleShow = () => setShowModal(true);
+
+ const [allUsers, setAllUsers] = useState([]);
+
+ const [sortedUsers, setSortedUsers] = useState([]);
+
+ const [viewType, setViewType] = useState('Available Members');
+
+ const [teamsList, setTeamsList] = useState<{ name: string; id: string }[]>([]);
+
+ const [selectedTeam, setSelectedTeam] = useState('');
+ const [selectedTeamId, setSelectedTeamId] = useState('');
+ const [selectedTeamName, setSelectedTeamName] = useState('');
+
+ // Dynamic filtering for Available/All Members dropdown
+
+ const [assignedUsers, setAssignedUsers] = useState([]);
+
+ const [selectedAvailable, setSelectedAvailable] = useState(null);
+ const [selectedAssigned, setSelectedAssigned] = useState(null);
+
+ const {
+ reset,
+ register,
+ handleSubmit,
+ setError,
+ formState: { errors }
+ } = useForm();
+
+ const getData = useCallback(() => {
+ getSecurityGroups().then(res => {
+ setGroups(
+ res.map(el => {
+ return {
+ value: el.name,
+ label: el.name
+ };
+ })
+ );
+ });
+ getOrganizationListSummary().then(res => {
+ setOrganizations(
+ res.content.map(el => {
+ return {
+ value: el.identifier,
+ label: el.name
+ };
+ })
+ );
+ });
+ }, []);
+
+ useEffect(() => {
+ getData();
+ }, [getData]);
+ const selectHandler = (selectedOption: MultiValue<{ value: string; label: string }>) => {
+ const values = selectedOption.map(selected => {
+ return selected;
+ });
+ setSelectedSecurityGroups(values);
+ };
+ const organizationSelectHandler = (selectedOption: MultiValue<{ value: string; label: string }>) => {
+ const values = selectedOption.map(selected => {
+ return selected;
+ });
+ setSelectedOrganizations(values);
+ };
+ const submitHandler = (formValues: RegisterValues) => {
+ let newUser: CreateUserRequest = {
+ username: formValues.username,
+ email: formValues.email === '' ? '' : formValues.email,
+ firstName: formValues.firstname,
+ lastName: formValues.lastname,
+ organizations: selectedOrganizations?.map(el => el.value) ?? [],
+ password: formValues.password,
+ securityGroups: selectedSecurityGroups?.map(el => el.value) ?? []
+ };
+ createUser(newUser).then((res: AxiosResponse | null) => {
+ console.log(res, 'the response of the create user');
+ if (res) {
+ toast.success('User created successfully!');
+ fetchTeamsData();
+ reset();
+ } else {
+ toast.error('Failed to create user');
+ }
+ });
+ };
+
+ /// users list logic
+
+ const fetchUsers = useCallback(async () => {
+ try {
+ setLoading(true);
+ const allUsers = await getUserList();
+ console.log('allUsers', allUsers);
+ const uniqueUsers = Array.from(new Map(allUsers.map(user => [user.identifier, user])).values());
+ setSortedUsers(uniqueUsers);
+ setUsers(sortUsers(uniqueUsers, sortField, direction));
+ } catch (error) {
+ } finally {
+ setLoading(false);
+ }
+ }, [sortField, direction]);
+
+ useEffect(() => {
+ fetchUsers();
+ }, []);
+
+ const sortUsers = (userList: UserModel[], field: string, direction: boolean): UserModel[] => {
+ return [...userList].sort((a, b) => {
+ if (field === 'firstName') {
+ return direction ? a.firstName.localeCompare(b.firstName) : b.firstName.localeCompare(a.firstName);
+ }
+ return 0;
+ });
+ };
+
+ const handleSortClick = (field: string) => {
+ const isSameField = sortField === field;
+ const newSortDirection = isSameField ? !direction : true;
+
+ setSortField(field);
+ setDirection(newSortDirection);
+
+ const sorted = sortUsers(sortedUsers, field, newSortDirection);
+ setUsers(sorted);
+ };
+
+ const handleSearchChange = (e: React.ChangeEvent) => {
+ setSearchTerm(e.target.value);
+ filterUsers(e.target.value);
+ };
+
+ const filterUsers = (term: string) => {
+ if (!term.trim()) {
+ setUsers(sortedUsers);
+
+ return;
+ }
+
+ const filtered = sortedUsers.filter(user =>
+ `${user.firstName} ${user.lastName}`.toLowerCase().includes(term.toLowerCase())
+ );
+
+ setUsers(filtered);
+ };
+
+ /// users list logic end here
+
+ /// teams logic
+ const fetchTeams = async () => {
+ setLoading(true);
+ try {
+ const response = await getOrganizationListSummary();
+
+ const teamNames = response.content.map(org => ({
+ name: org.name,
+ id: org.identifier
+ }));
+
+ setTeams(teamNames.map(team => team.name));
+ setTeamsList(teamNames);
+ } catch (error) {
+ console.error('Error fetching teams:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchTeams();
+ fetchTeams();
+ }, []);
+ const filteredTeams = teams.filter(team => team.toLowerCase().includes(teamSearchTerm.toLowerCase()));
+
+ const sortedTeams = [...filteredTeams].sort((a, b) => {
+ if (teamSortField === 'name') {
+ return teamSortDirection ? a.localeCompare(b) : b.localeCompare(a);
+ }
+ return 0;
+ });
+ const handleSortToggle = () => {
+ setTeamSortDirection(prev => !prev);
+ };
+
+ // teams end here
+
+ const {
+ register: registerTeams,
+ handleSubmit: handleSubmitTeams,
+ control,
+ reset: resetTeams,
+ setError: setErrorTeams,
+ formState: { errors: errorsTeams }
+ } = useForm();
+
+ useEffect(() => {
+ getOrganizationListSummary().then(res => {
+ setteamOrganization(res.content);
+ });
+ }, []);
+
+ const submitTeamHandler = (formValues: TeamsRegisterValues) => {
+ const organizationData = {
+ ...formValues,
+ type: 'TEAM',
+ partOf: '19c9a023-fff3-4aa8-a355-999effadc4cc',
+ active: true
+ };
+ toast.promise(createOrganization(organizationData), {
+ pending: 'Loading...',
+ success: {
+ render({ data }: { data: OrganizationModel }) {
+ resetTeams();
+ fetchTeamsData();
+ return `Organization with id: ${data.identifier} created successfully.`;
+ }
+ }
+ });
+ };
+
+ const handleTeamSelection = (teamId: string) => {
+ if (teamId === '') {
+ setSelectedTeam('');
+ setSelectedTeamName('');
+ setAssignedUsers([]);
+ return;
+ }
+
+ const selectedTeamObj = teamsList.find(team => team.id === teamId);
+
+ if (selectedTeamObj) {
+ setSelectedTeam(teamId);
+ setSelectedTeamName(selectedTeamObj.name);
+ setAssignedUsers([]);
+
+ fetchMembers(teamId);
+ }
+ };
+ const fetchMembers = async (organizationId: string) => {
+ try {
+ const members = await getOrganizationMembers(organizationId);
+ setAssignedUsers(members);
+ } catch (error) {
+ console.error('Error fetching members:', error);
+ }
+ };
+
+ // Move member to Team
+
+ const moveToTeam = async (userId: string) => {
+ console.error('User ID or Team ID is missing');
+
+ const user = users.find(u => u.identifier === userId);
+
+ if (!selectedTeam) {
+ console.error('Error: selectedTeamId is missing or undefined');
+ return;
+ }
+
+ if (user) {
+ const member: MemberModel = {
+ identifier: user.identifier,
+ firstName: user.firstName,
+ lastName: user.lastName,
+ email: user.email,
+ username: user.username,
+ role: 'default_role'
+ };
+
+ try {
+ const response = await addUserToOrganization(selectedTeam, user.username);
+ console.log(response, 'the response of the move to team');
+
+ if (Array.isArray(response) && response.length > 0) {
+ setAssignedUsers(prevAssigned => [...prevAssigned, member]);
+
+ setUsers(prevUsers => prevUsers.filter(u => u.identifier !== userId));
+ fetchUsers();
+ fetchTeams();
+ fetchMembers(selectedTeam);
+ toast.success('User moved to team successfully!');
+ fetchTeamsData();
+
+ setSelectedAvailable(null);
+ } else {
+ toast.error('Failed to add user to team');
+ console.error('Failed to add user to team');
+ }
+ } catch (error) {
+ console.error('Error during API request:', error);
+ }
+ }
+ };
+
+ /// delete user from the team assigned
+
+ const handleDeleteUser = async () => {
+ const userToDelete = assignedUsers.find(u => u.identifier === selectedAssigned);
+
+ if (userToDelete) {
+ const username = userToDelete.username;
+ const organizationId = selectedTeam;
+
+ try {
+ const response = await deleteUserFromOrganization(organizationId, username);
+ console.log(response, 'the response of the delete user');
+
+ if (Array.isArray(response) && !response.some(user => user.identifier === selectedAssigned)) {
+ setAssignedUsers(prevAssigned => prevAssigned.filter(user => user.identifier !== selectedAssigned));
+
+ // setUsers(prevUsers => prevUsers.filter(user => user.identifier !== selectedAssigned));
+ fetchUsers();
+ fetchTeams();
+ fetchMembers(organizationId);
+ toast.success('User deleted from team successfully!');
+ fetchTeamsData();
+ setSelectedAssigned(null);
+ } else {
+ console.error('Failed to delete user');
+ }
+ } catch (error) {
+ toast.error('Failed to delete user');
+ console.error('Error during delete request:', error);
+ }
+ }
+ };
+ const rightArrowDisabled = !selectedTeam;
+ return (
+
+
+
Manage Users & Teams
+
+
+
+
+
+
+ setActiveTab('viewUsers')} className={style.viewItemMenu}>
+
+ View Users
+
+
+
+ setActiveTab('createUser')}
+ className={style.userItemMenu}
+ >
+
+ Create User
+
+
+
+ setActiveTab('manageTeams')}
+ className={style.manageItemMenu}
+ >
+
+ Manage Teams
+
+
+
+
+
+ {activeTab === 'createUser' && (
+
+
+ Username
+
+ {errors.username && {errors.username.message} }
+
+
+
+
+
+ First Name
+
+ {errors.firstname && (
+ {errors.firstname.message}
+ )}
+
+
+
+
+ Last Name
+
+ {errors.lastname && {errors.lastname.message} }
+
+
+
+
+
+ Password
+
+ {errors.password && {errors.password.message} }
+
+
+
+ Email
+
+
+
+
+ Security Groups
+
+
+
+
+ Teams
+
+
+
+
+
+ Submit
+
+
+ Cancel
+
+
+
+
+ )}
+ {activeTab === 'manageTeams' && (
+
+
+
+ setViewType(e.target.value)}>
+ Available Members
+ All Members
+
+
+
+ handleTeamSelection(e.target.value)}>
+ Select a Team
+ {teamsList.map((team, index) => (
+
+ {team.name}
+
+ ))}
+
+
+
+
+
+
+ {' '}
+
+ Teams name
+
+ {errorsTeams.name && (
+ {errorsTeams.name.message}
+ )}
+
+
+
+
+ Save
+
+
+
+
+
+
+
+ {viewType}
+
+ {viewType === 'Available Members'
+ ? users
+ .filter(user => !user.organizations.length)
+ .map(user => (
+ setSelectedAvailable(user.identifier)}
+ >
+ {user.firstName}{' '}
+ {user.lastName}
+
+ ))
+ : users.map(user => (
+ setSelectedAvailable(user.identifier)}
+ >
+ {user.firstName}{' '}
+ {user.lastName}
+
+ ))}
+
+
+
+
+ {
+ if (selectedAvailable) {
+ moveToTeam(selectedAvailable);
+ } else {
+ alert('No member or team selected. Please select both a member and a team.');
+ }
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+ {selectedTeamName ? `${selectedTeamName}'s Members` : 'Members'}
+
+
+ {assignedUsers.length > 0 ? (
+
+ {assignedUsers.map(user => (
+ setSelectedAssigned(user.identifier)}
+ >
+
+ {user.firstName} {user.lastName}
+
+ ))}
+
+ ) : (
+ No members available.
+ )}
+
+
+
+ )}
+ {activeTab === 'viewUsers' && (
+
+
+
+
Users
+
+
+ handleSortClick('firstName')}
+ />
+ {/* */}
+
+
+
+ {loading ? (
+
Loading...
+ ) : (
+
+
+ {users.map(user => (
+
+
+ {`${user.firstName} ${user.lastName}`}
+
+ ))}
+
+
+ )}
+
+
+
+
+
Teams
+
+
+ setTeamSearchTerm(e.target.value)}
+ className={style.searchInput}
+ />
+
+
+ {/*
*/}
+
+
+
+ {loading ? (
+ Loading...
+ ) : (
+
+ {filteredTeams.length > 0 ? (
+ sortedTeams.map((team, index) => (
+
+ {team}
+
+ ))
+ ) : (
+ No teams found
+ )}
+
+ )}
+
+
+ )}
+
+
+
+
+ );
+}
diff --git a/src/features/planSimulation/reportsAPI/reportsApi.ts b/src/features/planSimulation/reportsAPI/reportsApi.ts
new file mode 100644
index 00000000..23a8717f
--- /dev/null
+++ b/src/features/planSimulation/reportsAPI/reportsApi.ts
@@ -0,0 +1,6 @@
+import api from '../../../api/axios';
+
+export const getReportForLocation = async (planid: string, locationId: string): Promise => {
+ const data = await api.get(`/task-details?planId=${planid}&locationId=${locationId}`).then(response => response.data);
+ return data;
+};
diff --git a/src/features/reporting/components/PerformanceDashboard/PerformanceDetailsModal/PerformanceDetailsModal.tsx b/src/features/reporting/components/PerformanceDashboard/PerformanceDetailsModal/PerformanceDetailsModal.tsx
index 90552246..fed5d71a 100644
--- a/src/features/reporting/components/PerformanceDashboard/PerformanceDetailsModal/PerformanceDetailsModal.tsx
+++ b/src/features/reporting/components/PerformanceDashboard/PerformanceDetailsModal/PerformanceDetailsModal.tsx
@@ -16,7 +16,7 @@ const PerformanceDetailsModal = ({ closeHandler, data, title, darkMode }: Props)
centered
onHide={closeHandler}
backdrop="static"
- dialogClassName='modal-90w'
+ dialogClassName="modal-90w"
contentClassName={darkMode ? 'bg-dark' : 'bg-white'}
>
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index 72b21b0e..a4cd4ca9 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -21,7 +21,8 @@
"TagManagement": "Tag Management",
"MetaDataImport": "Metadata Import",
"Resource Planning": "Resource Planning",
- "dataProcessingProgress": "Data Processing Progress"
+ "dataProcessingProgress": "Data Processing Progress",
+ "CampaignManage": "Campaign Management"
},
"publicPage": {
"supportedBy": "supported by",
diff --git a/src/index.tsx b/src/index.tsx
index 52948ded..c6bad6f7 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -10,6 +10,7 @@ import { ReactKeycloakProvider } from '@react-keycloak/web';
import keycloak from './keycloak';
import 'mapbox-gl/dist/mapbox-gl.css';
import { KeycloakInitOptions } from 'keycloak-js';
+import { PolygonProvider } from './contexts/PolygonContext';
const initOptions: KeycloakInitOptions = {
pkceMethod: 'S256',
@@ -21,7 +22,9 @@ ReactDOM.render(
-
+
+
+
diff --git a/src/pages/Campaign/Campaign.tsx b/src/pages/Campaign/Campaign.tsx
new file mode 100644
index 00000000..c03a6203
--- /dev/null
+++ b/src/pages/Campaign/Campaign.tsx
@@ -0,0 +1,31 @@
+import { useTranslation } from 'react-i18next';
+import { Route, Routes } from 'react-router-dom';
+import AuthGuard from '../../components/AuthGuard';
+import { ErrorPage } from '../../components/pages';
+import PageWrapper from '../../components/PageWrapper';
+import { REVEAL_SIMULATION } from '../../constants';
+import Simulation from '../../features/planSimulation/components/Simulation';
+import Campaign_Management from '../../features/planSimulation/components/CampaignManagement';
+
+const Campaign = () => {
+ const { t } = useTranslation();
+
+ return (
+
+
+
+
+
+ }
+ />
+
+ } />
+
+
+ );
+};
+
+export default Campaign;
diff --git a/src/pages/Campaign/index.ts b/src/pages/Campaign/index.ts
new file mode 100644
index 00000000..47f73474
--- /dev/null
+++ b/src/pages/Campaign/index.ts
@@ -0,0 +1,6 @@
+
+import Campaign from './Campaign';
+
+
+
+export default Campaign;
\ No newline at end of file
diff --git a/src/pages/PlanSimulationPage/PlanSimulation.tsx b/src/pages/PlanSimulationPage/PlanSimulation.tsx
index c11fb155..c6604c8a 100644
--- a/src/pages/PlanSimulationPage/PlanSimulation.tsx
+++ b/src/pages/PlanSimulationPage/PlanSimulation.tsx
@@ -10,7 +10,7 @@ const PlanSimulation = () => {
const { t } = useTranslation();
return (
-
+
{
- // console.log(e);
- // });
-
let initParentData: PlanningParentLocationResponse = {
features: [],
type: 'FeatureCollection',
@@ -167,8 +166,16 @@ export const initMap = (
style: style,
center: center,
zoom: zoom,
+ logoPosition: 'top-right',
+ attributionControl: false,
doubleClickZoom: false
});
+ mapboxInstance.addControl(
+ new AttributionControl({
+ compact: true
+ }),
+ 'bottom-left'
+ );
mapboxInstance.addControl(
new GeolocateControl({
positionOptions: {
@@ -188,6 +195,7 @@ export const initMap = (
//initialize an empty top layer for all labels
//this layer is used to prevent labels getting behind fill and border layers on loading of locations
mapboxInstance.on('load', () => {
+ mapboxInstance.resize();
mapboxInstance.addSource('label-source', {
type: 'geojson',
data: {
@@ -707,7 +715,7 @@ export const loadChildren = (map: Map, id: string, planId: string, opacity: numb
});
};
-export const createLocationLabel = (map: Map, data: any, center: Feature) => {
+export const createLocationLabel = (map: Map, data: any, center: Feature, color?: string) => {
if (map.getSource(data.identifier + 'Label') === undefined) {
map.addSource(data.identifier + 'Label', {
type: 'geojson',
@@ -717,7 +725,8 @@ export const createLocationLabel = (map: Map, data: any, center: Feature