NTP-22476 | Training module (#1064)

This commit is contained in:
Aman Chaturvedi
2025-01-21 15:39:13 +05:30
committed by GitHub
24 changed files with 663 additions and 54 deletions

View File

@@ -113,8 +113,8 @@ def jscFlavor = 'org.webkit:android-jsc:+'
def enableHermes = project.ext.react.get("enableHermes", false);
def VERSION_CODE = 231
def VERSION_NAME = "2.16.8"
def VERSION_CODE = 232
def VERSION_NAME = "2.16.9"
android {
namespace "com.avapp"

View File

@@ -1,7 +1,7 @@
{
"name": "AV_APP",
"version": "2.16.8",
"buildNumber": "231",
"version": "2.16.9",
"buildNumber": "232",
"private": true,
"scripts": {
"android:dev": "yarn move:dev && react-native run-android",

View File

@@ -0,0 +1,34 @@
import axiosInstance, { ApiKeys, getApiUrl } from '@components/utlis/apiHelper';
import {
setTrainingMaterialData,
setTrainingMaterialLoading,
} from '@reducers/trainingMaterialSlice';
import { AppDispatch } from '@store';
export const getTrainingMaterialList = () => (dispatch: AppDispatch) => {
dispatch(setTrainingMaterialLoading(true));
const url = getApiUrl(ApiKeys.GET_TRAINING_MATERIAL_LIST);
axiosInstance
.get(url)
.then((res) => {
if (res.data) {
dispatch(setTrainingMaterialLoading(false));
if (res?.data) {
dispatch(setTrainingMaterialData(res.data));
}
}
})
.finally(() => {
dispatch(setTrainingMaterialLoading(false));
});
};
export const getTrainingMaterialDetails = async (docRefId: string) => {
try {
const url = getApiUrl(ApiKeys.GET_TRAINING_MATERIAL_DETAILS, { docRefId });
const response = await axiosInstance.get(url);
return response.data;
} catch (error) {
throw error;
}
};

View File

@@ -0,0 +1,13 @@
import * as React from 'react';
import Svg, { Path } from 'react-native-svg';
const BookIcon = () => (
<Svg width={16} height={16} viewBox="0 0 16 16" fill="none">
<Path
d="M4.49935 14.6668C3.99935 14.6668 3.56879 14.4973 3.20768 14.1584C2.84657 13.8196 2.66602 13.4001 2.66602 12.9001V3.60011C2.66602 3.17789 2.79657 2.80011 3.05768 2.46678C3.31879 2.13345 3.66046 1.92234 4.08268 1.83345L10.666 0.533447V11.2001L4.34935 12.4668C4.24935 12.489 4.16602 12.5418 4.09935 12.6251C4.03268 12.7084 3.99935 12.8001 3.99935 12.9001C3.99935 13.0223 4.04935 13.1251 4.14935 13.2084C4.24935 13.2918 4.36602 13.3334 4.49935 13.3334H11.9993V2.66678H13.3327V14.6668H4.49935ZM5.99935 10.7834L9.33268 10.1334V2.16678L5.99935 2.81678V10.7834ZM4.66602 11.0501V3.08345L4.41602 3.13345C4.29379 3.15567 4.19379 3.20845 4.11602 3.29178C4.03824 3.37511 3.99935 3.47789 3.99935 3.60011V11.2168C4.0549 11.1946 4.11324 11.1751 4.17435 11.1584C4.23546 11.1418 4.29379 11.1279 4.34935 11.1168L4.66602 11.0501Z"
fill="#969696"
/>
</Svg>
);
export default BookIcon;

View File

@@ -1,7 +1,9 @@
import { COLORS } from '@rn-ui-lib/colors';
import { IconProps } from '@rn-ui-lib/icons/types';
import React from 'react';
import { G, Mask, Path, Rect, Svg } from 'react-native-svg';
const RightChevronIcon = () => {
const RightChevronIcon: React.FC<IconProps> = ({ fillColor = COLORS.TEXT.BLUE }) => {
return (
<Svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<Mask id="mask0_3190_8548" maskUnits="userSpaceOnUse" x="0" y="0" width="20" height="20">
@@ -10,7 +12,7 @@ const RightChevronIcon = () => {
<G mask="url(#mask0_3190_8548)">
<Path
d="M7.25065 14.4166C7.09787 14.2638 7.02148 14.0694 7.02148 13.8333C7.02148 13.5972 7.09787 13.4027 7.25065 13.25L10.5007 9.99996L7.25065 6.74996C7.09787 6.59718 7.02148 6.40274 7.02148 6.16663C7.02148 5.93051 7.09787 5.73607 7.25065 5.58329C7.40343 5.43051 7.59787 5.35413 7.83398 5.35413C8.07009 5.35413 8.26454 5.43051 8.41732 5.58329L12.2507 9.41663C12.334 9.49996 12.3932 9.59024 12.4282 9.68746C12.4626 9.78468 12.4798 9.88885 12.4798 9.99996C12.4798 10.1111 12.4626 10.2152 12.4282 10.3125C12.3932 10.4097 12.334 10.5 12.2507 10.5833L8.41732 14.4166C8.26454 14.5694 8.07009 14.6458 7.83398 14.6458C7.59787 14.6458 7.40343 14.5694 7.25065 14.4166Z"
fill="#0276FE"
fill={fillColor}
/>
</G>
</Svg>

View File

@@ -0,0 +1,13 @@
import * as React from 'react';
import Svg, { Path } from 'react-native-svg';
const TextMaterialIcon = () => (
<Svg width={16} height={16} viewBox="0 0 16 16" fill="none">
<Path
d="M0.5 15.5V13.8333H15.5V15.5H0.5ZM0.5 12.1667V3.83333H8.83333V12.1667H0.5ZM0.5 2.16667V0.5H15.5V2.16667H0.5ZM2.16667 10.5H7.16667V5.5H2.16667V10.5ZM10.5 12.1667V10.5H15.5V12.1667H10.5ZM10.5 8.83333V7.16667H15.5V8.83333H10.5ZM10.5 5.5V3.83333H15.5V5.5H10.5Z"
fill="#969696"
/>
</Svg>
);
export default TextMaterialIcon;

View File

@@ -0,0 +1,20 @@
import * as React from 'react';
import Svg, { G, Path, Defs, ClipPath, Rect } from 'react-native-svg';
const VideoIcon = () => (
<Svg width={20} height={20} viewBox="0 0 20 20" fill="none">
<G clipPath="url(#clip0_3668_44660)">
<Path
d="M8.33268 13.7501L13.3327 10.0001L8.33268 6.25008V13.7501ZM9.99935 1.66675C5.39935 1.66675 1.66602 5.40008 1.66602 10.0001C1.66602 14.6001 5.39935 18.3334 9.99935 18.3334C14.5993 18.3334 18.3327 14.6001 18.3327 10.0001C18.3327 5.40008 14.5993 1.66675 9.99935 1.66675ZM9.99935 16.6667C6.32435 16.6667 3.33268 13.6751 3.33268 10.0001C3.33268 6.32508 6.32435 3.33341 9.99935 3.33341C13.6743 3.33341 16.666 6.32508 16.666 10.0001C16.666 13.6751 13.6743 16.6667 9.99935 16.6667Z"
fill="#969696"
/>
</G>
<Defs>
<ClipPath id="clip0_3668_44660">
<Rect width={20} height={20} fill="white" />
</ClipPath>
</Defs>
</Svg>
);
export default VideoIcon;

View File

@@ -464,9 +464,9 @@ export const CLICKSTREAM_EVENT_NAMES = {
name: 'FA_VIEW_PAST_FEEDBACK_PREV_PAGE_FAILED',
description: 'FA_VIEW_PAST_FEEDBACK_PREV_PAGE_FAILED',
},
FA_VIEW_PHOTO_CLICKED: {
name: 'FA_VIEW_PHOTO_CLICKED',
description: 'FA_VIEW_PHOTO_CLICKED'
FA_VIEW_PHOTO_CLICKED: {
name: 'FA_VIEW_PHOTO_CLICKED',
description: 'FA_VIEW_PHOTO_CLICKED',
},
FA_CUSTOMER_DOCUMENT_CLICKED: {
name: 'FA_CUSTOMER_DOCUMENT_CLICKED',
@@ -513,7 +513,6 @@ export const CLICKSTREAM_EVENT_NAMES = {
name: 'FA_VIEW_ALL_ESCALATIONS_SCREEN_LANDED',
description: 'FA_VIEW_ALL_ESCALATIONS_SCREEN_LANDED',
},
// Notifications
FA_NOTIFICATION_ICON_CLICK: {
@@ -810,7 +809,7 @@ export const CLICKSTREAM_EVENT_NAMES = {
name: 'FA_FETCHED_CUSTOMER_DOCUMENTS',
description: 'FA_FETCHED_CUSTOMER_DOCUMENTS',
},
FA_FETCH_CUSTOMER_DOCUMENTS_FAILED: {
FA_FETCH_CUSTOMER_DOCUMENTS_FAILED: {
name: 'FA_FETCHED_CUSTOMER_DOCUMENTS_FAILED',
description: 'FA_FETCHED_CUSTOMER_DOCUMENTS_FAILED',
},
@@ -837,7 +836,7 @@ export const CLICKSTREAM_EVENT_NAMES = {
FA_CHANNEL_CLICKED_SHARE_JOURNEY: {
name: 'FA_CHANNEL_CLICKED_SHARE_JOURNEY',
description: 'FA_CHANNEL_CLICKED_SHARE_JOURNEY',
},
},
FA_PHONE_NUMBER_CLICKED_SHARE_JOURNEY: {
name: 'FA_PHONE_NUMBER_CLICKED_SHARE_JOURNEY',
description: 'FA_PHONE_NUMBER_CLICKED_SHARE_JOURNEY',
@@ -1137,7 +1136,7 @@ export const CLICKSTREAM_EVENT_NAMES = {
},
LITMUS_EXPERIMENT: {
name: 'LITMUS_EXPERIMENT',
description: 'LITMUS_EXPERIMENT'
description: 'LITMUS_EXPERIMENT',
},
//FEE WAIVE CLICKSTREAM EVENTS
@@ -1298,44 +1297,44 @@ export const CLICKSTREAM_EVENT_NAMES = {
description: 'Call banner clicked',
},
FA_READ_PERMISSION_ERROR: {
name: 'ERROR_IN_FETCHING_READ_PERMISSION',
description: 'Error in fetching read permission'
name: 'ERROR_IN_FETCHING_READ_PERMISSION',
description: 'Error in fetching read permission',
},
FA_READ_PERMISSION_NOT_PROVIDED: {
name: 'FA_READ_PERMISSION_NOT_PROVIDED',
description: 'Read permission not provided'
name: 'FA_READ_PERMISSION_NOT_PROVIDED',
description: 'Read permission not provided',
},
FA_CALLING_FEEDBACK_NUDGE_LOADED: {
name: 'FA_CALLING_FEEDBACK_NUDGE_LOADED',
description: 'Calling feedback nudge loaded'
description: 'Calling feedback nudge loaded',
},
FA_CALLING_FEEDBACK_NUDGE_FEEDBACK_BUTTON_CLICKED: {
name: 'FA_CALLING_FEEDBACK_NUDGE_FEEDBACK_BUTTON_CLICKED',
description: 'Fill feedback button clicked'
description: 'Fill feedback button clicked',
},
FA_CALLING_FEEDBACK_NUDGE_CLOSED: {
name: 'FA_CALLING_FEEDBACK_NUDGE_CLOSED',
description: 'Feedback nudge closed'
description: 'Feedback nudge closed',
},
FA_INSTALLING_CODEPUSH: {
name : 'FA_INSTALLING_CODEPUSH',
description: 'Codepush installation started'
name: 'FA_INSTALLING_CODEPUSH',
description: 'Codepush installation started',
},
FA_CODEPUSH_DEFAULT_STATUS: {
name : 'FA_CODEPUSH_DEFAULT_STATUS',
description: 'Codepush default fallback case'
name: 'FA_CODEPUSH_DEFAULT_STATUS',
description: 'Codepush default fallback case',
},
FA_CODEPUSH_UNKNOWN_ERROR: {
name : 'FA_CODEPUSH_UNKNOWN_ERROR',
description: 'Codepush unknown error'
name: 'FA_CODEPUSH_UNKNOWN_ERROR',
description: 'Codepush unknown error',
},
FA_FEEDBACK_IMAGE_NOT_FOUND: {
name: 'FA_FEEDBACK_IMAGE_NOT_FOUND',
description: 'Feedback image not found'
description: 'Feedback image not found',
},
FA_UNSYNC_FEEDBACK_CAPTURED: {
name: 'FA_UNSYNC_FEEDBACK_CAPTURED',
description: 'Unsync feedback captured'
description: 'Unsync feedback captured',
},
FA_UNSYNC_FEEDBACK_CAPTURING: {
name: 'FA_UNSYNC_FEEDBACK_CAPTURING',
@@ -1359,66 +1358,92 @@ export const CLICKSTREAM_EVENT_NAMES = {
},
FA_API_FAILED: {
name: 'FA_API_FAILED',
description: 'API failed'
description: 'API failed',
},
// Apk Update
FA_APK_UPDATE_DOWNLOAD_STARTED: {
name: 'FA_APK_UPDATE_DOWNLOAD_STARTED',
description: 'APK update download started'
description: 'APK update download started',
},
FA_APK_UPDATE_DOWNLOAD_SUCCESS: {
name: 'FA_APK_UPDATE_DOWNLOAD_SUCCESS',
description: 'APK update download completed'
description: 'APK update download completed',
},
FA_APK_UPDATE_DOWNLOAD_FAILED: {
name: 'FA_APK_UPDATE_DOWNLOAD_FAILED',
description: 'APK update download failed'
description: 'APK update download failed',
},
FA_APK_UPDATE_BUTTON_CLICKED: {
name: 'FA_APK_UPDATE_BUTTON_CLICKED',
description: 'APK update button clicked'
description: 'APK update button clicked',
},
FA_APK_UPDATE_INSTALL_STARTED: {
name: 'FA_APK_UPDATE_INSTALL_STARTED',
description: 'APK update installation started'
description: 'APK update installation started',
},
FA_APK_UPDATE_INSTALL_FAILED: {
name: 'FA_APK_UPDATE_INSTALL_FAILED',
description: 'APK update installation failed'
description: 'APK update installation failed',
},
FA_APK_UPDATE_FALLBACK_TRIGGERED: {
name: 'FA_APK_UPDATE_FALLBACK_TRIGGERED',
description: 'APK update fallback triggered'
description: 'APK update fallback triggered',
},
FA_APK_UPDATE_CORRUPTED_FILE_DOWNLOADED: {
name: 'FA_APK_UPDATE_CORRUPTED_FILE_DOWNLOADED',
description: 'APK update corrupted file downloaded'
description: 'APK update corrupted file downloaded',
},
FA_APK_UPDATE_INSTALL_SUCCESS: {
name: 'FA_APK_UPDATE_INSTALL_SUCCESS',
description: 'APK update installation success'
description: 'APK update installation success',
},
FA_POST_OPERATIVE_HOURS_SCREEN_LOADED: {
name: 'FA_POST_OPERATIVE_HOURS_SCREEN_LOADED',
description: 'Post operative hours screen loaded'
description: 'Post operative hours screen loaded',
},
FA_PERSIST_ORIGINAL_IMAGE_FAILURE: {
name: 'FA_PERSIST_ORIGINAL_IMAGE_FAILURE',
description: 'Failed to persist original image'
description: 'Failed to persist original image',
},
// Filter coachmarks
FA_FILTER_COACHMARKS_LOADED: {
name: 'FA_FILTER_COACHMARKS_LOADED',
description: 'Filter coachmarks loaded'
description: 'Filter coachmarks loaded',
},
FA_FILTER_COACHMARKS_FAILED: {
name: 'FA_FILTER_COACHMARKS_FAILED',
description: 'Filter coachmarks failed'
}
description: 'Filter coachmarks failed',
},
// Training module
FA_PROFILE_PAGE_TRAINING_MATERIAL_CLICKED: {
name: 'FA_PROFILE_PAGE_TRAINING_MATERIAL_CLICKED',
description: 'Training material clicked',
},
FA_TRAINING_MATERIAL_LIST_SCREEN_LOADED: {
name: 'FA_TRAINING_MATERIAL_LIST_SCREEN_LOADED',
description: 'Training material screen loaded',
},
FA_TRAINING_MATERIAL_ITEM_CLICKED: {
name: 'FA_TRAINING_MATERIAL_ITEM_CLICKED',
description: 'Training material item clicked',
},
FA_TRAINING_MATERIAL_ITEM_LOADED: {
name: 'FA_TRAINING_MATERIAL_ITEM_LOADED',
description: 'Training material item loaded',
},
FA_TRAINING_MATERIAL_ITEM_CLOSED: {
name: 'FA_TRAINING_MATERIAL_ITEM_CLOSED',
description: 'Training material item closed',
},
FA_TRAINING_MATERIAL_PDF_PAGE_CHANGED: {
name: 'FA_TRAINING_MATERIAL_PDF_PAGE_CHANGED',
description: 'Training material PDF page changed',
},
} as const;
export enum MimeType {
@@ -1565,4 +1590,4 @@ export const API_ERROR_MESSAGE = 'Oops! something went wrong';
export enum BuildFlavours {
FIELD_AGENTS = 'fieldAgents',
CALLING_AGENTS = 'callingAgents',
}
}

8
src/common/NewTag.tsx Normal file
View File

@@ -0,0 +1,8 @@
import Tag, { TagVariant } from '@rn-ui-lib/components/Tag';
import { GenericStyles } from '@rn-ui-lib/styles';
const NewTag = () => {
return <Tag style={GenericStyles.mr10} variant={TagVariant.blue} text="New" />;
};
export default NewTag;

View File

@@ -0,0 +1,92 @@
import { COLORS } from '@rn-ui-lib/colors';
import { GenericStyles } from '@rn-ui-lib/styles';
import React, { useEffect, useState } from 'react';
import { ActivityIndicator, SafeAreaView, StyleSheet } from 'react-native';
import PdfRendererView from 'react-native-pdf-renderer';
import { IPdfRenderer } from './interfaces';
import RNFetchBlob from 'react-native-blob-util';
import SuspenseLoader from '@rn-ui-lib/components/suspense_loader/SuspenseLoader';
import Text from '@rn-ui-lib/components/Text';
import { isFunction } from '@components/utlis/commonFunctions';
import { getFileType } from '@screens/caseDetails/PDFUtil';
import { DocumentContentType } from '@screens/caseDetails/interface';
const ERROR_STATE = 'ERROR';
const PdfRenderer: React.FC<IPdfRenderer> = ({ docId, onPageChange, getFileUri }) => {
const [pdfFilePath, setPdfFilePath] = useState('');
const isLoading = !pdfFilePath;
const error = pdfFilePath === ERROR_STATE;
const checkIfFileExists = async () => {
const cacheDirectory = RNFetchBlob.fs.dirs.CacheDir;
const cacheFilePath = `${cacheDirectory}/${docId}.pdf`;
const doesFileExist = await RNFetchBlob.fs.exists(cacheFilePath);
return { doesFileExist, cacheFilePath };
};
const saveFileToCache = async () => {
const { doesFileExist, cacheFilePath } = await checkIfFileExists();
if (doesFileExist) {
setPdfFilePath(cacheFilePath);
return;
}
const url = await getFileUri();
if (!url) {
setPdfFilePath(ERROR_STATE);
return;
}
const highQualityResponse = await RNFetchBlob.fetch('GET', url);
if (highQualityResponse.respInfo.status !== 200) {
setPdfFilePath(ERROR_STATE);
} else if (highQualityResponse.respInfo.status === 200) {
const highQualityImageBase64 = await highQualityResponse.base64();
const documentType = await getFileType(highQualityImageBase64);
if (documentType !== DocumentContentType.PDF) {
setPdfFilePath(ERROR_STATE);
return;
}
await RNFetchBlob.fs.writeFile(cacheFilePath, highQualityImageBase64, 'base64');
const exists = await RNFetchBlob.fs.exists(cacheFilePath);
if (exists) {
setPdfFilePath(cacheFilePath);
}
}
};
useEffect(() => {
saveFileToCache();
}, []);
const handlePageChange = (pageNumber: number) => {
if (isNaN(pageNumber) || !isFunction(onPageChange)) return;
onPageChange(pageNumber + 1);
};
return (
<SafeAreaView style={GenericStyles.fill}>
<SuspenseLoader loading={isLoading} fallBack={<ActivityIndicator size='large' color={COLORS.BASE.BLUE} />}>
{error ? (
<Text>Failed to load PDF</Text>
) : (
<PdfRendererView
style={[styles.pdf]}
source={`file:/${pdfFilePath}`}
distanceBetweenPages={16}
maxZoom={5}
onPageChange={handlePageChange}
/>
)}
</SuspenseLoader>
</SafeAreaView>
);
};
export const styles = StyleSheet.create({
pdf: {
flex: 1,
backgroundColor: COLORS.BACKGROUND.GREY_LIGHT_2,
},
});
export default PdfRenderer;

View File

@@ -0,0 +1,5 @@
export interface IPdfRenderer {
docId: string;
getFileUri: () => Promise<string>;
onPageChange?: (pageNumber: number) => void;
}

View File

@@ -112,6 +112,8 @@ export enum ApiKeys {
GET_REPAYMENTS = 'GET_REPAYMENTS',
GET_FEEDBACK_HISTORY = 'GET_FEEDBACK_HISTORY',
GET_PRIORTIY_FEEDBACK = 'GET_PRIORTIY_FEEDBACK',
GET_TRAINING_MATERIAL_LIST = 'GET_TRAINING_MATERIAL_LIST',
GET_TRAINING_MATERIAL_DETAILS = 'GET_TRAINING_MATERIAL_DETAILS',
}
export const API_URLS: Record<ApiKeys, string> = {} as Record<ApiKeys, string>;
@@ -164,8 +166,7 @@ API_URLS[ApiKeys.GET_PERFORMANCE_METRICS] = '/allocation-cycle/agent-performance
API_URLS[ApiKeys.GET_CASH_COLLECTED] = '/allocation-cycle/cash-collected-split';
API_URLS[ApiKeys.GET_TELEPHONE_NUMBERS] =
'/v2/collection-cases/telephones-view/{loanAccountNumber}';
API_URLS[ApiKeys.GET_TELEPHONE_NUMBERS_V2] =
'/collections/telephones-agent-call-activity-view';
API_URLS[ApiKeys.GET_TELEPHONE_NUMBERS_V2] = '/collections/telephones-agent-call-activity-view';
API_URLS[ApiKeys.FIRESTORE_INCONSISTENCY_INFO] = '/cases/sync-status';
API_URLS[ApiKeys.FIRESTORE_INCONSISTENCY_INFO_V2] = '/cases/v2/sync-status';
API_URLS[ApiKeys.GET_CASE_DETAILS_FROM_API] =
@@ -208,14 +209,15 @@ API_URLS[ApiKeys.SEND_COMMUNICATION_NAVI_ACCOUNT] = '/navi-communications/{loanA
API_URLS[ApiKeys.GENERATE_DYNAMIC_DOCUMENT] = '/documents/generate/{loanAccountNumber}';
API_URLS[ApiKeys.ALL_ESCALATIONS] = '/customer-escalation';
API_URLS[ApiKeys.DOWNLOAD_LATEST_APP] = 'https://longhorn.navi.com/api/app/download';
API_URLS[ApiKeys.GET_UNGROUPED_ADDRESSES] =
'/collection-cases/ungrouped/addresses';
API_URLS[ApiKeys.GET_UNGROUPED_ADDRESSES] = '/collection-cases/ungrouped/addresses';
API_URLS[ApiKeys.GET_GROUPED_ADDRESSES_AND_GEOLOCATIONS] =
'/collection-cases/grouped/addresses-geo-locations';
API_URLS[ApiKeys.GET_EMI_SCHEDULE] = '/collection-cases/emiSchedule';
API_URLS[ApiKeys.GET_REPAYMENTS] = '/collection-cases/repayments';
API_URLS[ApiKeys.GET_FEEDBACK_HISTORY] = '/feedback/filters';
API_URLS[ApiKeys.GET_PRIORTIY_FEEDBACK] = 'feedback/case-status';
API_URLS[ApiKeys.GET_PRIORTIY_FEEDBACK] = '/feedback/case-status';
API_URLS[ApiKeys.GET_TRAINING_MATERIAL_LIST] = '/training-page/content-list';
API_URLS[ApiKeys.GET_TRAINING_MATERIAL_DETAILS] = '/training-page/{docRefId}';
export const API_STATUS_CODE = {
OK: 200,
@@ -228,7 +230,7 @@ export const API_STATUS_CODE = {
INTERNAL_SERVER_ERROR: 500,
TOO_MANY_REQUESTS: 429,
GONE: 410,
POST_OPERATIVE_HOURS_ACTIVITY: 451
POST_OPERATIVE_HOURS_ACTIVITY: 451,
};
export const UNAUTHORIZED_VALUES = [API_STATUS_CODE.UNAUTHORIZED, API_STATUS_CODE.FORBIDDEN];
@@ -353,9 +355,9 @@ axiosInstance.interceptors.response.use(
);
if (
(config?.headers?.donotHandleError ||
donotHandleErrorOnStatusCode.includes(error?.response?.status)) &&
donotHandleErrorOnStatusCode.includes(error?.response?.status)) &&
// Logout even donotHandleError is true when status code is 401, 403
!config?.headers?.autoLogoutOnUnauthorized
!config?.headers?.autoLogoutOnUnauthorized
) {
return Promise.reject(error);
}

View File

@@ -0,0 +1,49 @@
import React, { useEffect } from 'react';
import WebView from 'react-native-webview';
import { IWebViewVideoPlayer } from './interfaces';
import { videoPlayerHTML } from './constants';
import SuspenseLoader from '@rn-ui-lib/components/suspense_loader/SuspenseLoader';
import { ActivityIndicator } from 'react-native';
import { COLORS } from '@rn-ui-lib/colors';
import Text from '@rn-ui-lib/components/Text';
const ERROR_STATE = 'ERROR';
const WebViewVideoPlayer: React.FC<IWebViewVideoPlayer> = ({ getVideoUrl }) => {
const [url, setUrl] = React.useState<string>('');
const isLoading = !url;
const error = url === ERROR_STATE;
const fetchVideoUrl = async () => {
const url = await getVideoUrl();
if (!url) {
setUrl(ERROR_STATE);
return;
}
setUrl(url);
};
useEffect(() => {
fetchVideoUrl();
}, []);
return (
<SuspenseLoader
fallBack={<ActivityIndicator size={'large'} color={COLORS.BASE.BLUE} />}
loading={isLoading}
>
{error ? (
<Text>Not able to load the video</Text>
) : (
<WebView
source={{ html: videoPlayerHTML(url) }}
style={{ flex: 1 }}
allowsFullscreenVideo={true}
mediaPlaybackRequiresUserAction={false}
/>
)}
</SuspenseLoader>
);
};
export default WebViewVideoPlayer;

View File

@@ -0,0 +1,11 @@
export const videoPlayerHTML = (url: string) => `<html>
<head>
<meta name="viewport"
content="initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
</head><body>
<video id="video" style={border-radius: 8px} width="100%" height="100%" controls controlsList="nodownload" onclick="postMessage('fullscreen')">
<source src="${url}" type='video/mp4'>
</video>
</body></html>`;

View File

@@ -0,0 +1,3 @@
export interface IWebViewVideoPlayer {
getVideoUrl: () => Promise<string>;
}

View File

@@ -0,0 +1,30 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { ITrainingMaterial } from '@screens/trainingMaterial/interfaces';
interface ITrainingMaterialSlice {
loading: boolean;
data: ITrainingMaterial[];
}
const initialState: ITrainingMaterialSlice = {
loading: false,
data: [],
};
const TrainingMaterialSlice = createSlice({
name: 'trainingMaterial',
initialState,
reducers: {
setTrainingMaterialLoading: (state, action: PayloadAction<boolean>) => {
state.loading = action.payload;
},
setTrainingMaterialData: (state, action: PayloadAction<ITrainingMaterial[]>) => {
state.data = action.payload;
},
},
});
export const { setTrainingMaterialLoading, setTrainingMaterialData } =
TrainingMaterialSlice.actions;
export default TrainingMaterialSlice.reducer;

View File

@@ -10,6 +10,8 @@ import store from '@store';
import { Alert } from 'react-native';
import { ProfileScreenStackEnum } from '../ProfileStack';
import CountComponent from '../CountComponent';
import BookIcon from '@assets/icons/BookIcon';
import NewTag from '@common/NewTag';
export const getNavigationLinks = () => {
const { isTeamLead, selectedAgent, featureFlags } = store?.getState().user;
@@ -37,6 +39,16 @@ export const getNavigationLinks = () => {
isNew: true,
NewComponent: CountComponent,
},
{
name: 'Training material',
icon: BookIcon,
isVisible: true,
onPress: () => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_PROFILE_PAGE_TRAINING_MATERIAL_CLICKED);
navigateToScreen(ProfileScreenStackEnum.TRAINING_MATERIAL);
},
NewComponent: NewTag
},
{
name: 'Logout',
icon: LogoutIcon,

View File

@@ -9,6 +9,8 @@ import Profile from '.';
import AgentIdCard from './AgentIdCard';
import MyDocuments from './MyDocuments';
import PDFFullScreen from '@screens/caseDetails/PDFFullScreen';
import TrainingMaterial from '@screens/trainingMaterial/TrainingMaterial';
import TrainingMaterialDetail from '@screens/trainingMaterial/TrainingMaterialDetail';
const Stack = createNativeStackNavigator();
@@ -20,6 +22,8 @@ export enum ProfileScreenStackEnum {
AGENT_ID_CARD = 'agentIdCard',
MY_DOCUMENTS = 'myDocuments',
PDF_FULL = 'pdfFull',
TRAINING_MATERIAL = 'trainingMaterial',
TRAINING_MATERIAL_DETAIL = 'trainingMaterialDetail',
}
const ProfileStack = () => {
@@ -47,6 +51,12 @@ const ProfileStack = () => {
component={PDFFullScreen}
options={{ ...DEFAULT_SCREEN_OPTIONS, orientation: 'all' }}
/>
<Stack.Screen name={ProfileScreenStackEnum.TRAINING_MATERIAL} component={TrainingMaterial} />
<Stack.Screen
name={ProfileScreenStackEnum.TRAINING_MATERIAL_DETAIL}
component={TrainingMaterialDetail}
options={{ ...DEFAULT_SCREEN_OPTIONS, orientation: 'all' }}
/>
</Stack.Navigator>
);
};

View File

@@ -0,0 +1,55 @@
import { goBack } from '@components/utlis/navigationUtlis';
import { useAppDispatch, useAppSelector } from '@hooks';
import { COLORS } from '@rn-ui-lib/colors';
import NavigationHeader from '@rn-ui-lib/components/NavigationHeader';
import SuspenseLoader from '@rn-ui-lib/components/suspense_loader/SuspenseLoader';
import { GenericStyles } from '@rn-ui-lib/styles';
import Layout from '@screens/layout/Layout';
import React, { useEffect } from 'react';
import { ActivityIndicator, ScrollView, View } from 'react-native';
import TrainingMaterialListItem from './TrainingMaterialListItem';
import { setShouldHideTabBar } from '@reducers/commonSlice';
import { addClickstreamEvent } from '@services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
import { getTrainingMaterialList } from '@actions/TrainingMaterialAction';
const TrainingMaterial = () => {
const loading = useAppSelector((state) => state.trainingMaterial.loading);
const trainingMaterial = useAppSelector((state) => state.trainingMaterial.data);
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(setShouldHideTabBar(true));
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_TRAINING_MATERIAL_LIST_SCREEN_LOADED);
dispatch(getTrainingMaterialList());
return () => {
dispatch(setShouldHideTabBar(false));
};
}, []);
return (
<Layout>
<NavigationHeader title="Training Material" onBack={goBack} />
<SuspenseLoader
loading={loading}
fallBack={
<View style={[GenericStyles.fill, GenericStyles.centerAlignedRow]}>
<ActivityIndicator size={'large'} color={COLORS.BASE.BLUE} />
</View>
}
>
<View style={[GenericStyles.pv16, GenericStyles.fill]}>
<ScrollView>
<View style={GenericStyles.ph16}>
{trainingMaterial?.map((item) => (
<TrainingMaterialListItem key={item.referenceId} trainingMaterialData={item} />
))}
</View>
</ScrollView>
</View>
</SuspenseLoader>
</Layout>
);
};
export default TrainingMaterial;

View File

@@ -0,0 +1,72 @@
import NavigationHeader from '@rn-ui-lib/components/NavigationHeader';
import Layout from '@screens/layout/Layout';
import React, { useEffect } from 'react';
import { TrainingMaterialDetailProps, TrainingMaterialContentType } from './interfaces';
import { goBack } from '@components/utlis/navigationUtlis';
import { View } from 'react-native';
import { GenericStyles } from '@rn-ui-lib/styles';
import WebViewVideoPlayer from '@components/webViewVideoPlayer/WebViewVideoPlayer';
import PdfRenderer from '@components/pdfRenderer/PdfRenderer';
import { addClickstreamEvent } from '@services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
import { getTrainingMaterialDetails } from '@actions/TrainingMaterialAction';
const TrainingMaterialDetail: React.FC<TrainingMaterialDetailProps> = (props) => {
const {
route: {
params: { trainingMaterialData },
},
} = props;
const { title, fileType, referenceId } = trainingMaterialData || {};
const isVideo = fileType === TrainingMaterialContentType.VIDEO;
const getFileUri = async () => {
try {
const data = await getTrainingMaterialDetails(referenceId);
return data?.signedUri;
} catch {
return '';
}
};
useEffect(() => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_TRAINING_MATERIAL_ITEM_LOADED, {
fileType,
referenceId,
});
return () => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_TRAINING_MATERIAL_ITEM_CLOSED, {
fileType,
referenceId,
});
};
}, []);
const handlePageChange = (pageNumber: number) => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_TRAINING_MATERIAL_PDF_PAGE_CHANGED, {
pageNumber,
referenceId,
});
};
return (
<Layout>
<NavigationHeader title={title} onBack={goBack} />
<View
style={[GenericStyles.fill, GenericStyles.whiteBackground, GenericStyles.centerAlignedRow]}
>
{isVideo ? (
<WebViewVideoPlayer getVideoUrl={getFileUri} />
) : (
<PdfRenderer
docId={referenceId}
onPageChange={handlePageChange}
getFileUri={getFileUri}
/>
)}
</View>
</Layout>
);
};
export default TrainingMaterialDetail;

View File

@@ -0,0 +1,99 @@
import React from 'react';
import { ITrainingMaterialListItem, TrainingMaterialContentType } from './interfaces';
import { Pressable, StyleSheet, View } from 'react-native';
import { GenericStyles } from '@rn-ui-lib/styles';
import { TrainingMaterialContentMap } from './constants';
import RightChevronIcon from '@assets/icons/RightChevronIcon';
import Text from '@rn-ui-lib/components/Text';
import { COLORS } from '@rn-ui-lib/colors';
import { BUSINESS_DATE_FORMAT, dateFormat } from '@rn-ui-lib/utils/dates';
import Tag, { TagVariant } from '@rn-ui-lib/components/Tag';
import { navigateToScreen } from '@components/utlis/navigationUtlis';
import { ProfileScreenStackEnum } from '@screens/Profile/ProfileStack';
import { addClickstreamEvent } from '@services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
import { pluralise } from '@components/utlis/commonFunctions';
const TrainingMaterialListItem: React.FC<ITrainingMaterialListItem> = ({
trainingMaterialData,
}) => {
const { fileType, title, metadata, createdAt, isNewMaterial, referenceId } =
trainingMaterialData || {};
const { icon, interaction, metadataKey } = TrainingMaterialContentMap[fileType] || {};
const metadataValue = metadata?.[metadataKey];
const handleMaterialPress = () => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_TRAINING_MATERIAL_ITEM_CLICKED, {
referenceId,
});
navigateToScreen(ProfileScreenStackEnum.TRAINING_MATERIAL_DETAIL, {
trainingMaterialData,
});
};
return (
<Pressable
style={[
GenericStyles.mb8,
GenericStyles.whiteBackground,
GenericStyles.br8,
GenericStyles.overflowHidden,
GenericStyles.p16,
GenericStyles.elevation2,
]}
onPress={handleMaterialPress}
>
{isNewMaterial ? <Tag variant={TagVariant.blue} text="New" style={styles.newTag} /> : null}
<View style={[GenericStyles.row, GenericStyles.spaceBetween, GenericStyles.alignCenter]}>
<View style={[GenericStyles.row, GenericStyles.fill]}>
{icon}
<View style={GenericStyles.ml8}>
<Text dark style={styles.lh14} numberOfLines={3}>
{title}
</Text>
<View style={GenericStyles.row}>
<Text small>{interaction}</Text>
<Text style={styles.bullet} small>
{' '}
{' '}
</Text>
<Text small>
{fileType === TrainingMaterialContentType.PDF
? `${metadataValue} ${pluralise(metadataValue as number, 'page', 'pages')}`
: metadataValue}
</Text>
<Text style={styles.bullet} small>
{' '}
{' '}
</Text>
<Text small>{dateFormat(new Date(createdAt), BUSINESS_DATE_FORMAT)}</Text>
</View>
</View>
</View>
<View style={GenericStyles.ml16}>
<RightChevronIcon fillColor={COLORS.TEXT.LIGHT} />
</View>
</View>
</Pressable>
);
};
const styles = StyleSheet.create({
newTag: {
position: 'absolute',
top: 0,
right: 0,
borderTopWidth: 0,
borderRightWidth: 0,
borderTopLeftRadius: 0,
borderBottomRightRadius: 0,
},
bullet: {
color: COLORS.TEXT.GREY_1,
},
lh14: {
lineHeight: 14,
},
});
export default TrainingMaterialListItem;

View File

@@ -0,0 +1,20 @@
import TextMaterialIcon from '@assets/icons/TextMaterialIcon';
import { TrainingMaterialContentType } from './interfaces';
import VideoIcon from '@assets/icons/VideoIcon';
import PDFFullScreen from '@screens/caseDetails/PDFFullScreen';
import WebViewVideoPlayer from '@components/webViewVideoPlayer/WebViewVideoPlayer';
export const TrainingMaterialContentMap = {
[TrainingMaterialContentType.PDF]: {
interaction: 'Reading',
icon: <TextMaterialIcon />,
metadataKey: 'totalPages',
component: PDFFullScreen,
},
[TrainingMaterialContentType.VIDEO]: {
interaction: 'Video',
icon: <VideoIcon />,
metadataKey: 'duration',
component: WebViewVideoPlayer,
},
};

View File

@@ -0,0 +1,32 @@
export enum TrainingMaterialContentType {
PDF = 'PDF',
VIDEO = 'VIDEO',
}
export interface ITrainingMaterial {
referenceId: string;
title: string;
fileType: TrainingMaterialContentType;
createdAt: string;
isNewMaterial: boolean;
metadata: Record<string, string | number>;
}
export interface ITrainingMaterialListItem {
trainingMaterialData: ITrainingMaterial;
}
export interface TrainingMaterialDetailProps {
route: {
params: {
trainingMaterialData: ITrainingMaterial;
};
};
}
export interface ITrainingMaterialDetail {
signedUri: string;
referenceId: string;
loading: boolean;
showError: boolean;
}

View File

@@ -37,6 +37,7 @@ import topFeedbacksSlice from '@reducers/topFeedbacksSlice';
import escalationSlice from '@reducers/escalationSlice';
import postOperationalHourRestrictionsSlice from '@reducers/postOperationalHourRestrictionsSlice';
import skipTracingAddressesSlice from '@reducers/skipTracingAddressesSlice';
import trainingMaterialSlice from '@reducers/trainingMaterialSlice';
const rootReducer = combineReducers({
case: caseReducer,
@@ -73,7 +74,8 @@ const rootReducer = combineReducers({
documentsSlice: documentsSlice,
topFeedbacks: topFeedbacksSlice,
escalationSlice: escalationSlice,
postOperationalHourRestrictionsSlice: postOperationalHourRestrictionsSlice
postOperationalHourRestrictionsSlice: postOperationalHourRestrictionsSlice,
trainingMaterial: trainingMaterialSlice,
});
const persistConfig = {
@@ -97,7 +99,7 @@ const persistConfig = {
'feedbackFilters',
'litmusExperiment',
'activeCall',
'appUpdate'
'appUpdate',
],
blackList: [
'case',