From 45d6db828596c15cd71d5a503dfd6c1508c64830 Mon Sep 17 00:00:00 2001 From: Aman Chaturvedi Date: Tue, 24 Dec 2024 13:11:28 +0530 Subject: [PATCH] TP-21010 | Training module --- src/assets/icons/BookIcon.tsx | 13 +++ src/assets/icons/RightChevronIcon.tsx | 6 +- src/assets/icons/TextMaterialIcon.tsx | 13 +++ src/assets/icons/VideoIcon.tsx | 20 ++++ src/common/Constants.ts | 97 +++++++++++-------- src/components/pdfRenderer/PdfRenderer.tsx | 70 +++++++++++++ src/components/pdfRenderer/interfaces.ts | 5 + src/components/utlis/apiHelper.ts | 2 +- .../webViewVideoPlayer/WebViewVideoPlayer.tsx | 17 ++++ .../webViewVideoPlayer/constants.ts | 11 +++ .../webViewVideoPlayer/interfaces.ts | 3 + src/reducer/trainingMaterialSlice.ts | 58 +++++++++++ src/screens/Profile/Navigation/constants.ts | 10 ++ src/screens/Profile/ProfileStack.tsx | 10 ++ .../trainingMaterial/TrainingMaterial.tsx | 56 +++++++++++ .../TrainingMaterialDetail.tsx | 56 +++++++++++ .../TrainingMaterialListItem.tsx | 94 ++++++++++++++++++ src/screens/trainingMaterial/constants.tsx | 20 ++++ src/screens/trainingMaterial/interfaces.ts | 25 +++++ src/store/store.ts | 6 +- 20 files changed, 549 insertions(+), 43 deletions(-) create mode 100644 src/assets/icons/BookIcon.tsx create mode 100644 src/assets/icons/TextMaterialIcon.tsx create mode 100644 src/assets/icons/VideoIcon.tsx create mode 100644 src/components/pdfRenderer/PdfRenderer.tsx create mode 100644 src/components/pdfRenderer/interfaces.ts create mode 100644 src/components/webViewVideoPlayer/WebViewVideoPlayer.tsx create mode 100644 src/components/webViewVideoPlayer/constants.ts create mode 100644 src/components/webViewVideoPlayer/interfaces.ts create mode 100644 src/reducer/trainingMaterialSlice.ts create mode 100644 src/screens/trainingMaterial/TrainingMaterial.tsx create mode 100644 src/screens/trainingMaterial/TrainingMaterialDetail.tsx create mode 100644 src/screens/trainingMaterial/TrainingMaterialListItem.tsx create mode 100644 src/screens/trainingMaterial/constants.tsx create mode 100644 src/screens/trainingMaterial/interfaces.ts diff --git a/src/assets/icons/BookIcon.tsx b/src/assets/icons/BookIcon.tsx new file mode 100644 index 00000000..a87b7655 --- /dev/null +++ b/src/assets/icons/BookIcon.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; +import Svg, { Path } from 'react-native-svg'; + +const BookIcon = () => ( + + + +); + +export default BookIcon; diff --git a/src/assets/icons/RightChevronIcon.tsx b/src/assets/icons/RightChevronIcon.tsx index 69e11441..002a2652 100644 --- a/src/assets/icons/RightChevronIcon.tsx +++ b/src/assets/icons/RightChevronIcon.tsx @@ -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 = ({ fillColor = COLORS.TEXT.BLUE }) => { return ( @@ -10,7 +12,7 @@ const RightChevronIcon = () => { diff --git a/src/assets/icons/TextMaterialIcon.tsx b/src/assets/icons/TextMaterialIcon.tsx new file mode 100644 index 00000000..36b8e98a --- /dev/null +++ b/src/assets/icons/TextMaterialIcon.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; +import Svg, { Path } from 'react-native-svg'; + +const TextMaterialIcon = () => ( + + + +); + +export default TextMaterialIcon; diff --git a/src/assets/icons/VideoIcon.tsx b/src/assets/icons/VideoIcon.tsx new file mode 100644 index 00000000..a96db6a9 --- /dev/null +++ b/src/assets/icons/VideoIcon.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; +import Svg, { G, Path, Defs, ClipPath, Rect } from 'react-native-svg'; + +const VideoIcon = () => ( + + + + + + + + + + +); + +export default VideoIcon; diff --git a/src/common/Constants.ts b/src/common/Constants.ts index 51d9e77a..afd8ed3b 100644 --- a/src/common/Constants.ts +++ b/src/common/Constants.ts @@ -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', @@ -1133,7 +1132,7 @@ export const CLICKSTREAM_EVENT_NAMES = { }, LITMUS_EXPERIMENT: { name: 'LITMUS_EXPERIMENT', - description: 'LITMUS_EXPERIMENT' + description: 'LITMUS_EXPERIMENT', }, //FEE WAIVE CLICKSTREAM EVENTS @@ -1294,107 +1293,129 @@ 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_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_PDF_PAGE_CHANGED: { + name: 'FA_TRAINING_MATERIAL_PDF_PAGE_CHANGED', + description: 'Training material PDF page changed', + }, } as const; export enum MimeType { @@ -1541,4 +1562,4 @@ export const API_ERROR_MESSAGE = 'Oops! something went wrong'; export enum BuildFlavours { FIELD_AGENTS = 'fieldAgents', CALLING_AGENTS = 'callingAgents', -} \ No newline at end of file +} diff --git a/src/components/pdfRenderer/PdfRenderer.tsx b/src/components/pdfRenderer/PdfRenderer.tsx new file mode 100644 index 00000000..e00a743b --- /dev/null +++ b/src/components/pdfRenderer/PdfRenderer.tsx @@ -0,0 +1,70 @@ +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'; + +const ERROR_STATE = 'ERROR'; + +const PdfRenderer: React.FC = ({ docId, url, onPageChange }) => { + const [pdfFilePath, setPdfFilePath] = useState(''); + const isLoading = !pdfFilePath; + const error = pdfFilePath === ERROR_STATE; + + const saveFileToCache = async () => { + const cacheDirectory = RNFetchBlob.fs.dirs.CacheDir; + const cacheFilePath = `${cacheDirectory}/${docId}.pdf`; + const doesFileExist = await RNFetchBlob.fs.exists(cacheFilePath); + if (doesFileExist) { + setPdfFilePath(cacheFilePath); + 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(); + await RNFetchBlob.fs.writeFile(cacheFilePath, highQualityImageBase64, 'base64'); + setPdfFilePath(cacheFilePath); + } + }; + + useEffect(() => { + saveFileToCache(); + }, [url]); + + const handlePageChange = (pageNumber: number) => { + onPageChange?.(pageNumber); + }; + + return ( + + }> + {error ? ( + Failed to load PDF + ) : ( + + )} + + + ); +}; + +export const styles = StyleSheet.create({ + pdf: { + flex: 1, + backgroundColor: COLORS.BACKGROUND.GREY_LIGHT_2, + }, +}); + +export default PdfRenderer; diff --git a/src/components/pdfRenderer/interfaces.ts b/src/components/pdfRenderer/interfaces.ts new file mode 100644 index 00000000..5075386e --- /dev/null +++ b/src/components/pdfRenderer/interfaces.ts @@ -0,0 +1,5 @@ +export interface IPdfRenderer { + docId: string; + url: string; + onPageChange?: (pageNumber: number) => void; +} \ No newline at end of file diff --git a/src/components/utlis/apiHelper.ts b/src/components/utlis/apiHelper.ts index 3c1ef760..03e35bbe 100644 --- a/src/components/utlis/apiHelper.ts +++ b/src/components/utlis/apiHelper.ts @@ -215,7 +215,7 @@ API_URLS[ApiKeys.GET_GROUPED_ADDRESSES_AND_GEOLOCATIONS] = 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'; export const API_STATUS_CODE = { OK: 200, diff --git a/src/components/webViewVideoPlayer/WebViewVideoPlayer.tsx b/src/components/webViewVideoPlayer/WebViewVideoPlayer.tsx new file mode 100644 index 00000000..cb73d6a1 --- /dev/null +++ b/src/components/webViewVideoPlayer/WebViewVideoPlayer.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import WebView from 'react-native-webview'; +import { IWebViewVideoPlayer } from './interfaces'; +import { videoPlayerHTML } from './constants'; + +const WebViewVideoPlayer: React.FC = ({ url }) => { + return ( + + ); +}; + +export default WebViewVideoPlayer; diff --git a/src/components/webViewVideoPlayer/constants.ts b/src/components/webViewVideoPlayer/constants.ts new file mode 100644 index 00000000..b9cfe5f1 --- /dev/null +++ b/src/components/webViewVideoPlayer/constants.ts @@ -0,0 +1,11 @@ +export const videoPlayerHTML = (url: string) => ` + + + + + + +`; diff --git a/src/components/webViewVideoPlayer/interfaces.ts b/src/components/webViewVideoPlayer/interfaces.ts new file mode 100644 index 00000000..ee1c2d86 --- /dev/null +++ b/src/components/webViewVideoPlayer/interfaces.ts @@ -0,0 +1,3 @@ +export interface IWebViewVideoPlayer { + url: string; +} \ No newline at end of file diff --git a/src/reducer/trainingMaterialSlice.ts b/src/reducer/trainingMaterialSlice.ts new file mode 100644 index 00000000..119fd42a --- /dev/null +++ b/src/reducer/trainingMaterialSlice.ts @@ -0,0 +1,58 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { + ITrainingMaterial, + TrainingMaterialContentType, +} from '@screens/trainingMaterial/interfaces'; + +interface ITrainingMaterialSlice { + loading: boolean; + data: ITrainingMaterial[]; +} + +const MOCK_DATA = [ + { + documentReferenceId: 'doc1', + topic: 'If Customer is demanding settlement', + contentType: TrainingMaterialContentType.PDF, + createdAt: '2021-06-02T00:00:00.000Z', + isNewMaterial: true, + metadata: { + duration: '', + pageCount: '10 Pages', + }, + }, + { + documentReferenceId: 'doc2', + topic: 'How to fill daily commitment', + contentType: TrainingMaterialContentType.VIDEO, + createdAt: '2021-06-02T00:00:00.000Z', + isNewMaterial: false, + metadata: { + duration: '10 min', + pageCount: '', + }, + }, +]; + +const initialState: ITrainingMaterialSlice = { + loading: false, + data: MOCK_DATA, +}; + +const TrainingMaterialSlice = createSlice({ + name: 'trainingMaterial', + initialState, + reducers: { + setTrainingMaterialLoading: (state, action: PayloadAction) => { + state.loading = action.payload; + }, + setTrainingMaterialData: (state, action: PayloadAction) => { + state.data = action.payload; + }, + }, +}); + +export const { setTrainingMaterialLoading, setTrainingMaterialData } = + TrainingMaterialSlice.actions; + +export default TrainingMaterialSlice.reducer; diff --git a/src/screens/Profile/Navigation/constants.ts b/src/screens/Profile/Navigation/constants.ts index 039769ea..cc4076bb 100644 --- a/src/screens/Profile/Navigation/constants.ts +++ b/src/screens/Profile/Navigation/constants.ts @@ -10,6 +10,7 @@ import store from '@store'; import { Alert } from 'react-native'; import { ProfileScreenStackEnum } from '../ProfileStack'; import CountComponent from '../CountComponent'; +import BookIcon from '@assets/icons/BookIcon'; export const getNavigationLinks = () => { const { isTeamLead, selectedAgent, featureFlags } = store?.getState().user; @@ -37,6 +38,15 @@ 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); + }, + }, { name: 'Logout', icon: LogoutIcon, diff --git a/src/screens/Profile/ProfileStack.tsx b/src/screens/Profile/ProfileStack.tsx index 0e42b135..c58a70ce 100644 --- a/src/screens/Profile/ProfileStack.tsx +++ b/src/screens/Profile/ProfileStack.tsx @@ -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' }} /> + + ); }; diff --git a/src/screens/trainingMaterial/TrainingMaterial.tsx b/src/screens/trainingMaterial/TrainingMaterial.tsx new file mode 100644 index 00000000..7a509432 --- /dev/null +++ b/src/screens/trainingMaterial/TrainingMaterial.tsx @@ -0,0 +1,56 @@ +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'; + +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); + return () => { + dispatch(setShouldHideTabBar(false)); + }; + }, []); + + return ( + + + + + + } + > + + + + {trainingMaterial.map((item) => ( + + ))} + + + + + + ); +}; + +export default TrainingMaterial; diff --git a/src/screens/trainingMaterial/TrainingMaterialDetail.tsx b/src/screens/trainingMaterial/TrainingMaterialDetail.tsx new file mode 100644 index 00000000..8f408fb8 --- /dev/null +++ b/src/screens/trainingMaterial/TrainingMaterialDetail.tsx @@ -0,0 +1,56 @@ +import NavigationHeader from '@rn-ui-lib/components/NavigationHeader'; +import Layout from '@screens/layout/Layout'; +import React, { useEffect } from 'react'; +import { ITrainingMaterialDetail, 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'; + +const TrainingMaterialDetail: React.FC = (props) => { + const { + route: { + params: { trainingMaterialData }, + }, + } = props; + const { topic, contentType, documentReferenceId } = trainingMaterialData; + const isVideo = contentType === TrainingMaterialContentType.VIDEO; + + useEffect(() => { + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_TRAINING_MATERIAL_ITEM_LOADED, { + contentType, + documentReferenceId, + }); + }, []); + + const handlePageChange = (pageNumber: number) => { + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_TRAINING_MATERIAL_PDF_PAGE_CHANGED, { + pageNumber, + documentReferenceId, + }); + }; + + return ( + + + + {isVideo ? ( + + ) : ( + + )} + + + ); +}; + +export default TrainingMaterialDetail; diff --git a/src/screens/trainingMaterial/TrainingMaterialListItem.tsx b/src/screens/trainingMaterial/TrainingMaterialListItem.tsx new file mode 100644 index 00000000..321b365f --- /dev/null +++ b/src/screens/trainingMaterial/TrainingMaterialListItem.tsx @@ -0,0 +1,94 @@ +import React from 'react'; +import { ITrainingMaterialListItem } 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'; + +const TrainingMaterialListItem: React.FC = ({ + trainingMaterialData, +}) => { + const { contentType, topic, metadata, createdAt, isNewMaterial, documentReferenceId } = + trainingMaterialData || {}; + const { icon, interaction, metadataKey } = TrainingMaterialContentMap[contentType] || {}; + const metadataValue = metadata?.[metadataKey]; + + const handleMaterialPress = () => { + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_TRAINING_MATERIAL_ITEM_CLICKED, { + documentReferenceId, + }); + navigateToScreen(ProfileScreenStackEnum.TRAINING_MATERIAL_DETAIL, { + trainingMaterialData, + }); + }; + + return ( + + {isNewMaterial ? : null} + + + {icon} + + + {topic} + + + + {interaction} + + + {' '} + ●{' '} + + + {metadataValue} + + + + {dateFormat(new Date(createdAt), BUSINESS_DATE_FORMAT)} + + + + + + + + + + ); +}; + +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, + }, +}); + +export default TrainingMaterialListItem; diff --git a/src/screens/trainingMaterial/constants.tsx b/src/screens/trainingMaterial/constants.tsx new file mode 100644 index 00000000..c63c6467 --- /dev/null +++ b/src/screens/trainingMaterial/constants.tsx @@ -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: , + metadataKey: 'pageCount', + component: PDFFullScreen, + }, + [TrainingMaterialContentType.VIDEO]: { + interaction: 'Video', + icon: , + metadataKey: 'duration', + component: WebViewVideoPlayer, + }, +}; diff --git a/src/screens/trainingMaterial/interfaces.ts b/src/screens/trainingMaterial/interfaces.ts new file mode 100644 index 00000000..56799fbf --- /dev/null +++ b/src/screens/trainingMaterial/interfaces.ts @@ -0,0 +1,25 @@ +export enum TrainingMaterialContentType { + PDF = 'PDF', + VIDEO = 'VIDEO', +} + +export interface ITrainingMaterial { + documentReferenceId: string; + topic: string; + contentType: TrainingMaterialContentType; + createdAt: string; + isNewMaterial: boolean; + metadata: Record; +} + +export interface ITrainingMaterialListItem { + trainingMaterialData: ITrainingMaterial; +} + +export interface ITrainingMaterialDetail { + route: { + params: { + trainingMaterialData: ITrainingMaterial; + }; + }; +} diff --git a/src/store/store.ts b/src/store/store.ts index 7c5abb5d..6f82d789 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -36,6 +36,7 @@ import documentsSlice from '@reducers/documentsSlice'; import topFeedbacksSlice from '@reducers/topFeedbacksSlice'; import escalationSlice from '@reducers/escalationSlice'; import postOperationalHourRestrictionsSlice from '@reducers/postOperationalHourRestrictionsSlice'; +import trainingMaterialSlice from '@reducers/trainingMaterialSlice'; const rootReducer = combineReducers({ case: caseReducer, @@ -71,7 +72,8 @@ const rootReducer = combineReducers({ documentsSlice: documentsSlice, topFeedbacks: topFeedbacksSlice, escalationSlice: escalationSlice, - postOperationalHourRestrictionsSlice: postOperationalHourRestrictionsSlice + postOperationalHourRestrictionsSlice: postOperationalHourRestrictionsSlice, + trainingMaterial: trainingMaterialSlice, }); const persistConfig = { @@ -95,7 +97,7 @@ const persistConfig = { 'feedbackFilters', 'litmusExperiment', 'activeCall', - 'appUpdate' + 'appUpdate', ], blackList: [ 'case',