NTP-8094 | Case Card Revamp (#983)

Co-authored-by: aishwarya.srivastava <aishwarya.srivastava@navi.com>
This commit is contained in:
Mantri Ramkishor
2024-12-03 19:15:10 +05:30
committed by GitHub
parent aff6f6ba2c
commit a530b4e9ed
27 changed files with 712 additions and 48 deletions

View File

@@ -9,6 +9,7 @@ import { logError } from '../components/utlis/errorUtils';
import { type IDocument, removeDocumentByQuestionKey } from '../reducer/feedbackImagesSlice';
import { addClickstreamEvent } from '@services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
import { PAST_FEEDBACK_PAGE_SIZE } from '@screens/caseDetails/feedback/pastFeedbackCommon';
export const getRepaymentsData = (loanAccountNumber: string) => (dispatch: AppDispatch) => {
dispatch(setRepaymentsLoading({ loanAccountNumber, isLoading: true }));
@@ -81,7 +82,7 @@ export const getFeedbackHistory = (loanAccountNumber: string) => (dispatch: AppD
{
loan_account_number: loanAccountNumber,
page_no: 0,
page_size: 5,
page_size: PAST_FEEDBACK_PAGE_SIZE,
}
);
axiosInstance

View File

@@ -77,27 +77,23 @@ export const getPastFeedbacksOnAddresses = (pastFeedbackPayload: IPastFeedbacksP
});
};
export const getTopFeedbacks = (loanAccountNumber: string) => (dispatch: AppDispatch) => {
// TODO: Change API Endpoint
const url = getApiUrl(ApiKeys.PAST_FEEDBACK_ON_ADDRESSES);
dispatch(setTopFeedbacksLoading({ loanAccountNumber, isLoading: true }));
export const getTopFeedbacks = (caseId: string) => (dispatch: AppDispatch) => {
const url = getApiUrl(ApiKeys.GET_PRIORTIY_FEEDBACK);
dispatch(setTopFeedbacksLoading({ caseId, isLoading: true }));
return axiosInstance
.get(url, {
params: { loanAccountNumber },
params: { caseReferenceId: caseId },
})
.then((response) => {
dispatch(
setTopFeedbacks({
loanAccountNumber,
feedbacks: [
response?.data?.data?.currentMonthFeedbackStatus,
response?.data?.data?.lastMonthFeedbackStatus,
],
caseId,
feedbacks: response?.data || [],
})
);
})
.catch((err) => {
dispatch(setTopFeedbacksLoading({ loanAccountNumber, isLoading: false }));
logError(err);
});
})
.finally(() => dispatch(setTopFeedbacksLoading({ caseId, isLoading: false })));
};

View File

@@ -12,10 +12,10 @@ const LocationDistanceIcon = (props: ILocationDistanceIcon) => {
const backgroundColor = props?.backgroundColor || COLORS.BACKGROUND.BLUE;
return (
<Svg width="16" height="20" viewBox="0 0 16 20" fill="none">
<Svg width="16" height="16" viewBox="0 0 16 20" fill="none">
<Rect width="16" height="40" fill={backgroundColor} />
<Mask id="mask0_15800_100880" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="20">
<Rect width="16" height="20" fill="#D9D9D9" />
<Mask id="mask0_15800_100880" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
<Rect width="16" height="16" fill="#D9D9D9" />
</Mask>
<G transform={`translate(0, -2)`}>
<G mask="url(#mask0_15800_100880)" transform="translate(0,4)">

View File

@@ -0,0 +1,16 @@
import React from 'react';
import { Path, Svg } from 'react-native-svg';
const TagPlaceholderIcon = () => {
return (
<Svg width="70" height="27" viewBox="0 0 70 27" fill="none">
<Path
d="M70 2C70 0.895431 69.1046 0 68 0H2C0.895432 0 0 0.895431 0 2V18C0 19.1046 0.895432 20 2 20H70V2Z"
fill="#D1E7FF"
/>
<Path d="M66 20L66 26.5L70 20L66 20Z" fill="#A0CBFD" />
</Svg>
);
};
export default TagPlaceholderIcon;

View File

@@ -44,6 +44,7 @@ import { NUDGE_BOTTOM_SHEET_DEFAULT_STATE } from './constants';
import { useBackHandler } from '@hooks/useBackHandler';
import { CALLING_NUDGE } from '@screens/caseDetails/CallingFlow/constants';
import { isFunction } from '@components/utlis/commonFunctions';
import { getTopFeedbacks } from '@actions/feedbackActions';
import { setPostOperationalHourRestrictions } from '@reducers/postOperationalHourRestrictionsSlice';
import { getSyncTime } from '@hooks/capturingApi';
@@ -228,6 +229,7 @@ const Widget: React.FC<IWidget> = (props) => {
caseId,
});
}
dispatch(getTopFeedbacks(caseId));
dispatch(
deleteJourney({
caseId,

View File

@@ -111,6 +111,7 @@ export enum ApiKeys {
GET_EMI_SCHEDULE = 'GET_EMI_SCHEDULE',
GET_REPAYMENTS = 'GET_REPAYMENTS',
GET_FEEDBACK_HISTORY = 'GET_FEEDBACK_HISTORY',
GET_PRIORTIY_FEEDBACK = 'GET_PRIORTIY_FEEDBACK',
}
export const API_URLS: Record<ApiKeys, string> = {} as Record<ApiKeys, string>;
@@ -215,6 +216,7 @@ API_URLS[ApiKeys.GET_GROUPED_ADDRESSES_AND_GEOLOCATIONS] =
API_URLS[ApiKeys.GET_EMI_SCHEDULE] = '/collection-cases/{loanAccountNumber}/emiSchedule';
API_URLS[ApiKeys.GET_REPAYMENTS] = '/collection-cases/{loanAccountNumber}/repayments';
API_URLS[ApiKeys.GET_FEEDBACK_HISTORY] = '/feedback/filters';
API_URLS[ApiKeys.GET_PRIORTIY_FEEDBACK] = 'feedback/case-status';
export const API_STATUS_CODE = {
OK: 200,

View File

@@ -4,7 +4,7 @@ interface ITopFeedback {
status: string;
color: string;
referenceId: string;
offset: number;
orderOffset: number;
createdAt: string;
type: string;
}
@@ -23,16 +23,16 @@ const TopFeedbacksSlice = createSlice({
initialState,
reducers: {
setTopFeedbacks: (state, action) => {
const { loanAccountNumber, feedbacks } = action.payload || {};
state[loanAccountNumber] = {
...(state[loanAccountNumber] || {}),
const { caseId, feedbacks } = action.payload || {};
state[caseId] = {
...(state[caseId] || {}),
feedbacks,
};
},
setTopFeedbacksLoading: (state, action) => {
const { loanAccountNumber, isLoading } = action.payload || {};
state[loanAccountNumber] = {
...(state?.[loanAccountNumber] || {}),
const { caseId, isLoading } = action.payload || {};
state[caseId] = {
...(state?.[caseId] || {}),
isLoading: isLoading,
};
},

View File

@@ -2,7 +2,6 @@ import React, { useMemo } from 'react';
import { Text, View, ViewProps, StyleSheet, Pressable } from 'react-native';
import { GenericStyles, getShadowStyle } from '../../../RN-UI-LIB/src/styles';
import { CaseTypes, ICaseItemCaseDetailObj } from './interface';
import ListItem from './ListItem';
import Button from '../../../RN-UI-LIB/src/components/Button';
import { navigateToScreen } from '../../components/utlis/navigationUtlis';
import { useAppSelector } from '../../hooks';
@@ -13,6 +12,7 @@ import LocationIcon from '@assets/icons/LocationIcon';
import ArrowRightOutlineIcon from '@rn-ui-lib/icons/ArrowRightOutlineIcon';
import { addClickstreamEvent } from '@services/clickstreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants';
import CaseListItem from './CaseItem/CaseListItem';
interface ICaseItemProps extends ViewProps {
caseDetailObj: ICaseItemCaseDetailObj;
@@ -125,7 +125,7 @@ const CaseItem: React.FC<ICaseItemProps> = ({
default:
return (
<View {...restProps}>
<ListItem
<CaseListItem
caseListItemDetailObj={getCaseItemCaseDetailObj}
shouldBatchAvatar={shouldBatchAvatar}
isCompleted={isCompleted}

View File

@@ -0,0 +1,41 @@
import { COLORS } from '@rn-ui-lib/colors';
import Text from '@rn-ui-lib/components/Text';
import { GenericStyles } from '@rn-ui-lib/styles';
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { ICaseDetailKeyValue } from '../interface';
const CaseDetailKeyValue = (props: ICaseDetailKeyValue) => {
const { title, value, showDot = false } = props;
return (
<View style={[GenericStyles.row, GenericStyles.alignCenter]}>
<Text small style={[styles.caseStatusText]} bold>
<Text small bold style={styles.caseStatusTextTitle}>
{title}
</Text>{' '}
{value ?? '--'}
</Text>
{showDot ? <View style={styles.dot} /> : null}
</View>
);
};
const styles = StyleSheet.create({
caseStatusText: {
color: COLORS.TEXT.GREY_32465B,
fontWeight: '500',
},
caseStatusTextTitle: {
color: COLORS.TEXT.LIGHT,
fontWeight: '500',
},
dot: {
height: 4,
width: 4,
borderRadius: 2,
marginHorizontal: 8,
backgroundColor: COLORS.TEXT.GREY_1,
},
});
export default CaseDetailKeyValue;

View File

@@ -0,0 +1,254 @@
import React, { memo, useMemo } from 'react';
import { Pressable, StyleSheet, View } from 'react-native';
import Heading from '../../../../RN-UI-LIB/src/components/Heading';
import Text from '../../../../RN-UI-LIB/src/components/Text';
import { GenericStyles, getShadowStyle } from '../../../../RN-UI-LIB/src/styles';
import { getCurrentScreen, navigateToScreen } from '../../../components/utlis/navigationUtlis';
import { useAppDispatch, useAppSelector } from '../../../hooks';
import { setPinnedRank, setSelectedTodoListMap } from '../../../reducer/allCasesSlice';
import CaseItemAvatar from '../CaseItemAvatar';
import { CaseStatuses, ICaseItemAvatarCaseDetailObj, ICaseListItem } from '../interface';
import { COLORS } from '../../../../RN-UI-LIB/src/styles/colors';
import { CLICKSTREAM_EVENT_NAMES } from '../../../common/Constants';
import { addClickstreamEvent } from '../../../services/clickstreamEventService';
import { formatAmount } from '../../../../RN-UI-LIB/src/utlis/amount';
import RoundCheckIcon from '../../../../RN-UI-LIB/src/Icons/RoundCheckIcon';
import { getDocumentList, pluralise } from '../../../components/utlis/commonFunctions';
import { toast } from '../../../../RN-UI-LIB/src/components/toast';
import { COMPLETED_STATUSES, ToastMessages } from '../constants';
import { VisitPlanStatus } from '../../../reducer/userSlice';
import { CaseDetailStackEnum } from '@screens/caseDetails/CaseDetailStack';
import { PageRouteEnum } from '@screens/auth/ProtectedRouter';
import VisitPlanTag from './VisitPlanTag';
import CaseStatus from './CaseStatus';
import FeedbackStatus from './FeedbackStatus';
import CaseDetailKeyValue from './CaseDetailKeyValue';
import Escalation from '../Escalation/Escalation';
const CaseListItem: React.FC<ICaseListItem> = (props) => {
const {
caseListItemDetailObj,
isCompleted,
isTodoItem,
shouldBatchAvatar,
allCasesView,
nearbyCaseView,
isVisitPlan,
} = props;
const {
caseReferenceId: caseId,
isIntermediateOrSelectedTodoCaseItem,
caseStatus,
caseType,
dpdBucket,
dpdCycle,
daysTillDeallocation,
pinRank,
isSynced,
totalOverdueAmount,
escalationData,
} = caseListItemDetailObj;
const isVisitPlanStatusLocked = useAppSelector(
(state) => state.user?.lock?.visitPlanStatus === VisitPlanStatus.LOCKED
);
const is1To30FieldAgent = useAppSelector((state) => state.user?.is1To30FieldAgent);
const isTeamLead = useAppSelector((state) => state.user.isTeamLead);
const activeEscalationCount = Number(escalationData?.activeEscalationCount ?? 0);
const pastEscalationCount = Number(escalationData?.pastEscalationCount ?? 0);
const totalEscalationsCount = activeEscalationCount + pastEscalationCount;
const dispatch = useAppDispatch();
const handleAvatarClick = () => {
if (isTodoItem || caseStatus === CaseStatuses.CLOSED) {
return;
}
if (isVisitPlanStatusLocked) {
toast({
type: 'info',
text1: ToastMessages.CASES_SELECTION_DISABLED,
});
return;
}
if (pinRank) {
dispatch(
setSelectedTodoListMap({
pinRank,
caseReferenceId: caseId,
})
);
} else {
dispatch(
setPinnedRank({
caseReferenceId: caseId,
})
);
}
};
const handleCaseClick = async () => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.AV_CASE_LIST_CASE_CLICKED, {
caseId,
screen: getCurrentScreen().name === 'Profile' ? 'Completed Cases' : getCurrentScreen().name, // todo: need to update use router
caseType,
});
if (nearbyCaseView) {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_NEARBY_CASE_CLICKED, {
caseId,
});
}
navigateToScreen(PageRouteEnum.CASE_DETAIL_STACK, {
screen: CaseDetailStackEnum.COLLECTION_CASE_DETAIL,
params: { caseId },
});
};
const isCaseSelected = !isTodoItem && !!isIntermediateOrSelectedTodoCaseItem;
const address =
caseListItemDetailObj?.currentTask?.metadata?.addressLine ||
caseListItemDetailObj?.addressString;
const customerName =
caseListItemDetailObj.customerInfo?.name ||
caseListItemDetailObj.customerInfo?.customerName ||
caseListItemDetailObj.customerName;
const getCaseItemAvatarCaseDetailObj = useMemo(
(): ICaseItemAvatarCaseDetailObj => ({
isPinned: pinRank ? true : false,
isCaseSynced: isSynced as boolean,
customerName: customerName,
caseId,
documentList: getDocumentList(caseListItemDetailObj) || [],
caseType: caseType,
imageUri: caseListItemDetailObj?.imageUri || '',
}),
[
caseType,
isSynced,
pinRank,
caseListItemDetailObj?.customerInfo?.documents,
caseListItemDetailObj?.documents,
caseListItemDetailObj?.imageUri,
]
);
const isCaseItemPinnedMainView = getCaseItemAvatarCaseDetailObj.isPinned && allCasesView;
const caseCompleted = COMPLETED_STATUSES.includes(caseStatus);
const showVisitPlanBtn =
!(caseCompleted || isCaseItemPinnedMainView) &&
!isTodoItem &&
!isCompleted &&
!isTeamLead &&
!nearbyCaseView;
const showInVisitPlanTag = isCaseItemPinnedMainView && !caseCompleted;
return (
<Pressable
style={[
styles.listItem,
getShadowStyle(2),
isCaseSelected ? styles.backgroundBlueLight : GenericStyles.whiteBackground,
]}
onPress={handleCaseClick}
>
<Escalation escalationData={escalationData} />
<View
style={[
GenericStyles.centerAlignedRow,
GenericStyles.justifyStart,
isCaseSelected ? styles.backgroundBlue : styles.backgroundSilverLight,
GenericStyles.w100,
GenericStyles.p12,
!totalEscalationsCount ? GenericStyles.brt8 : {},
]}
>
<CaseItemAvatar
caseDetailObj={getCaseItemAvatarCaseDetailObj}
shouldBatchAvatar={shouldBatchAvatar}
/>
<View style={[GenericStyles.pl8, GenericStyles.alignStart, GenericStyles.flex90]}>
<View style={[GenericStyles.w80, GenericStyles.pr16]}>
<Heading ellipsizeMode="tail" numberOfLines={1} type="h5" bold dark>
{customerName}
</Heading>
</View>
<CaseStatus caseListItemDetailObj={caseListItemDetailObj} isVisitPlan={isVisitPlan} />
</View>
</View>
{showVisitPlanBtn ? (
<Pressable onPress={handleAvatarClick} style={styles.selectBtn}>
<RoundCheckIcon focused={isCaseSelected} />
</Pressable>
) : null}
{showInVisitPlanTag ? <VisitPlanTag totalEscalationsCount={totalEscalationsCount} /> : null}
<View style={GenericStyles.p12}>
<Text light ellipsizeMode="tail" style={[GenericStyles.fontSize13, GenericStyles.mb16]}>
{address ? address : 'Address not available'}
</Text>
{is1To30FieldAgent ? (
<View>
<CaseDetailKeyValue title="Total due" value={formatAmount(totalOverdueAmount, false)} />
<View style={[GenericStyles.row, GenericStyles.wrap]}>
<CaseDetailKeyValue showDot title="Cycle" value={dpdCycle} />
<CaseDetailKeyValue
title="Deallocation in"
value={
daysTillDeallocation
? `${daysTillDeallocation} day${pluralise(daysTillDeallocation, '', 's')}`
: '--'
}
/>
</View>
</View>
) : (
<View style={[GenericStyles.row, GenericStyles.wrap]}>
<CaseDetailKeyValue
showDot
title="Total due"
value={formatAmount(totalOverdueAmount, false)}
/>
<CaseDetailKeyValue title="Bucket" value={dpdBucket} />
</View>
)}
<FeedbackStatus caseListItemDetailObj={caseListItemDetailObj} />
</View>
</Pressable>
);
};
const styles = StyleSheet.create({
listItem: {
backgroundColor: COLORS.BACKGROUND.PRIMARY,
borderRadius: 8,
marginVertical: 10,
position: 'relative',
},
selectBtn: {
position: 'absolute',
paddingTop: 12,
right: 0,
paddingRight: 12,
width: 80,
height: 80,
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'flex-end',
},
backgroundBlue: {
backgroundColor: COLORS.BACKGROUND.BLUE,
},
backgroundSilverLight: {
backgroundColor: COLORS.BACKGROUND.SILVER_LIGHT_3,
},
backgroundBlueLight: {
backgroundColor: COLORS.BACKGROUND.BLUE_LIGHT_3,
},
});
export default memo(CaseListItem);

View File

@@ -0,0 +1,57 @@
import { GenericStyles } from '@rn-ui-lib/styles';
import React from 'react';
import { View } from 'react-native';
import { paymentStatusMapping } from '../utils';
import Tag, { TagVariant } from '@rn-ui-lib/components/Tag';
import LocationDistanceIcon from '@assets/icons/LocationDistanceIcon';
import { COLORS } from '@rn-ui-lib/colors';
import { useAppSelector } from '@hooks';
import { TABS_KEYS } from '../constants';
import { ICaseStatus } from '../interface';
const CaseStatus = (props: ICaseStatus) => {
const { caseListItemDetailObj, isVisitPlan } = props;
const { collectionTag, paymentStatus, caseReferenceId } = caseListItemDetailObj || {};
const distanceMapOfNearbyCases =
useAppSelector((state) => state.nearbyCasesSlice.caseReferenceIdToDistanceMap) || {};
const selectedTab = useAppSelector((state) => state?.nearbyCasesSlice?.sortTabSelected);
const distanceOfCaseItem = distanceMapOfNearbyCases.get(caseReferenceId);
const isNearestCaseView = selectedTab === TABS_KEYS.NEAREST_CASE;
return (
<View style={[GenericStyles.centerAlignedRow, GenericStyles.mt2]}>
{paymentStatus ? (
<View style={[GenericStyles.mr8]}>
<Tag
variant={paymentStatusMapping[paymentStatus]?.variant || TagVariant.alert}
text={(paymentStatusMapping[paymentStatus]?.label || paymentStatus) as string}
/>
</View>
) : null}
{collectionTag ? (
<View style={[GenericStyles.mr8]}>
<Tag variant={TagVariant.white} text={collectionTag} />
</View>
) : null}
{!isVisitPlan && distanceOfCaseItem ? (
<Tag
tagIcon={
<LocationDistanceIcon
iconColor={isNearestCaseView ? COLORS.TEXT.BLUE_DARK_2 : COLORS.TEXT.GREY_3}
backgroundColor={
isNearestCaseView ? COLORS.BACKGROUND.BLUE_LIGHT_3 : COLORS.BACKGROUND.PRIMARY
}
/>
}
style={GenericStyles.pl2}
text={Number(distanceOfCaseItem?.toFixed(1)) + ' KM'}
variant={isNearestCaseView ? TagVariant.darkGray1 : TagVariant.white}
/>
) : null}
</View>
);
};
export default CaseStatus;

View File

@@ -0,0 +1,77 @@
import { COLORS } from '@rn-ui-lib/colors';
import Text from '@rn-ui-lib/components/Text';
import { GenericStyles } from '@rn-ui-lib/styles';
import React from 'react';
import { StyleSheet } from 'react-native';
import { View } from 'react-native';
import { IFeedbackStatus } from '../interface';
import { feedbackStatusColorMapping } from '../utils';
const FeedbackStatus = (props: IFeedbackStatus) => {
const { caseListItemDetailObj } = props;
const { currentMonthFeedbackStatus, lastMonthFeedbackStatus } = caseListItemDetailObj || {};
return (
<View>
<View style={[styles.dashedBorder, GenericStyles.mv16]} />
<View style={[GenericStyles.row, GenericStyles.justifyContentSpaceBetween]}>
<View>
<Text small style={GenericStyles.lh18}>
Current month
</Text>
<Text
small
style={[
styles.feedbackStatus,
{
color:
feedbackStatusColorMapping[
currentMonthFeedbackStatus?.color as keyof typeof feedbackStatusColorMapping
] ?? COLORS.TEXT.BLACK,
},
]}
>
{currentMonthFeedbackStatus?.status
? currentMonthFeedbackStatus?.status
: 'Not attempted'}
</Text>
</View>
<View style={GenericStyles.alignItemsFlexEnd}>
<Text small style={GenericStyles.lh18}>
Last month
</Text>
<Text
small
style={[
styles.feedbackStatus,
{
color:
feedbackStatusColorMapping[
lastMonthFeedbackStatus?.color as keyof typeof feedbackStatusColorMapping
] ?? COLORS.TEXT.BLACK,
},
]}
>
{lastMonthFeedbackStatus?.status ? lastMonthFeedbackStatus?.status : 'Not attempted'}
</Text>
</View>
</View>
</View>
);
};
const styles = StyleSheet.create({
feedbackStatus: {
lineHeight: 18,
marginTop: 4,
color: COLORS.TEXT.BLACK,
fontWeight: '500',
},
dashedBorder: {
borderTopWidth: 1,
borderColor: COLORS.BORDER.PRIMARY,
borderStyle: 'dashed',
},
});
export default FeedbackStatus;

View File

@@ -0,0 +1,46 @@
import TagPlaceholderIcon from '@assets/icons/TagPlaceholderIcon';
import { COLORS } from '@rn-ui-lib/colors';
import Text from '@rn-ui-lib/components/Text';
import { GenericStyles } from '@rn-ui-lib/styles';
import React from 'react';
import { StyleSheet } from 'react-native';
import { View } from 'react-native';
interface IVisitPlanTag {
totalEscalationsCount: number;
}
const VisitPlanTag = (props: IVisitPlanTag) => {
const { totalEscalationsCount } = props;
return (
<View
style={[
GenericStyles.absolute,
styles.visitPlanContainer,
totalEscalationsCount ? styles.top42 : {},
]}
>
<TagPlaceholderIcon />
<Text style={[GenericStyles.fontSize10, styles.visitPlanText]}>In visit plan</Text>
</View>
);
};
const styles = StyleSheet.create({
visitPlanContainer: {
right: -4,
top: 12,
},
top42: {
top: 42,
},
visitPlanText: {
position: 'absolute',
left: 9,
color: COLORS.TEXT.DARK,
fontWeight: '500',
lineHeight: 18
},
});
export default VisitPlanTag;

View File

@@ -410,7 +410,7 @@ const CasesList: React.FC<ICasesList> = ({
{visitPlansUpdating ? (
<Animated.View style={[styles.fillOverlay, { top: headerHeightValue }]} />
) : null}
<View style={[GenericStyles.fill, isPullToRefreshBannerVisible ? styles.mt64 : styles.mt16]}>
<View style={[GenericStyles.fill, isPullToRefreshBannerVisible ? styles.mt64 : GenericStyles.mt12]}>
{filteredCasesListWithCTA.length > 0 ? (
<FlashList
key={selectedTab}
@@ -516,22 +516,22 @@ const styles = StyleSheet.create({
opacity: 0.6,
},
list: {
paddingHorizontal: 12,
paddingHorizontal: 16,
paddingTop: HEADER_HEIGHT_MAX,
paddingBottom: 5,
paddingBottom: 10,
},
visitPlanList: {
paddingHorizontal: 12,
paddingHorizontal: 16,
paddingTop: VISIT_PLAN_HEADER_HEIGHT_MAX,
paddingBottom: 10,
},
listWithQuickFilters: {
paddingHorizontal: 12,
paddingHorizontal: 16,
paddingTop: HEADER_HEIGHT_MAX_WITH_QUICK_FILTERS,
paddingBottom: 5,
paddingBottom: 10,
},
visitPlanListWithQuickFilters: {
paddingHorizontal: 12,
paddingHorizontal: 16,
paddingTop: VISIT_PLAN_HEADER_HEIGHT_MAX_WITH_QUICK_FILTERS,
paddingBottom: 10,
},

View File

@@ -0,0 +1,41 @@
import React from 'react';
import { View } from 'react-native';
import EscalationItem from './EscalationItem';
import { EscalationData } from '@screens/caseDetails/interface';
interface IEscalation {
escalationData?: EscalationData;
}
const Escalation = (props: IEscalation) => {
const { escalationData } = props;
const isActiveEscalationCase = Number(escalationData?.activeEscalationCount) > 0;
const activeEscalationCount = Number(escalationData?.activeEscalationCount);
const pastEscalationCount = Number(escalationData?.pastEscalationCount);
const totalEscalationsCount = activeEscalationCount + pastEscalationCount;
if (!escalationData || totalEscalationsCount === 0) {
return null;
}
return (
<View>
{isActiveEscalationCase ? (
<EscalationItem
escalationText={`${activeEscalationCount} Active Escalation${
activeEscalationCount === 1 ? '' : 's'
}`}
isActiveEscalationCase
/>
) : (
<EscalationItem
escalationText={`${pastEscalationCount} Past Escalation${
pastEscalationCount === 1 ? '' : 's'
}`}
/>
)}
</View>
);
};
export default Escalation;

View File

@@ -0,0 +1,62 @@
import FlagIcon from '@assets/icons/FlagIcon';
import { COLORS } from '@rn-ui-lib/colors';
import Text from '@rn-ui-lib/components/Text';
import { GenericStyles } from '@rn-ui-lib/styles';
import React from 'react';
import { StyleSheet } from 'react-native';
import { View } from 'react-native';
interface IEscalationItem {
escalationText: string;
isActiveEscalationCase?: boolean;
}
const EscalationItem = (props: IEscalationItem) => {
const { escalationText, isActiveEscalationCase = false } = props;
return (
<View
style={[
styles.escalationContainer,
GenericStyles.row,
GenericStyles.alignCenter,
GenericStyles.w100,
isActiveEscalationCase ? styles.redContainer : styles.yellowContainer,
]}
>
<FlagIcon fillColor={isActiveEscalationCase ? COLORS.TEXT.RED : COLORS.TEXT.YELLOW_LIGHT} />
<Text
small
style={[
styles.escalationText,
{ color: isActiveEscalationCase ? COLORS.TEXT.RED : COLORS.TEXT.YELLOW_LIGHT },
]}
>
{escalationText}
</Text>
</View>
);
};
const styles = StyleSheet.create({
escalationContainer: {
height: 30,
paddingLeft: 12,
paddingRight: 8,
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
borderWidth: 1,
},
escalationText: {
marginLeft: 4,
},
redContainer: {
borderColor: COLORS.BORDER.RED,
backgroundColor: COLORS.BACKGROUND.RED,
},
yellowContainer: {
borderColor: COLORS.BORDER.YELLOW,
backgroundColor: COLORS.BACKGROUND.YELLOW_LIGHT,
},
});
export default EscalationItem;

View File

@@ -355,3 +355,29 @@ export interface IEscalationSummary {
activeEscalationCount: number;
recentEscalationDetails: IRecentEscalationDetails;
}
export interface ICaseListItem {
caseListItemDetailObj: ICaseItemCaseDetailObj;
isTodoItem?: boolean;
isCompleted?: boolean;
shouldBatchAvatar?: boolean;
allCasesView?: boolean;
isAgentDashboard?: boolean;
nearbyCaseView?: boolean;
isVisitPlan?: boolean;
}
export interface ICaseDetailKeyValue {
title: string;
value: string;
showDot?: boolean;
}
export interface ICaseStatus {
caseListItemDetailObj: ICaseItemCaseDetailObj;
isVisitPlan?: boolean;
}
export interface IFeedbackStatus{
caseListItemDetailObj: ICaseItemCaseDetailObj;
}

View File

@@ -8,6 +8,7 @@ import {
FeedbackStatus,
IDocumentItem,
INearbyCaseItemObj,
PaymentStatus,
} from '../caseDetails/interface';
import {
BOTTOM_TAB_ROUTES,
@@ -28,6 +29,8 @@ import { logError } from '@components/utlis/errorUtils';
import { setAgentsDocumentsData, setDocumentsData } from '@reducers/documentsSlice';
import { useWindowDimensions } from 'react-native';
import { toast } from '@rn-ui-lib/components/toast';
import { TagVariant } from '@rn-ui-lib/components/Tag';
import { COLORS } from '@rn-ui-lib/colors';
export const getAttemptedList = (
filteredCasesList: ICaseItem[],
@@ -322,3 +325,22 @@ export const calculateBottomSheetHeight = (rowLength = 0) => {
const dynamicHeight = ((rowLength * rowHeight + headerOffset) / SCREEN_HEIGHT) * 100;
return Math.min(dynamicHeight, maxHeightPercentage);
};
export const paymentStatusMapping: Record<
PaymentStatus,
{ label: PaymentStatus | string; variant: TagVariant }
> = {
[PaymentStatus.Paid]: { label: PaymentStatus.Paid, variant: TagVariant.success },
[PaymentStatus['Partially Paid']]: {
label: PaymentStatus['Partially Paid'],
variant: TagVariant.yellow,
},
[PaymentStatus.Unpaid]: { label: PaymentStatus.Unpaid, variant: TagVariant.alert },
[PaymentStatus.Closed]: { label: PaymentStatus.Closed, variant: TagVariant.error },
};
export const feedbackStatusColorMapping = {
green: COLORS.TEXT.GREEN,
red: COLORS.TEXT.RED,
gray: COLORS.TEXT.BLACK,
};

View File

@@ -22,6 +22,7 @@ import { COLORS } from '@rn-ui-lib/colors';
import { syncActiveCallDetails } from '@actions/callRecordingActions';
import { getUngroupedAddress } from '@actions/addressGeolocationAction';
import EscalationsSection from '@screens/escalations/EscalationsSection';
import TopFeedbacks from './feedback/TopFeedbacks';
interface ICaseDetails {
route: {
@@ -59,7 +60,6 @@ const CollectionCaseDetails: React.FC<ICaseDetails> = (props) => {
useEffect(() => {
if (caseId) dispatch(setSelectedCaseId(caseId));
dispatch(getFeedbackHistory(loanAccountNumber));
return () => {
dispatch(setSelectedCaseId(''));
};
@@ -130,6 +130,7 @@ const CollectionCaseDetails: React.FC<ICaseDetails> = (props) => {
) : null}
<EmiDetailsSection caseId={caseId} />
<CollectMoneySection caseId={caseId} />
<TopFeedbacks caseId={caseId} />
<FeedbackDetailsSection caseId={caseId} />
</Animated.View>
</ScrollView>

View File

@@ -35,12 +35,12 @@ import Filters, { TFilterOptions } from '../../../components/filters/Filters';
import { _map } from '../../../../RN-UI-LIB/src/utlis/common';
import ChevronDown from '../../../assets/icons/ChevronDown';
import ChevronUp from '../../../assets/icons/ChevronUp';
import { PAST_FEEDBACK_PAGE_SIZE } from '@screens/caseDetails/feedback/pastFeedbackCommon';
const FEEDBACK_PAGE_TITLE = 'All feedbacks';
const ADDRESS_FEEDBACK_PAGE_TITLE = 'Address feedback';
const SCROLL_LAYOUT_OFFSET = 10;
const FEEDBACK_PER_PAGE = 5;
interface IFeedbackDetailContainer {
route: {
@@ -123,7 +123,7 @@ const FeedbackDetailContainer: React.FC<IFeedbackDetailContainer> = ({ route: ro
{
loan_account_number: loanAccountNumber,
page_no: currentPage - 1,
page_size: FEEDBACK_PER_PAGE,
page_size: PAST_FEEDBACK_PAGE_SIZE,
customerRecahble: false,
addressReferenceIds,
},

View File

@@ -16,6 +16,8 @@ import { shareToWhatsapp } from '../../../services/FeedbackWhatsApp';
import { CaseDetailStackEnum } from '../CaseDetailStack';
import { feedbackTypeIcon } from '@screens/caseDetails/feedback/FeedbackDetailItem';
import Tag, { TagVariant } from '@rn-ui-lib/components/Tag';
import { feedbackStatusColorMapping } from '@screens/allCases/utils';
import { PAST_FEEDBACK_PAGE_SIZE } from '@screens/caseDetails/feedback/pastFeedbackCommon';
interface IFeedbackListItem {
feedbackItem: IFeedback | IUnSyncedFeedbackItem;
@@ -41,7 +43,9 @@ const FeedbackListItem: React.FC<IFeedbackListItem> = ({
};
navigateToScreen(CaseDetailStackEnum.PAST_FEEDBACK_DETAIL, {
...commonParams,
pageNo: isTopFeedbackItem ? (feedbackItem as IFeedback)?.offset : 1,
pageNo: isTopFeedbackItem
? Math.ceil(((feedbackItem as IFeedback)?.orderOffset ?? 0) / PAST_FEEDBACK_PAGE_SIZE)
: 1,
});
};
const [isWhastappSendLoading, setIsWhatsappSendLoading] = useState(false);
@@ -98,6 +102,12 @@ const FeedbackListItem: React.FC<IFeedbackListItem> = ({
style={[
styles.textHeading,
feedbackItem.type === FEEDBACK_TYPE.GEN_AI_BOT_FIELD ? styles.capitalized : null,
{
color:
feedbackStatusColorMapping[
feedbackItem?.color as keyof typeof feedbackStatusColorMapping
] ?? COLORS.TEXT.BLACK,
},
]}
>
{sanitizeString(feedbackItem.interactionStatus)}
@@ -116,7 +126,7 @@ const FeedbackListItem: React.FC<IFeedbackListItem> = ({
</View>
) : null}
</View>
{feedbackItem?.type === FEEDBACK_TYPE.FIELD_VISIT ? (
{feedbackItem?.type === FEEDBACK_TYPE.FIELD_VISIT && !isTopFeedbackItem ? (
<TouchableOpacity
style={styles.ShareButton}
hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }}
@@ -186,7 +196,7 @@ const styles = StyleSheet.create({
paddingHorizontal: 0,
},
topFeedbackItem: {
paddingTop: 40,
paddingTop: 36,
paddingHorizontal: 16,
},
currentMonthFeedback: {

View File

@@ -20,14 +20,14 @@ const TopFeedbacks = (props: ITopFeedbacks) => {
const caseDetail = useAppSelector((state: RootState) => state.allCases.caseDetails[caseId]) || {};
const { loanAccountNumber } = caseDetail || {};
const feedbackList = useAppSelector(
(state: RootState) => state.topFeedbacks?.[loanAccountNumber as string]?.feedbacks || []
(state: RootState) => state.topFeedbacks?.[caseId as string]?.feedbacks || []
);
const isLoading = useAppSelector(
(state: RootState) => state.topFeedbacks?.[loanAccountNumber as string]?.isLoading || false
(state: RootState) => state.topFeedbacks?.[caseId as string]?.isLoading || false
);
useEffect(() => {
dispatch(getTopFeedbacks(loanAccountNumber));
dispatch(getTopFeedbacks(caseId));
}, []);
if (!feedbackList?.length) return null;
@@ -60,7 +60,7 @@ const TopFeedbacks = (props: ITopFeedbacks) => {
</View>
<View style={[GenericStyles.whiteBackground, GenericStyles.br8, getShadowStyle(2)]}>
<SuspenseLoader
loading={isLoading && !feedbackList?.length}
loading={isLoading}
fallBack={<FeedbackLoading arrayLength={2} isTopFeedbackItem />}
>
{feedbackList?.map((feedbackItem: any, idx: number) => (

View File

@@ -0,0 +1 @@
export const PAST_FEEDBACK_PAGE_SIZE = 5;

View File

@@ -262,6 +262,11 @@ export interface EmploymentDetails {
employerName: string;
}
export interface FeedbackStatusObj {
status: string;
color: string
}
export interface CaseDetail {
id: string;
allocationReferenceId?: string;
@@ -316,7 +321,9 @@ export interface CaseDetail {
employmentDetail?: EmploymentDetails;
unpaidDays?: number;
addressStringType?: string;
escalationData ?: escalationData;
currentMonthFeedbackStatus?: FeedbackStatusObj;
lastMonthFeedbackStatus?:FeedbackStatusObj;
escalationData ?: EscalationData;
daysTillDeallocation: number;
}
@@ -325,7 +332,7 @@ export interface recentEscalationDetails {
customerVoice : string;
}
export interface escalationData {
export interface EscalationData {
activeEscalationCount : number;
pastEscalationCount : number;
recentEscalationDetails : recentEscalationDetails;

View File

@@ -78,6 +78,7 @@ export interface IFeedback {
interactionStatus: InteractionStatuses;
tagTitle?: string;
offset?: number;
color?: string;
}
export interface IUnSyncedFeedbackItem {
@@ -85,6 +86,7 @@ export interface IUnSyncedFeedbackItem {
createdAt: string;
isSynced: boolean;
type?: FEEDBACK_TYPE;
color?: string;
}
export const CALLING_FEEDBACKS = [FEEDBACK_TYPE.CALL_BRIDGE, FEEDBACK_TYPE.SELF_CALL];