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 ( +
+ +
+ ) + }, [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