diff --git a/src/frontend/src/api/projects.ts b/src/frontend/src/api/projects.ts index 77303366..f3c613f1 100644 --- a/src/frontend/src/api/projects.ts +++ b/src/frontend/src/api/projects.ts @@ -1,6 +1,7 @@ /* eslint-disable import/prefer-default-export */ import { UseQueryOptions, useQuery } from '@tanstack/react-query'; import { getProjectsList, getProjectDetail } from '@Services/createproject'; +import { getTaskStates } from '@Services/project'; import { getUserProfileInfo } from '@Services/common'; export const useGetProjectsListQuery = ( @@ -27,6 +28,19 @@ export const useGetProjectsDetailQuery = ( }); }; +export const useGetTaskStatesQuery = ( + projectId: string, + queryOptions?: Partial, +) => { + return useQuery({ + queryKey: ['project-task-states'], + queryFn: () => getTaskStates(projectId), + select: (res: any) => res.data, + enabled: !!projectId, + ...queryOptions, + }); +}; + export const useGetUserDetailsQuery = ( queryOptions?: Partial, ) => { diff --git a/src/frontend/src/components/IndividualProject/MapSection/index.tsx b/src/frontend/src/components/IndividualProject/MapSection/index.tsx index 615c2c67..fbca9d8a 100644 --- a/src/frontend/src/components/IndividualProject/MapSection/index.tsx +++ b/src/frontend/src/components/IndividualProject/MapSection/index.tsx @@ -1,14 +1,29 @@ -import { useEffect } from 'react'; -import { useTypedSelector } from '@Store/hooks'; +/* eslint-disable no-unused-vars */ +import { useParams } from 'react-router-dom'; +import { useCallback, useEffect, useState } from 'react'; +import { useTypedSelector, useTypedDispatch } from '@Store/hooks'; import { useMapLibreGLMap } from '@Components/common/MapLibreComponents'; import VectorLayer from '@Components/common/MapLibreComponents/Layers/VectorLayer'; import MapContainer from '@Components/common/MapLibreComponents/MapContainer'; import BaseLayerSwitcher from '@Components/common/MapLibreComponents/BaseLayerSwitcher'; -import { LngLatBoundsLike, Map } from 'maplibre-gl'; +import { GeojsonType } from '@Components/common/MapLibreComponents/types'; +import AsyncPopup from '@Components/common/MapLibreComponents/AsyncPopup'; import getBbox from '@turf/bbox'; import { FeatureCollection } from 'geojson'; +import { LngLatBoundsLike, Map } from 'maplibre-gl'; +import PopupUI from '@Components/common/MapLibreComponents/PopupUI'; +import { setProjectState } from '@Store/actions/project'; +import { useGetTaskStatesQuery } from '@Api/projects'; export default function MapSection() { + const { id } = useParams(); + const dispatch = useTypedDispatch(); + + const [tasksBoundaryLayer, setTasksBoundaryLayer] = useState | null>(null); + const { map, isMapLoaded } = useMapLibreGLMap({ mapOptions: { zoom: 5, @@ -18,14 +33,66 @@ export default function MapSection() { disableRotation: true, }); - const tasksGeojson = useTypedSelector(state => state.project.tasksGeojson); - const projectArea = useTypedSelector(state => state.project.projectArea); + const selectedTaskId = useTypedSelector( + state => state.project.selectedTaskId, + ); + const tasksData = useTypedSelector(state => state.project.tasksData); + + const { data: taskStates } = useGetTaskStatesQuery(id as string, { + enabled: !!tasksData, + }); + + // create combined geojson from individual tasks from the API + useEffect(() => { + if (!map || !tasksData) return; + + // @ts-ignore + const taskStatus: Record = taskStates?.reduce( + (acc: Record, task: Record) => { + acc[task.task_id] = task.state; + return acc; + }, + {}, + ); + const features = tasksData?.map(taskObj => { + return { + type: 'Feature', + geometry: { ...taskObj.outline_geojson.geometry }, + properties: { + ...taskObj.outline_geojson.properties, + state: taskStatus?.[`${taskObj.id}`] || null, + }, + }; + }); + const taskBoundariesFeatcol = { + type: 'FeatureCollection', + SRID: { + type: 'name', + properties: { + name: 'EPSG:3857', + }, + }, + features, + }; + setTasksBoundaryLayer(taskBoundariesFeatcol); + }, [map, taskStates, tasksData]); + // zoom to layer in the project area useEffect(() => { - if (!projectArea) return; - const bbox = getBbox(projectArea as FeatureCollection); + if (!tasksBoundaryLayer) return; + const bbox = getBbox(tasksBoundaryLayer as FeatureCollection); map?.fitBounds(bbox as LngLatBoundsLike, { padding: 25 }); - }, [map, projectArea]); + }, [map, tasksBoundaryLayer]); + + const getPopUpButtonText = (taskState: string) => { + if (taskState === 'UNLOCKED_FOR_MAP') return 'Request for Mapping'; + if (taskState === '') return ''; + return 'nothing'; + }; + + const getPopupUI = useCallback((properties: Record) => { + return
This task is available for mapping
; + }, []); return ( - {tasksGeojson?.map(singleTask => ( + {tasksBoundaryLayer && ( - ))} + )} + ) => { + dispatch(setProjectState({ selectedTaskId: properties.id })); + }} + buttonText="Lock Task" + /> ); diff --git a/src/frontend/src/components/common/MapLibreComponents/AsyncPopup/index.tsx b/src/frontend/src/components/common/MapLibreComponents/AsyncPopup/index.tsx index 4e8de814..e51aea64 100644 --- a/src/frontend/src/components/common/MapLibreComponents/AsyncPopup/index.tsx +++ b/src/frontend/src/components/common/MapLibreComponents/AsyncPopup/index.tsx @@ -4,6 +4,8 @@ import { useEffect, useRef, useState } from 'react'; import { renderToString } from 'react-dom/server'; import { Popup } from 'maplibre-gl'; import type { MapMouseEvent } from 'maplibre-gl'; +import 'maplibre-gl/dist/maplibre-gl.css'; +import '@Components/common/MapLibreComponents/map.css'; import { Button } from '@Components/RadixComponents/Button'; import Skeleton from '@Components/RadixComponents/Skeleton'; import { IAsyncPopup } from '../types'; @@ -21,6 +23,7 @@ export default function AsyncPopup({ handleBtnClick, isLoading = false, onClose, + buttonText = 'View More', }: IAsyncPopup) { const [properties, setProperties] = useState | null>( null, @@ -70,7 +73,7 @@ export default function AsyncPopup({ {isLoading ? ( ) : ( -

{title}

+

{title}

)}
{!isLoading && ( -
+
)} diff --git a/src/frontend/src/components/common/MapLibreComponents/PopupUI/index.tsx b/src/frontend/src/components/common/MapLibreComponents/PopupUI/index.tsx index 1fce6555..70a42a80 100644 --- a/src/frontend/src/components/common/MapLibreComponents/PopupUI/index.tsx +++ b/src/frontend/src/components/common/MapLibreComponents/PopupUI/index.tsx @@ -35,7 +35,7 @@ export default function PopupUI({ data = {} }: IPopupUIProps) { ); return ( -
    +
      {popupData && Object.keys(popupData).map(key => (
    • ) => void; isLoading?: boolean; onClose?: () => void; + buttonText?: string; } export type DrawModeTypes = DrawMode | null | undefined; diff --git a/src/frontend/src/services/project.ts b/src/frontend/src/services/project.ts new file mode 100644 index 00000000..ea173685 --- /dev/null +++ b/src/frontend/src/services/project.ts @@ -0,0 +1,11 @@ +/* eslint-disable import/prefer-default-export */ +import { authenticated, api } from '.'; + +export const getTaskStates = (projectId: string) => + api.get(`/tasks/states/${projectId}`); + +export const postTaskStatus = ( + projectId: string, + taskId: string, + data: Record, +) => authenticated(api).post(`/tasks/event/${projectId}/${taskId}`, data); diff --git a/src/frontend/src/store/slices/project.ts b/src/frontend/src/store/slices/project.ts index 714d0de4..d82d922c 100644 --- a/src/frontend/src/store/slices/project.ts +++ b/src/frontend/src/store/slices/project.ts @@ -4,14 +4,16 @@ import persist from '@Store/persist'; export interface ProjectState { individualProjectActiveTab: string; - tasksGeojson: Record[] | null; + tasksData: Record[] | null; projectArea: Record | null; + selectedTaskId: string; } const initialState: ProjectState = { individualProjectActiveTab: 'tasks', - tasksGeojson: null, + tasksData: null, projectArea: null, + selectedTaskId: '', }; const setProjectState: CaseReducer< diff --git a/src/frontend/src/views/IndividualProject/index.tsx b/src/frontend/src/views/IndividualProject/index.tsx index 4aae0f7c..2cbaf8ac 100644 --- a/src/frontend/src/views/IndividualProject/index.tsx +++ b/src/frontend/src/views/IndividualProject/index.tsx @@ -46,7 +46,7 @@ export default function IndividualProject() { onSuccess: (res: any) => dispatch( setProjectState({ - tasksGeojson: res.tasks, + tasksData: res.tasks, projectArea: res.outline_geojson, }), ),