Browse Source

WIP: add user history tracking on Map Monitoring

pull/2/head
ardhi 2 years ago
parent
commit
3bba1787f1
  1. 88
      src/appredux/modules/map/actions.js
  2. 12
      src/appredux/modules/map/reducers.js
  3. 7
      src/components/MapLeftContent/index.js
  4. 15
      src/components/MapRightContent/index.js
  5. 32
      src/components/PopupButtonActions/index.js
  6. 77
      src/components/RoutingBarV2/RoutingBarV2.css
  7. 89
      src/components/RoutingBarV2/index.js
  8. 45
      src/services/api/modules/map_monitoring/index.js
  9. 9
      src/views/MapMonitoring/MapMonitoring.css
  10. 47
      src/views/MapMonitoring/index.js

88
src/appredux/modules/map/actions.js

@ -1,4 +1,5 @@
import moment from "moment"; import moment from "moment";
import { toast } from "react-toastify";
import ApiMapMonitoring from "../../../services/api/modules/map_monitoring"; import ApiMapMonitoring from "../../../services/api/modules/map_monitoring";
import { store } from "../../store"; 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_PROJECT_TREE = 'SET_PROJECT_TREE';
export const SET_USER_POINTS = 'SET_USER_POINTS'; export const SET_USER_POINTS = 'SET_USER_POINTS';
export const SET_SELECTED_FEATURE = 'SET_SELECTED_FEATURE'; 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 => { export const setMymap = obj => dispatch => {
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 () => { export const getUserPoints = async () => {
let userPoints = await ApiMapMonitoring.search(); let userPoints = await ApiMapMonitoring.search();
// console.log('userPoints', userPoints); // console.log('userPoints', userPoints);
@ -84,6 +103,7 @@ export const getUserPoints = async () => {
} }
feature.properties = { feature.properties = {
"user_id": n.user_id,
"Name": n.join_first_name ? n.join_first_name : '-', "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 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 : '-', "Clock in location": n.clock_in_loc ? n.clock_in_loc : '-',
@ -102,3 +122,71 @@ export const getUserPoints = async () => {
store.dispatch(setUserPoints(featureCollection)); 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));
}

12
src/appredux/modules/map/reducers.js

@ -6,7 +6,9 @@ import {
SET_USER_POINTS, SET_USER_POINTS,
SET_SELECTED_FEATURE, SET_SELECTED_FEATURE,
SET_OPEN_LEFT, SET_OPEN_LEFT,
SET_OPEN_RIGHT SET_OPEN_RIGHT,
SET_ROUTINGBAR_VISIBLE,
SET_IS_SEARCHING_ROUTE
} from "./actions"; } from "./actions";
const initialState = { const initialState = {
@ -17,7 +19,9 @@ const initialState = {
userHistory: null, userHistory: null,
projectTree: null, projectTree: null,
userPoints: null, userPoints: null,
selectedFeature: null selectedFeature: null,
routingBarVisible: false,
isSearchingRoute: false
} }
function mapReducer(state = initialState, action) { function mapReducer(state = initialState, action) {
@ -38,6 +42,10 @@ function mapReducer(state = initialState, action) {
return { ...state, userPoints: action.payload } return { ...state, userPoints: action.payload }
case SET_SELECTED_FEATURE: case SET_SELECTED_FEATURE:
return { ...state, selectedFeature: action.payload } 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: default:
return state; return state;
} }

7
src/components/MapLeftContent/index.js

@ -100,11 +100,12 @@ const MapLeftContent = () => {
const Content = useMemo(() => { const Content = useMemo(() => {
return ( return (
<div style={{ maxHeight: '90vh', overflow: 'auto', paddingRight: 10 }}> <div style={{ paddingRight: 10 }}>
<div style={{ backgroundColor: '#f0f0f0', padding: 10, marginBottom: 5, fontWeight: 'bold', fontSize: 16 }}> <div style={{ backgroundColor: '#f0f0f0', padding: 10, marginBottom: 5, fontWeight: 'bold', fontSize: 16 }}>
Project List Project List
</div> </div>
<div style={{ maxHeight: '80vh', overflow: 'auto' }}>
{mapLoading ? {mapLoading ?
<ContentLoader <ContentLoader
speed={2} speed={2}
@ -148,11 +149,9 @@ const MapLeftContent = () => {
: :
null null
} }
</> </>
} }
</div>
</div> </div>
) )
}, [mapLoading]) }, [mapLoading])

15
src/components/MapRightContent/index.js

@ -1,15 +1,8 @@
import React, { useEffect, useMemo, useRef, useState } from 'react' 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 { useSelector } from 'react-redux';
import PopupButtonActions from '../PopupButtonActions';
import './styles.css' import './styles.css'
const { Search } = Input;
const MapRightContent = () => { const MapRightContent = () => {
const { mapLoading, selectedFeature } = useSelector(state => state.mapReducer); const { mapLoading, selectedFeature } = useSelector(state => state.mapReducer);
@ -62,11 +55,12 @@ const MapRightContent = () => {
const Content = useMemo(() => { const Content = useMemo(() => {
return ( return (
<div style={{ maxHeight: '90vh', overflow: 'auto', paddingLeft: 10 }}> <div style={{ paddingLeft: 10 }}>
<div style={{ backgroundColor: '#f0f0f0', padding: 10, marginBottom: 5, fontWeight: 'bold', fontSize: 16 }}> <div style={{ backgroundColor: '#f0f0f0', padding: 10, marginBottom: 5, fontWeight: 'bold', fontSize: 16 }}>
Detail Information Detail Information
</div> </div>
<div style={{height: '75vh', overflow: 'auto'}}>
<table className="table popup-table"> <table className="table popup-table">
<tbody> <tbody>
<tr> <tr>
@ -97,6 +91,9 @@ const MapRightContent = () => {
</tbody> </tbody>
</table> </table>
</div> </div>
<PopupButtonActions />
</div>
) )
}, [selectedFeature]) }, [selectedFeature])

32
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 (
<div style={{ paddingLeft: 10, bottom: 10, textAlign: 'center', margin: 'auto' }}>
<Tooltip title="View Tracking History">
<Button shape="circle" color='' icon={<Icon icon={routeIcon} width={15} height={15} />} size="large"
onClick={handleRoute}
/>
</Tooltip>
</div>
)
}, [selectedFeature])
return Content;
}
export default PopupButtonActions;

77
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;
}

89
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 (
<div className="routingbar-container">
<div className="routingbar-content">
<p className="routingbar-header-content">
<span className="routingbar-title">User History</span><br />
<span className="routingbar-sales-name">{selectedFeature?.properties.Name}</span><br />
</p>
<span className="routingbar-close" onClick={handleCloseRoutingBar}><Icon icon={closeCircleOutline} width={25} height={25} /></span>
<div className="routingbar-label-container">
<span className="routingbar-label">Select Start and End Time</span>
</div>
<div className="routingbar-body-content">
<div className="routingbar-range-picker">
<RangePicker
showTime={{ format: 'HH:mm' }}
format="YYYY-MM-DD HH:mm"
placeholder={['Start Time', 'End Time']}
onChange={onChange}
onOk={onOk}
disabled={isSearchingRoute}
/>
</div>
<div className="routingbar-apply-button-container">
{/* <Button
color="primary" size="sm" className="routingbar-apply-button"
disabled={isSearchingRoute}
onClick={applyRouting}
>Search</Button> */}
<Button
type="primary"
size="small"
disabled={isSearchingRoute}
loading={isSearchingRoute}
onClick={applyRouting}>
Search
</Button>
</div>
</div>
</div>
</div>
)
}
export default RoutingBar;

45
src/services/api/modules/map_monitoring/index.js

@ -69,4 +69,49 @@ export default class ApiMapMonitoring extends RequestApi {
return {status: false, data: null}; 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};
});
}
} }

9
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%;
}

47
src/views/MapMonitoring/index.js

@ -9,6 +9,13 @@ import { useSelector } from 'react-redux';
import MapLeftContent from '../../components/MapLeftContent'; import MapLeftContent from '../../components/MapLeftContent';
import MapRightContent from '../../components/MapRightContent'; import MapRightContent from '../../components/MapRightContent';
import { removeLayerByName } from '../../utils/MapUtils'; 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 = () => { const MapMonitoring = () => {
@ -24,7 +31,7 @@ const MapMonitoring = () => {
lng: 120.13025155062624 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 [gridMiddle, setGridMiddle] = useState(GRID_MIDDLE);
const [gridLeft, setGridLeft] = useState(0); const [gridLeft, setGridLeft] = useState(0);
const [gridRight, setGridRight] = useState(0); const [gridRight, setGridRight] = useState(0);
@ -46,14 +53,28 @@ const MapMonitoring = () => {
if (userPoints) { if (userPoints) {
let userPointLayer = L.geoJson(userPoints, { let userPointLayer = L.geoJson(userPoints, {
name: 'userPointLayer', name: 'userPointLayer',
onEachFeature: onEachFeatureUserPoints, onEachFeature: onEachFeatureUserPoints
}); });
userPointLayer.addTo(mymap); userPointLayer.addTo(mymap);
mymap.fitBounds(userPointLayer.getBounds());
} }
} }
}, [userPoints]) }, [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 = () => { const initMap = () => {
let mymap = L.map('map-area', { let mymap = L.map('map-area', {
center: center, center: center,
@ -64,9 +85,12 @@ const MapMonitoring = () => {
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' }).addTo(mymap); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' }).addTo(mymap);
mymap.on('click', (e) => { mymap.on('click', (e) => {
if (!store.getState().mapReducer.routingBarVisible) {
// only can close popup when routing mode is not visible
removeLayerByName('popupTemp'); removeLayerByName('popupTemp');
store.dispatch(setOpenRight(false)); store.dispatch(setOpenRight(false));
store.dispatch(setSelectedFeature(null)); store.dispatch(setSelectedFeature(null));
}
}) })
} }
@ -99,7 +123,10 @@ const MapMonitoring = () => {
const onEachFeatureUserPoints = (feature, layer) => { const onEachFeatureUserPoints = (feature, layer) => {
layer.on('click', function(e) { layer.on('click', function(e) {
L.DomEvent.stopPropagation(e); L.DomEvent.stopPropagation(e);
if (!store.getState().mapReducer.routingBarVisible) {
// proceed only when routing mode is not visible
showHighLight(feature); showHighLight(feature);
}
}); });
} }
@ -130,18 +157,30 @@ const MapMonitoring = () => {
<Col span={gridMiddle}> <Col span={gridMiddle}>
<div id="map-area" style={{ height: '90vh', width: '100%' }} ref={mapRef}></div> <div id="map-area" style={{ height: '90vh', width: '100%' }} ref={mapRef}></div>
<button <button
title='List Projects' title='Project List'
style={{position: 'absolute', top: 80, left: 10, zIndex: 999, backgroundColor:'white', backgroundSize:'34px 34px', width: '34px', height: '34px', borderRadius: '2px', borderWidth: '1px', borderColor: 'grey'}} style={{position: 'absolute', top: 80, left: 10, zIndex: 999, backgroundColor:'white', backgroundSize:'34px 34px', width: '34px', height: '34px', borderRadius: '2px', borderWidth: '1px', borderColor: 'grey'}}
onClick={() => store.dispatch(setOpenLeft(!openLeft))}> onClick={() => store.dispatch(setOpenLeft(!openLeft))}>
<i className='fa fa-list'></i> <i className='fa fa-list'></i>
</button> </button>
{ routingBarVisible && <RoutingBar /> }
{ isSearchingRoute && (
<div className="loader-container">
<Loader
type="TailSpin"
color="#36D7B7"
height={100}
width={100}
/>
</div>
)}
</Col> </Col>
<Col span={gridRight}> <Col span={gridRight}>
<MapRightContent /> <MapRightContent />
</Col> </Col>
<ToastContainer autoClose={5000} />
</Row> </Row>
) )
}, [openLeft, openRight, gridLeft, gridMiddle, gridRight]) }, [openLeft, openRight, gridLeft, gridMiddle, gridRight, routingBarVisible ])
const renderGridMap = () => { const renderGridMap = () => {
let middle = GRID_MIDDLE; let middle = GRID_MIDDLE;

Loading…
Cancel
Save