From 3bba1787f19aeb15cf70c9403c446a10b405c08d Mon Sep 17 00:00:00 2001 From: ardhi Date: Tue, 14 Feb 2023 01:43:52 +0700 Subject: [PATCH] WIP: add user history tracking on Map Monitoring --- src/appredux/modules/map/actions.js | 88 ++++++++++++++++++ src/appredux/modules/map/reducers.js | 12 ++- src/components/MapLeftContent/index.js | 87 +++++++++--------- src/components/MapRightContent/index.js | 73 ++++++++------- src/components/PopupButtonActions/index.js | 32 +++++++ src/components/RoutingBarV2/RoutingBarV2.css | 77 ++++++++++++++++ src/components/RoutingBarV2/index.js | 89 +++++++++++++++++++ .../api/modules/map_monitoring/index.js | 45 ++++++++++ src/views/MapMonitoring/MapMonitoring.css | 9 ++ src/views/MapMonitoring/index.js | 55 ++++++++++-- 10 files changed, 475 insertions(+), 92 deletions(-) create mode 100644 src/components/PopupButtonActions/index.js create mode 100644 src/components/RoutingBarV2/RoutingBarV2.css create mode 100644 src/components/RoutingBarV2/index.js create mode 100644 src/views/MapMonitoring/MapMonitoring.css diff --git a/src/appredux/modules/map/actions.js b/src/appredux/modules/map/actions.js index 72eca0e..42d9f5f 100644 --- a/src/appredux/modules/map/actions.js +++ b/src/appredux/modules/map/actions.js @@ -1,4 +1,5 @@ import moment from "moment"; +import { toast } from "react-toastify"; import ApiMapMonitoring from "../../../services/api/modules/map_monitoring"; import { store } from "../../store"; @@ -10,6 +11,8 @@ export const SET_USER_HISTORY = 'SET_USER_HISTORY'; export const SET_PROJECT_TREE = 'SET_PROJECT_TREE'; export const SET_USER_POINTS = 'SET_USER_POINTS'; export const SET_SELECTED_FEATURE = 'SET_SELECTED_FEATURE'; +export const SET_ROUTINGBAR_VISIBLE = 'SET_ROUTINGBAR_VISIBLE'; +export const SET_IS_SEARCHING_ROUTE = 'SET_IS_SEARCHING_ROUTE'; export const setMymap = obj => dispatch => { dispatch({ @@ -67,6 +70,22 @@ export const setSelectedFeature = obj => dispatch => { }) } +export const setRoutingBarVisible = obj => dispatch => { + dispatch({ + type: SET_ROUTINGBAR_VISIBLE, + payload: obj + }) +} + +export const setIsSearchingRoute = obj => dispatch => { + dispatch({ + type: SET_IS_SEARCHING_ROUTE, + payload: obj + }) +} + + + export const getUserPoints = async () => { let userPoints = await ApiMapMonitoring.search(); // console.log('userPoints', userPoints); @@ -84,6 +103,7 @@ export const getUserPoints = async () => { } feature.properties = { + "user_id": n.user_id, "Name": n.join_first_name ? n.join_first_name : '-', "Clock in time": n.clock_in ? moment(n.clock_in).format('YYYY-MM-DD HH:mm:ss') : '-', "Clock in location": n.clock_in_loc ? n.clock_in_loc : '-', @@ -101,4 +121,72 @@ export const getUserPoints = async () => { store.dispatch(setUserPoints(featureCollection)); } +} + +export const getUserHistory = async (userId, dateString) => { + console.log('[map->action.js] getUserHistory', userId, dateString); + + store.dispatch(setIsSearchingRoute(true)); + let userHistory = await ApiMapMonitoring.searchUserHistory(userId, dateString); + console.log('userHistory', userHistory); + + if (userHistory.status && userHistory.data && userHistory.data.length > 0) { + let startIdx = 0; + let endIdx = userHistory.data.length - 1; + let featureCollection = { + "type": "FeatureCollection", + "features": [] + } + + // set waypoint start + let featurePointStart = { + "type": "Feature", + "properties": { + "type": "start", + "wptime": userHistory.data[startIdx].wptime + }, + "geometry": { + "type": "Point", + "coordinates": [parseFloat(userHistory.data[startIdx].lon), parseFloat(userHistory.data[startIdx].lat)] + } + } + featureCollection.features.push(featurePointStart); + + // set waypoint end + let featurePointEnd = { + "type": "Feature", + "properties": { + "type": "end", + "wptime": userHistory.data[endIdx].wptime + }, + "geometry": { + "type": "Point", + "coordinates": [parseFloat(userHistory.data[endIdx].lon), parseFloat(userHistory.data[endIdx].lat)] + } + } + featureCollection.features.push(featurePointEnd); + + // build waypoint line + let featureLine = { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [] + } + } + + userHistory.data.map(n => { + featureLine.geometry.coordinates.push([parseFloat(n.lon), parseFloat(n.lat)]) + }); + featureCollection.features.push(featureLine); + + store.dispatch(setUserHistory(featureCollection)); + store.dispatch(setIsSearchingRoute(false)); + } + // kalo gak ada historynya + else if (!userHistory.status && !userHistory.data) { + toast.warn("Couldn't find user history at the selected time. Please select another range of time."); + } + store.dispatch(setIsSearchingRoute(false)); } \ No newline at end of file diff --git a/src/appredux/modules/map/reducers.js b/src/appredux/modules/map/reducers.js index ba7ad8e..26f9e57 100644 --- a/src/appredux/modules/map/reducers.js +++ b/src/appredux/modules/map/reducers.js @@ -6,7 +6,9 @@ import { SET_USER_POINTS, SET_SELECTED_FEATURE, SET_OPEN_LEFT, - SET_OPEN_RIGHT + SET_OPEN_RIGHT, + SET_ROUTINGBAR_VISIBLE, + SET_IS_SEARCHING_ROUTE } from "./actions"; const initialState = { @@ -17,7 +19,9 @@ const initialState = { userHistory: null, projectTree: null, userPoints: null, - selectedFeature: null + selectedFeature: null, + routingBarVisible: false, + isSearchingRoute: false } function mapReducer(state = initialState, action) { @@ -38,6 +42,10 @@ function mapReducer(state = initialState, action) { return { ...state, userPoints: action.payload } case SET_SELECTED_FEATURE: return { ...state, selectedFeature: action.payload } + case SET_ROUTINGBAR_VISIBLE: + return { ...state, routingBarVisible: action.payload } + case SET_IS_SEARCHING_ROUTE: + return { ...state, isSearchingRoute: action.payload } default: return state; } diff --git a/src/components/MapLeftContent/index.js b/src/components/MapLeftContent/index.js index edcd37b..76590d4 100644 --- a/src/components/MapLeftContent/index.js +++ b/src/components/MapLeftContent/index.js @@ -25,7 +25,7 @@ const MapLeftContent = () => { console.log('selected', selectedKeys, info); }; const onCheck = (checkedKeys, info) => { - console.log('onCheck', checkedKeys, info); + console.log('onCheck', checkedKeys, info); if (checkedKeys.length < 1) { store.dispatch(setUserPoints(null)); @@ -100,59 +100,58 @@ const MapLeftContent = () => { const Content = useMemo(() => { return ( -
+
Project List
- {mapLoading ? - - - - - - - - - - - : - <> - {/* + {mapLoading ? + + + + + + + + + + + : + <> + {/* */} - { - projectTree ? - - : - null - } - - - - } - + { + projectTree ? + + : + null + } + + } +
) }, [mapLoading]) diff --git a/src/components/MapRightContent/index.js b/src/components/MapRightContent/index.js index 4e30b9b..2ea2b57 100644 --- a/src/components/MapRightContent/index.js +++ b/src/components/MapRightContent/index.js @@ -1,15 +1,8 @@ import React, { useEffect, useMemo, useRef, useState } from 'react' -import L from 'leaflet'; -import { Button, Col, Drawer, Input, Row, Spin, Tree } from 'antd'; -import ApiProject from '../../services/api/modules/project'; -import { store } from '../../appredux/store'; -import { setMapLoading, setProjectTree } from '../../appredux/modules/map/actions'; -import ContentLoader from 'react-content-loader'; import { useSelector } from 'react-redux'; +import PopupButtonActions from '../PopupButtonActions'; import './styles.css' -const { Search } = Input; - const MapRightContent = () => { const { mapLoading, selectedFeature } = useSelector(state => state.mapReducer); @@ -62,40 +55,44 @@ const MapRightContent = () => { const Content = useMemo(() => { return ( -
+
Detail Information
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Name:{selectedFeature?.properties['Name']}
Clock in time:{selectedFeature?.properties['Clock in time']}
Clock in location:{selectedFeature?.properties['Clock in location']}
Clock out time:{selectedFeature?.properties['Clock out time']}
Clock out location:{selectedFeature?.properties['Clock out location']}
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name:{selectedFeature?.properties['Name']}
Clock in time:{selectedFeature?.properties['Clock in time']}
Clock in location:{selectedFeature?.properties['Clock in location']}
Clock out time:{selectedFeature?.properties['Clock out time']}
Clock out location:{selectedFeature?.properties['Clock out location']}
+
+ +
) }, [selectedFeature]) diff --git a/src/components/PopupButtonActions/index.js b/src/components/PopupButtonActions/index.js new file mode 100644 index 0000000..3c0b7d4 --- /dev/null +++ b/src/components/PopupButtonActions/index.js @@ -0,0 +1,32 @@ +import { Button, Tooltip } from 'antd'; +import React, { useEffect, useMemo, useRef, useState } from 'react' +import { useSelector } from 'react-redux'; +import routeIcon from '@iconify/icons-fa-solid/route'; +import {Icon} from '@iconify/react'; +import { store } from '../../appredux/store'; +import { setRoutingBarVisible } from '../../appredux/modules/map/actions'; + +const PopupButtonActions = () => { + const { selectedFeature } = useSelector(state => state.mapReducer); + + const handleRoute = () => { + console.log('handleRoute'); + store.dispatch(setRoutingBarVisible(true)); + } + + const Content = useMemo(() => { + return ( +
+ +
+ ) + }, [selectedFeature]) + + return Content; +} + +export default PopupButtonActions; \ No newline at end of file diff --git a/src/components/RoutingBarV2/RoutingBarV2.css b/src/components/RoutingBarV2/RoutingBarV2.css new file mode 100644 index 0000000..2653fe4 --- /dev/null +++ b/src/components/RoutingBarV2/RoutingBarV2.css @@ -0,0 +1,77 @@ +.routingbar-container { + z-index: 999; + position: absolute; + top: 0; +} + +.routingbar-content { + text-align: center; + display: block; + position: relative; + top: 10px; + left: 15%; + padding: 5px; + z-index: 50; + background-color: rgba(0,0,0,0.5); + border-radius: 10px; + width: 470px; +} + +.routingbar-header-content { + color: #ffffff; + line-height: 17px; + margin-top: 10px; +} + +.routingbar-title { + font-size: 18px; + font-weight: bold; +} + +.routingbar-sales-name { + font-size: 12px; +} + +.routingbar-close { + float: right; + margin-right: 5px; + margin-top: -60px; + cursor: pointer; + color: #ffffff; +} + +.routingbar-body-content { + margin-bottom: 5px; +} + +.routingbar-label-container { + text-align: center !important; + /* margin-left: 63px; */ + margin-bottom: 5px; + font-weight: 700; +} + +.routingbar-label { + color: #ffffff; + font-size: 12px; +} + +.routingbar-range-picker { + margin-bottom: 10px; + display: inline-block; +} + +.routingbar-apply-button-container { + display: inline-block; + margin-left: 10px; + margin-top: -100px; + top: -100px; +} + +.routingbar-apply-button { + margin-top: -5px !important; +} + +.text-white { + color: #FFFFFF; +} \ No newline at end of file diff --git a/src/components/RoutingBarV2/index.js b/src/components/RoutingBarV2/index.js new file mode 100644 index 0000000..f9c9d46 --- /dev/null +++ b/src/components/RoutingBarV2/index.js @@ -0,0 +1,89 @@ +import React, { Component, useState } from 'react'; +// import { Button, ButtonGroup, UncontrolledTooltip } from 'reactstrap'; +import { Icon, InlineIcon } from '@iconify/react'; +import closeCircleOutline from '@iconify/icons-ion/close-circle-outline'; +import moment from 'moment'; +import './RoutingBarV2.css'; +import { useSelector } from 'react-redux'; +import { getUserHistory, setRoutingBarVisible, setUserHistory } from '../../appredux/modules/map/actions'; +import { store } from '../../appredux/store'; +import { Button, DatePicker } from 'antd'; +import { removeLayerByName } from '../../utils/MapUtils'; + +const { RangePicker } = DatePicker; + +const RoutingBar = () => { + + const { selectedFeature, routingBarVisible, isSearchingRoute } = useSelector(state => state.mapReducer); + const [dateString, setDateString] = useState(null); + + const onChange = (value, dateString) => { + console.log('Selected Time: ', value); + console.log('Formatted Selected Time: ', dateString); + } + + const onOk = (value) => { + let dateString = []; + dateString[0] = moment(value[0]).format("YYYY-MM-DD HH:mm"); + dateString[1] = moment(value[1]).format("YYYY-MM-DD HH:mm"); + setDateString(dateString) + } + + const applyRouting = () => { + let userId = selectedFeature?.properties.user_id; + getUserHistory(userId, dateString); + } + + const handleCloseRoutingBar = () => { + store.dispatch(setRoutingBarVisible(false)) + removeLayerByName('userHistoryLayer'); + store.dispatch(setUserHistory(null)); + } + + return ( +
+
+

+ User History
+ {selectedFeature?.properties.Name}
+

+ + +
+ Select Start and End Time +
+ +
+
+ +
+ +
+ {/* */} + + +
+
+
+
+ ) +} +export default RoutingBar; \ No newline at end of file diff --git a/src/services/api/modules/map_monitoring/index.js b/src/services/api/modules/map_monitoring/index.js index 1f1936b..6a989cb 100644 --- a/src/services/api/modules/map_monitoring/index.js +++ b/src/services/api/modules/map_monitoring/index.js @@ -69,4 +69,49 @@ export default class ApiMapMonitoring extends RequestApi { return {status: false, data: null}; }); } + + + static async searchUserHistory(userId, dateString) { + const URL = `${BASE_SIMPRO_LUMEN}/waypoint/search` + const payload = { + "paging": { "start": 0, "length": -1 }, + "columns": [ + { "name": "user_id", "logic_operator": "=", "value": userId }, + { "name": "wptime", "logic_operator": "range", "value": dateString[0], "value1": dateString[1] } + ], + "orders": { "columns": ["wptime"], "ascending": true } + } + + return await RequestApi.Request().post( + URL, + payload, + RequestApi.HeaderWithToken()).then(res => { + if (res) { + if (res && res.data && res.data.data) { + // console.log('ApiPresence search', res.data.data) + if (res.data.data.length > 0) { + return {status: true, data: res.data.data}; + } + else { + return {status: false, data: null} + } + } + else { + return {status: false, data: null}; + } + } + else { + alert("Please check your internet connection.", "error"); + return {status: false, data: null}; + } + }).catch(e => { + // console.log('error search presence', e); + let error = JSON.parse(JSON.stringify(e)); + console.log('error search presence', error); + if (error.message) { + alert(error.message); + } + return {status: false, data: null}; + }); + } } \ No newline at end of file diff --git a/src/views/MapMonitoring/MapMonitoring.css b/src/views/MapMonitoring/MapMonitoring.css new file mode 100644 index 0000000..1ad7e4c --- /dev/null +++ b/src/views/MapMonitoring/MapMonitoring.css @@ -0,0 +1,9 @@ +.loader-container { + text-align: center; + align-content: center; + top: 100px; + position: fixed; + z-index: 999999; + align-self: center; + margin-left: 45%; +} \ No newline at end of file diff --git a/src/views/MapMonitoring/index.js b/src/views/MapMonitoring/index.js index 10985c2..3623a77 100644 --- a/src/views/MapMonitoring/index.js +++ b/src/views/MapMonitoring/index.js @@ -9,6 +9,13 @@ import { useSelector } from 'react-redux'; import MapLeftContent from '../../components/MapLeftContent'; import MapRightContent from '../../components/MapRightContent'; import { removeLayerByName } from '../../utils/MapUtils'; +import RoutingBar from '../../components/RoutingBarV2'; +import Loader from "react-loader-spinner"; +import "react-loader-spinner/dist/loader/css/react-spinner-loader.css"; +import { ToastContainer, toast } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; +import './MapMonitoring.css'; + const MapMonitoring = () => { @@ -24,7 +31,7 @@ const MapMonitoring = () => { lng: 120.13025155062624 } - const {userPoints, mymap, openLeft, openRight} = useSelector(state => state.mapReducer); + const {userPoints, mymap, openLeft, openRight, routingBarVisible, userHistory, isSearchingRoute} = useSelector(state => state.mapReducer); const [gridMiddle, setGridMiddle] = useState(GRID_MIDDLE); const [gridLeft, setGridLeft] = useState(0); const [gridRight, setGridRight] = useState(0); @@ -46,14 +53,28 @@ const MapMonitoring = () => { if (userPoints) { let userPointLayer = L.geoJson(userPoints, { name: 'userPointLayer', - onEachFeature: onEachFeatureUserPoints, + onEachFeature: onEachFeatureUserPoints }); userPointLayer.addTo(mymap); + mymap.fitBounds(userPointLayer.getBounds()); } } }, [userPoints]) + useEffect(() => { + if (mymap) { + removeLayerByName('userHistoryLayer'); + if (userHistory) { + let userHistoryLayer = L.geoJson(userHistory, { + name: 'userHistoryLayer' + }); + userHistoryLayer.addTo(mymap); + mymap.fitBounds(userHistoryLayer.getBounds()); + } + } + }, [userHistory]) + const initMap = () => { let mymap = L.map('map-area', { center: center, @@ -64,9 +85,12 @@ const MapMonitoring = () => { L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors' }).addTo(mymap); mymap.on('click', (e) => { - removeLayerByName('popupTemp'); - store.dispatch(setOpenRight(false)); - store.dispatch(setSelectedFeature(null)); + if (!store.getState().mapReducer.routingBarVisible) { + // only can close popup when routing mode is not visible + removeLayerByName('popupTemp'); + store.dispatch(setOpenRight(false)); + store.dispatch(setSelectedFeature(null)); + } }) } @@ -99,7 +123,10 @@ const MapMonitoring = () => { const onEachFeatureUserPoints = (feature, layer) => { layer.on('click', function(e) { L.DomEvent.stopPropagation(e); - showHighLight(feature); + if (!store.getState().mapReducer.routingBarVisible) { + // proceed only when routing mode is not visible + showHighLight(feature); + } }); } @@ -130,18 +157,30 @@ const MapMonitoring = () => {
+ { routingBarVisible && } + { isSearchingRoute && ( +
+ +
+ )} + ) - }, [openLeft, openRight, gridLeft, gridMiddle, gridRight]) + }, [openLeft, openRight, gridLeft, gridMiddle, gridRight, routingBarVisible ]) const renderGridMap = () => { let middle = GRID_MIDDLE;