From ee4575d4e81e35de23a974e8ff1803b458daab4c Mon Sep 17 00:00:00 2001 From: ardhi Date: Thu, 16 Feb 2023 20:59:33 +0700 Subject: [PATCH] add leaflet.markercluster and leaflet-control-geocoder --- package.json | 2 + src/appredux/modules/map/actions.js | 23 ++- src/appredux/modules/map/reducers.js | 8 +- src/components/MapLeftContent/index.js | 6 +- src/components/MapRightContent/index.js | 138 +++++++++++------- src/services/api/base.js | 14 +- .../api/modules/map_monitoring/index.js | 71 +++++---- src/utils/MapUtils.js | 14 ++ src/views/MapMonitoring/MapMonitoring.css | 10 +- src/views/MapMonitoring/index.js | 79 +++++----- 10 files changed, 241 insertions(+), 124 deletions(-) diff --git a/package.json b/package.json index 456954b..2283b53 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,9 @@ "jspdf": "^2.5.1", "jspdf-autotable": "^3.5.25", "leaflet": "^1.8.0", + "leaflet-control-geocoder": "^2.4.0", "leaflet-draw": "^1.0.4", + "leaflet.markercluster": "^1.5.3", "moment": "^2.24.0", "node-sass": "^4.12.0", "numeral": "^2.0.6", diff --git a/src/appredux/modules/map/actions.js b/src/appredux/modules/map/actions.js index 7b1da5c..442deb3 100644 --- a/src/appredux/modules/map/actions.js +++ b/src/appredux/modules/map/actions.js @@ -13,6 +13,7 @@ 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 SET_SELECTED_PROJECT_IDS = 'SET_SELECTED_PROJECT_IDS'; export const setMymap = obj => dispatch => { dispatch({ @@ -84,6 +85,22 @@ export const setIsSearchingRoute = obj => dispatch => { }) } +export const setSelectedProjectIds = obj => dispatch => { + // filter to remove 'all' key + let projectIds = obj || []; + if (obj && obj.length > 0) { + let allIdx = obj.findIndex(n => n === 'all'); + let pos = allIdx + 1; + if (allIdx > -1) { + projectIds = obj.slice(pos); + } + } + + dispatch({ + type: SET_SELECTED_PROJECT_IDS, + payload: projectIds + }) +} export const getUserPoints = async () => { @@ -104,12 +121,14 @@ export const getUserPoints = async () => { feature.properties = { "user_id": n.user_id, - "Name": n.join_first_name ? n.join_first_name : '-', + "Name": n.name ? n.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 : '-', - "image": n.image_selfie ? n.image_selfie : '' // still dummy + "image": n.image_selfie ? n.image_selfie : '', + "Projects": n.projects ? n.projects : null, + "presence_status": n.presence_status ? n.presence_status : null } feature.geometry = { diff --git a/src/appredux/modules/map/reducers.js b/src/appredux/modules/map/reducers.js index 26f9e57..5f2a682 100644 --- a/src/appredux/modules/map/reducers.js +++ b/src/appredux/modules/map/reducers.js @@ -8,7 +8,8 @@ import { SET_OPEN_LEFT, SET_OPEN_RIGHT, SET_ROUTINGBAR_VISIBLE, - SET_IS_SEARCHING_ROUTE + SET_IS_SEARCHING_ROUTE, + SET_SELECTED_PROJECT_IDS } from "./actions"; const initialState = { @@ -21,7 +22,8 @@ const initialState = { userPoints: null, selectedFeature: null, routingBarVisible: false, - isSearchingRoute: false + isSearchingRoute: false, + setSelectedProjectIds: [] } function mapReducer(state = initialState, action) { @@ -46,6 +48,8 @@ function mapReducer(state = initialState, action) { return { ...state, routingBarVisible: action.payload } case SET_IS_SEARCHING_ROUTE: return { ...state, isSearchingRoute: action.payload } + case SET_SELECTED_PROJECT_IDS: + return { ...state, selectedProjectIds: action.payload } default: return state; } diff --git a/src/components/MapLeftContent/index.js b/src/components/MapLeftContent/index.js index 76590d4..d17aa25 100644 --- a/src/components/MapLeftContent/index.js +++ b/src/components/MapLeftContent/index.js @@ -3,7 +3,7 @@ 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 { getUserPoints, setMapLoading, setOpenRight, setProjectTree, setSelectedFeature, setSelectedProjectIds, 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'; @@ -26,13 +26,15 @@ const MapLeftContent = () => { }; const onCheck = (checkedKeys, info) => { console.log('onCheck', checkedKeys, info); - if (checkedKeys.length < 1) { + console.log('clear all user points'); store.dispatch(setUserPoints(null)); store.dispatch(setOpenRight(false)); store.dispatch(setSelectedFeature(null)); + store.dispatch(setSelectedProjectIds([])) return; } + store.dispatch(setSelectedProjectIds(checkedKeys)) getUserPoints(); }; diff --git a/src/components/MapRightContent/index.js b/src/components/MapRightContent/index.js index 2ea2b57..ae26400 100644 --- a/src/components/MapRightContent/index.js +++ b/src/components/MapRightContent/index.js @@ -1,89 +1,129 @@ import React, { useEffect, useMemo, useRef, useState } from 'react' import { useSelector } from 'react-redux'; import PopupButtonActions from '../PopupButtonActions'; +import DEFAULT_USER_ICON from '../../assets/img/avatars/user.png'; import './styles.css' +import { BASE_SIMPRO_LUMEN_IMAGE } from '../../const/ApiConst'; +import { Button, Image } from 'antd'; +import {Icon} from '@iconify/react'; +import closeCircle from '@iconify/icons-mdi/close-circle'; +import closeIcon from '@iconify/icons-mdi/close'; +import { closePopup } from '../../utils/MapUtils'; 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); + // 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 renderListProject = (projects) => { + if (projects && projects.length > 0) { 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 +
- + + + + - + + + + + + - + - + - + diff --git a/src/services/api/base.js b/src/services/api/base.js index b720686..73d4460 100644 --- a/src/services/api/base.js +++ b/src/services/api/base.js @@ -35,24 +35,34 @@ export default class RequestApi { axios.interceptors.response.use( response => response, async (error) => { - // console.log('error axios', error); + // console.log('error axios', JSON.stringify(error)); if (error) { // console.log('stringify', JSON.stringify(error)); const err = JSON.parse(JSON.stringify(error)); + console.log('error', err); if (err.name === 'AxiosError' && err.code === 'ERR_NETWORK') { alert(err.message); return; } if (err.response) { - if (err.response.status === 307 || err.response.status === 403) { + if (err.response.status === 307 || err.response.status === 403 || err.response.status === 401) { // console.log(err.response); alert('Token expired, please re-login'); // clearAllState(); window.localStorage.clear(); + window.location.reload(); // this.props.history.replace('/login'); return; } } + if (err && err.message && err.message.includes('401')) { + alert('Token expired, please re-login'); + // clearAllState(); + window.localStorage.clear(); + window.location.reload(); + // this.props.history.replace('/login'); + return; + } return Promise.reject(error); } } diff --git a/src/services/api/modules/map_monitoring/index.js b/src/services/api/modules/map_monitoring/index.js index e5b46fb..b78f6e5 100644 --- a/src/services/api/modules/map_monitoring/index.js +++ b/src/services/api/modules/map_monitoring/index.js @@ -1,46 +1,55 @@ import moment from "moment"; +import { store } from "../../../../appredux/store"; 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 { selectedProjectIds } = store.getState().mapReducer; + // const URL = `${BASE_SIMPRO_LUMEN}/presence/search` + const URL = `${BASE_SIMPRO_LUMEN}/map-monitoring/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 + // } + // } + 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 - } + "project_id": selectedProjectIds } + console.log('payload', payload); return await RequestApi.Request().post( URL, payload, RequestApi.HeaderWithToken()).then(res => { + // console.log('res map-monitoring', JSON.stringify(res)); if (res) { if (res && res.data && res.data.data) { // console.log('ApiPresence search', res.data.data) diff --git a/src/utils/MapUtils.js b/src/utils/MapUtils.js index 04803b0..fd0214e 100644 --- a/src/utils/MapUtils.js +++ b/src/utils/MapUtils.js @@ -1,3 +1,4 @@ +import { setOpenRight, setSelectedFeature } from "../appredux/modules/map/actions"; import { store } from "../appredux/store"; export const removeLayerByName = (layerName) => { @@ -24,4 +25,17 @@ export const removeLayerByName = (layerName) => { mymap.removeLayer(layerToRemove[i]); } } +} + +export const closePopup = () => { + const { mymap, routingBarVisible } = store.getState().mapReducer; + if (!routingBarVisible) { + // only can close popup when routing mode is not visible + removeLayerByName('popupTemp'); + store.dispatch(setOpenRight(false)); + store.dispatch(setSelectedFeature(null)); + // if (mymap) { + // mymap.invalidateSize(); + // } + } } \ No newline at end of file diff --git a/src/views/MapMonitoring/MapMonitoring.css b/src/views/MapMonitoring/MapMonitoring.css index 34f2c44..1e4a5ad 100644 --- a/src/views/MapMonitoring/MapMonitoring.css +++ b/src/views/MapMonitoring/MapMonitoring.css @@ -8,7 +8,7 @@ margin-left: 45%; } -.image-marker img { +.image-marker-green img { height: 40px !important; width: 40px !important; border-radius: 50%; @@ -16,6 +16,14 @@ border-color: green; } +.image-marker-red img { + height: 40px !important; + width: 40px !important; + border-radius: 50%; + border: solid; + border-color: red; +} + .image-marker-active img { height: 40px !important; width: 40px !important; diff --git a/src/views/MapMonitoring/index.js b/src/views/MapMonitoring/index.js index cda19a4..322e485 100644 --- a/src/views/MapMonitoring/index.js +++ b/src/views/MapMonitoring/index.js @@ -3,12 +3,12 @@ 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 { getUserPoints, setMapLoading, setMymap, setOpenLeft, setOpenRight, setProjectTree, setSelectedFeature, setSelectedProjectIds, 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 { closePopup, 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"; @@ -17,7 +17,11 @@ import "react-toastify/dist/ReactToastify.css"; import './MapMonitoring.css'; import { BASE_SIMPRO_LUMEN_IMAGE } from '../../const/ApiConst'; import DEFAULT_USER_ICON from '../../assets/img/avatars/user.png'; - +import 'leaflet.markercluster/dist/MarkerCluster.Default.css' +import 'leaflet.markercluster/dist/MarkerCluster.css' +import 'leaflet.markercluster/dist/leaflet.markercluster.js' +import 'leaflet-control-geocoder/dist/Control.Geocoder.css' +import 'leaflet-control-geocoder/dist/Control.Geocoder.js' const MapMonitoring = () => { @@ -33,11 +37,18 @@ const MapMonitoring = () => { lng: 120.13025155062624 } - const {userPoints, mymap, openLeft, openRight, routingBarVisible, userHistory, isSearchingRoute} = useSelector(state => state.mapReducer); + const {userPoints, mymap, openLeft, openRight, routingBarVisible, userHistory, isSearchingRoute, selectedFeature} = useSelector(state => state.mapReducer); const [gridMiddle, setGridMiddle] = useState(GRID_MIDDLE); const [gridLeft, setGridLeft] = useState(0); const [gridRight, setGridRight] = useState(0); + let markerCluster = L.markerClusterGroup({ + name: "userPointLayer", + // disableClusteringAtZoom: 17, + showCoverageOnHover: false, + // spiderfyOnMaxZoom: false + }); + useEffect(() => { initMap(); getMapLeftContent(); @@ -58,7 +69,8 @@ const MapMonitoring = () => { onEachFeature: onEachFeatureUserPoints, pointToLayer: pointToLayerUserPoints }); - userPointLayer.addTo(mymap); + // userPointLayer.addTo(mymap); + mymap.addLayer(markerCluster); mymap.fitBounds(userPointLayer.getBounds()); } } @@ -87,14 +99,10 @@ const MapMonitoring = () => { store.dispatch(setMymap(mymap)); // setMymap(mymap); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors' }).addTo(mymap); - + // add searching location (nominatim) on map + L.Control.geocoder().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)); - } + closePopup(); }) } @@ -102,7 +110,7 @@ const MapMonitoring = () => { const getMapLeftContent = async () => { store.dispatch(setMapLoading(true)); let project = await ApiProject.list(); - console.log('project', project); + // console.log('project', project); if (project && project.status && project.data && project.data.length > 0) { let projectData = [ { @@ -118,6 +126,9 @@ const MapMonitoring = () => { }) store.dispatch(setProjectTree(projectData)); // console.log('projectData', projectData); + let selectedProject = project.data.map(n => n.id); + // console.log('selectedProject', selectedProject); + store.dispatch(setSelectedProjectIds(selectedProject)) getUserPoints(); store.dispatch(setMapLoading(false)); } @@ -134,18 +145,22 @@ const MapMonitoring = () => { }); } - const pointToLayerUserPoints = (feature, latlng) => { - // console.log('feature', feature); - // create a marker style - // let logoMarkerStyle = L.Icon.extend({ - // options: { - // iconSize: [85, 90], - // iconAnchor: [38, 86], - // popupAnchor: [0, -80] - // } - // }); + const renderClassMarker = (feature) => { + let output = 'image-marker-red'; + if (selectedFeature?.properties?.user_id === feature?.properties?.user_id) { + output = 'image-marker-active'; + } + else { + if (feature?.properties.presence_status === true) { + output = 'image-marker-green' + } + } - // let logoMarker = new logoMarkerStyle({iconUrl: `${BASE_SIMPRO_LUMEN_IMAGE}/${feature.properties.image}`}); + return output; + } + + // styling points geojson + const pointToLayerUserPoints = (feature, latlng) => { let imgSrc = DEFAULT_USER_ICON; if (feature && feature.properties && feature.properties.image && feature.properties.image !== '') { imgSrc = `${BASE_SIMPRO_LUMEN_IMAGE}/${feature.properties.image}` @@ -153,26 +168,20 @@ const MapMonitoring = () => { let img = `` let logoMarker = L.divIcon({ html: img, - className: 'image-marker', + // className: feature?.properties?.presence_status ? 'image-marker-green' : 'image-marker-red', + className: renderClassMarker(feature), iconSize: [40, 40], iconAnchor: [20, 50], popupAnchor: [0, -80] }); - // let logoMarkerActive = L.divIcon({ - // html: img, - // className: 'image-marker-active', - // iconSize: [40, 40], - // iconAnchor: [20, 50], - // popupAnchor: [0, -80] - // }); - // read the coordinates from your marker let lat = feature.geometry.coordinates[1]; let lon = feature.geometry.coordinates[0]; // create a new marker using the icon style let marker = new L.Marker([lat,lon],{icon: logoMarker}); + markerCluster.addLayer(marker); return marker; } @@ -238,7 +247,7 @@ const MapMonitoring = () => { return ( - +
@@ -261,7 +270,7 @@ const MapMonitoring = () => { )} - +
{selectedFeature?.properties['image'] ? + + : + + }
Name : {selectedFeature?.properties['Name']}
Projects:{renderListProject(selectedFeature?.properties['Projects'])}
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']}