TP-50439 map enhancements v2 (#878)

* TP-50439 | map enhancements

* TP-50439 | Map enhancements v2

* TP-50439 | fix missing icon houston

* TP-50439 | revert config files changes

* TP-50439 | revert debug extension console log

* TP-50439 | fix

* TP-50439 | fixes

* TP-50439 | fixes

* TP-50439 | zindex var

* TP-50439 | live location external routing changes

* TP-50439 | external Live Location Fixes

* TP-50439 | internal map view fixes

* TP-50439 | fixes

* TP-50439 | revert api helper

* TP-50439 | submodule update

* TP-50439 | feedback selection fix

* TP-50439 | fix

---------

Co-authored-by: yashmantri <mantri.ramkishor@navi.com>
This commit is contained in:
Aman Chaturvedi
2024-03-08 20:21:02 +05:30
committed by GitHub
parent 8442f199eb
commit 6caa7c8bfa
42 changed files with 946 additions and 447 deletions

View File

@@ -37,6 +37,7 @@
},
"rules": {
"react/react-in-jsx-scope": "off",
"react/display-name": "off",
"react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error",
"react-hooks/rules-of-hooks": "error",

View File

@@ -1,26 +1,27 @@
window.config = {
BFF_SERVICE_BASE_URL: '/api',
EXTENSION_PLUGIN_USERS_LIST: [],
APM_BASE_URL: 'https://apm-server.np.navi-sa.in/',
APM_APP_NAME: 'collections-portal',
AUTH_BASE_URL: 'https://dev-dark-knight.np.navi-sa.in',
AUTH_CLIENT_ID: 'KBDpUxvr5S',
SENTRY_DSN: 'https://6f03f79661684b70a2e501dde312402d@glitchtip.cmd.navi-tech.in/126',
BUILD_TIME: 0,
ENABLE_SSO: "true",
ENV: "dev",
FCM_apiKey: 'AIzaSyAmUlPwmxtjehTcPBOH3HGxGP-BdWvC9JY',
FCM_authDomain: 'longhornchat-qa.firebaseapp.com',
FCM_databaseURL: 'https://address-verification-app-default-rtdb.firebaseio.com',
FCM_projectId: 'longhornchat-qa',
FCM_storageBucket: 'longhornchat-qa.appspot.com',
FCM_messagingSenderId: '767234038375',
FCM_appId: '1:767234038375:web:a6804341cbaa889b5c12c2',
FCM_measurementId: 'G-CV730KBYP6',
FCM_VapidKey: 'BBWfszq15lfSiA2IdSIi9mwo8vK52D47wlQBSHyMbdyaufuy5b13DAvNETyB-dvxROz5C_JKuOobbEfZt4Dpvzk',
DISABLE_AMEYO_TOOLBAR: "false",
GOOGLE_CAPTCHA_SITE_KEY: "6LezfLIlAAAAABGHea7siv00VaZhRjfcPoCEI6_c",
GOOGLE_MAPS_KEY: '',
GOOGLE_MAP_ID: '',
HRC_CALL_AUTO_ACCEPT_TIMEOUT: "2",
BFF_SERVICE_BASE_URL: '/api',
EXTENSION_PLUGIN_USERS_LIST: [],
APM_BASE_URL: 'https://apm-server.np.navi-sa.in/',
APM_APP_NAME: 'collections-portal',
AUTH_BASE_URL: 'https://dev-dark-knight.np.navi-sa.in',
AUTH_CLIENT_ID: 'KBDpUxvr5S',
SENTRY_DSN: 'https://6f03f79661684b70a2e501dde312402d@glitchtip.cmd.navi-tech.in/126',
BUILD_TIME: 0,
ENABLE_SSO: 'true',
ENV: 'dev',
FCM_apiKey: 'AIzaSyAmUlPwmxtjehTcPBOH3HGxGP-BdWvC9JY',
FCM_authDomain: 'longhornchat-qa.firebaseapp.com',
FCM_databaseURL: 'https://address-verification-app-default-rtdb.firebaseio.com',
FCM_projectId: 'longhornchat-qa',
FCM_storageBucket: 'longhornchat-qa.appspot.com',
FCM_messagingSenderId: '767234038375',
FCM_appId: '1:767234038375:web:a6804341cbaa889b5c12c2',
FCM_measurementId: 'G-CV730KBYP6',
FCM_VapidKey:
'BBWfszq15lfSiA2IdSIi9mwo8vK52D47wlQBSHyMbdyaufuy5b13DAvNETyB-dvxROz5C_JKuOobbEfZt4Dpvzk',
DISABLE_AMEYO_TOOLBAR: 'false',
GOOGLE_CAPTCHA_SITE_KEY: '6LezfLIlAAAAABGHea7siv00VaZhRjfcPoCEI6_c',
GOOGLE_MAPS_KEY: '',
GOOGLE_MAP_ID: '',
HRC_CALL_AUTO_ACCEPT_TIMEOUT: '2'
};

View File

@@ -141,6 +141,7 @@
--z-index-external-sensei-toggle-tabs: 3;
--z-index-external-sensei-header: 4;
--z-index-dropdown-picker: 10;
--z-index-map-options: 10;
--z-index-side-logout-btn: 42;
--z-index-ameyo-disconnect-btn: 99;
--z-index-leaderboard-overlay: 99;
@@ -150,6 +151,7 @@
--z-index-autocomplete-dropdown: 100;
--z-index-ameyo-collapsible-toolbar: 100;
--z-index-autocomplete-picker: 100;
--z-index-maps-loader-overlay: 100;
--z-index-sticky-note: 999;
--z-index-audio-player: 1001;
--z-index-leaderboard-overlay-active: 1002;

View File

@@ -17,6 +17,7 @@ const SingleAutocompleteDropdown = forwardRef<HTMLInputElement, ISingleAutocompl
error,
disabled,
sortOptions,
hideCloseOption,
customSortFunction,
customOptionTemplate,
onSearch,
@@ -209,7 +210,7 @@ const SingleAutocompleteDropdown = forwardRef<HTMLInputElement, ISingleAutocompl
/>
</div>
<div className={styles.iconsContainer}>
{selectedOption && !disabled ? (
{selectedOption && !disabled && !hideCloseOption ? (
<CloseIcon className={styles.clearAll} onClick={handleClearAll} />
) : null}
<ArrowIcon width={9} />

View File

@@ -32,6 +32,7 @@ export interface ISingleAutocompleteDropdown extends ComponentPropsWithRef<'inpu
error?: string;
disabled?: boolean;
sortOptions?: boolean;
hideCloseOption?: boolean;
onSearch?: (searchTerm: string) => void;
onSelectionChange: (selectedOptions: IOption | null) => void;
customOptionTemplate?: (option: IOption) => React.ReactNode;

View File

@@ -16,7 +16,9 @@ export const DASHBOARD_URL = location.origin + '/calling-agent/dashboard';
export const ADMIN_CASES_URL = location.origin + '/admin';
export const persistQueryParamsRoutes = [
APP_ROUTES_PATHS_WITH_PATH_PARAM.SENSEI.external_allocation_view
APP_ROUTES_PATHS_WITH_PATH_PARAM.SENSEI.external_allocation_view,
APP_ROUTES_PATHS_WITH_PATH_PARAM.SENSEI.external_live_location,
APP_ROUTES_PATHS_WITH_PATH_PARAM.SENSEI.live_location
];
export default SideBarItems;

View File

@@ -25,7 +25,7 @@ import Switch from '@navi/web-ui/lib/primitives/Switch/Switch';
import { refreshCallData, setManualLoginCreds } from 'src/reducers/humanReminderSlice';
import { ExtensionHandler } from 'src/utils/extension.utils';
import { AMEYO_STATUS_CODES } from 'src/service/naviExtension.service';
import APP_ROUTES from 'src/layout/Routes';
import APP_ROUTES, { APP_ROUTES_PATHS_WITH_PATH_PARAM } from 'src/layout/Routes';
import { toast } from '@navi/web-ui/lib/primitives/Toast/core/toast';
import {
AMEYO_SESSION_EVENTS,
@@ -404,7 +404,7 @@ function SideNavBar({ isDc97User, isHRCChatUser }: ISideNavbarProps) {
route={
externalAgencyPerformanceDashboard
? APP_ROUTES.SENSEI_EXTERNAL.relativePath
: APP_ROUTES.PERFORMANCE_DASHBOARD.path
: APP_ROUTES_PATHS_WITH_PATH_PARAM.SENSEI.daily_planning
}
linkClickHandler={linkClickHandler}
currentPathname={pathname}
@@ -510,19 +510,6 @@ function SideNavBar({ isDc97User, isHRCChatUser }: ISideNavbarProps) {
/>
) : null}
{fieldTLAndClusterAndZonalManagers ? (
<SidebarLinks
key={APP_ROUTES.SENSEI.path}
activeIcon={<AgencyPincodeMappingIcon />}
inActiveIcon={<AgencyPincodeMappingIcon />}
name={'Tracker'}
route={APP_ROUTES.SENSEI.relativePath}
linkClickHandler={linkClickHandler}
currentPathname={pathname}
currentSearch={search}
clickStreamEvent={CLICKSTREAM_EVENT_NAMES.LH_FIELD_DASHBOARD_SIDE_PANEL_CLICK}
/>
) : null}
{isPincodeMappingVisible ? (
<SidebarLinks
key={APP_ROUTES.AGENCY_PINCODE_MAPPING.path}

View File

@@ -105,6 +105,8 @@ const HideTopNavBarRoutes = [
APP_ROUTES_PATHS_WITH_PATH_PARAM.SENSEI.overall_performance,
APP_ROUTES_PATHS_WITH_PATH_PARAM.SENSEI.field_governance,
APP_ROUTES_PATHS_WITH_PATH_PARAM.SENSEI.external_daily_planning,
APP_ROUTES_PATHS_WITH_PATH_PARAM.SENSEI.live_location,
APP_ROUTES_PATHS_WITH_PATH_PARAM.SENSEI.external_live_location,
APP_ROUTES_PATHS_WITH_PATH_PARAM.SENSEI.external_allocation_view,
APP_ROUTES.AMEYO_UTILITY.path,
APP_ROUTES.GENERATE_AMEYO_PASSWORD.path,

View File

@@ -27,6 +27,8 @@ export const APP_ROUTES_PATHS_WITH_PATH_PARAM = {
overall_performance: '/sensei-external/overall_performance',
field_governance: '/sensei-external/field_governance',
external_daily_planning: '/sensei-external/daily_planning',
external_live_location: '/sensei-external/live_location',
live_location: '/sensei/live_location',
overall_metric: '/sensei/overall_metric',
external_allocation_view: '/sensei-external/allocation_view'
}

View File

@@ -13,6 +13,7 @@ $headerzIndex: 3;
.headerTitle {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
@@ -20,27 +21,13 @@ $headerzIndex: 3;
color: var(--navi-color-gray-c3);
}
.lastRefreshedContainer {
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
}
.refresh {
cursor: pointer;
.refreshIcon {
margin-right: 4px;
.refreshWrapper {
color: var(--blue-base);
align-items: center;
display: flex;
.refreshIcon {
margin-right: 4px;
fill {
color: var(--blue-base);
}
fill {
color: var(--blue-base);
}
}
}

View File

@@ -1,19 +1,9 @@
import LocationIcon from '@cp/src/assets/icons/Location';
import APP_ROUTES from '@cp/src/layout/Routes';
import { LIVE_LOCATION_SOURCE } from '@cp/src/pages/LiveLocationTracker/constants/LiveLocatonTrackerConstants';
import { getLiveLocationParams } from '@cp/src/pages/LiveLocationTracker/utils';
import { getCaseAllocationViewData } from '@cp/src/pages/MapAllocationView/actions/CaseAllocationActions';
import { showAgentsLiveLocation } from '@cp/src/pages/PerformanceDashboard/PerformanceDashboard';
import {
PERFORMANCE_DASHBOARD_CLICKSTREAM,
PERFORMANCE_DASHBOARD_EVENTS
} from '@cp/src/service/clickStream.constant';
import { addClickstreamEvent } from '@cp/src/service/clickStreamEventService';
import { RootState } from '@cp/src/store';
import { DateFormat, dateFormat } from '@cp/src/utils/DateHelper';
import { readQueryParams } from '@cp/src/utils/QueryParamsHelper';
import { RefreshIcon } from '@navi/web-ui/lib/icons';
import { Button, Typography } from '@navi/web-ui/lib/primitives';
import { Typography } from '@navi/web-ui/lib/primitives';
import { useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate, useParams } from 'react-router-dom';
@@ -24,24 +14,36 @@ import {
getDpdBucketData
} from '../../actions/ExternalDashboardSenseiActions';
import { TabsKey } from '../../constants';
import { getLastUpdatedAt } from '../../utils';
import {
getFieldGovernanceLastUpdatedAt,
getLastUpdatedAt,
getOverallPerformanceLastUpdatedAt
} from '../../utils';
import styles from './index.module.scss';
import { fetchCommitments, fetchPTPDetails } from '@cp/src/pages/sensei/actions';
import { AGENT_LIVE_LOCATION } from '@cp/src/pages/LiveLocationTracker/constants/LiveLocatonTrackerConstants';
import { getAgentDetails } from '@cp/src/pages/LiveLocationTracker/utils';
import { getAgentsLocations } from '@cp/src/pages/LiveLocationTracker/actions/LiveLocationTrackerActions';
import { FilterTypes } from '@cp/src/pages/LiveLocationTracker/constants/LiveLocationTrackerInterfaces';
const Header = () => {
const queryParams = readQueryParams();
const agentLiveLocationParams = queryParams?.[AGENT_LIVE_LOCATION] || {};
const navigate = useNavigate();
const dispatch = useDispatch();
const { tabId = '' } = useParams();
const { allLastUpdatedAt, userData, featureFlags, dailyPlanningLastUpdatedAt } = useSelector(
(state: RootState) => ({
allLastUpdatedAt: state?.externalDashboardSensei?.allLastUpdatedAt,
userData: state?.common?.userData,
featureFlags: state?.common?.featureFlags,
dailyPlanningLastUpdatedAt: state?.sensai?.dailyPlanning?.commitmentsTable?.lastUpdatedAt
})
);
const {
allLastUpdatedAt,
featureFlags,
dailyPlanningLastUpdatedAt,
liveLocationTrackerLastUpdatedAt
} = useSelector((state: RootState) => ({
allLastUpdatedAt: state?.externalDashboardSensei?.allLastUpdatedAt,
featureFlags: state?.common?.featureFlags,
dailyPlanningLastUpdatedAt: state?.sensai?.dailyPlanning?.commitmentsTable?.lastUpdatedAt,
liveLocationTrackerLastUpdatedAt: state.liveLocationTracker?.lastUpdatedAt
}));
const {
overallPerformanceOverallMetricsTable,
@@ -51,79 +53,103 @@ const Header = () => {
agentAllocationMapViewFlag
} = featureFlags || {};
const lastUpdatedAt = useMemo(
() => getLastUpdatedAt({ ...allLastUpdatedAt, dailyPlanningLastUpdatedAt }, tabId),
[tabId, allLastUpdatedAt, dailyPlanningLastUpdatedAt]
);
const { roles } = userData || {};
const handleRefresh = () => {
switch (tabId) {
case TabsKey.OVERALL_PERFORMANCE:
if (overallPerformanceOverallMetricsTable) dispatch(getDpdBucketData());
if (overallPerformanceAgentPerformanceTable) dispatch(getAgentPerformanceTLData());
break;
case TabsKey.FIELD_GOVERNANCE:
if (fieldGovernanceAgencyPerformanceTable) dispatch(getAgencyDetailsData());
if (fieldGovernanceAgentPerformanceTable) dispatch(getAgentPerformanceData());
break;
case TabsKey.DAILY_PLANNING:
dispatch(fetchCommitments());
dispatch(fetchPTPDetails());
break;
const { lastUpdatedAt, handleRefresh } = useMemo(() => {
let handleRefresh = null;
let lastUpdatedAt = null;
case TabsKey.ALLOCATION_VIEW:
if (agentAllocationMapViewFlag) dispatch(getCaseAllocationViewData());
switch (tabId) {
case TabsKey.DAILY_PLANNING: {
handleRefresh = () => {
dispatch(fetchCommitments());
dispatch(fetchPTPDetails());
};
lastUpdatedAt = dailyPlanningLastUpdatedAt;
break;
}
case TabsKey.OVERALL_PERFORMANCE: {
handleRefresh = () => {
if (overallPerformanceOverallMetricsTable) dispatch(getDpdBucketData());
if (overallPerformanceAgentPerformanceTable) dispatch(getAgentPerformanceTLData());
};
lastUpdatedAt = getOverallPerformanceLastUpdatedAt(allLastUpdatedAt);
break;
}
case TabsKey.FIELD_GOVERNANCE: {
handleRefresh = () => {
if (fieldGovernanceAgencyPerformanceTable) dispatch(getAgencyDetailsData());
if (fieldGovernanceAgentPerformanceTable) dispatch(getAgentPerformanceData());
};
lastUpdatedAt = getFieldGovernanceLastUpdatedAt(allLastUpdatedAt);
break;
}
case TabsKey.ALLOCATION_VIEW: {
const isAllocationFiltersSelected = Object.keys(queryParams?.allocationView || {})?.length;
if (!isAllocationFiltersSelected) return { lastUpdatedAt: null, handleRefresh: null };
handleRefresh = () => {
if (agentAllocationMapViewFlag) dispatch(getCaseAllocationViewData());
};
lastUpdatedAt = allLastUpdatedAt?.allocationViewLastUpdatedAt;
break;
}
case TabsKey.LIVE_LOCATION:
if (!agentLiveLocationParams?.AGENCY) {
return { lastUpdatedAt: null, handleRefresh: null };
}
handleRefresh = () => {
if (agentLiveLocationParams?.AGENTID) {
dispatch(
getAgentDetails(
{
date: agentLiveLocationParams?.DATE,
referenceId: agentLiveLocationParams?.AGENTID
},
navigate
)
);
} else {
const selectedTeamLead = agentLiveLocationParams[FilterTypes.TEAM_LEAD];
const selectedAgency = agentLiveLocationParams[FilterTypes.AGENCY];
const payload = {
agencyCodes: [selectedAgency],
teamLeadReferenceIds: selectedTeamLead ? [selectedTeamLead] : []
};
dispatch(getAgentsLocations(payload, false));
}
};
lastUpdatedAt = liveLocationTrackerLastUpdatedAt;
break;
default:
break;
}
};
const enableAgentLiveLocation = useMemo(() => showAgentsLiveLocation(roles), [roles]);
const handleLiveLocationClick = () => {
addClickstreamEvent(
PERFORMANCE_DASHBOARD_CLICKSTREAM[PERFORMANCE_DASHBOARD_EVENTS.LH_AGENT_TRACKING_CLICKED],
{ userId: userData?.referenceId }
);
const updatedParams = getLiveLocationParams(LIVE_LOCATION_SOURCE.EXTERNAL_SENSEI);
navigate(`${APP_ROUTES.LIVE_LOCATION_TRACKER.path}${updatedParams}`);
};
const queryParams = readQueryParams();
const isAllocationFiltersSelected = Object.keys(queryParams?.allocationView || {})?.length;
const showExtraInfo = tabId === TabsKey.ALLOCATION_VIEW ? isAllocationFiltersSelected : true;
return { lastUpdatedAt, handleRefresh };
}, [
tabId,
queryParams?.SELECTED_FEEDBACK_ID,
allLastUpdatedAt,
dailyPlanningLastUpdatedAt,
liveLocationTrackerLastUpdatedAt
]);
return (
<div className={styles.header}>
<div className={styles.headerTitle}>
<Typography variant="h3">Agent Dashboard</Typography>
{showExtraInfo ? (
<>
<div className="flex gap-2">
{lastUpdatedAt ? (
<Typography variant="p5" className={styles.lastUpdatedAt}>
Last updated at{' '}
{lastUpdatedAt
? dateFormat(new Date(lastUpdatedAt), DateFormat.LONG_DATE_FORMAT_WITH_TIME)
: '--'}
</Typography>
) : null}
{handleRefresh ? (
<div className={styles.refresh} onClick={handleRefresh}>
<RefreshIcon className={styles.refreshIcon} color="var(--navi-color-blue-base)" />
</div>
</>
) : null}
</div>
<div className={styles.lastRefreshedContainer}>
{enableAgentLiveLocation ? (
<div>
<Button
onClick={handleLiveLocationClick}
startAdornment={<LocationIcon />}
variant="text"
>
Live location tracker
</Button>
</div>
) : null}
) : null}
</div>
</div>
</div>
);

View File

@@ -28,6 +28,7 @@ export enum TabsKey {
OVERALL_PERFORMANCE = 'overall_performance',
FIELD_GOVERNANCE = 'field_governance',
DAILY_PLANNING = 'daily_planning',
LIVE_LOCATION = 'live_location',
ALLOCATION_VIEW = 'allocation_view'
}

View File

@@ -10,7 +10,7 @@ import { RootState } from '@cp/src/store';
import { addClickstreamEvent } from '@cp/src/service/clickStreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '@cp/src/service/clickStream.constant';
import Header from './components/Header';
import APP_ROUTES, { APP_ROUTES_PATHS_WITH_PATH_PARAM } from '@cp/src/layout/Routes';
import APP_ROUTES from '@cp/src/layout/Routes';
import { interpolatePathParams } from '@cp/src/utils/interpolate';
import { LocalStorage } from '@cp/src/utils/StorageUtils';
@@ -26,12 +26,7 @@ const ExternalDashboardSensei = () => {
const paramsMap: Record<string, string> = LocalStorage.getItem('paramsMap') || {};
const handleTabChange = (updateTabId: TabItemKey) => {
let currentParams = '';
if (tabId !== updateTabId) {
if (updateTabId === TabsKey.ALLOCATION_VIEW) {
currentParams =
paramsMap[APP_ROUTES_PATHS_WITH_PATH_PARAM.SENSEI.external_allocation_view] || '';
}
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.LH_FIELD_DASHBOARD_TAB_SWITCH, {
currentTab: tabId,
newTab: updateTabId
@@ -39,6 +34,7 @@ const ExternalDashboardSensei = () => {
const updatedUrl = interpolatePathParams(APP_ROUTES.SENSEI_EXTERNAL.path, {
tabId: String(updateTabId)
});
const currentParams = paramsMap[updatedUrl] || '';
navigate(updatedUrl + currentParams);
}
};
@@ -57,17 +53,14 @@ const ExternalDashboardSensei = () => {
TabsKey.FIELD_GOVERNANCE,
TabsKey.OVERALL_PERFORMANCE,
TabsKey.DAILY_PLANNING,
TabsKey.ALLOCATION_VIEW
TabsKey.ALLOCATION_VIEW,
TabsKey.LIVE_LOCATION
].includes(tabId as TabsKey);
if (isTabAccessNotAvailable || noTabSelected) {
const tabId = dashboardTabs?.[0]?.relativePath;
let currentParams = '';
if (tabId.includes(TabsKey.ALLOCATION_VIEW)) {
currentParams =
paramsMap[APP_ROUTES_PATHS_WITH_PATH_PARAM.SENSEI.external_allocation_view] || '';
}
navigate(tabId + currentParams);
const relativePath = dashboardTabs?.[0]?.relativePath;
const currentParams = paramsMap[relativePath] || '';
navigate(relativePath + currentParams);
}
}, [tabId, dashboardTabs]);

View File

@@ -155,6 +155,7 @@ export interface IAllLastUpdateAt {
agentPerformanceTlLastUpdatedAt: number;
dailyPlanningLastUpdatedAt: number;
allocationViewLastUpdatedAt: number;
liveLocationTrackerLastUpdatedAt: string | Date;
}
export interface IExternalSenseiDashboard {

View File

@@ -7,10 +7,16 @@ import AllocationView from '../MapAllocationView';
import OverallPerformance from './OverallPerformance';
import { FIELD_GOVERNANCE_TABS_VALUE, IAllLastUpdateAt } from './types';
import ExternalDailyPlanning from './components/DailyPlanning';
import LiveLocationTracker from '../LiveLocationTracker';
export const getTabs = (featureFlags: IFeatureFlags, tabId: string) => {
const { overallPerformanceTab, fieldGovernanceTab, fcmDashboard, agentAllocationMapViewFlag } =
featureFlags || {};
const {
overallPerformanceTab,
fieldGovernanceTab,
fcmDashboard,
agentAllocationMapViewFlag,
liveLocationTrackerFeatureFlag
} = featureFlags || {};
const tabs = [];
@@ -41,6 +47,15 @@ export const getTabs = (featureFlags: IFeatureFlags, tabId: string) => {
});
}
if (liveLocationTrackerFeatureFlag) {
tabs.push({
key: TabsKey.LIVE_LOCATION,
value: 'Live location tracker',
component: <LiveLocationTracker />,
relativePath: '/sensei-external/live_location'
});
}
if (agentAllocationMapViewFlag) {
tabs.push({
key: TabsKey.ALLOCATION_VIEW,
@@ -121,6 +136,30 @@ export const setInitialOverallPerformanceTabParams = (
navigate(updatedParams);
};
export const getOverallPerformanceLastUpdatedAt = (allLastUpdatedAt: IAllLastUpdateAt) => {
const { dpdBucketLastUpdatedAt, agentPerformanceTlLastUpdatedAt } = allLastUpdatedAt;
if (!dpdBucketLastUpdatedAt) return agentPerformanceTlLastUpdatedAt;
if (!agentPerformanceTlLastUpdatedAt) return dpdBucketLastUpdatedAt;
if (dpdBucketLastUpdatedAt < agentPerformanceTlLastUpdatedAt) {
return dpdBucketLastUpdatedAt;
}
return agentPerformanceTlLastUpdatedAt;
};
export const getFieldGovernanceLastUpdatedAt = (allLastUpdatedAt: IAllLastUpdateAt) => {
const { agencyDetailsLastUpdatedAt, agentPerformanceLastUpdatedAt } = allLastUpdatedAt;
if (!agencyDetailsLastUpdatedAt) return agentPerformanceLastUpdatedAt;
if (!agentPerformanceLastUpdatedAt) return agencyDetailsLastUpdatedAt;
if (agencyDetailsLastUpdatedAt < agentPerformanceLastUpdatedAt) {
return agencyDetailsLastUpdatedAt;
}
return agentPerformanceLastUpdatedAt;
};
export const getLastUpdatedAt = (allLastUpdatedAt: IAllLastUpdateAt, tabId: string) => {
const {
dpdBucketLastUpdatedAt,
@@ -128,7 +167,8 @@ export const getLastUpdatedAt = (allLastUpdatedAt: IAllLastUpdateAt, tabId: stri
agentPerformanceLastUpdatedAt,
agentPerformanceTlLastUpdatedAt,
dailyPlanningLastUpdatedAt,
allocationViewLastUpdatedAt
allocationViewLastUpdatedAt,
liveLocationTrackerLastUpdatedAt
} = allLastUpdatedAt;
if (tabId === TabsKey.OVERALL_PERFORMANCE) {
@@ -158,4 +198,7 @@ export const getLastUpdatedAt = (allLastUpdatedAt: IAllLastUpdateAt, tabId: stri
if (tabId === TabsKey.ALLOCATION_VIEW) {
return allocationViewLastUpdatedAt;
}
if (tabId === TabsKey.LIVE_LOCATION) {
return liveLocationTrackerLastUpdatedAt;
}
};

View File

@@ -3,14 +3,22 @@ import useMap from '../../hooks/useMap';
import styles from './agentMarker.module.scss';
import Tooltip from '../Tooltip';
import { getNameInitials } from 'src/utils/commonUtils';
import { AgentMarkerProps, ColorMap } from '../../constants/LiveLocationTrackerInterfaces';
import { useSelector } from 'react-redux';
import {
AgentMarkerProps,
ColorMap,
LOCATION_TYPE_FILTERS
} from '../../constants/LiveLocationTrackerInterfaces';
import { useDispatch, useSelector } from 'react-redux';
import { RootState } from '../../../../store';
import { addClickstreamEvent } from 'src/service/clickStreamEventService';
import { AgentTrackingEvents } from 'src/service/clickStream.constant';
import AgentMarkerIcon from './AgentMarkerIcon';
import { shouldZoomIn, loadCustomPopup } from '../../utils';
import { ICustomPopup } from '../../types';
import { AGENT_LIVE_LOCATION, PinColorMapping } from '../../constants/LiveLocatonTrackerConstants';
import { setSelectedFeedback } from '../../reducers/LiveLocationTrackerSlice';
import { createQueryParams, readQueryParams } from '@cp/src/utils/QueryParamsHelper';
import { useNavigate } from 'react-router-dom';
export const colorMap: ColorMap = {
yellow: {
@@ -38,19 +46,11 @@ export interface InfoWindowRef {
close: () => void;
}
const AgentMarker = ({
color,
position,
name,
lastUpdatedAt,
referenceId,
type,
lanNo,
interactionId,
interactionStatus,
activeCustomPopup,
refId
}: AgentMarkerProps) => {
export const AgentMarker = ({ agentLocation, activeCustomPopup, position }: AgentMarkerProps) => {
const { name, referenceId, type, lanNo, interactionId, activityLevel, location, zIndex } =
agentLocation;
const refId = referenceId || lanNo || interactionId || '';
const color = PinColorMapping[activityLevel] ?? 'red';
const map = useMap();
const markerRef = useRef(null);
const markerInstance: any = useRef(null);
@@ -59,15 +59,24 @@ const AgentMarker = ({
const initials = getNameInitials(name);
const isInfoWindowVisible = useRef(false);
const customPopupRef = useRef<ICustomPopup | null>(null);
const dispatch = useDispatch();
const navigate = useNavigate();
const { [AGENT_LIVE_LOCATION]: queryParams = {} } = readQueryParams();
const { selectedAgent, isShowAllInfoWindow, userId, selectedFeedback } = useSelector(
(store: RootState) => ({
const { selectedAgent, isShowAllInfoWindow, userId, selectedFeedback, agentAllocations } =
useSelector((store: RootState) => ({
selectedAgent: store?.liveLocationTracker?.selectedAgent,
isShowAllInfoWindow: store?.liveLocationTracker?.showAllInfoWindows,
userId: store?.common?.userData?.referenceId,
selectedFeedback: store?.liveLocationTracker?.selectedFeedback
})
);
selectedFeedback: store?.liveLocationTracker?.selectedFeedback,
agentAllocations: store?.liveLocationTracker?.agentAllocations?.data
}));
const selectedFeedbackRef = useRef(selectedFeedback);
useEffect(() => {
selectedFeedbackRef.current = selectedFeedback;
}, [selectedFeedback]);
useEffect(() => {
if (
@@ -103,6 +112,33 @@ const AgentMarker = ({
}
}, [selectedFeedback]);
const feedbackClickHandler = () => {
if (!lanNo) {
return;
}
const selectedFeedback = selectedFeedbackRef.current;
const data = agentAllocations?.[lanNo];
const isSelectedFeedbackClicked = selectedFeedback?.interactionId === interactionId;
dispatch(
isSelectedFeedbackClicked
? setSelectedFeedback(null)
: setSelectedFeedback({
lanNo,
interactionId,
feedbackLocation: location,
caseLocation: data?.location,
isSuspicious: type === LOCATION_TYPE_FILTERS.SUSPICIOUS_FEEDBACK
})
);
const newQueryParams = {
...queryParams
};
const url = createQueryParams({
[AGENT_LIVE_LOCATION]: newQueryParams
});
navigate(url);
};
useEffect(() => {
if (map?.state?.mapLoaderInstance) {
map?.state?.mapLoaderInstance
@@ -111,7 +147,8 @@ const AgentMarker = ({
markerInstance.current = new AdvancedMarkerElement({
position: position,
map: map.state.mapInstance,
content: markerRef.current
content: markerRef.current,
zIndex
});
const CustomPopup = loadCustomPopup(type);
if (tooltipRef.current) {
@@ -133,6 +170,12 @@ const AgentMarker = ({
name
}
});
if (
type === LOCATION_TYPE_FILTERS.GENUINE_FEEDBACK ||
type === LOCATION_TYPE_FILTERS.SUSPICIOUS_FEEDBACK
) {
feedbackClickHandler();
}
} else {
addClickstreamEvent(AgentTrackingEvents.LH_AGENT_TRACKING_PIN_CLICKED, {
userId,
@@ -181,12 +224,8 @@ const AgentMarker = ({
<AgentMarkerIcon type={type} color={color} isFocused={isFocused} initials={initials} />
<div className={styles.tooltip}>
<Tooltip
name={name}
lastUpdateActivity={lastUpdatedAt}
ref={tooltipRef}
type={type}
lanNo={lanNo}
interactionStatus={interactionStatus}
agentLocation={agentLocation}
closeTooltip={handleCloseTooltip}
/>
</div>

View File

@@ -1,4 +1,3 @@
import { BorderedInput } from '@navi/web-ui/lib/primitives';
import React, { useEffect, useMemo, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
@@ -15,7 +14,6 @@ import styles from './index.module.scss';
import { _map } from 'src/utils/commonUtils';
import { IFeedback, LOCATION_TYPE_FILTERS } from '../../constants/LiveLocationTrackerInterfaces';
import { setSelectedFeedback } from '../../reducers/LiveLocationTrackerSlice';
import useMap from '../../hooks/useMap';
import DateTimePickerComponent from '@cp/src/components/DateTimePicker/DateTimePickerComponent';
import { DATE_TIME_TYPE } from '@cp/src/components/DateTimePicker/constants';
@@ -35,6 +33,7 @@ const AccordionBody = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const polylineRef = useRef<any>(null);
const feedbackRefs = useRef<Record<string, HTMLDivElement | null>>({});
const { [AGENT_LIVE_LOCATION]: queryParams = {} } = readQueryParams();
@@ -86,6 +85,19 @@ const AccordionBody = () => {
return lanKeys;
}, [agentAllocations]);
useEffect(() => {
if (selectedFeedback && !queryParams?.SELECTED_FEEDBACK_ID) {
// scroll to the selected feedback view
const feedbackRef = feedbackRefs.current[selectedFeedback.lanNo];
if (feedbackRef) {
feedbackRef.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
}
}, [selectedFeedback, queryParams?.SELECTED_FEEDBACK_ID]);
const feedbackClickHandler = (lanNo: string, feedback: IFeedback) => {
const data = agentAllocations?.[lanNo];
const isSelectedFeedbackClicked = selectedFeedback?.interactionId === feedback?.interactionId;
@@ -103,7 +115,7 @@ const AccordionBody = () => {
);
addClickstreamEvent(
isSelectedFeedbackClicked
? AgentTrackingEvents.LH_MAP_FEEDBACK_CLICKED
? AgentTrackingEvents.LH_MAP_FEEDBACK_DOUBLE_CLICKED
: AgentTrackingEvents.LH_MAP_FEEDBACK_CLICKED,
{
userId,
@@ -129,7 +141,7 @@ const AccordionBody = () => {
if (selectedAgent) {
return (
<>
<div>
<div className={styles.dateFilterContainer}>
<div className={styles.activityLog}>ACTIVITY LOG</div>
@@ -143,18 +155,21 @@ const AccordionBody = () => {
containerClasses={styles.activityDatePicker}
/>
</div>
{agentAllocationsLoading ? null : lanKeys?.length ? (
lanKeys.map(lanNo => (
<FeedbackDetail
key={lanNo}
lanNo={lanNo}
feedbackClickHandler={feedback => feedbackClickHandler(lanNo, feedback)}
/>
))
) : (
<div className={styles.noDataFound}>No Feedbacks Found</div>
)}
</>
<div className="overflow-y-auto max-h-[calc(100vh-312px)]">
{agentAllocationsLoading ? null : lanKeys?.length ? (
lanKeys.map(lanNo => (
<FeedbackDetail
key={lanNo}
ref={el => (feedbackRefs.current[lanNo] = el)}
lanNo={lanNo}
feedbackClickHandler={feedback => feedbackClickHandler(lanNo, feedback)}
/>
))
) : (
<div className={styles.noDataFound}>No Feedbacks Found</div>
)}
</div>
</div>
);
}
@@ -169,9 +184,11 @@ const AccordionBody = () => {
))}
</div>
{agentLocations?.length ? (
agentLocations?.map(item => (
<AgentDetail key={item.referenceId} item={item} containerStyle={styles.agentHover} />
))
<div className="overflow-y-auto max-h-[calc(100vh-248px)]">
{agentLocations?.map(item => (
<AgentDetail key={item.referenceId} item={item} containerStyle={styles.agentHover} />
))}
</div>
) : (
<div className={styles.noDataFound}>No Agents Found</div>
)}

View File

@@ -36,7 +36,7 @@ const AgentDetail = (props: AgentDetailProps) => {
userId: state?.common?.userData?.referenceId
}));
const { selectedAgent } = liveLocationTracker || {};
const { name = '', activityLevel, referenceId, lastUpdatedAt } = item || {};
const { name = '', activityLevel, referenceId, lastUpdatedAt, totalAllocatedCases } = item || {};
const color = PinColorMapping[activityLevel];
@@ -133,6 +133,7 @@ const AgentDetail = (props: AgentDetailProps) => {
last seen {date} {time}
</div>
)}
<div className={styles.agentLastSeen}>Allocated: {totalAllocatedCases || 0}</div>
</div>
</div>
{showNextIcon && <ArrowRightSolidIcon width={14} />}

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { useSelector } from 'react-redux';
import FeedbackTriangleIcon from 'src/assets/icons/FeedbackTriangleIcon';
import RedirectionIcon from 'src/assets/icons/RedirectionIcon';
import APP_ROUTES from 'src/layout/Routes';
import { TAB_KEYS } from 'src/pages/CaseDetails/constants';
@@ -20,7 +20,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from 'src/components/TooltipV
import ChatBubbleExclamation from 'src/assets/images/icons/ChatBubbleExclamation';
import ChatBubble from 'src/assets/images/icons/ChatBubble';
const FeedbackDetail = (props: FeedbackDetailProps) => {
const FeedbackDetail = React.forwardRef<HTMLDivElement, FeedbackDetailProps>((props, ref) => {
const { lanNo, feedbackClickHandler } = props;
const {
agentAllocations: { data: agentAllocations },
@@ -63,6 +63,7 @@ const FeedbackDetail = (props: FeedbackDetailProps) => {
const isFeedbackSelected = selectedFeedback?.interactionId === feedback?.interactionId;
return (
<div
ref={ref}
key={feedback?.interactionId}
className={cx(styles.feedbackItemWrapper, {
[styles.feedbackSelected]: isFeedbackSelected
@@ -123,6 +124,6 @@ const FeedbackDetail = (props: FeedbackDetailProps) => {
})}
</>
);
};
});
export default FeedbackDetail;

View File

@@ -1,5 +1,6 @@
.agentsAccordionContainer {
width: 260px;
font-family: 'Inter';
.accordion {
border-radius: 8px;
@@ -19,10 +20,7 @@
.accordionBody {
margin: 0;
}
.content {
max-height: calc(100vh - 230px);
overflow-y: hidden;
}
}
@@ -153,8 +151,6 @@
.dateFilterContainer {
padding: 12px 12px 6px 12px;
border-bottom: 1px solid var(--navi-color-gray-border);
position: sticky;
top: 0;
background: var(--navi-color-gray-bg-primary);
.activityLog {

View File

@@ -1,4 +1,4 @@
.googleMapContainer {
width: 100%;
height: calc(100vh - 60px);
height: calc(100vh - 100px); // 100px is the height of the header
}

View File

@@ -0,0 +1,68 @@
@import '../../../../assets/styles/animations';
.blurMapContainer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 2;
.blurMapImg {
object-fit: cover;
width: 100%;
height: 100%;
filter: blur(12.5px);
}
}
.mapPlaceholderContainer {
padding: 20px 16px;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: absolute;
left: 50%;
top: 25%;
transform: translate(-50%);
span {
color: var(--black-base);
}
}
.loader {
border: 8px solid var(--grayscale-background-secondary);
border-top: 8px solid var(--navi-color-blue-light);
border-radius: 50%;
width: 60px;
height: 60px;
animation: spin 2s linear infinite;
}
.autoCompleteMainContainer {
width: 100%;
}
.autoCompleteContainer {
background: white;
margin-top: 4px;
}
.optionLabel {
font-size: 14px;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.0125em;
color: var(--navi-color-gray-c1);
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.selectPickerWrapper {
padding: 0;
max-height: calc((100vh - 100px - 25%) / 2);
}

View File

@@ -0,0 +1,72 @@
import styles from './index.module.scss';
import { Typography } from '@primitives';
import { SelectPickerValue } from '@cp/src/components/interfaces';
import { useNavigate } from 'react-router-dom';
import { QueryParamTypeMapping } from '../../../ExternalDashboardSensei/constants';
import { createQueryParams, readQueryParams } from '@cp/src/utils/QueryParamsHelper';
import { SingleSelectAutoComplete } from '@navi/web-ui/lib/components';
import { useDispatch, useSelector } from 'react-redux';
import { RootState } from '@cp/src/store';
import { addClickstreamEvent } from '@cp/src/service/clickStreamEventService';
import { AgentTrackingEvents } from '@cp/src/service/clickStream.constant';
import AllocationMapPlaceholder from '@cp/src/pages/MapAllocationView/components/AllocationMapPlaceholder';
import { AGENT_LIVE_LOCATION } from '../../constants/LiveLocatonTrackerConstants';
import { FilterTypes } from '../../constants/LiveLocationTrackerInterfaces';
import { useEffect } from 'react';
import { getFilterData } from '../../actions/LiveLocationTrackerActions';
const LocationAgencySearch = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const params = readQueryParams();
const { filterTypes, filterData } = useSelector((state: RootState) => state.liveLocationTracker);
const { data = [] } = filterData['AGENCY'];
useEffect(() => {
// Get agency filter data
dispatch(getFilterData());
}, []);
const { caseAllocation } = useSelector((state: RootState) => ({
caseAllocation: state.caseAllocation
}));
const { reportingAgencies } = caseAllocation || {};
const agencyDropdownHandler = (action: SelectPickerValue) => {
if (!action) return;
const queryParams = { ...params };
if (!queryParams?.[AGENT_LIVE_LOCATION]) queryParams[AGENT_LIVE_LOCATION] = {};
if (!queryParams[AGENT_LIVE_LOCATION]?.[FilterTypes.AGENCY]) {
queryParams[AGENT_LIVE_LOCATION][FilterTypes.AGENCY] = action;
}
// addClickstreamEvent(AgentTrackingEvents.LH_MAP_AUTOCOMPLETE_FILTER_APPLIED, {
// value: action,
// page: ALLOCATION_PAGE
// });
const updatedParams = createQueryParams(queryParams);
navigate(updatedParams);
};
return (
<AllocationMapPlaceholder showImage>
<Typography variant="h2" as="span">
Select an agency to view locations
</Typography>
<SingleSelectAutoComplete
placeholder="Select agency"
noResultTitle=""
options={data}
mainContainerClassName={styles.autoCompleteMainContainer}
containerClassName={styles.autoCompleteContainer}
selectPickerWrapperClass={styles.selectPickerWrapper}
updateSelectedOptions={agencyDropdownHandler}
variant="bordered"
customOptionTemplate={option => <div className={styles.optionLabel}>{option.label}</div>}
/>
</AllocationMapPlaceholder>
);
};
export default LocationAgencySearch;

View File

@@ -1,10 +1,10 @@
.blurMapContainer {
position: absolute;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10;
z-index: var(--z-index-maps-loader-overlay);
.blurMapImg {
object-fit: cover;
@@ -29,6 +29,9 @@
}
.blurImageWrapper {
position: absolute;
top: 0px;
left: 0px;
z-index: 2;
}

View File

@@ -4,11 +4,7 @@ import AgentMarker from '../AgentMarker';
import useMap from '../../hooks/useMap';
import { useSelector } from 'react-redux';
import { RootState } from 'src/store';
import {
AGENT_LIVE_LOCATION,
FilterMap,
PinColorMapping
} from '../../constants/LiveLocatonTrackerConstants';
import { AGENT_LIVE_LOCATION, FilterMap } from '../../constants/LiveLocatonTrackerConstants';
import styles from './index.module.scss';
import { readQueryParams } from 'src/utils/QueryParamsHelper';
import {
@@ -18,8 +14,6 @@ import {
} from '../../constants/LiveLocationTrackerInterfaces';
import { INHOUSE_AGENCY_CODE } from 'src/components/constant';
import MapPlaceholder from '../MapPlaceholder';
import AgentsAccordion from '../AgentsAccordion';
import PinFilters from '../PinFilters';
import { ICustomPopup } from '../../types';
const defaultMapOptions = {
@@ -48,7 +42,6 @@ function MapView() {
const {
filterTypes,
loadingFilters,
selectedAgent,
agentLocations,
mapLocations,
agentLocationsLoading,
@@ -84,7 +77,7 @@ function MapView() {
map.state.mapInstance.setCenter(bounds.getCenter()); //or use custom center
map.state.mapInstance.fitBounds(bounds);
const currentZoomLevel = map.state.mapInstance.getZoom();
const newLevel = currentZoomLevel - 1;
const newLevel = currentZoomLevel - 2;
map.state.mapInstance.setZoom(newLevel);
});
}
@@ -165,17 +158,19 @@ function MapView() {
}, [queryParams?.DATE]);
useEffect(() => {
if (!queryParams.SELECTED_FEEDBACK_ID && polylineRef.current) {
if (!queryParams?.SELECTED_FEEDBACK_ID && polylineRef.current) {
polylineRef.current.setMap(null);
return;
}
zoomOverFeedback(selectedFeedback?.feedbackLocation, selectedFeedback?.caseLocation);
drawLineBetweenFeedbackAndCase(
selectedFeedback?.feedbackLocation,
selectedFeedback?.caseLocation,
selectedFeedback?.isSuspicious
);
}, [queryParams?.SELECTED_FEEDBACK_ID]);
if (queryParams?.SELECTED_FEEDBACK_ID) {
zoomOverFeedback(selectedFeedback?.feedbackLocation, selectedFeedback?.caseLocation);
drawLineBetweenFeedbackAndCase(
selectedFeedback?.feedbackLocation,
selectedFeedback?.caseLocation,
selectedFeedback?.isSuspicious
);
}
}, [queryParams?.SELECTED_FEEDBACK_ID, selectedFeedback]);
useEffect(() => {
if (!mapLocations?.length) {
@@ -212,66 +207,39 @@ function MapView() {
const showBlurMap = !mapLocations?.length && !loadingFilters && !isFiltersSelected();
const googleMapsContainer = useMemo(
() => (
const googleMapsContainer = useMemo(() => {
return (
<GoogleMapsContainer mapOptions={defaultMapOptions}>
<div>
{mapLocations?.map(agentLocation => {
const {
location,
name = '',
activityLevel,
referenceId,
lastUpdatedAt,
type,
lanNo,
interactionId,
interactionStatus
} = agentLocation || {};
const { location } = agentLocation || {};
const { latitude: lat, longitude: lng } = location || {};
if (!lat || !lng) {
return null;
}
const refId = referenceId || lanNo || interactionId || '';
const color = PinColorMapping[activityLevel] ?? 'red';
return (
<AgentMarker
name={name}
type={type}
color={color}
refId={refId}
key={referenceId}
lanNo={lanNo}
key={agentLocation.referenceId}
agentLocation={agentLocation}
position={{ lat: Number(lat), lng: Number(lng) }}
lastUpdatedAt={lastUpdatedAt}
referenceId={referenceId}
interactionId={interactionId}
interactionStatus={interactionStatus}
activeCustomPopup={activeCustomPopup}
/>
);
})}
</div>
</GoogleMapsContainer>
),
[mapLocations]
);
);
}, [mapLocations]);
return (
<div className={styles.mapContainer}>
<div className={styles.mapContainer} tabIndex={-1}>
{googleMapsContainer}
{filteredAgentLocations?.length === 0 && !agentLocationsLoading ? (
{!filteredAgentLocations?.length && !agentLocationsLoading ? (
<MapPlaceholder showImage text={'Location data not found for selected filters !'} />
) : null}
{showBlurMap && !agentLocationsLoading ? (
<MapPlaceholder showImage text={'Select a filter to view location of agents'} />
) : null}
{agentLocations?.length && (
<div className={styles.mapToolbarContainer}>
<AgentsAccordion />
</div>
)}
{selectedAgent && <PinFilters />}
</div>
);
}

View File

@@ -1,7 +1,4 @@
.pinFiltersContainer {
position: absolute;
top: 16px;
left: 292px; // width of Agents Panel
background-color: var(--bg-primary);
padding: 8px 8px 8px 12px;
border-radius: 8px;

View File

@@ -1,4 +1,4 @@
import { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import styles from './index.module.scss';
import cx from 'classnames';
import { createQueryParams, readQueryParams } from 'src/utils/QueryParamsHelper';
@@ -25,11 +25,20 @@ const PinFilters = () => {
const { [AGENT_LIVE_LOCATION]: queryParams = {} } = readQueryParams();
const navigate = useNavigate();
const dispatch = useDispatch();
// map of selected filters
const locationTypeParams = useMemo(() => {
const locationTypeArray = queryParams?.LOCATION_TYPE?.split(',') || [];
const locationTypeParams: Record<string, boolean> = {};
locationTypeArray.forEach((locationType: LOCATION_TYPE_FILTERS) => {
locationTypeParams[locationType] = true;
});
return locationTypeParams;
}, [queryParams?.LOCATION_TYPE]);
useEffect(() => {
if (!agentLocationHistory?.loading && !agentAllocations?.loading) {
const { data, disabledFilters } = getMapLocations(
queryParams?.LOCATION_TYPE,
locationTypeParams,
agentAllocations?.data,
agentLocationHistory?.data
);
@@ -48,13 +57,42 @@ const PinFilters = () => {
const filterClickHandler = (value: string) => {
if (disabledFilters[value]) return;
let updatedFilters = queryParams?.LOCATION_TYPE;
if (value === LOCATION_TYPE_FILTERS.ALL && updatedFilters === LOCATION_TYPE_FILTERS.ALL) {
return;
}
addClickstreamEvent(AgentTrackingEvents.LH_MAP_FILTER_CLICKED, {
userId,
agentId: queryParams?.AGENTID,
filterValue: value
});
if (locationTypeParams?.[value]) {
// remove filter if already applied
updatedFilters = updatedFilters?.replace(value, '');
} else {
if (value === LOCATION_TYPE_FILTERS.ALL) {
// remove other filter if all filter is applied
updatedFilters = LOCATION_TYPE_FILTERS.ALL;
} else {
// remove all filter if any other filter is applied
updatedFilters = updatedFilters?.replace(LOCATION_TYPE_FILTERS.ALL, '');
updatedFilters += `,${value}`;
}
}
// remove leading and trailing commas
updatedFilters = updatedFilters?.replace(/^,+|,+$/g, '');
const url = createQueryParams({
[AGENT_LIVE_LOCATION]: { ...queryParams, LOCATION_TYPE: value, SELECTED_FEEDBACK_ID: '' }
[AGENT_LIVE_LOCATION]: {
...queryParams,
LOCATION_TYPE: updatedFilters || LOCATION_TYPE_FILTERS.ALL,
SELECTED_FEEDBACK_ID: ''
}
});
navigate(url);
};
@@ -63,10 +101,10 @@ const PinFilters = () => {
<div className={styles.pinFiltersContainer}>
<div className={styles.filterBy}>Filter by: </div>
{locationsPinFilters.map(filter => (
<>
<React.Fragment key={filter.value}>
<div
className={cx(styles.filterItem, {
[styles.active]: queryParams?.LOCATION_TYPE === filter.value,
[styles.active]: locationTypeParams?.[filter.value],
[styles.disabled]: disabledFilters[filter.value]
})}
onClick={() => filterClickHandler(filter?.value)}
@@ -75,7 +113,7 @@ const PinFilters = () => {
<div>{filter.label}</div>
</div>
{filter.value === LOCATION_TYPE_FILTERS.ALL && <div className={styles.verticalLine} />}
</>
</React.Fragment>
))}
</div>
);

View File

@@ -1,46 +1,83 @@
import React from 'react';
import { Typography } from '@navi/web-ui/lib/primitives';
import styles from './tooltip.module.scss';
import { forwardRef } from 'react';
import { DateFormat, dateFormat, formatDate, isToday } from 'src/utils/DateHelper';
import { LOCATION_TYPE_FILTERS } from '../../constants/LiveLocationTrackerInterfaces';
import {
IAgentLocation,
LOCATION_TYPE_FILTERS
} from '../../constants/LiveLocationTrackerInterfaces';
import RedirectionIcon from 'src/assets/icons/RedirectionIcon';
import { useSelector } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import APP_ROUTES from 'src/layout/Routes';
import { TAB_KEYS } from 'src/pages/CaseDetails/constants';
import { RootState } from 'src/store';
import { interpolatePathParams } from 'src/utils/interpolate';
import { CloseIcon } from '@navi/web-ui/lib/icons';
import { getMaxInputDate } from '../../utils';
import { createQueryParams, readQueryParams } from '@cp/src/utils/QueryParamsHelper';
import { AGENT_LIVE_LOCATION } from '../../constants/LiveLocatonTrackerConstants';
import { useNavigate } from 'react-router-dom';
import { setSelectedAgent } from '../../reducers/LiveLocationTrackerSlice';
import {
getAgentAllocations,
getAgentLocationHistory
} from '../../actions/LiveLocationTrackerActions';
type ToolTipProps = {
name: string;
lastUpdateActivity: string;
style?: any;
type?: LOCATION_TYPE_FILTERS;
lanNo?: string;
interactionStatus?: string;
agentLocation: IAgentLocation;
style?: React.CSSProperties;
closeTooltip: () => void;
};
function Tooltip(props: ToolTipProps, ref: any) {
const Tooltip = forwardRef((props: ToolTipProps, ref: any) => {
const { agentLocation, style, closeTooltip }: ToolTipProps = props;
const {
name,
lastUpdateActivity,
style,
lastUpdatedAt,
type,
lanNo,
interactionStatus,
closeTooltip
}: ToolTipProps = props;
const date = isToday(lastUpdateActivity)
? 'Today'
: formatDate(lastUpdateActivity, DateFormat.DD_MMM);
const time = dateFormat(new Date(lastUpdateActivity), DateFormat.HH_mm_ampm) || '---';
addressText,
phoneNumber,
referenceId
} = agentLocation;
const date = isToday(lastUpdatedAt) ? 'Today' : formatDate(lastUpdatedAt, DateFormat.DD_MMM);
const time = dateFormat(new Date(lastUpdatedAt), DateFormat.HH_mm_ampm) || '---';
const { liveLocationTracker, userId } = useSelector((state: RootState) => ({
liveLocationTracker: state.liveLocationTracker,
userId: state?.common?.userData?.referenceId
}));
const { selectedAgent } = liveLocationTracker || {};
const {
agentAllocations: { data: agentAllocations }
} = useSelector((state: RootState) => state.liveLocationTracker);
const { [AGENT_LIVE_LOCATION]: queryParams = {} } = readQueryParams();
const navigate = useNavigate();
const dispatch = useDispatch();
const selectAgentHandler = () => {
if (!selectedAgent) {
const maxInputDate = getMaxInputDate('YYYY-MM-DD');
const payload = { date: maxInputDate, referenceId };
const url = createQueryParams({
[AGENT_LIVE_LOCATION]: {
...queryParams,
AGENTID: referenceId,
LOCATION_TYPE: LOCATION_TYPE_FILTERS.ALL,
DATE: maxInputDate
}
});
navigate(url);
dispatch(setSelectedAgent(agentLocation));
dispatch(getAgentLocationHistory(payload));
dispatch(getAgentAllocations(payload));
}
};
const viewCaseHandler = () => {
if (lanNo) {
const data = agentAllocations?.[lanNo];
@@ -68,9 +105,12 @@ function Tooltip(props: ToolTipProps, ref: any) {
<Typography variant="h4" className={styles.lanNo}>
{lanNo}
</Typography>
<Typography variant="h4" className={styles.addressText}>
{addressText}
</Typography>
<div className={styles.viewCaseCtaWrapper} onClick={viewCaseHandler}>
<div className={styles.viewCaseCta}>View Case</div>
<RedirectionIcon width={12} height={12} />
<RedirectionIcon width={14} height={14} />
</div>
</div>
);
@@ -82,8 +122,21 @@ function Tooltip(props: ToolTipProps, ref: any) {
<CloseIcon />
</div>
{name && (
<Typography variant="h4" className={styles.nameText}>
{name}
<div className="flex items-center gap-1 justify-center pr-4">
<Typography variant="h4" className={styles.nameText}>
{name}
</Typography>
<RedirectionIcon
width={16}
height={16}
className="cursor-pointer"
onClick={selectAgentHandler}
/>
</div>
)}
{phoneNumber && (
<Typography variant="h4" className={styles.phoneNumber}>
{phoneNumber}
</Typography>
)}
{interactionStatus && (
@@ -98,6 +151,6 @@ function Tooltip(props: ToolTipProps, ref: any) {
)}
</div>
);
}
});
export default forwardRef(Tooltip);
export default Tooltip;

View File

@@ -4,17 +4,15 @@
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
.nameText {
font-size: 17px;
font-weight: 500;
line-height: 24px;
letter-spacing: -0.2px;
margin-bottom: 5px;
margin-right: 16px;
text-align: center;
text-wrap: nowrap;
}
.lastUpdatedAt {
@@ -24,7 +22,8 @@
line-height: 20px;
letter-spacing: -0.13px;
color: var(--navi-color-gray-c2);
margin-right: 16px;
padding-right: 16px;
text-align: center;
}
.casesNameText {
@@ -33,11 +32,11 @@
font-weight: 500;
line-height: 18px;
letter-spacing: -0.12px;
margin-right: 16px;
padding-right: 18px;
}
.lanNo {
color: var(--navi-color-gray-c3);
color: var(--navi-color-gray-c2);
font-size: 12px;
font-weight: 400;
line-height: 18px;
@@ -50,13 +49,33 @@
font-weight: 400;
line-height: 18px;
letter-spacing: -0.12px;
margin-right: 16px;
text-align: center;
margin-right: 8px;
}
.phoneNumber {
color: var(--navi-color-gray-c2);
font-size: 12px;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.12px;
text-align: center;
}
.addressText {
color: var(--navi-color-gray-c3);
font-size: 12px;
font-weight: 400;
line-height: 18px;
margin-top: 4px;
letter-spacing: -0.12px;
}
.viewCaseCtaWrapper {
display: flex;
align-items: center;
justify-content: center;
margin-top: 2px;
width: fit-content;
cursor: pointer;
}
@@ -71,7 +90,7 @@
.closeIcon {
position: absolute;
padding: 8px;
padding: 8px 8px 4px 4px;
top: 2px;
right: 0px;
cursor: pointer;

View File

@@ -1,17 +1,13 @@
import { useEffect, useMemo } from 'react';
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
getAgentsLocations,
getFilterData,
getFilterTypes
} from '../../actions/LiveLocationTrackerActions';
import { getFilterData } from '../../actions/LiveLocationTrackerActions';
import { RootState } from 'src/store';
import { FilterTypes } from '../../constants/LiveLocationTrackerInterfaces';
import { _map } from 'src/utils/commonUtils';
import styles from './TopBar.module.scss';
import { FilterMap, AGENT_LIVE_LOCATION } from '../../constants/LiveLocatonTrackerConstants';
import { createQueryParams, readQueryParams } from 'src/utils/QueryParamsHelper';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import {
setAgentLocations,
setFilterData,
@@ -19,73 +15,22 @@ import {
setSelectedAgent
} from '../../reducers/LiveLocationTrackerSlice';
import SingleAutocompleteDropdown from 'src/components/AutocompleteDropdown/SingleAutocompleteDropdown';
import { INHOUSE_AGENCY_CODE } from 'src/components/constant';
import { addClickstreamEvent } from 'src/service/clickStreamEventService';
import { AgentTrackingEvents } from 'src/service/clickStream.constant';
const Filters = () => {
const dispatch = useDispatch();
const { filterTypes, filterData, agentLocations } = useSelector(
(state: RootState) => state.liveLocationTracker
);
const { filterTypes, filterData } = useSelector((state: RootState) => state.liveLocationTracker);
const userId = useSelector((state: RootState) => state.common.userData?.referenceId);
const [searchParams] = useSearchParams();
const { filters } = filterTypes;
const { [AGENT_LIVE_LOCATION]: queryParams = {} } = readQueryParams();
const navigate = useNavigate();
const pinsPayload = useMemo(() => {
const selectedAgency =
queryParams[FilterTypes.AGENCY] || filterData[FilterTypes.AGENCY]?.data[0]?.value;
const selectedTeamLead = queryParams[FilterTypes.TEAM_LEAD];
return {
agencyCodes: [selectedAgency],
teamLeadReferenceIds: selectedTeamLead ? [selectedTeamLead] : []
};
}, [searchParams]);
useEffect(() => {
// Get agency filter data
dispatch(getFilterData());
}, [filterTypes]);
useEffect(() => {
if (!filters?.length) {
dispatch(getFilterTypes());
return;
}
const applyFilterRecursively = (filterType: FilterTypes) => {
const selectedFilter = queryParams[filterType];
const { nextFilter } = FilterMap[filterType];
if (!selectedFilter) {
return;
}
// If next filter is not selected or not in the list of filters, we need to fetch the pins data
if (!nextFilter || !filters.includes(nextFilter)) {
dispatch(getAgentsLocations(pinsPayload));
return;
}
if (filterType === FilterTypes.AGENCY) {
dispatch(getAgentsLocations(pinsPayload));
}
const payload = {
type: nextFilter,
agencyCodes: [selectedFilter]
};
dispatch(getFilterData(payload));
// recursively call the function to apply the next filter
applyFilterRecursively(nextFilter);
};
applyFilterRecursively(FilterTypes.AGENCY);
}, [queryParams?.AGENCY, queryParams?.TEAM_LEAD, filters]);
const handleFilterChange = (value: string, filterType: FilterTypes) => {
dispatch(setMapLocations({ data: [] }));
const nextFilter = FilterMap[filterType]?.nextFilter;
@@ -173,6 +118,7 @@ const Filters = () => {
containerClasses={styles.dropdown}
disabled={disabled}
sortOptions
hideCloseOption={filterType === FilterTypes.AGENCY}
/>
);
})}

View File

@@ -67,12 +67,11 @@
.filters {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
.dropdown {
background-color: var(--bg-primary);
box-shadow: var(--box-shadow-3) !important;
width: 250px;
height: 36px;
}

View File

@@ -15,17 +15,12 @@ import { createQueryParams, readQueryParams } from '../../../../utils/QueryParam
import { getAgentsLocations } from '../../actions/LiveLocationTrackerActions';
import { RootState } from '../../../../store';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import isToday from 'dayjs/plugin/isToday';
import usePolling from '../../../../hooks/usePolling';
import { getAgentDetails } from '../../utils';
import { resetMapData, setSelectedAgent } from '../../reducers/LiveLocationTrackerSlice';
import { INHOUSE_AGENCY_CODE } from 'src/components/constant';
import { useMemo } from 'react';
dayjs.extend(relativeTime);
dayjs.extend(isToday);
function TopBar() {
const navigate = useNavigate();
const dispatch = useDispatch();

View File

@@ -43,6 +43,7 @@ export interface IAgentAllocation {
customerName: string;
location: ILocation;
feedbacks: IFeedback[];
addressText: string;
}
export interface ILocation {
@@ -54,6 +55,7 @@ export interface IAgentLocationHistory {
referenceId: string;
location: ILocation;
epochTimestamp: number;
zIndex?: number;
}
export interface IAgentLocation {
@@ -68,6 +70,8 @@ export interface IAgentLocation {
lanNo?: string;
interactionId?: string;
interactionStatus?: string;
addressText?: string;
totalAllocatedCases: number;
}
interface IFilterTypes {
@@ -91,6 +95,8 @@ export interface MapPinLocation {
interactionId?: string;
interactionStatus?: string;
referenceId?: string;
addressText?: string;
zIndex?: number;
}
export enum FilterTypes {
@@ -142,20 +148,12 @@ export type ColorMap = Record<
>;
export interface AgentMarkerProps {
color: string;
name: string;
lastUpdatedAt: string;
position: {
lat: number;
lng: number;
};
referenceId: string;
type?: LOCATION_TYPE_FILTERS;
lanNo?: string;
interactionId?: string;
interactionStatus?: string;
activeCustomPopup: React.MutableRefObject<ICustomPopup | null>;
refId: string;
agentLocation: IAgentLocation;
}
export enum LOCATION_TYPE_FILTERS {

View File

@@ -2,7 +2,31 @@
position: relative;
.overlay {
z-index: 10;
z-index: var(--z-index-maps-loader-overlay);
width: 100%;
height: 100%;
position: fixed;
}
.agentFilters {
position: absolute;
top: 24px;
left: 298px;
z-index: var(--z-index-map-options);
}
.agentAccordion {
position: absolute;
top: 24px;
left: 24px;
z-index: var(--z-index-map-options);
}
.pinFilters {
position: absolute;
top: 24px;
left: 298px;
z-index: var(--z-index-map-options);
}
}

View File

@@ -1,17 +1,28 @@
import TopBar from './components/TopBar';
import MapView from './components/MapView';
import Footer from './components/Footer/Footer';
import styles from './index.module.scss';
import Loader from 'src/components/Loader/Loader';
import { RootState } from 'src/store';
import { useSelector } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import cx from 'classnames';
import { useEffect } from 'react';
import { useEffect, useMemo } from 'react';
import { addClickstreamEvent } from 'src/service/clickStreamEventService';
import { AgentTrackingEvents } from 'src/service/clickStream.constant';
import MapPlaceholder from './components/MapPlaceholder';
import { readQueryParams } from 'src/utils/QueryParamsHelper';
import { AGENT_LIVE_LOCATION } from './constants/LiveLocatonTrackerConstants';
import { AGENT_LIVE_LOCATION, FilterMap } from './constants/LiveLocatonTrackerConstants';
import Filters from './components/TopBar/Filters';
import AgentsAccordion from './components/AgentsAccordion';
import PinFilters from './components/PinFilters';
import {
getAgentsLocations,
getFilterData,
getFilterTypes
} from './actions/LiveLocationTrackerActions';
import { FilterTypes } from './constants/LiveLocationTrackerInterfaces';
import { useSearchParams } from 'react-router-dom';
import { resetMapData } from './reducers/LiveLocationTrackerSlice';
import LocationAgencySearch from './components/LocationAgencySearch';
function LiveLocationTracker() {
const {
@@ -20,20 +31,42 @@ function LiveLocationTracker() {
selectedAgent,
agentLocationHistory: { loading: isAgentLocationHistoryloading },
agentAllocations: { loading: isAgentAllocationsloading },
agentLocationsLoading
agentLocations,
agentLocationsLoading,
filterTypes,
filterData
} = useSelector((state: RootState) => ({
loadingFilters: state.liveLocationTracker.loadingFilters,
userId: state?.common?.userData?.referenceId,
selectedAgent: state.liveLocationTracker.selectedAgent,
agentLocationHistory: state.liveLocationTracker.agentLocationHistory,
agentAllocations: state.liveLocationTracker.agentAllocations,
agentLocationsLoading: state.liveLocationTracker.agentLocationsLoading
agentLocationsLoading: state.liveLocationTracker.agentLocationsLoading,
agentLocations: state.liveLocationTracker.agentLocations,
filterTypes: state.liveLocationTracker.filterTypes,
filterData: state.liveLocationTracker.filterData
}));
const { filters } = filterTypes || {};
const dispatch = useDispatch();
const [searchParams] = useSearchParams();
const { [AGENT_LIVE_LOCATION]: queryParams = {} } = readQueryParams();
const pinsPayload = useMemo(() => {
const selectedAgency =
queryParams[FilterTypes.AGENCY] || filterData[FilterTypes.AGENCY]?.data[0]?.value;
const selectedTeamLead = queryParams[FilterTypes.TEAM_LEAD];
return {
agencyCodes: [selectedAgency],
teamLeadReferenceIds: selectedTeamLead ? [selectedTeamLead] : []
};
}, [searchParams]);
useEffect(() => {
addClickstreamEvent(AgentTrackingEvents.LH_AGENT_TRACKING_PAGE_LOAD, { userId });
return () => {
// reset map data on unmount
dispatch(resetMapData());
};
}, []);
useEffect(() => {
@@ -41,18 +74,78 @@ function LiveLocationTracker() {
localStorage.setItem('live-location-tracker-params', window.location.search);
}, [queryParams]);
useEffect(() => {
if (!filters?.length) {
dispatch(getFilterTypes());
return;
}
const applyFilterRecursively = (filterType: FilterTypes) => {
const selectedFilter = queryParams[filterType];
const { nextFilter } = FilterMap[filterType];
if (!selectedFilter) {
return;
}
// If next filter is not selected or not in the list of filters, we need to fetch the pins data
if (!nextFilter || !filters.includes(nextFilter)) {
dispatch(getAgentsLocations(pinsPayload));
return;
}
if (filterType === FilterTypes.AGENCY) {
dispatch(getAgentsLocations(pinsPayload));
}
const payload = {
type: nextFilter,
agencyCodes: [selectedFilter]
};
dispatch(getFilterData(payload));
// recursively call the function to apply the next filter
applyFilterRecursively(nextFilter);
};
applyFilterRecursively(FilterTypes.AGENCY);
}, [queryParams?.AGENCY, queryParams?.TEAM_LEAD, filters]);
const mapLoading =
isAgentAllocationsloading || isAgentLocationHistoryloading || agentLocationsLoading;
if (!queryParams?.AGENCY)
return (
<div className={cx(styles.container, 'h-[calc(100vh-100px)]')}>
<LocationAgencySearch />
</div>
);
return (
<div className={styles.container}>
<TopBar />
{!selectedAgent && !mapLoading ? (
<div className={styles.agentFilters}>
<Filters />
</div>
) : null}
{!mapLoading ? (
<div className={styles.agentAccordion}>
<AgentsAccordion />
</div>
) : null}
{selectedAgent && !mapLoading ? (
<div className={styles.pinFilters}>
<PinFilters />
</div>
) : null}
<MapView />
<Footer />
<Loader show={loadingFilters} className={cx('overlay', styles.overlay)} animate={false} />
{(isAgentAllocationsloading || isAgentLocationHistoryloading || agentLocationsLoading) && (
{mapLoading && (
<MapPlaceholder
text={
agentLocationsLoading
? 'Fetching agents locations'
: `Fetching all of ${selectedAgent?.name}'s locations`
selectedAgent?.name
? `Fetching all of ${selectedAgent?.name}'s locations`
: 'Fetching agents locations'
}
/>
)}

View File

@@ -112,6 +112,7 @@ const liveLocationTrackerSlice = createSlice({
...state.agentAllocations,
...action.payload
};
state.lastUpdatedAt = new Date(getServerTime()).toString();
},
setSelectedFeedback: (state, action) => {
state.selectedFeedback = action.payload;

View File

@@ -5,7 +5,6 @@ import {
AgentDetailsPayload,
IAgentAllocation,
IAgentLocationHistory,
IFeedback,
ISelectedFeedback,
LOCATION_TYPE_FILTERS,
MapPinLocation
@@ -17,37 +16,53 @@ import styles from './components/AgentMarker/agentMarker.module.scss';
import { v4 as uuidv4 } from 'uuid';
import { LocalStorage } from '@cp/src/utils/StorageUtils';
enum MapPinsZIndices {
CASES = 1,
AGENT = 2,
GENUINE_FEEDBACK = 3,
SUSPICIOUS_FEEDBACK = 4
}
export const getAllocationPins = (
data: Record<string, IAgentAllocation>,
agentLocationHistory: IAgentLocationHistory[]
) => {
const casesLocation: MapPinLocation[] = [];
const feedbacksLocation: IFeedback[] = [];
const feedbacksLocation = [];
const suspiciousFeedback: MapPinLocation[] = [];
const genuineFeedback: MapPinLocation[] = [];
const agentLocations: IAgentLocationHistory[] = [];
for (const lanNo in data) {
if (data?.[lanNo]?.location) {
casesLocation.push({
type: LOCATION_TYPE_FILTERS.CASES,
location: data?.[lanNo]?.location,
lanNo,
name: data?.[lanNo]?.customerName
name: data?.[lanNo]?.customerName,
addressText: data?.[lanNo]?.addressText,
zIndex: MapPinsZIndices.CASES
});
}
if (data?.[lanNo]?.feedbacks) {
feedbacksLocation.push(...data[lanNo].feedbacks);
const feedbacks = data[lanNo].feedbacks.map(feedback => ({
...feedback,
lanNo
}));
feedbacksLocation.push(...feedbacks);
}
}
feedbacksLocation?.forEach((feedback: IFeedback) => {
feedbacksLocation?.forEach(feedback => {
if (feedback.suspicious) {
suspiciousFeedback?.push({
type: LOCATION_TYPE_FILTERS.SUSPICIOUS_FEEDBACK,
location: feedback?.location,
interactionId: feedback?.interactionId,
lastUpdatedAt: formatEpochTime(feedback?.epochTimestamp),
interactionStatus: feedback?.interactionStatus
interactionStatus: feedback?.interactionStatus,
lanNo: feedback?.lanNo,
zIndex: MapPinsZIndices.SUSPICIOUS_FEEDBACK
});
return;
}
@@ -56,13 +71,24 @@ export const getAllocationPins = (
location: feedback?.location,
interactionId: feedback?.interactionId,
lastUpdatedAt: formatEpochTime(feedback?.epochTimestamp),
interactionStatus: feedback?.interactionStatus
interactionStatus: feedback?.interactionStatus,
lanNo: feedback?.lanNo,
zIndex: MapPinsZIndices.GENUINE_FEEDBACK
});
});
agentLocationHistory?.forEach((locationData: IAgentLocationHistory) => {
agentLocations.push({
...locationData,
zIndex: MapPinsZIndices.AGENT
});
});
return {
casesLocation,
suspiciousFeedback,
genuineFeedback,
agentLocations,
disabledFilters: {
[LOCATION_TYPE_FILTERS.AGENT]: !agentLocationHistory?.length,
[LOCATION_TYPE_FILTERS.CASES]: !casesLocation?.length,
@@ -86,38 +112,39 @@ export const getAgentLocationFormattedData = (data: IAgentLocationHistory[]) =>
};
export const getMapLocations = (
locationFilter: string,
locationFilters: Record<string, boolean>,
agentAllocations: Record<string, IAgentAllocation> | null,
agentLocationHistory: IAgentLocationHistory[]
) => {
const { casesLocation, suspiciousFeedback, genuineFeedback, disabledFilters } = getAllocationPins(
agentAllocations ?? {},
agentLocationHistory
);
const { casesLocation, suspiciousFeedback, genuineFeedback, agentLocations, disabledFilters } =
getAllocationPins(agentAllocations ?? {}, agentLocationHistory);
switch (locationFilter) {
case LOCATION_TYPE_FILTERS.AGENT:
return { data: agentLocationHistory, disabledFilters };
// Initialize an empty array to hold the combined data for all selected filters
let combinedData: Array<MapPinLocation | IAgentLocationHistory> = [];
case LOCATION_TYPE_FILTERS.GENUINE_FEEDBACK:
return { data: genuineFeedback, disabledFilters };
case LOCATION_TYPE_FILTERS.SUSPICIOUS_FEEDBACK:
return { data: suspiciousFeedback, disabledFilters };
case LOCATION_TYPE_FILTERS.CASES:
return { data: casesLocation, disabledFilters };
default:
return {
data: [
...casesLocation,
...agentLocationHistory,
...genuineFeedback,
...suspiciousFeedback
],
disabledFilters
};
// Check if the array includes specific filters and add the corresponding data
if (locationFilters?.[LOCATION_TYPE_FILTERS.CASES]) {
combinedData = [...combinedData, ...casesLocation];
}
if (locationFilters?.[LOCATION_TYPE_FILTERS.AGENT]) {
combinedData = [...combinedData, ...agentLocations];
}
if (locationFilters?.[LOCATION_TYPE_FILTERS.GENUINE_FEEDBACK]) {
combinedData = [...combinedData, ...genuineFeedback];
}
if (locationFilters?.[LOCATION_TYPE_FILTERS.SUSPICIOUS_FEEDBACK]) {
combinedData = [...combinedData, ...suspiciousFeedback];
}
// If no specific filters are selected or the ALL filter is selected, combine all data
if (locationFilters?.[LOCATION_TYPE_FILTERS.ALL]) {
combinedData = [...casesLocation, ...agentLocations, ...genuineFeedback, ...suspiciousFeedback];
}
return {
data: combinedData,
disabledFilters
};
};
export const getMaxInputDate = (format: string) => {

View File

@@ -68,7 +68,7 @@ export enum Roles {
ROLE_TELE_ALLOCATIONS_ACCESS = 'ROLE_TELE_ALLOCATIONS_ACCESS',
ROLE_COLLECTION_HEAD = 'ROLE_COLLECTION_HEAD',
NAVI_INHOUSE_FIELD_CLUSTER_MANAGER = 'NAVI_INHOUSE_FIELD_CLUSTER_MANAGER',
NAVI_INHOUSE_FIELD_ZONAL_MANAGER = 'NAVI_INHOUSE_FIELD_ZONAL_MANAGER',
NAVI_INHOUSE_FIELD_ZONAL_MANAGER = 'ROLE_NAVI_INHOUSE_FIELD_ZONAL_MANAGER',
ROLE_NAVI_FIELD_AGENCY_TEAM_LEAD = 'ROLE_NAVI_FIELD_AGENCY_TEAM_LEAD',
ROLE_HUMAN_REMINDER_TEAM_LEAD = 'ROLE_HUMAN_REMINDER_TEAM_LEAD',
ROLE_HUMAN_REMINDER_CHAT_AGENT = 'ROLE_HUMAN_REMINDER_CHAT_AGENT',

View File

@@ -1,5 +1,5 @@
import Tabs, { TabItem } from '@navi/web-ui/lib/components/Tabs';
import { useEffect } from 'react';
import { useEffect, useMemo } from 'react';
import { TABS as SENSEI_TABS, TAB_KEYS } from './Tabs';
import { RootState } from '@cp/src/store';
@@ -11,19 +11,49 @@ import { useDispatch, useSelector } from 'react-redux';
import { useNavigate, useParams } from 'react-router-dom';
import APP_ROUTES from 'src/layout/Routes';
import { DateFormat, dateFormat } from 'src/utils/DateHelper';
import { fetchCommitments, fetchPTPDetails } from './actions';
import {
fetchCommitments,
fetchPTPDetails,
getOverallMetricInput,
getOverallMetricOutput
} from './actions';
import styles from './index.module.scss';
import { getAgentDetails, getLiveLocationParams } from '../LiveLocationTracker/utils';
import { AGENT_LIVE_LOCATION } from '../LiveLocationTracker/constants/LiveLocatonTrackerConstants';
import { readQueryParams } from '@cp/src/utils/QueryParamsHelper';
import { FilterTypes } from '../LiveLocationTracker/constants/LiveLocationTrackerInterfaces';
import { getAgentsLocations } from '../LiveLocationTracker/actions/LiveLocationTrackerActions';
import LiveLocationTracker from '../LiveLocationTracker';
import { LocalStorage } from '@cp/src/utils/StorageUtils';
const Index = () => {
const { tabId = '' } = useParams();
const navigate = useNavigate();
const dispatch = useDispatch();
const { lastUpdatedAt } = useSelector((state: RootState) => ({
lastUpdatedAt: state?.sensai?.dailyPlanning?.commitmentsTable?.lastUpdatedAt
const { [AGENT_LIVE_LOCATION]: queryParams = {} } = readQueryParams();
const {
lastUpdatedAtDailyPlanning,
lastUpdatedAtOverallMetric,
lastUpdatedAtLiveLocation,
featureFlags
} = useSelector((state: RootState) => ({
lastUpdatedAtDailyPlanning: state?.sensai?.dailyPlanning?.commitmentsTable?.lastUpdatedAt,
lastUpdatedAtOverallMetric: state?.sensai?.overallMetrics?.inputTable?.lastUpdatedAt,
lastUpdatedAtLiveLocation: state?.liveLocationTracker?.lastUpdatedAt,
featureFlags: state?.common?.featureFlags
}));
const handleTabChange = (tabId: TabItemKey) => {
// to add this function once we have other tabs
navigate(interpolatePathParams(APP_ROUTES.SENSEI.path, { tabId }));
const { liveLocationTrackerFeatureFlag } = featureFlags || {};
const paramsMap: Record<string, string> = LocalStorage.getItem('paramsMap') || {};
const handleTabChange = (updateTabId: TabItemKey) => {
if (tabId !== updateTabId) {
const updatedUrl = interpolatePathParams(APP_ROUTES.SENSEI.path, {
tabId: String(updateTabId)
});
const currentParams = paramsMap[updatedUrl] || '';
navigate(updatedUrl + currentParams);
}
};
useEffect(() => {
@@ -32,41 +62,98 @@ const Index = () => {
}
}, [tabId]);
const handleRefresh = () => {
const { lastUpdatedAt, handleRefresh } = useMemo(() => {
let handleRefresh = null;
let lastUpdatedAt = null;
switch (tabId) {
case TAB_KEYS.DAILY_PLANNING:
dispatch(fetchCommitments());
dispatch(fetchPTPDetails());
case TAB_KEYS.DAILY_PLANNING: {
handleRefresh = () => {
dispatch(fetchCommitments());
dispatch(fetchPTPDetails());
};
lastUpdatedAt = lastUpdatedAtDailyPlanning;
break;
}
case TAB_KEYS.OVERALL_METRIC: {
handleRefresh = () => {
dispatch(getOverallMetricOutput());
dispatch(getOverallMetricInput());
};
lastUpdatedAt = lastUpdatedAtOverallMetric;
break;
}
case TAB_KEYS.LIVE_LOCATION: {
if (!queryParams?.AGENCY) {
return { lastUpdatedAt: null, handleRefresh: null };
}
handleRefresh = () => {
if (queryParams?.AGENTID) {
dispatch(
getAgentDetails(
{ date: queryParams?.DATE, referenceId: queryParams?.AGENTID },
navigate
)
);
} else {
const selectedTeamLead = queryParams[FilterTypes.TEAM_LEAD];
const selectedAgency = queryParams[FilterTypes.AGENCY];
const payload = {
agencyCodes: [selectedAgency],
teamLeadReferenceIds: selectedTeamLead ? [selectedTeamLead] : []
};
dispatch(getAgentsLocations(payload, false));
}
};
lastUpdatedAt = lastUpdatedAtLiveLocation;
break;
}
default:
break;
}
};
return { lastUpdatedAt, handleRefresh };
}, [tabId, queryParams?.SELECTED_FEEDBACK_ID]);
const senseiTabs = useMemo(() => {
const tabs = [...SENSEI_TABS];
if (liveLocationTrackerFeatureFlag) {
tabs.push({
key: TAB_KEYS.LIVE_LOCATION,
value: 'Live location tracker',
component: <LiveLocationTracker />
});
}
return tabs;
}, [featureFlags]);
return (
<div className={styles.content}>
<div className={styles.header}>
<Typography variant="h3">FCM Dashboard</Typography>
<div className={styles.lastRefreshedContainer}>
<Typography variant="p5">
{dateFormat(new Date(lastUpdatedAt), DateFormat.LONG_DATE_FORMAT_WITH_TIME)}
</Typography>
<div className={styles.refresh} onClick={handleRefresh}>
<Typography as={'span'} variant="h4" className={styles.refreshWrapper}>
<RefreshIcon className={styles.refreshIcon} color="var(--navi-color-blue-base)" />
Refresh
{lastUpdatedAt ? (
<Typography variant="p5">
{dateFormat(new Date(lastUpdatedAt), DateFormat.LONG_DATE_FORMAT_WITH_TIME)}
</Typography>
</div>
) : null}
{handleRefresh ? (
<div className={styles.refresh} onClick={handleRefresh}>
<Typography as={'span'} variant="h4" className={styles.refreshWrapper}>
<RefreshIcon className={styles.refreshIcon} color="var(--navi-color-blue-base)" />
Refresh
</Typography>
</div>
) : null}
</div>
</div>
<Tabs
tabsClassName={styles.tabContainerClass}
selectedTabKey={tabId}
onTabChange={handleTabChange}
contentClassName={styles.tabContent}
>
{SENSEI_TABS.map(tab => {
{senseiTabs.map(tab => {
return (
<TabItem key={tab.key} label={tab.value}>
{tab.component}

View File

@@ -530,6 +530,8 @@ const excludedRedirectionEndPointsList = [
APP_ROUTES_PATHS_WITH_PATH_PARAM.SENSEI.field_governance,
APP_ROUTES_PATHS_WITH_PATH_PARAM.SENSEI.external_daily_planning,
APP_ROUTES_PATHS_WITH_PATH_PARAM.SENSEI.overall_metric,
APP_ROUTES_PATHS_WITH_PATH_PARAM.SENSEI.live_location,
APP_ROUTES_PATHS_WITH_PATH_PARAM.SENSEI.external_live_location,
APP_ROUTES_PATHS_WITH_PATH_PARAM.SENSEI.external_allocation_view
];

View File

@@ -1,4 +1,8 @@
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import isTodayDayJs from 'dayjs/plugin/isToday';
dayjs.extend(relativeTime);
dayjs.extend(isTodayDayJs);
export const monthNames = [
'Jan',

View File

@@ -352,6 +352,7 @@ export function copyToClipboard(value: string) {
}
export const getNameInitials = (name: string) => {
if (!name) return '';
const initials = name.match(/\b\w/g) || [];
return ((initials.shift() || '') + (initials.pop() || '')).toUpperCase();
};