diff --git a/package.json b/package.json
index bb4a5fa..456954b 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,7 @@
"@iconify/icons-uil": "^1.0.6",
"@iconify/react": "^1.1.1",
"@nicholasadamou/react-iframe": "^1.0.3",
+ "@reduxjs/toolkit": "^1.9.2",
"@terrestris/ol-util": "^7.2.0",
"@terrestris/react-geo": "^12.0.0",
"alasql": "^1.7.3",
@@ -76,12 +77,16 @@
"react-leaflet-draw": "^0.19.8",
"react-loader-spinner": "^3.1.5",
"react-notifications": "^1.7.2",
+ "react-redux": "^8.0.5",
"react-router-dom": "^5.0.1",
"react-select": "^4.3.1",
"react-slick": "^0.28.1",
"react-tiny-fab": "^4.0.4",
"react-toastify": "^5.5.0",
"reactstrap": "^8.0.0",
+ "redux": "^4.2.1",
+ "redux-persist": "^6.0.0",
+ "redux-thunk": "^2.4.2",
"simple-line-icons": "^2.4.1",
"slick-carousel": "^1.8.1",
"underscore": "^1.13.1",
diff --git a/src/App.js b/src/App.js
index b4515cf..357336a 100644
--- a/src/App.js
+++ b/src/App.js
@@ -2,6 +2,9 @@ import React, { Component } from 'react';
import { HashRouter, Route, Switch } from 'react-router-dom';
import './App.scss';
import 'react-notifications/lib/notifications.css';
+import { Provider } from 'react-redux';
+import { PersistGate } from 'redux-persist/integration/react';
+import { persistor, store } from './appredux/store';
const loading = () =>
Loading...
;
@@ -16,26 +19,28 @@ const SiopasMap = React.lazy(() => import('./views/Map'));
class App extends Component {
- render() {
- return (
-
-
-
- } />
- {/* } /> */}
- } />
- } />
- } />
- } />
- } />
- {/* } />*/}
- {/* } />*/}
- } />
-
-
-
- );
- }
+ render() {
+ return (
+
+
+
+
+ } />
+ {/* } /> */}
+ } />
+ } />
+ } />
+ } />
+ } />
+ {/* } />*/}
+ {/* } />*/}
+ } />
+
+
+
+
+ );
+ }
}
export default App;
diff --git a/src/appredux/modules/map/actions.js b/src/appredux/modules/map/actions.js
new file mode 100644
index 0000000..42d9f5f
--- /dev/null
+++ b/src/appredux/modules/map/actions.js
@@ -0,0 +1,192 @@
+import moment from "moment";
+import { toast } from "react-toastify";
+import ApiMapMonitoring from "../../../services/api/modules/map_monitoring";
+import { store } from "../../store";
+
+export const SET_MYMAP = 'SET_MYMAP';
+export const SET_OPEN_LEFT = 'SET_OPEN_LEFT';
+export const SET_OPEN_RIGHT = 'SET_OPEN_RIGHT';
+export const SET_MAP_LOADING = 'SET_MAP_LOADING';
+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({
+ type: SET_MYMAP,
+ payload: obj
+ })
+}
+
+export const setOpenLeft = obj => dispatch => {
+ dispatch({
+ type: SET_OPEN_LEFT,
+ payload: obj
+ })
+}
+
+export const setOpenRight = obj => dispatch => {
+ dispatch({
+ type: SET_OPEN_RIGHT,
+ payload: obj
+ })
+}
+
+export const setMapLoading = obj => dispatch => {
+ dispatch({
+ type: SET_MAP_LOADING,
+ payload: obj
+ })
+}
+
+export const setUserHistory = obj => dispatch => {
+ dispatch({
+ type: SET_USER_HISTORY,
+ payload: obj
+ })
+}
+
+export const setProjectTree = obj => dispatch => {
+ dispatch({
+ type: SET_PROJECT_TREE,
+ payload: obj
+ })
+}
+
+export const setUserPoints = obj => dispatch => {
+ dispatch({
+ type: SET_USER_POINTS,
+ payload: obj
+ })
+}
+
+export const setSelectedFeature = obj => dispatch => {
+ dispatch({
+ type: SET_SELECTED_FEATURE,
+ payload: obj
+ })
+}
+
+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);
+ if (userPoints.status && userPoints.data && userPoints.data.length > 0) {
+ let featureCollection = {
+ "type": "FeatureCollection",
+ "features": []
+ }
+
+ userPoints.data.map(n => {
+ let feature = {
+ "type": "Feature",
+ "properties": null,
+ "geometry": null
+ }
+
+ 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 : '-',
+ "Clock out time": n.clock_out ? moment(n.clock_out).format('YYYY-MM-DD HH:mm:ss') : '-',
+ "Clock out location": n.clock_out_loc ? n.clock_out_loc : '-',
+ }
+
+ feature.geometry = {
+ "type": "Point",
+ "coordinates": [n.clock_in_lng, n.clock_in_lat]
+ }
+
+ featureCollection.features.push(feature);
+ });
+
+ 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
new file mode 100644
index 0000000..26f9e57
--- /dev/null
+++ b/src/appredux/modules/map/reducers.js
@@ -0,0 +1,54 @@
+import {
+ SET_MYMAP,
+ SET_MAP_LOADING,
+ SET_USER_HISTORY,
+ SET_PROJECT_TREE,
+ SET_USER_POINTS,
+ SET_SELECTED_FEATURE,
+ SET_OPEN_LEFT,
+ SET_OPEN_RIGHT,
+ SET_ROUTINGBAR_VISIBLE,
+ SET_IS_SEARCHING_ROUTE
+} from "./actions";
+
+const initialState = {
+ mymap: null,
+ openLeft: true,
+ openRight: false,
+ mapLoading: false,
+ userHistory: null,
+ projectTree: null,
+ userPoints: null,
+ selectedFeature: null,
+ routingBarVisible: false,
+ isSearchingRoute: false
+}
+
+function mapReducer(state = initialState, action) {
+ switch (action.type) {
+ case SET_MYMAP:
+ return { ...state, mymap: action.payload }
+ case SET_OPEN_LEFT:
+ return { ...state, openLeft: action.payload }
+ case SET_OPEN_RIGHT:
+ return { ...state, openRight: action.payload }
+ case SET_MAP_LOADING:
+ return { ...state, mapLoading: action.payload }
+ case SET_USER_HISTORY:
+ return { ...state, userHistory: action.payload }
+ case SET_PROJECT_TREE:
+ return { ...state, projectTree: action.payload }
+ case SET_USER_POINTS:
+ 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;
+ }
+}
+
+export default mapReducer;
\ No newline at end of file
diff --git a/src/appredux/reducers.js b/src/appredux/reducers.js
new file mode 100644
index 0000000..eb53202
--- /dev/null
+++ b/src/appredux/reducers.js
@@ -0,0 +1,46 @@
+// import { parse, stringify } from 'flatted';
+import { combineReducers } from 'redux';
+import { persistReducer, createTransform } from 'redux-persist';
+import storage from 'redux-persist/lib/storage';
+
+// modules
+import mapReducer from './modules/map/reducers';
+
+const rootReducer = combineReducers({
+ mapReducer
+});
+
+// export const transformCircular = createTransform(
+// // (inboundState, key) => JSON.stringify(inboundState),
+// // (outboundState, key) => JSON.parse(outboundState),
+// (inboundState, key) => stringify(inboundState),
+// (outboundState, key) => parse(outboundState),
+// )
+
+
+// const SetTransform = createTransform(
+// // transform state on its way to being serialized and persisted.
+// (inboundState, key) => {
+// // convert mySet to an Array.
+// return { ...inboundState, mySet: [...inboundState.mySet] };
+// },
+// // transform state being rehydrated
+// (outboundState, key) => {
+// // convert mySet back to a Set.
+// return { ...outboundState, mySet: new Set(outboundState.mySet) };
+// },
+// // define which reducers this transform gets called for.
+// { whitelist: ['mapReducer'] }
+// );
+
+const persistConfig = {
+ key: 'root',
+ storage,
+ // blacklist: [], // to be not persisted
+ // transforms: [SetTransform]
+ // transforms: [transformCircular]
+};
+
+// export default persistReducer(persistConfig, rootReducer);
+
+export default rootReducer;
\ No newline at end of file
diff --git a/src/appredux/store.js b/src/appredux/store.js
new file mode 100644
index 0000000..ed2c43c
--- /dev/null
+++ b/src/appredux/store.js
@@ -0,0 +1,46 @@
+import { configureStore } from "@reduxjs/toolkit";
+import { applyMiddleware, compose, createStore } from "redux";
+// import thunk from "redux-thunk";
+import { persistStore } from 'redux-persist';
+import thunk from "redux-thunk";
+import thunkMiddleware from 'redux-thunk';
+import persistedReducer from "./reducers";
+
+// const enhancers = [
+// applyMiddleware(
+// thunkMiddleware,
+// // createLogger({
+// // collapsed: true,
+// // // eslint-disable-next-line no-undef
+// // predicate: () => __DEV__,
+// // }),
+// ),
+// ];
+
+// /* eslint-disable no-undef */
+// const composeEnhancers =
+// (__DEV__ &&
+// typeof window !== 'undefined' &&
+// window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) ||
+// compose;
+// /* eslint-enable no-undef */
+
+// const enhancer = composeEnhancers(...enhancers);
+
+// export const store = createStore(persistedReducer, {}, enhancer)
+// // export const store = configureStore(persistedReducer, enhancer);
+// // export const store = configureStore( {
+// // persistedReducer,
+// // enhancers: enhancer
+// // })
+// export const persistor = persistStore(store);
+
+
+
+export const store = configureStore({
+ reducer: persistedReducer,
+ devTools: process.env.NODE_ENV !== 'production',
+ middleware: [thunk]
+})
+
+export const persistor = persistStore(store)
\ No newline at end of file
diff --git a/src/components/MapLeftContent/index.js b/src/components/MapLeftContent/index.js
new file mode 100644
index 0000000..76590d4
--- /dev/null
+++ b/src/components/MapLeftContent/index.js
@@ -0,0 +1,162 @@
+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 { getUserPoints, setMapLoading, setOpenRight, setProjectTree, setSelectedFeature, setUserPoints } from '../../appredux/modules/map/actions';
+import ContentLoader from 'react-content-loader';
+import { useSelector } from 'react-redux';
+import ApiMapMonitoring from '../../services/api/modules/map_monitoring';
+
+const { Search } = Input;
+
+const MapLeftContent = () => {
+ // const { mapLoading } = store.getState().mapReducer;
+ const { mapLoading, projectTree } = useSelector(state => state.mapReducer);
+ // const [expandedKeys, setExpandedKeys] = useState([]);
+ // const [searchValue, setSearchValue] = useState('');
+ // const [autoExpandParent, setAutoExpandParent] = useState(true);
+ // const onExpand = (newExpandedKeys) => {
+ // setExpandedKeys(newExpandedKeys);
+ // setAutoExpandParent(false);
+ // };
+
+ const onSelect = (selectedKeys, info) => {
+ console.log('selected', selectedKeys, info);
+ };
+ const onCheck = (checkedKeys, info) => {
+ console.log('onCheck', checkedKeys, info);
+
+ if (checkedKeys.length < 1) {
+ store.dispatch(setUserPoints(null));
+ store.dispatch(setOpenRight(false));
+ store.dispatch(setSelectedFeature(null));
+ return;
+ }
+ getUserPoints();
+ };
+
+ // const getParentKey = (key, tree) => {
+ // let parentKey;
+ // for (let i = 0; i < tree.length; i++) {
+ // const node = tree[i];
+ // if (node.children) {
+ // if (node.children.some((item) => item.key === key)) {
+ // parentKey = node.key;
+ // } else if (getParentKey(key, node.children)) {
+ // parentKey = getParentKey(key, node.children);
+ // }
+ // }
+ // }
+ // return parentKey;
+ // };
+
+ // const treeData = useMemo(() => {
+ // const loop = (data) =>
+ // data.map((item) => {
+ // const strTitle = item.title;
+ // const index = strTitle.indexOf(searchValue);
+ // const beforeStr = strTitle.substring(0, index);
+ // const afterStr = strTitle.slice(index + searchValue.length);
+ // const title =
+ // index > -1 ? (
+ //
+ // {beforeStr}
+ // {searchValue}
+ // {afterStr}
+ //
+ // ) : (
+ // {strTitle}
+ // );
+ // if (item.children) {
+ // return {
+ // title,
+ // key: item.key,
+ // children: loop(item.children),
+ // };
+ // }
+ // return {
+ // title,
+ // key: item.key,
+ // };
+ // });
+ // return loop(projectTree);
+ // }, [searchValue]);
+
+ // const onChangeSearch = (e) => {
+ // const { value } = e.target;
+ // const newExpandedKeys = projectTree
+ // .map((item) => {
+ // if (item.title.indexOf(value) > -1) {
+ // return getParentKey(item.key, projectTree);
+ // }
+ // return null;
+ // })
+ // .filter((item, i, self) => item && self.indexOf(item) === i);
+ // setExpandedKeys(newExpandedKeys);
+ // setSearchValue(value);
+ // setAutoExpandParent(true);
+ // };
+
+ const Content = useMemo(() => {
+ return (
+
+
+ Project List
+
+
+
+ {mapLoading ?
+
+
+
+
+
+
+
+
+
+
+ :
+ <>
+ {/* */}
+ {
+ projectTree ?
+
+ :
+ null
+ }
+ >
+ }
+
+
+ )
+ }, [mapLoading])
+
+ return Content;
+}
+
+export default MapLeftContent;
\ No newline at end of file
diff --git a/src/components/MapRightContent/index.js b/src/components/MapRightContent/index.js
new file mode 100644
index 0000000..2ea2b57
--- /dev/null
+++ b/src/components/MapRightContent/index.js
@@ -0,0 +1,103 @@
+import React, { useEffect, useMemo, useRef, useState } from 'react'
+import { useSelector } from 'react-redux';
+import PopupButtonActions from '../PopupButtonActions';
+import './styles.css'
+
+const MapRightContent = () => {
+ const { mapLoading, selectedFeature } = useSelector(state => state.mapReducer);
+
+ const PopupContent = useMemo(() => {
+ console.log('selectedFeature', selectedFeature);
+ if (selectedFeature && selectedFeature.properties) {
+ // let content = [];
+ // for (let key in selectedFeature.properties) {
+ // content.push(
+ // {`${key}`} |
+ // : |
+ // {`${selectedFeature.properties[key]}`} |
+ //
)
+ // }
+ // console.log('content', content);
+
+ return (
+
+
+
+ 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])
+
+ 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']} |
+
+
+
+
+
+
+
+ )
+ }, [selectedFeature])
+
+ return Content;
+}
+
+export default MapRightContent;
\ No newline at end of file
diff --git a/src/components/MapRightContent/styles.css b/src/components/MapRightContent/styles.css
new file mode 100644
index 0000000..2a226a3
--- /dev/null
+++ b/src/components/MapRightContent/styles.css
@@ -0,0 +1,9 @@
+table th, .table td {
+ padding: 0.25rem !important;
+ vertical-align: top;
+ border-top: 1px solid #c8ced3;
+}
+
+.td-popup-label {
+ color: #808080;
+}
\ No newline at end of file
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 (
+
+
+ } size="large"
+ onClick={handleRoute}
+ />
+
+
+ )
+ }, [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/const/ApiConst.js b/src/const/ApiConst.js
index 569f5e3..1c8c8d9 100644
--- a/src/const/ApiConst.js
+++ b/src/const/ApiConst.js
@@ -113,6 +113,7 @@ export const TOKEN_ADW = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIxMjAyI
// export let BASE_OSPRO = "https://ospro-api.ospro.id";
export let BASE_OSPRO = "https://adw-api.ospro.id";
+// export let BASE_OSPRO = "http://localhost:8444";
// export let BASE_OSPRO = "http://192.168.1.123:8444"; // local
// export let BASE_OSPRO = "http://103.73.125.81:8444"; // ip public adw
export let BASE_SIMPRO_LUMEN = `${BASE_OSPRO}/api`;
diff --git a/src/routes.js b/src/routes.js
index 355572c..cd38e1e 100644
--- a/src/routes.js
+++ b/src/routes.js
@@ -46,6 +46,7 @@ const UserShift = React.lazy(() => import('./views/SimproV2/UserShift'));
const DashboardBOD = React.lazy(() => import('./views/Dashboard/DashboardBOD'));
const DashboardCustomer = React.lazy(() => import('./views/Dashboard/DashboardCustomer'));
const DashboardProject = React.lazy(() => import('./views/Dashboard/DashboardProject'));
+const MapMonitoring = React.lazy(() => import('./views/MapMonitoring'));
const routes = [
{ path: '/', exact: true, name: 'Home' },
@@ -77,7 +78,6 @@ const routes = [
{ path: '/divisi', exact: true, name: 'Divisi', component: Divisi },
{ path: '/satuan', exact: true, name: 'Satuan', component: Satuan },
{ path: '/config-alert', exact: true, name: 'Config Alert', component: ConfigAlert },
-
{ path: '/checklist-k3', exact: true, name: 'Checklist K3', component: ChecklistK3 },
{ path: '/absensi', exact: true, name: 'Absensi', component: Absensi },
{ path: '/divisi-karyawan', exact: true, name: 'Divisi Karyawan', component: DivisiKaryawan },
@@ -100,6 +100,7 @@ const routes = [
{ path: '/user-admin', exact: true, name: 'User Admin', component: UserAdmin },
{ path: '/user-shift', exact: true, name: 'Shift', component: UserShift },
{ path: '/working-hour', exact: true, name: 'Working Hour', component: Shift },
+ { path: '/map-monitoring', exact: true, name: 'Map Monitoring', component: MapMonitoring },
// { path: '/dashboard-project/:ID/:GANTTID', exact: true, name: 'Dashboard Project', component: DashboardProject },
];
diff --git a/src/services/api/base.js b/src/services/api/base.js
new file mode 100644
index 0000000..b720686
--- /dev/null
+++ b/src/services/api/base.js
@@ -0,0 +1,99 @@
+import axios from 'axios';
+export default class RequestApi {
+ // static Request() {
+ // // axios.interceptors.request.use(function (config) {
+ // // const token = localStorage.getItem('token')
+ // // config.headers.Authorization = token;
+
+ // // return config;
+ // // });
+ // const token = localStorage.getItem('token')
+
+ // let instance = axios.create({
+ // headers: {
+ // 'Content-Type': 'application/json',
+ // "Authorization": `Bearer ${token}`
+ // }
+ // })
+
+ // instance.interceptors.response.use(
+ // (response) => response,
+ // async (error) => {
+ // // const originalRequest = error.config;
+ // if (error.response.status === 307 || error.response.status === 403) {
+ // console.log(error.response);
+ // }
+
+ // return Promise.reject(error);
+ // }
+ // );
+
+ // return instance;
+ // }
+
+ static Request() {
+ axios.interceptors.response.use(
+ response => response,
+ async (error) => {
+ // console.log('error axios', error);
+ if (error) {
+ // console.log('stringify', JSON.stringify(error));
+ const err = JSON.parse(JSON.stringify(error));
+ if (err.name === 'AxiosError' && err.code === 'ERR_NETWORK') {
+ alert(err.message);
+ return;
+ }
+ if (err.response) {
+ if (err.response.status === 307 || err.response.status === 403) {
+ // console.log(err.response);
+ alert('Token expired, please re-login');
+ // clearAllState();
+ window.localStorage.clear();
+ // this.props.history.replace('/login');
+ return;
+ }
+ }
+ return Promise.reject(error);
+ }
+ }
+ );
+ return axios;
+ }
+
+ static Header() {
+ let header = {
+ headers: {
+ "Content-Type": "application/json",
+ // 'Cache-Control': 'no-cache'
+ }
+ }
+ return header;
+ }
+
+ static HeaderWithToken() {
+ const token = localStorage.getItem('token')
+ let header = {
+ headers: {
+ "Content-Type": "application/json",
+ "Authorization": `Bearer ${token}`,
+ // 'Cache-Control': 'no-cache',
+ }
+ }
+ return header;
+ }
+
+ static HeaderMultipart() {
+ const token = localStorage.getItem('token')
+ let header = {
+ headers: {
+ "Content-Type": "multipart/form-data",
+ "Authorization": `Bearer ${token}`,
+ // 'Cache-Control': 'no-cache',
+ }
+ }
+ return header;
+ }
+
+}
+export const AXIOS = RequestApi.Request();
+
diff --git a/src/services/api/modules/map_monitoring/index.js b/src/services/api/modules/map_monitoring/index.js
new file mode 100644
index 0000000..6a989cb
--- /dev/null
+++ b/src/services/api/modules/map_monitoring/index.js
@@ -0,0 +1,117 @@
+import moment from "moment";
+import { BASE_SIMPRO_LUMEN } from "../../../../const/ApiConst";
+import RequestApi from '../../base';
+
+export default class ApiMapMonitoring extends RequestApi {
+ static async search() {
+ const URL = `${BASE_SIMPRO_LUMEN}/presence/search`
+ const dateFrom = moment().subtract(7,'d').format('YYYY-MM-DD 00:00:00');
+ const dateTo = moment().format('YYYY-MM-DD 23:59:00');
+ const payload = {
+ "columns": [
+ {
+ "logic_operator": "range",
+ "name": "created_at",
+ "operator": "AND",
+ "value": dateFrom,
+ "value1": dateTo
+ }
+ ],
+ "joins": [
+ {
+ "column_join": "user_id",
+ "column_results": [
+ "username", "name"
+ ],
+ "name": "m_users"
+ }
+ ],
+ "orders": {
+ "ascending": false,
+ "columns": [
+ "created_at"
+ ]
+ },
+ "paging": {
+ "length": 25,
+ "start": 0
+ }
+ }
+ 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};
+ });
+ }
+
+
+ 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/services/api/modules/project/index.js b/src/services/api/modules/project/index.js
new file mode 100644
index 0000000..b058929
--- /dev/null
+++ b/src/services/api/modules/project/index.js
@@ -0,0 +1,62 @@
+import { BASE_SIMPRO_LUMEN } from "../../../../const/ApiConst";
+import RequestApi from '../../base';
+
+export default class ApiProject extends RequestApi {
+ static async list() {
+ // const user_id = store.getState().userReducer && store.getState().userReducer.user && store.getState().userReducer.user.data_user ? store.getState().userReducer.user.data_user.id : 0;
+ const URL = `${BASE_SIMPRO_LUMEN}/project/list`
+ // const payload = {
+ // "paging": { "start": 0, "length": 25 },
+ // // "columns": [
+ // // { "name": "user_id", "logic_operator": "=", "value": user_id }
+ // // ],
+ // // "joins": [
+ // // {
+ // // "name": "m_proyek",
+ // // "column_join": "proyek_id",
+ // // "column_results": [
+ // // "nama", "kode_sortname", "mulai_proyek", "akhir_proyek"
+ // // ]
+ // // },
+ // // {
+ // // "name": "m_activity",
+ // // "column_join": "activity_id",
+ // // "column_results": [
+ // // "name", "start_date", "end_date", "persentase_progress"
+ // // ]
+ // // }
+ // // ],
+ // "orders": { "columns": ["id"], "ascending": false }
+ // }
+ return await RequestApi.Request().get(
+ URL,
+ RequestApi.HeaderWithToken()).then(res => {
+ if (res) {
+ if (res && res.data && res.data.data) {
+ // console.log('ApiProject 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 project', e);
+ let error = JSON.parse(JSON.stringify(e));
+ console.log('error search project', error);
+ if (error.message) {
+ alert(error.message);
+ }
+ return {status: false, data: null};
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/services/base.js b/src/services/base.js
deleted file mode 100644
index 8166693..0000000
--- a/src/services/base.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import axios from 'axios';
-export default class RequestApi {
- // constructor() {
- // this.Request = this.Request.bind(this);
- // }
-
- static Request() {
- // axios.interceptors.request.use(function (config) {
- // const token = localStorage.getItem('token')
- // config.headers.Authorization = token;
-
- // return config;
- // });
- const token = localStorage.getItem('token')
-
- let instance = axios.create({
- headers: {
- 'Content-Type': 'application/json',
- "Authorization": `Bearer ${token}`
- }
- })
-
- instance.interceptors.response.use(
- (response) => response,
- async (error) => {
- // const originalRequest = error.config;
- if (error.response.status === 307 || error.response.status === 403) {
- console.log(error.response);
- }
-
- return Promise.reject(error);
- }
- );
-
- return instance;
- }
-
- static getToken() { }
-}
-export const AXIOS = RequestApi.Request();
-
diff --git a/src/utils/MapUtils.js b/src/utils/MapUtils.js
new file mode 100644
index 0000000..04803b0
--- /dev/null
+++ b/src/utils/MapUtils.js
@@ -0,0 +1,27 @@
+import { store } from "../appredux/store";
+
+export const removeLayerByName = (layerName) => {
+ const {mymap} = store.getState().mapReducer;
+ var layerToRemove = [];
+ mymap.eachLayer(function(layer) {
+ if (layer.wmsParams) {
+ if (layer.wmsParams.layers) {
+ let layerWmsName = layer.wmsParams.layers.split(':')[1];
+ if (layerName === layerWmsName) {
+ layerToRemove.push(layer)
+ }
+ }
+ }
+ else {
+ if (layer.options && layer.options.name && layer.options.name === layerName) {
+ layerToRemove.push(layer);
+ }
+ }
+ });
+
+ if (layerToRemove.length > 0) {
+ for (let i = 0; i < layerToRemove.length; i++) {
+ mymap.removeLayer(layerToRemove[i]);
+ }
+ }
+}
\ 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
new file mode 100644
index 0000000..3623a77
--- /dev/null
+++ b/src/views/MapMonitoring/index.js
@@ -0,0 +1,217 @@
+import React, { useEffect, useMemo, useRef, useState } from 'react'
+import L from 'leaflet';
+import { Button, Col, Drawer, Row, Spin, Tree } from 'antd';
+import ApiProject from '../../services/api/modules/project';
+import { store } from '../../appredux/store';
+import { getUserPoints, setMapLoading, setMymap, setOpenLeft, setOpenRight, setProjectTree, setSelectedFeature, setUserPoints } from '../../appredux/modules/map/actions';
+import ContentLoader from 'react-content-loader';
+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 = () => {
+
+ const GRID_LEFT = 6;
+ const GRID_MIDDLE = 12;
+ const GRID_RIGHT = 6;
+ const GRID_TOTAL = GRID_LEFT+GRID_MIDDLE+GRID_RIGHT;
+ const mapRef = useRef()
+ const center = {
+ // lat: -6.200000,
+ // lng: 106.816666
+ lat: -2.4809807277842437,
+ lng: 120.13025155062624
+
+ }
+ 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);
+
+ useEffect(() => {
+ initMap();
+ getMapLeftContent();
+ }, [])
+
+ useEffect(() => {
+ renderGridMap();
+ }, [openLeft, openRight])
+
+ useEffect(() => {
+ // console.log('userPoints changes', userPoints);
+ if (mymap) {
+ removeLayerByName('popupTemp');
+ removeLayerByName('userPointLayer');
+ if (userPoints) {
+ let userPointLayer = L.geoJson(userPoints, {
+ name: 'userPointLayer',
+ 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,
+ zoom: 5
+ });
+ store.dispatch(setMymap(mymap));
+ // setMymap(mymap);
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors' }).addTo(mymap);
+
+ mymap.on('click', (e) => {
+ 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));
+ }
+ })
+ }
+
+ // init for left content panel, get projects and build tree select antd
+ const getMapLeftContent = async () => {
+ store.dispatch(setMapLoading(true));
+ let project = await ApiProject.list();
+ console.log('project', project);
+ if (project && project.status && project.data && project.data.length > 0) {
+ let projectData = [
+ {
+ "title": 'All',
+ "key": 'all',
+ "children": []
+ }
+ ];
+ project.data.map(n => {
+ n.key = n.id;
+ n.title = n.nama;
+ projectData[0].children.push(n);
+ })
+ store.dispatch(setProjectTree(projectData));
+ // console.log('projectData', projectData);
+ getUserPoints();
+ store.dispatch(setMapLoading(false));
+ }
+ store.dispatch(setMapLoading(false));
+ }
+
+ const onEachFeatureUserPoints = (feature, layer) => {
+ layer.on('click', function(e) {
+ L.DomEvent.stopPropagation(e);
+ if (!store.getState().mapReducer.routingBarVisible) {
+ // proceed only when routing mode is not visible
+ showHighLight(feature);
+ }
+ });
+ }
+
+ function showHighLight(feature) {
+ removeLayerByName('popupTemp');
+ // add highlight
+ L.geoJSON(feature, {
+ style: function(feature) {
+ return {color: 'blue'}
+ },
+ name: 'popupTemp',
+ onEachFeature: function (feature, layer) {
+ layer.name = 'popupTemp'
+ },
+ }).addTo(mymap);
+
+ // opening right panel
+ store.dispatch(setOpenRight(!openRight));
+ store.dispatch(setSelectedFeature(feature));
+ }
+
+ const MapContent = useMemo(() => {
+ return (
+
+
+
+
+
+
+
+ { routingBarVisible && }
+ { isSearchingRoute && (
+
+
+
+ )}
+
+
+
+
+
+
+ )
+ }, [openLeft, openRight, gridLeft, gridMiddle, gridRight, routingBarVisible ])
+
+ const renderGridMap = () => {
+ let middle = GRID_MIDDLE;
+ let left = GRID_LEFT;
+ let right = GRID_RIGHT;
+ if (openLeft && openRight){
+ middle = GRID_MIDDLE;
+ left = GRID_LEFT;
+ right = GRID_RIGHT;
+ }
+ else if (!openLeft && openRight) {
+ middle = GRID_TOTAL - GRID_RIGHT;
+ left = 0;
+ right = GRID_RIGHT;
+ }
+ else if (openLeft && !openRight) {
+ middle = GRID_TOTAL - GRID_LEFT;
+ left = GRID_LEFT;
+ right = 0;
+ }
+ else if (!openLeft && !openRight) {
+ middle = GRID_TOTAL;
+ left = 0;
+ right = 0;
+ }
+ setGridMiddle(middle);
+ setGridLeft(left);
+ setGridRight(right);
+ }
+
+ return MapContent
+}
+
+export default MapMonitoring;
\ No newline at end of file