diff --git a/src/frontend/src/api/DataConflation.ts b/src/frontend/src/api/DataConflation.ts new file mode 100644 index 0000000000..37fca90805 --- /dev/null +++ b/src/frontend/src/api/DataConflation.ts @@ -0,0 +1,19 @@ +import axios from 'axios'; +import { DataConflationActions } from '@/store/slices/DataConflationSlice'; + +export const SubmissionConflationGeojsonService: Function = (url: string) => { + return async (dispatch) => { + const getSubmissionGeojsonConflation = async (url) => { + try { + dispatch(DataConflationActions.SetSubmissionConflationGeojsonLoading(true)); + const getSubmissionConflationGeojsonResponse = await axios.get(url); + dispatch(DataConflationActions.SetSubmissionConflationGeojson(getSubmissionConflationGeojsonResponse.data)); + dispatch(DataConflationActions.SetSubmissionConflationGeojsonLoading(false)); + } catch (error) { + dispatch(DataConflationActions.SetSubmissionConflationGeojsonLoading(false)); + } + }; + + await getSubmissionGeojsonConflation(url); + }; +}; diff --git a/src/frontend/src/api/ProjectTaskStatus.ts b/src/frontend/src/api/ProjectTaskStatus.ts index a87aff019a..09f0a008c8 100755 --- a/src/frontend/src/api/ProjectTaskStatus.ts +++ b/src/frontend/src/api/ProjectTaskStatus.ts @@ -4,41 +4,49 @@ import CoreModules from '@/shared/CoreModules'; import { CommonActions } from '@/store/slices/CommonSlice'; import { projectTaskBoundriesType } from '@/models/project/projectModel'; -const UpdateTaskStatus = ( +export const UpdateTaskStatus = ( url: string, - style: any, - existingData: projectTaskBoundriesType[], currentProjectId: string, - feature: Record, - taskId: number, + taskId: string, body: any, params: { project_id: string }, + style?: any, + existingData?: projectTaskBoundriesType[], + feature?: Record, ) => { return async (dispatch) => { - const updateTask = async (url: string, body: any, feature: Record, params: { project_id: string }) => { + const updateTask = async ( + url: string, + body: any, + params: { project_id: string }, + feature?: Record, + ) => { try { dispatch(CommonActions.SetLoading(true)); const response = await CoreModules.axios.post(url, body, { params }); dispatch(ProjectActions.UpdateProjectTaskActivity(response.data)); - await feature.setStyle(style); + if (feature && style) { + await feature.setStyle(style); - // assign userId to locked_by_user if status is locked_for_mapping or locked_for_validation - const prevProperties = feature.getProperties(); - const isTaskLocked = ['LOCKED_FOR_MAPPING', 'LOCKED_FOR_VALIDATION'].includes(response.data.status); - const updatedProperties = { ...prevProperties, locked_by_user: isTaskLocked ? body.id : null }; - feature.setProperties(updatedProperties); + // assign userId to locked_by_user if status is locked_for_mapping or locked_for_validation + const prevProperties = feature.getProperties(); + const isTaskLocked = ['LOCKED_FOR_MAPPING', 'LOCKED_FOR_VALIDATION'].includes(response.data.status); + const updatedProperties = { ...prevProperties, locked_by_user: isTaskLocked ? body.id : null }; + feature.setProperties(updatedProperties); + + dispatch( + ProjectActions.UpdateProjectTaskBoundries({ + projectId: currentProjectId, + taskId, + locked_by_uid: body?.id, + locked_by_username: body?.username, + task_status: response.data.status, + }), + ); + } - dispatch( - ProjectActions.UpdateProjectTaskBoundries({ - projectId: currentProjectId, - taskId, - locked_by_uid: body?.id, - locked_by_username: body?.username, - task_status: response.data.status, - }), - ); dispatch(CommonActions.SetLoading(false)); dispatch( HomeActions.SetSnackBar({ @@ -60,8 +68,6 @@ const UpdateTaskStatus = ( ); } }; - await updateTask(url, body, feature, params); + await updateTask(url, body, params, feature); }; }; - -export default UpdateTaskStatus; diff --git a/src/frontend/src/components/DataConflation/ConflationMap/MapLegend.tsx b/src/frontend/src/components/DataConflation/ConflationMap/MapLegend.tsx new file mode 100644 index 0000000000..bae2b5eec7 --- /dev/null +++ b/src/frontend/src/components/DataConflation/ConflationMap/MapLegend.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import Accordion from '@/components/common/Accordion'; +import useOutsideClick from '@/hooks/useOutsideClick'; + +const legendArray = [ + { name: 'No conflicts', color: '#40AC8C' }, + { name: 'Tag conflicts', color: '#DB9D35' }, + { name: 'Geometry conflicts', color: '#3C4A5E' }, + { name: 'Both conflicts', color: '#EB8B8B' }, +]; + +const MapLegends = () => { + return ( +
+ {legendArray?.map((legend) => ( +
+
+

{legend.name}

+
+ ))} +
+ ); +}; + +const MapLegend = () => { + const [legendRef, legendToggle, handleLegendToggle] = useOutsideClick(); + + return ( + <> + } + header={ +
+

Legend

+
+ } + onToggle={() => { + handleLegendToggle(); + }} + className="fmtm-py-0 !fmtm-pb-0 fmtm-rounded-lg hover:fmtm-bg-gray-50" + collapsed={!legendToggle} + /> + + ); +}; + +export default MapLegend; diff --git a/src/frontend/src/components/DataConflation/ConflationMap/index.tsx b/src/frontend/src/components/DataConflation/ConflationMap/index.tsx new file mode 100644 index 0000000000..423279d9cb --- /dev/null +++ b/src/frontend/src/components/DataConflation/ConflationMap/index.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { MapContainer as MapComponent, useOLMap } from '@/components/MapComponent/OpenLayersComponent'; +import LayerSwitcherControl from '@/components/MapComponent/OpenLayersComponent/LayerSwitcher/index'; +import MapLegend from '@/components/DataConflation/ConflationMap/MapLegend'; +import Button from '@/components/common/Button'; +import { useAppSelector } from '@/types/reduxTypes'; +import { VectorLayer } from '@/components/MapComponent/OpenLayersComponent/Layers'; +import { useDispatch } from 'react-redux'; +import { DataConflationActions } from '@/store/slices/DataConflationSlice'; + +const ConflationMap = () => { + const dispatch = useDispatch(); + + const submissionConflationGeojson = useAppSelector((state) => state.dataconflation.submissionConflationGeojson); + const submissionConflationGeojsonLoading = useAppSelector( + (state) => state.dataconflation.submissionConflationGeojsonLoading, + ); + + const { mapRef, map } = useOLMap({ + center: [0, 0], + zoom: 4, + }); + + return ( + <> + + { + dispatch( + DataConflationActions.SetSelectedFeatureOSMId( + feature?.getProperties()?.xid + ? feature.getProperties().xid + : feature.getProperties()?.osm_id.toString(), + ), + ); + }} + /> + +
+ +
+
+
+
+ + ); +}; + +export default ConflationMap; diff --git a/src/frontend/src/components/DataConflation/SubmissionConflation/MergeAttributes.tsx b/src/frontend/src/components/DataConflation/SubmissionConflation/MergeAttributes.tsx new file mode 100644 index 0000000000..cbedb3b618 --- /dev/null +++ b/src/frontend/src/components/DataConflation/SubmissionConflation/MergeAttributes.tsx @@ -0,0 +1,145 @@ +import React, { useState } from 'react'; +import { Modal } from '@/components/common/Modal'; +import Button from '@/components/common/Button'; +import Table, { TableHeader } from '@/components/common/CustomTable'; + +type mergeAttributesPropType = { + selectedConflateMethod: 'submission_feature' | 'osm_feature' | 'merge_attributes' | ''; + setSelectedConflateMethod: (value: '') => void; + submissionTags: Record; + osmTags: Record; +}; + +const MergeAttributes = ({ + selectedConflateMethod, + setSelectedConflateMethod, + submissionTags, + osmTags, +}: mergeAttributesPropType) => { + const [chosenAttribute, setChosenAttribute] = useState({}); + + const tableData: any = []; + for (const [key, value] of Object.entries(osmTags)) { + if (submissionTags?.[key] && submissionTags?.[key] !== value) { + tableData.push({ name: key, osm: value, submission: submissionTags?.[key] }); + } + } + + return ( + <> + Merge Data With OSM

} + description={ +
+
+ {}} + isLoading={false} + > +
{row?.name}
} + /> + ( +
{ + e.preventDefault(); + e.stopPropagation(); + setChosenAttribute((prev) => ({ ...prev, [row?.name]: row?.osm })); + }} + title={row?.osm} + > + + +
+ )} + /> + ( +
{ + e.preventDefault(); + e.stopPropagation(); + setChosenAttribute((prev) => ({ ...prev, [row?.name]: row?.submission })); + }} + title={row?.submission} + > + + +
+ )} + /> +
+
+
+
+
+ } + open={selectedConflateMethod === 'merge_attributes'} + onOpenChange={() => setSelectedConflateMethod('')} + className="fmtm-max-w-[70rem] fmtm-rounded-xl" + /> + + ); +}; + +export default MergeAttributes; diff --git a/src/frontend/src/components/DataConflation/SubmissionConflation/index.tsx b/src/frontend/src/components/DataConflation/SubmissionConflation/index.tsx new file mode 100644 index 0000000000..63ff235ac6 --- /dev/null +++ b/src/frontend/src/components/DataConflation/SubmissionConflation/index.tsx @@ -0,0 +1,139 @@ +import React, { useState } from 'react'; +import Button from '@/components/common/Button'; +import AssetModules from '@/shared/AssetModules'; +import MergeAttributes from '@/components/DataConflation/SubmissionConflation/MergeAttributes'; +import CoreModules from '@/shared/CoreModules'; +import { useAppSelector } from '@/types/reduxTypes'; + +const TagsSkeleton = () => ( + <> + {Array.from({ length: 6 }).map((_, index) => ( +
+ + +
+ ))} + +); + +const RenderTags = ({ tag }: { tag: [string, any] }) => ( +
+

{tag?.[0]}

+

{tag?.[1]}

+
+); + +const SubmissionConflation = () => { + const [selectedConflateMethod, setSelectedConflateMethod] = useState< + 'submission_feature' | 'osm_feature' | 'merge_attributes' | '' + >(''); + + const submissionConflationGeojson = useAppSelector((state) => state.dataconflation.submissionConflationGeojson); + const selectedFeatureOSMId = useAppSelector((state) => state.dataconflation.selectedFeatureOSMId); + const submissionConflationGeojsonLoading = useAppSelector( + (state) => state.dataconflation.submissionConflationGeojsonLoading, + ); + + const selectedFeature = submissionConflationGeojson?.features?.find( + (feature) => feature.properties?.xid === selectedFeatureOSMId, + ); + const filteredSubmissionTags = {}; + for (const [key, value] of Object.entries(selectedFeature?.properties)) { + if (value !== null) { + filteredSubmissionTags[key] = value; + } + } + + return ( + <> + +
+
+
+

SUBMISSION #457

+
+ {submissionConflationGeojsonLoading ? ( + + ) : filteredSubmissionTags ? ( + <> + {Object.entries(filteredSubmissionTags).map((tag) => { + const [key, value] = tag; + if (value) return ; + })} + + ) : ( + <> + )} +
+
+ +
+

OSM TAGS

+
+ {submissionConflationGeojsonLoading ? ( + + ) : selectedFeature?.tags ? ( + <> + {Object.entries(selectedFeature?.tags).map((tag) => ( + + ))} + + ) : ( + <> + )} +
+
+ +
+
+
+
+ + ); +}; + +export default SubmissionConflation; diff --git a/src/frontend/src/components/DataConflation/TaskInfo.tsx b/src/frontend/src/components/DataConflation/TaskInfo.tsx new file mode 100644 index 0000000000..641b239dc8 --- /dev/null +++ b/src/frontend/src/components/DataConflation/TaskInfo.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import Button from '@/components/common/Button'; +import AssetModules from '@/shared/AssetModules'; +import CoreModules from '@/shared/CoreModules'; +import { useAppSelector } from '@/types/reduxTypes'; + +const TaskInfo = () => { + const navigate = useNavigate(); + const { taskId, projectId } = useParams(); + + const submissionConflationGeojson = useAppSelector((state) => state.dataconflation.submissionConflationGeojson); + const submissionConflationGeojsonLoading = useAppSelector( + (state) => state.dataconflation.submissionConflationGeojsonLoading, + ); + const submissionFeatures = submissionConflationGeojson?.features; + + const featureCount = submissionFeatures?.length || 0; + let geometryConflictCount = 0; + let tagConflictCount = 0; + let noTagConflictCount = 0; + + submissionFeatures?.map((feature) => { + if (!feature) return; + + // geom conflict count + if (feature?.properties?.overlap_percent < 90) { + geometryConflictCount += 1; + } + + if (!feature?.tags) return; + let tagConflict = false; + let noConflict = false; + + // tag conflict count + for (const [key, value] of Object.entries(feature?.tags)) { + if (feature?.properties?.[key] && feature?.properties?.[key] !== value) { + tagConflict = true; + } else if (feature?.properties?.overlap_percent > 90) { + noConflict = true; + } + } + if (tagConflict) tagConflictCount += 1; + if (noConflict) noTagConflictCount += 1; + }); + + const taskInfoConstants = [ + { name: 'Total Feature', count: featureCount - geometryConflictCount }, + { name: 'Number of geometry conflicts', count: geometryConflictCount }, + { name: 'Number of tag conflicts', count: tagConflictCount }, + { name: 'No conflicts', count: noTagConflictCount }, + ]; + + return ( +
+
navigate(`/project/${projectId}`)} + className="fmtm-flex fmtm-items-center fmtm-mb-5 fmtm-cursor-pointer hover:fmtm-text-primaryRed fmtm-duration-300 fmtm-w-fit" + > + +

BACK

+
+
+
+

Task #{taskId}

+ +
+ {submissionConflationGeojsonLoading ? ( + <> + {Array.from({ length: 4 }).map((_, index) => ( + + ))} + + ) : ( + + {taskInfoConstants?.map((info) => ( + + + + + + ))} +
{info?.name}:{info?.count}
+ )} +
+
+ +
+
+
+
+ ); +}; + +export default TaskInfo; diff --git a/src/frontend/src/components/DialogTaskActions.tsx b/src/frontend/src/components/DialogTaskActions.tsx index cd5288c36c..e9e7d7fbeb 100755 --- a/src/frontend/src/components/DialogTaskActions.tsx +++ b/src/frontend/src/components/DialogTaskActions.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; import environment from '@/environment'; -import ProjectTaskStatus from '@/api/ProjectTaskStatus'; +import { UpdateTaskStatus } from '@/api/ProjectTaskStatus'; import MapStyles from '@/hooks/MapStyles'; import CoreModules from '@/shared/CoreModules'; import { CommonActions } from '@/store/slices/CommonSlice'; @@ -77,24 +77,32 @@ export default function Dialog({ taskId, feature }: dialogPropType) { } }, [projectTaskActivityList, taskId, feature]); - const handleOnClick = (event) => { - const status = taskStatusEnum[event.currentTarget.dataset.btnid]; + const handleOnClick = async (event: React.MouseEvent) => { + const btnId = event.currentTarget.dataset.btnid; + if (!btnId) return; + const status = taskStatusEnum[btnId]; const authDetailsCopy = authDetails != null ? { ...authDetails } : {}; - const geoStyle = geojsonStyles[event.currentTarget.dataset.btnid]; - if (event.currentTarget.dataset.btnid != undefined) { + const geoStyle = geojsonStyles[btnId]; + if (btnId != undefined) { if (authDetailsCopy.hasOwnProperty('id')) { - dispatch( - ProjectTaskStatus( + // if (btnId === 'MERGE_WITH_OSM') { + // navigate(`/conflate-data/${currentProjectId}/${taskId}`); + // return; + // } + await dispatch( + UpdateTaskStatus( `${import.meta.env.VITE_API_URL}/tasks/${currentStatus?.id}/new-status/${status}`, - geoStyle, - taskBoundaryData, currentProjectId, - feature, - taskId, + taskId.toString(), authDetailsCopy, { project_id: currentProjectId }, + geoStyle, + taskBoundaryData, + feature, ), ); + if (btnId === 'LOCKED_FOR_VALIDATION') + navigate(`/project-submissions/${params.id}?tab=table&task_id=${taskId}`); } else { dispatch( CommonActions.SetSnackBar({ diff --git a/src/frontend/src/components/ProjectDetailsV2/FeatureSelectionPopup.tsx b/src/frontend/src/components/ProjectDetailsV2/FeatureSelectionPopup.tsx index 55ec04733e..c316488ccf 100644 --- a/src/frontend/src/components/ProjectDetailsV2/FeatureSelectionPopup.tsx +++ b/src/frontend/src/components/ProjectDetailsV2/FeatureSelectionPopup.tsx @@ -10,7 +10,7 @@ import environment from '@/environment'; import { useParams } from 'react-router-dom'; import { UpdateEntityStatus } from '@/api/Project'; import { TaskFeatureSelectionProperties } from '@/store/types/ITask'; -import ProjectTaskStatus from '@/api/ProjectTaskStatus'; +import { UpdateTaskStatus } from '@/api/ProjectTaskStatus'; import MapStyles from '@/hooks/MapStyles'; type TaskFeatureSelectionPopupPropType = { @@ -28,7 +28,7 @@ const TaskFeatureSelectionPopup = ({ featureProperties, taskId, taskFeature }: T const entityOsmMap = CoreModules.useAppSelector((state) => state.project.entityOsmMap); const authDetails = CoreModules.useAppSelector((state) => state.login.authDetails); - const currentProjectId = params.id; + const currentProjectId = params.id || ''; const [task_status, set_task_status] = useState('READY'); const projectData = CoreModules.useAppSelector((state) => state.project.projectTaskBoundries); const projectIndex = projectData.findIndex((project) => project.id == currentProjectId); @@ -134,15 +134,15 @@ const TaskFeatureSelectionPopup = ({ featureProperties, taskId, taskFeature }: T if (task_status === 'READY') { dispatch( - ProjectTaskStatus( + UpdateTaskStatus( `${import.meta.env.VITE_API_URL}/tasks/${currentTaskInfo?.id}/new-status/1`, - geoStyle, - taskBoundaryData, currentProjectId, - taskFeature, - taskId, + taskId.toString(), authDetails, { project_id: currentProjectId }, + geoStyle, + taskBoundaryData, + taskFeature, ), ); } diff --git a/src/frontend/src/components/ProjectSubmissions/SubmissionsTable.tsx b/src/frontend/src/components/ProjectSubmissions/SubmissionsTable.tsx index a81a041874..482a1905e2 100644 --- a/src/frontend/src/components/ProjectSubmissions/SubmissionsTable.tsx +++ b/src/frontend/src/components/ProjectSubmissions/SubmissionsTable.tsx @@ -21,6 +21,7 @@ import UpdateReviewStatusModal from '@/components/ProjectSubmissions/UpdateRevie import { useAppSelector } from '@/types/reduxTypes'; import { camelToFlat } from '@/utilfunctions/commonUtils'; import useDocumentTitle from '@/utilfunctions/useDocumentTitle'; +import { UpdateTaskStatus } from '@/api/ProjectTaskStatus'; import { filterType } from '@/store/types/ISubmissions'; const SubmissionsTable = ({ toggleView }) => { @@ -28,7 +29,7 @@ const SubmissionsTable = ({ toggleView }) => { const [searchParams, setSearchParams] = useSearchParams(); const initialFilterState: filterType = { - task_id: searchParams.get('task_id') ? searchParams?.get('task_id') : null, + task_id: searchParams.get('task_id') ? searchParams?.get('task_id') || null : null, submitted_by: searchParams.get('submitted_by'), review_state: searchParams.get('review_state'), submitted_date: searchParams.get('submitted_date'), @@ -50,9 +51,18 @@ const SubmissionsTable = ({ toggleView }) => { const projectInfo = useAppSelector((state) => state.project.projectInfo); const josmEditorError = useAppSelector((state) => state.task.josmEditorError); const downloadSubmissionLoading = useAppSelector((state) => state.task.downloadSubmissionLoading); - const projectTaskBoundries = useAppSelector((state) => state.project.projectTaskBoundries); - const projectIndex = projectTaskBoundries.findIndex((project) => project.id == +projectId); - const taskList = projectTaskBoundries[projectIndex]?.taskBoundries; + const authDetails = CoreModules.useAppSelector((state) => state.login.authDetails); + const updateTaskStatusLoading = useAppSelector((state) => state.common.loading); + + const projectData = useAppSelector((state) => state.project.projectTaskBoundries); + const projectIndex = projectData.findIndex((project) => project.id == +projectId); + const taskBoundaryData = useAppSelector((state) => state.project.projectTaskBoundries); + const currentStatus = { + ...taskBoundaryData?.[projectIndex]?.taskBoundries?.filter((task) => { + return task?.index === +filter.task_id; + })?.[0], + }; + const taskList = projectData[projectIndex]?.taskBoundries; const [numberOfFilters, setNumberOfFilters] = useState(0); const [paginationPage, setPaginationPage] = useState(1); @@ -150,7 +160,7 @@ const SubmissionsTable = ({ toggleView }) => { const clearFilters = () => { setSearchParams({ tab: 'table' }); - setFilter({ task_id: null, submitted_by: null, review_state: null, submitted_date: null }); + setFilter({ task_id: '', submitted_by: null, review_state: null, submitted_date: null }); }; function getValueByPath(obj: any, path: string) { @@ -205,6 +215,19 @@ const SubmissionsTable = ({ toggleView }) => { } }; + const handleTaskMap = async () => { + await dispatch( + UpdateTaskStatus( + `${import.meta.env.VITE_API_URL}/tasks/${currentStatus.id}/new-status/4`, + projectId, + filter?.task_id || '', + authDetails || {}, + { project_id: projectId }, + ), + ); + navigate(`/project/${projectId}`); + }; + useEffect(() => { const filteredParams = filterParams(filter); setSearchParams({ tab: 'table', ...filteredParams }); @@ -228,7 +251,7 @@ const SubmissionsTable = ({ toggleView }) => { }} /> -
+
{ +
- {toggleView}
diff --git a/src/frontend/src/components/common/Button.tsx b/src/frontend/src/components/common/Button.tsx index f916ac273f..8f10befa02 100644 --- a/src/frontend/src/components/common/Button.tsx +++ b/src/frontend/src/components/common/Button.tsx @@ -52,7 +52,7 @@ const Button = ({ data-testid="test-button" type={type === 'submit' ? 'submit' : 'button'} onClick={onClick} - className={`fmtm-text-lg fmtm-group fmtm-flex fmtm-items-center fmtm-gap-2 ${btnStyle( + className={`fmtm-text-lg fmtm-group fmtm-flex fmtm-items-center fmtm-gap-2 fmtm-outline-none ${btnStyle( isLoading || disabled ? 'disabled' : btnType, className, )}`} diff --git a/src/frontend/src/components/common/CustomTable.tsx b/src/frontend/src/components/common/CustomTable.tsx index 0a3e6a0d31..e4411fb976 100644 --- a/src/frontend/src/components/common/CustomTable.tsx +++ b/src/frontend/src/components/common/CustomTable.tsx @@ -244,7 +244,9 @@ export default class Table extends Component { style: { cursor: 'pointer' }, })} className={`${trClassName && trClassName(row)} ${ - flag?.toLowerCase() === 'primarytable' ? 'hover:fmtm-bg-[#F2E3E3]' : '' + flag?.toLowerCase() === 'primarytable' + ? `${(index + 1) % 2 === 0 ? '!fmtm-bg-[#F3F3F3]' : 'fmtm-bg-white'}` + : '' } fmtm-cursor-pointer fmtm-ease-in fmtm-duration-100 fmtm-h-[50px] fmtm-items-baseline fmtm-relative fmtm-bg-white`} > diff --git a/src/frontend/src/components/common/Modal.tsx b/src/frontend/src/components/common/Modal.tsx index 520d18fb95..e656117868 100644 --- a/src/frontend/src/components/common/Modal.tsx +++ b/src/frontend/src/components/common/Modal.tsx @@ -99,7 +99,7 @@ DialogDescription.displayName = DialogPrimitive.Description.displayName; const Modal = ({ dialogOpen, title, description, open, onOpenChange, className }: IModalProps) => { return ( - {dialogOpen} + {dialogOpen && {dialogOpen}} {title} diff --git a/src/frontend/src/environment.ts b/src/frontend/src/environment.ts index a1575d4e0a..2268422ed5 100755 --- a/src/frontend/src/environment.ts +++ b/src/frontend/src/environment.ts @@ -36,7 +36,7 @@ export default { { key: 'Mapping Needed', value: 'INVALIDATED', btnBG: 'transparent' }, ], }, - { label: 'VALIDATED', action: [] }, + // { label: 'VALIDATED', action: [{ key: 'Merge data with OSM', value: 'MERGE_WITH_OSM', btnBG: 'gray' }] }, { label: 'INVALIDATED', action: [{ key: 'Map Again', value: 'LOCKED_FOR_MAPPING', btnBG: 'gray' }] }, { label: 'BAD', action: [] }, // "SPLIT", diff --git a/src/frontend/src/routes.jsx b/src/frontend/src/routes.jsx index 176b3967a5..3f28e6bb46 100755 --- a/src/frontend/src/routes.jsx +++ b/src/frontend/src/routes.jsx @@ -15,6 +15,7 @@ import ErrorBoundary from '@/views/ErrorBoundary'; import ProjectDetailsV2 from '@/views/ProjectDetailsV2'; import ProjectSubmissions from '@/views/ProjectSubmissions'; import ManageProject from '@/views/ManageProject'; +import DataConflation from '@/views/DataConflation'; const routes = createBrowserRouter([ { @@ -206,6 +207,18 @@ const routes = createBrowserRouter([ ), }, + { + path: '/conflate-data/:projectId/:taskId', + element: ( + + Loading...}> + + + + + + ), + }, { path: '*', element: , diff --git a/src/frontend/src/store/Store.ts b/src/frontend/src/store/Store.ts index e3c55b70d0..e972e0019d 100755 --- a/src/frontend/src/store/Store.ts +++ b/src/frontend/src/store/Store.ts @@ -9,6 +9,7 @@ import LoginSlice from '@/store/slices/LoginSlice'; import OrganisationSlice from '@/store/slices/organisationSlice'; import SubmissionSlice from '@/store/slices/SubmissionSlice'; import TaskSlice from '@/store/slices/TaskSlice'; +import DataConflationSlice from '@/store/slices/DataConflationSlice'; import { persistReducer } from 'redux-persist'; import { combineReducers, configureStore } from '@reduxjs/toolkit'; @@ -36,6 +37,7 @@ const rootReducer = combineReducers({ common: CommonSlice.reducer, submission: SubmissionSlice.reducer, task: TaskSlice.reducer, + dataconflation: DataConflationSlice.reducer, }); // Infer the `RootState` and `AppDispatch` types from the store itself export type RootState = ReturnType; diff --git a/src/frontend/src/store/slices/DataConflationSlice.ts b/src/frontend/src/store/slices/DataConflationSlice.ts new file mode 100644 index 0000000000..95ec6f4b36 --- /dev/null +++ b/src/frontend/src/store/slices/DataConflationSlice.ts @@ -0,0 +1,27 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { DataConflationStateTypes } from '@/store/types/IDataConflation'; + +const initialState: DataConflationStateTypes = { + submissionConflationGeojsonLoading: false, + submissionConflationGeojson: null, + selectedFeatureOSMId: null, +}; + +const DataConflationSlice = createSlice({ + name: 'dataconflation', + initialState: initialState, + reducers: { + SetSubmissionConflationGeojsonLoading(state, action) { + state.submissionConflationGeojsonLoading = action.payload; + }, + SetSubmissionConflationGeojson(state, action) { + state.submissionConflationGeojson = action.payload; + }, + SetSelectedFeatureOSMId(state, action) { + state.selectedFeatureOSMId = action.payload; + }, + }, +}); + +export const DataConflationActions = DataConflationSlice.actions; +export default DataConflationSlice; diff --git a/src/frontend/src/store/types/IDataConflation.ts b/src/frontend/src/store/types/IDataConflation.ts new file mode 100644 index 0000000000..052e76df77 --- /dev/null +++ b/src/frontend/src/store/types/IDataConflation.ts @@ -0,0 +1,5 @@ +export type DataConflationStateTypes = { + submissionConflationGeojsonLoading: boolean; + submissionConflationGeojson: Record | null; + selectedFeatureOSMId: string | null; +}; diff --git a/src/frontend/src/views/DataConflation.tsx b/src/frontend/src/views/DataConflation.tsx new file mode 100644 index 0000000000..c586c3f81d --- /dev/null +++ b/src/frontend/src/views/DataConflation.tsx @@ -0,0 +1,50 @@ +import React, { useEffect } from 'react'; +import ConflationMap from '@/components/DataConflation/ConflationMap'; +import TaskInfo from '@/components/DataConflation/TaskInfo'; +import SubmissionConflation from '@/components/DataConflation/SubmissionConflation'; +import { useDispatch } from 'react-redux'; +import { SubmissionConflationGeojsonService } from '@/api/DataConflation'; +import { useParams } from 'react-router-dom'; +import { useAppSelector } from '@/types/reduxTypes'; + +const DataConflation = () => { + const dispatch = useDispatch(); + const { projectId, taskId } = useParams(); + const selectedFeatureOSMId = useAppSelector((state) => state.dataconflation.selectedFeatureOSMId); + + useEffect(() => { + dispatch( + SubmissionConflationGeojsonService( + `${ + import.meta.env.VITE_API_URL + }/submission/conflate-submission-geojson/?project_id=${projectId}&task_id=${taskId}`, + ), + ); + }, []); + + return ( +
+
+
+ +
+
+
+ +
+ {selectedFeatureOSMId && ( +
+ +
+ )} +
+
+
+ ); +}; + +export default DataConflation;